UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

623 lines 26.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const cache_watcher_service_1 = require("./cache-watcher.service"); const telescope_service_1 = require("../../core/services/telescope.service"); const cache_watcher_config_1 = require("./cache-watcher.config"); describe('CacheWatcherService', () => { let service; let telescopeService; beforeEach(async () => { const mockTelescopeService = { record: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ cache_watcher_service_1.CacheWatcherService, { provide: telescope_service_1.TelescopeService, useValue: mockTelescopeService, }, { provide: 'CACHE_WATCHER_CONFIG', useValue: cache_watcher_config_1.defaultCacheWatcherConfig, }, ], }).compile(); service = module.get(cache_watcher_service_1.CacheWatcherService); telescopeService = module.get(telescope_service_1.TelescopeService); }); afterEach(() => { jest.clearAllMocks(); }); describe('initialization', () => { it('should be defined', () => { expect(service).toBeDefined(); }); it('should initialize with default metrics', () => { const metrics = service.getMetrics(); expect(metrics.totalOperations).toBe(0); expect(metrics.hitCount).toBe(0); expect(metrics.missCount).toBe(0); expect(metrics.healthScore).toBe(100); }); it('should start periodic processing on module init', async () => { await service.onModuleInit(); }); }); describe('cache operation tracking', () => { it('should track cache hit', () => { const context = { id: 'cache-hit-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'user:123', value: { id: 123, name: 'John' }, hit: true, startTime: new Date(), duration: 10, }; service.trackCacheOperation(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ type: 'cache', content: expect.objectContaining({ cache: expect.objectContaining({ operation: 'get', key: 'user:123', hit: true, }), }), })); const metrics = service.getMetrics(); expect(metrics.totalOperations).toBe(1); expect(metrics.hitCount).toBe(1); expect(metrics.hitRate).toBe(100); }); it('should track cache miss', () => { const context = { id: 'cache-miss-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'user:456', hit: false, startTime: new Date(), duration: 5, }; service.trackCacheOperation(context); const metrics = service.getMetrics(); expect(metrics.totalOperations).toBe(1); expect(metrics.missCount).toBe(1); expect(metrics.missRate).toBe(100); }); it('should track cache error', () => { const context = { id: 'cache-error-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.SET, key: 'user:789', value: { id: 789, name: 'Jane' }, hit: false, startTime: new Date(), duration: 50, error: { message: 'Connection timeout', code: 'TIMEOUT' }, }; service.trackCacheOperation(context); const metrics = service.getMetrics(); expect(metrics.totalOperations).toBe(1); expect(metrics.errorCount).toBe(1); expect(metrics.errorRate).toBe(100); }); it('should not track operation if disabled', () => { const disabledConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, enabled: false, }; const disabledService = new cache_watcher_service_1.CacheWatcherService(telescopeService, disabledConfig); const context = { id: 'disabled-cache-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'test:key', hit: true, startTime: new Date(), }; disabledService.trackCacheOperation(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should respect sampling rate', () => { const samplingConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, sampleRate: 0, }; const samplingService = new cache_watcher_service_1.CacheWatcherService(telescopeService, samplingConfig); const context = { id: 'sampled-cache-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'test:key', hit: true, startTime: new Date(), }; samplingService.trackCacheOperation(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); }); describe('filtering and exclusions', () => { it('should exclude operations based on configuration', () => { const excludeConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, excludeOperations: ['get'], }; const excludeService = new cache_watcher_service_1.CacheWatcherService(telescopeService, excludeConfig); const context = { id: 'excluded-cache-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'test:key', hit: true, startTime: new Date(), }; excludeService.trackCacheOperation(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should exclude keys based on patterns', () => { const excludeConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, excludeKeyPatterns: ['temp:*', 'debug:*'], }; const excludeService = new cache_watcher_service_1.CacheWatcherService(telescopeService, excludeConfig); const context = { id: 'excluded-key-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'temp:session:123', hit: true, startTime: new Date(), }; excludeService.trackCacheOperation(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should include only specified key patterns', () => { const includeConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, includeKeyPatterns: ['user:*'], }; const includeService = new cache_watcher_service_1.CacheWatcherService(telescopeService, includeConfig); const excludedContext = { id: 'excluded-pattern-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'session:123', hit: true, startTime: new Date(), }; includeService.trackCacheOperation(excludedContext); expect(telescopeService.record).not.toHaveBeenCalled(); const includedContext = { id: 'included-pattern-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'user:123', hit: true, startTime: new Date(), }; includeService.trackCacheOperation(includedContext); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ type: 'cache', content: expect.objectContaining({ cache: expect.objectContaining({ key: 'user:123', }), }), })); }); }); describe('data sanitization', () => { it('should sanitize sensitive keys', () => { const context = { id: 'sensitive-key-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'auth:token:abc123def456', hit: true, startTime: new Date(), }; service.trackCacheOperation(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ cache: expect.objectContaining({ key: expect.stringContaining('[HASH]'), }), }), })); }); it('should sanitize large values', () => { const largeValue = 'x'.repeat(2000); const context = { id: 'large-value-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.SET, key: 'large:data', value: largeValue, hit: false, startTime: new Date(), }; service.trackCacheOperation(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ value: expect.objectContaining({ _truncated: true, _size: expect.any(Number), }), }), })); }); it('should sanitize sensitive fields in values', () => { const sensitiveValue = { id: 123, name: 'John', password: 'secret123', token: 'abc123def456', }; const context = { id: 'sensitive-value-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.SET, key: 'user:123', value: sensitiveValue, hit: false, startTime: new Date(), }; const captureConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, captureValues: true, }; const captureService = new cache_watcher_service_1.CacheWatcherService(telescopeService, captureConfig); captureService.trackCacheOperation(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ value: expect.objectContaining({ id: 123, name: 'John', password: '[REDACTED]', token: '[REDACTED]', }), }), })); }); it('should limit key length', () => { const longKey = 'very:long:key:' + 'x'.repeat(300); const context = { id: 'long-key-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: longKey, hit: true, startTime: new Date(), }; service.trackCacheOperation(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ cache: expect.objectContaining({ key: expect.stringMatching(/.*\.\.\.$/), }), }), })); }); }); describe('key pattern analysis', () => { it('should extract and track key patterns', () => { const keys = [ 'user:123', 'user:456', 'user:789', 'session:abc123', 'session:def456', ]; keys.forEach((key, index) => { const context = { id: `pattern-${index}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key, hit: index < 3, startTime: new Date(), duration: 10, }; service.trackCacheOperation(context); }); const metrics = service.getMetrics(); expect(metrics.topKeyPatterns).toHaveLength(2); const userPattern = metrics.topKeyPatterns.find(p => p.pattern.includes('user')); expect(userPattern).toBeDefined(); expect(userPattern.count).toBe(3); expect(userPattern.hitRate).toBe(100); const sessionPattern = metrics.topKeyPatterns.find(p => p.pattern.includes('session')); expect(sessionPattern).toBeDefined(); expect(sessionPattern.count).toBe(2); expect(sessionPattern.hitRate).toBe(0); }); }); describe('metrics calculation', () => { it('should calculate hit and miss rates correctly', () => { const operations = [ { hit: true, duration: 5 }, { hit: true, duration: 8 }, { hit: false, duration: 15 }, { hit: false, duration: 20 }, { hit: true, duration: 6 }, ]; operations.forEach((op, index) => { const context = { id: `op-${index}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${index}`, hit: op.hit, startTime: new Date(), duration: op.duration, }; service.trackCacheOperation(context); }); const metrics = service.getMetrics(); expect(metrics.totalOperations).toBe(5); expect(metrics.hitCount).toBe(3); expect(metrics.missCount).toBe(2); expect(metrics.hitRate).toBe(60); expect(metrics.missRate).toBe(40); }); it('should track slow operations', () => { const slowConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, slowOperationThreshold: 50, }; const slowService = new cache_watcher_service_1.CacheWatcherService(telescopeService, slowConfig); const context = { id: 'slow-op-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'slow:key', hit: true, startTime: new Date(), duration: 100, }; slowService.trackCacheOperation(context); const metrics = slowService.getMetrics(); expect(metrics.slowOperations).toBe(1); }); it('should calculate average response time', () => { const responseTimes = [10, 20, 30, 40, 50]; responseTimes.forEach((time, index) => { const context = { id: `response-time-${index}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${index}`, hit: true, startTime: new Date(), duration: time, }; service.trackCacheOperation(context); }); const metrics = service.getMetrics(); expect(metrics.averageResponseTime).toBe(30); }); }); describe('alerting system', () => { it('should generate hit rate alerts', (done) => { const alertConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, alertThresholds: { hitRate: 80, missRate: 20, avgResponseTime: 100, errorRate: 5, memoryUsage: 80, connectionCount: 100, timeWindow: 300000, }, }; const alertService = new cache_watcher_service_1.CacheWatcherService(telescopeService, alertConfig); alertService.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('hit_rate'); expect(alert.severity).toBe('medium'); expect(alert.message).toContain('hit rate below threshold'); done(); }); for (let i = 0; i < 5; i++) { const context = { id: `low-hit-${i}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${i}`, hit: i === 0, startTime: new Date(), duration: 10, }; alertService.trackCacheOperation(context); } }); it('should generate slow operation alerts', (done) => { const alertConfig = { ...cache_watcher_config_1.defaultCacheWatcherConfig, alertThresholds: { hitRate: 0, missRate: 100, avgResponseTime: 50, errorRate: 100, memoryUsage: 100, connectionCount: 1000, timeWindow: 300000, }, }; const alertService = new cache_watcher_service_1.CacheWatcherService(telescopeService, alertConfig); alertService.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('slow_operations'); expect(alert.severity).toBe('medium'); expect(alert.message).toContain('Slow cache operation'); done(); }); const context = { id: 'slow-alert-1', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'slow:key', hit: true, startTime: new Date(), duration: 100, }; alertService.trackCacheOperation(context); }); }); describe('health calculation', () => { it('should calculate cache health correctly', () => { const goodOperations = [ { hit: true, duration: 5 }, { hit: true, duration: 8 }, { hit: true, duration: 6 }, { hit: false, duration: 15 }, ]; goodOperations.forEach((op, index) => { const context = { id: `health-${index}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${index}`, hit: op.hit, startTime: new Date(), duration: op.duration, cacheInstance: 'test-cache', }; service.trackCacheOperation(context); }); const health = service.getCacheHealth('test-cache'); expect(health).toBeDefined(); expect(Array.isArray(health)).toBe(false); const cacheHealth = health; expect(cacheHealth.instance).toBe('test-cache'); expect(cacheHealth.status).toBe('healthy'); expect(cacheHealth.score).toBeGreaterThan(70); }); it('should return all cache instances health', () => { const instances = ['cache-1', 'cache-2']; instances.forEach((instance, index) => { const context = { id: `multi-health-${index}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${index}`, hit: true, startTime: new Date(), duration: 10, cacheInstance: instance, }; service.trackCacheOperation(context); }); const allHealth = service.getCacheHealth(); expect(Array.isArray(allHealth)).toBe(true); expect(allHealth.length).toBe(2); }); }); describe('public API', () => { it('should provide metrics stream', (done) => { const metricsStream = service.getMetricsStream(); metricsStream.subscribe(metrics => { expect(metrics).toBeDefined(); expect(metrics.totalOperations).toBeGreaterThanOrEqual(0); done(); }); const context = { id: 'stream-test', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'stream:key', hit: true, startTime: new Date(), }; service.trackCacheOperation(context); }); it('should return recent operations', () => { const operations = Array.from({ length: 5 }, (_, i) => ({ id: `recent-${i}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${i}`, hit: true, startTime: new Date(), })); operations.forEach(op => service.trackCacheOperation(op)); const recent = service.getRecentOperations(3); expect(recent).toHaveLength(3); expect(recent[0].id).toBe('recent-4'); }); it('should return operations by type', () => { const operations = [ { operation: cache_watcher_service_1.CacheOperation.GET }, { operation: cache_watcher_service_1.CacheOperation.SET }, { operation: cache_watcher_service_1.CacheOperation.GET }, ]; operations.forEach((op, index) => { const context = { id: `type-${index}`, timestamp: new Date(), operation: op.operation, key: `key:${index}`, hit: true, startTime: new Date(), }; service.trackCacheOperation(context); }); const getOps = service.getOperationsByType(cache_watcher_service_1.CacheOperation.GET); expect(getOps).toHaveLength(2); expect(getOps.every(op => op.operation === cache_watcher_service_1.CacheOperation.GET)).toBe(true); }); it('should acknowledge alerts', () => { const alerts = []; service.getAlertsStream().subscribe(alert => alerts.push(alert)); const context = { id: 'alert-test', timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: 'alert:key', hit: true, startTime: new Date(), duration: 200, }; service.trackCacheOperation(context); setTimeout(() => { if (alerts.length > 0) { const alert = alerts[0]; const acknowledged = service.acknowledgeAlert(alert.id); expect(acknowledged).toBe(true); expect(alert.acknowledged).toBe(true); } }, 100); }); }); describe('cleanup and resource management', () => { it('should cleanup on destroy', () => { const destroySpy = jest.spyOn(service.destroy$, 'next'); const completeSpy = jest.spyOn(service.destroy$, 'complete'); service.onModuleDestroy(); expect(destroySpy).toHaveBeenCalled(); expect(completeSpy).toHaveBeenCalled(); }); it('should limit history size', () => { const maxSize = service.maxHistorySize; for (let i = 0; i < maxSize + 100; i++) { const context = { id: `cleanup-${i}`, timestamp: new Date(), operation: cache_watcher_service_1.CacheOperation.GET, key: `key:${i}`, hit: true, startTime: new Date(), }; service.trackCacheOperation(context); } const history = service.cacheHistory; expect(history.length).toBeLessThanOrEqual(maxSize); }); }); }); //# sourceMappingURL=cache-watcher.service.spec.js.map