UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

781 lines 31.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const performance_correlation_service_1 = require("./performance-correlation.service"); const telescope_service_1 = require("./telescope.service"); describe('PerformanceCorrelationService', () => { let service; let telescopeService; beforeEach(async () => { const mockTelescopeService = { record: jest.fn(), getEntries: jest.fn(), clearEntries: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ performance_correlation_service_1.PerformanceCorrelationService, { provide: telescope_service_1.TelescopeService, useValue: mockTelescopeService, }, ], }).compile(); service = module.get(performance_correlation_service_1.PerformanceCorrelationService); 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.totalRequests).toBe(0); expect(metrics.averageResponseTime).toBe(0); expect(metrics.errorRate).toBe(0); }); it('should start periodic processing on module init', async () => { await service.onModuleInit(); }); }); describe('correlation tracking', () => { it('should correlate request with query data', (done) => { const traceId = 'trace-123'; service.getCorrelationStream().subscribe(correlation => { expect(correlation.traceId).toBe(traceId); expect(correlation.request).toBeDefined(); expect(correlation.query).toBeDefined(); expect(correlation.performance.totalDuration).toBe(1000); expect(correlation.performance.queryCount).toBe(1); done(); }); const requestData = { traceId, requestId: 'req-123', method: 'GET', path: '/api/users', duration: 1000, userId: 'user-123', }; service.correlateWatcherData('request', requestData); const queryData = { traceId, requestId: 'req-123', query: 'SELECT * FROM users', duration: 500, userId: 'user-123', }; service.correlateWatcherData('query', queryData); setTimeout(() => { const oldRequestData = { ...requestData, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should correlate multiple watcher types', (done) => { const traceId = 'trace-multi-456'; service.getCorrelationStream().subscribe(correlation => { expect(correlation.traceId).toBe(traceId); expect(correlation.request).toBeDefined(); expect(correlation.query).toBeDefined(); expect(correlation.cache).toBeDefined(); expect(correlation.exception).toBeDefined(); expect(correlation.performance.queryCount).toBe(2); expect(correlation.performance.cacheOperations).toBe(1); expect(correlation.performance.exceptionsThrown).toBe(1); done(); }); const baseData = { traceId, requestId: 'req-456', userId: 'user-456', }; service.correlateWatcherData('request', { ...baseData, method: 'POST', path: '/api/users', duration: 2000, }); service.correlateWatcherData('query', { ...baseData, query: 'SELECT * FROM users', duration: 800, }); service.correlateWatcherData('query', { ...baseData, query: 'INSERT INTO users...', duration: 300, }); service.correlateWatcherData('cache', { ...baseData, operation: 'get', key: 'user:456', duration: 10, }); service.correlateWatcherData('exception', { ...baseData, errorType: 'ValidationError', message: 'Invalid input', }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'POST', path: '/api/users', duration: 2000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should handle correlation without traceId', () => { const dataWithoutTrace = { requestId: 'req-no-trace', method: 'GET', path: '/api/test', duration: 500, }; expect(() => { service.correlateWatcherData('request', dataWithoutTrace); }).not.toThrow(); }); }); describe('bottleneck analysis', () => { it('should identify database bottlenecks', (done) => { const traceId = 'trace-db-bottleneck'; service.getCorrelationStream().subscribe(correlation => { expect(correlation.bottlenecks).toBeDefined(); expect(correlation.bottlenecks.length).toBeGreaterThan(0); const dbBottleneck = correlation.bottlenecks.find(b => b.type === 'query'); expect(dbBottleneck).toBeDefined(); expect(dbBottleneck.severity).toBe('high'); expect(dbBottleneck.percentage).toBeGreaterThan(70); done(); }); const baseData = { traceId, requestId: 'req-db-slow', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/reports', duration: 5000, }); service.correlateWatcherData('query', { ...baseData, query: 'SELECT * FROM large_table', duration: 4000, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/reports', duration: 5000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should identify cache bottlenecks', (done) => { const traceId = 'trace-cache-bottleneck'; service.getCorrelationStream().subscribe(correlation => { const cacheBottleneck = correlation.bottlenecks.find(b => b.type === 'cache'); expect(cacheBottleneck).toBeDefined(); expect(cacheBottleneck.severity).toBe('medium'); done(); }); const baseData = { traceId, requestId: 'req-cache-slow', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/cached-data', duration: 2000, }); service.correlateWatcherData('cache', { ...baseData, operation: 'get', key: 'slow:key', duration: 600, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/cached-data', duration: 2000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should identify exception bottlenecks', (done) => { const traceId = 'trace-exception-bottleneck'; service.getCorrelationStream().subscribe(correlation => { const exceptionBottleneck = correlation.bottlenecks.find(b => b.type === 'exception'); expect(exceptionBottleneck).toBeDefined(); expect(exceptionBottleneck.severity).toBe('high'); done(); }); const baseData = { traceId, requestId: 'req-many-exceptions', }; service.correlateWatcherData('request', { ...baseData, method: 'POST', path: '/api/process', duration: 3000, }); for (let i = 0; i < 5; i++) { service.correlateWatcherData('exception', { ...baseData, errorType: 'ValidationError', message: `Validation error ${i}`, }); } setTimeout(() => { const oldRequestData = { ...baseData, method: 'POST', path: '/api/process', duration: 3000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should identify memory bottlenecks', (done) => { const traceId = 'trace-memory-bottleneck'; service.getCorrelationStream().subscribe(correlation => { const memoryBottleneck = correlation.bottlenecks.find(b => b.type === 'memory'); expect(memoryBottleneck).toBeDefined(); expect(memoryBottleneck.severity).toBe('medium'); done(); }); const baseData = { traceId, requestId: 'req-memory-heavy', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/memory-intensive', duration: 1000, performance: { memoryUsage: 200 * 1024 * 1024, }, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/memory-intensive', duration: 1000, performance: { memoryUsage: 200 * 1024 * 1024, }, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); }); describe('recommendations generation', () => { it('should generate query optimization recommendations', (done) => { const traceId = 'trace-query-recommendations'; service.getCorrelationStream().subscribe(correlation => { expect(correlation.recommendations).toBeDefined(); expect(correlation.recommendations.length).toBeGreaterThan(0); const hasQueryRecommendation = correlation.recommendations.some(rec => rec.includes('query') || rec.includes('database')); expect(hasQueryRecommendation).toBe(true); done(); }); const baseData = { traceId, requestId: 'req-query-rec', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/data', duration: 3000, }); for (let i = 0; i < 15; i++) { service.correlateWatcherData('query', { ...baseData, query: `SELECT * FROM table${i}`, duration: 200, }); } setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/data', duration: 3000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should generate cache optimization recommendations', (done) => { const traceId = 'trace-cache-recommendations'; service.getCorrelationStream().subscribe(correlation => { const hasCacheRecommendation = correlation.recommendations.some(rec => rec.includes('cache')); expect(hasCacheRecommendation).toBe(true); done(); }); const baseData = { traceId, requestId: 'req-cache-rec', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/cached', duration: 2000, }); for (let i = 0; i < 25; i++) { service.correlateWatcherData('cache', { ...baseData, operation: 'get', key: `key:${i}`, duration: 10, }); } setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/cached', duration: 2000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should generate async processing recommendations', (done) => { const traceId = 'trace-async-recommendations'; service.getCorrelationStream().subscribe(correlation => { const hasAsyncRecommendation = correlation.recommendations.some(rec => rec.includes('async')); expect(hasAsyncRecommendation).toBe(true); done(); }); const baseData = { traceId, requestId: 'req-async-rec', }; service.correlateWatcherData('request', { ...baseData, method: 'POST', path: '/api/long-process', duration: 8000, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'POST', path: '/api/long-process', duration: 8000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); }); describe('health score calculation', () => { it('should calculate high health score for good performance', (done) => { const traceId = 'trace-good-health'; service.getCorrelationStream().subscribe(correlation => { expect(correlation.healthScore).toBeGreaterThan(80); done(); }); const baseData = { traceId, requestId: 'req-good-health', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/fast', duration: 200, }); service.correlateWatcherData('query', { ...baseData, query: 'SELECT * FROM users WHERE id = ?', duration: 50, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/fast', duration: 200, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should calculate low health score for poor performance', (done) => { const traceId = 'trace-poor-health'; service.getCorrelationStream().subscribe(correlation => { expect(correlation.healthScore).toBeLessThan(50); done(); }); const baseData = { traceId, requestId: 'req-poor-health', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/slow', duration: 15000, }); for (let i = 0; i < 5; i++) { service.correlateWatcherData('exception', { ...baseData, errorType: 'Error', message: `Error ${i}`, }); } setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/slow', duration: 15000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); }); describe('performance metrics', () => { it('should update performance metrics from correlations', () => { const traceIds = ['trace-1', 'trace-2', 'trace-3']; traceIds.forEach((traceId, index) => { const baseData = { traceId, requestId: `req-${index}`, }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: `/api/test${index}`, duration: (index + 1) * 1000, }); if (index === 2) { service.correlateWatcherData('exception', { ...baseData, errorType: 'Error', message: 'Test error', }); } }); setTimeout(() => { const metrics = service.getMetrics(); expect(metrics.totalRequests).toBe(3); expect(metrics.averageResponseTime).toBe(2000); expect(metrics.errorRate).toBe(33.33); }, 200); }); it('should calculate correlation coefficients', () => { const traceIds = ['trace-corr-1', 'trace-corr-2', 'trace-corr-3']; traceIds.forEach((traceId, index) => { const baseData = { traceId, requestId: `req-corr-${index}`, }; const queryTime = (index + 1) * 500; const responseTime = (index + 1) * 1000; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: `/api/corr${index}`, duration: responseTime, }); service.correlateWatcherData('query', { ...baseData, query: `SELECT * FROM table${index}`, duration: queryTime, }); }); setTimeout(() => { const metrics = service.getMetrics(); expect(metrics.correlations).toBeDefined(); expect(metrics.correlations.queryToResponse).toBeGreaterThan(0.5); }, 200); }); }); describe('alerting system', () => { it('should generate bottleneck alerts', (done) => { service.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('bottleneck'); expect(alert.severity).toBe('critical'); expect(alert.message).toContain('Critical performance bottleneck'); done(); }); const traceId = 'trace-alert-bottleneck'; const baseData = { traceId, requestId: 'req-alert-bottleneck', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/critical', duration: 5000, }); service.correlateWatcherData('query', { ...baseData, query: 'SELECT * FROM huge_table', duration: 4500, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/critical', duration: 5000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should generate performance degradation alerts', (done) => { service.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('degradation'); expect(alert.severity).toBe('high'); expect(alert.message).toContain('Slow response detected'); done(); }); const traceId = 'trace-alert-degradation'; const baseData = { traceId, requestId: 'req-alert-degradation', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/degraded', duration: 12000, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/degraded', duration: 12000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should generate anomaly alerts', (done) => { service.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('anomaly'); expect(alert.severity).toBe('medium'); expect(alert.message).toContain('High exception count'); done(); }); const traceId = 'trace-alert-anomaly'; const baseData = { traceId, requestId: 'req-alert-anomaly', }; service.correlateWatcherData('request', { ...baseData, method: 'POST', path: '/api/anomaly', duration: 2000, }); for (let i = 0; i < 5; i++) { service.correlateWatcherData('exception', { ...baseData, errorType: 'Error', message: `Exception ${i}`, }); } setTimeout(() => { const oldRequestData = { ...baseData, method: 'POST', path: '/api/anomaly', duration: 2000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); }); describe('public API', () => { it('should provide metrics stream', (done) => { const metricsStream = service.getMetricsStream(); metricsStream.subscribe(metrics => { expect(metrics).toBeDefined(); expect(metrics.totalRequests).toBeGreaterThanOrEqual(0); done(); }); service.correlateWatcherData('request', { traceId: 'test-stream', method: 'GET', path: '/test', duration: 1000, }); setTimeout(() => { }, 100); }); it('should return recent correlations', () => { const correlations = []; service.getCorrelationStream().subscribe(correlation => { correlations.push(correlation); }); const traceIds = ['trace-recent-1', 'trace-recent-2', 'trace-recent-3']; traceIds.forEach((traceId, index) => { service.correlateWatcherData('request', { traceId, requestId: `req-${index}`, method: 'GET', path: `/api/recent${index}`, duration: 1000, }); }); setTimeout(() => { const recent = service.getRecentCorrelations(2); expect(recent).toHaveLength(Math.min(2, correlations.length)); }, 200); }); it('should find correlation by trace ID', () => { const traceId = 'trace-find-me'; service.correlateWatcherData('request', { traceId, requestId: 'req-find-me', method: 'GET', path: '/api/findme', duration: 1000, }); setTimeout(() => { const correlation = service.getCorrelationsByTraceId(traceId); expect(correlation).toBeDefined(); expect(correlation?.traceId).toBe(traceId); }, 200); }); it('should return bottlenecks by component', () => { const traceId = 'trace-component-bottleneck'; service.getCorrelationStream().subscribe(() => { const bottlenecks = service.getBottlenecksByComponent('database'); expect(bottlenecks).toBeDefined(); expect(Array.isArray(bottlenecks)).toBe(true); }); const baseData = { traceId, requestId: 'req-component-bottleneck', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/db-heavy', duration: 3000, }); service.correlateWatcherData('query', { ...baseData, query: 'SELECT * FROM big_table', duration: 2500, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/db-heavy', duration: 3000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); }, 100); }); it('should acknowledge alerts', () => { const alerts = []; service.getAlertsStream().subscribe(alert => alerts.push(alert)); const traceId = 'trace-acknowledge-alert'; const baseData = { traceId, requestId: 'req-acknowledge-alert', }; service.correlateWatcherData('request', { ...baseData, method: 'GET', path: '/api/alert-test', duration: 15000, }); setTimeout(() => { const oldRequestData = { ...baseData, method: 'GET', path: '/api/alert-test', duration: 15000, timestamp: new Date(Date.now() - 10000), }; service.correlateWatcherData('request', oldRequestData); 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); }, 100); }); it('should return active traces', () => { const traceIds = ['trace-active-1', 'trace-active-2']; traceIds.forEach((traceId, index) => { service.correlateWatcherData('request', { traceId, requestId: `req-active-${index}`, method: 'GET', path: `/api/active${index}`, duration: 1000, }); }); const activeTraces = service.getActiveTraces(); expect(activeTraces).toBeDefined(); expect(Array.isArray(activeTraces)).toBe(true); expect(activeTraces.length).toBeGreaterThan(0); }); }); 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.onDestroy(); expect(destroySpy).toHaveBeenCalled(); expect(completeSpy).toHaveBeenCalled(); }); it('should cleanup old active traces', () => { service.correlateWatcherData('request', { traceId: 'trace-cleanup-test', requestId: 'req-cleanup-test', method: 'GET', path: '/api/cleanup', duration: 1000, }); const activeTraces = service.getActiveTraces(); expect(activeTraces.length).toBeGreaterThan(0); service.cleanupOldTraces(); const activeTracesAfter = service.getActiveTraces(); expect(activeTracesAfter.length).toBe(activeTraces.length); }); }); }); //# sourceMappingURL=performance-correlation.service.spec.js.map