UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

450 lines 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const query_watcher_service_1 = require("./query-watcher.service"); const telescope_service_1 = require("../../core/services/telescope.service"); describe('QueryWatcherService', () => { let service; let telescopeService; beforeEach(async () => { const mockTelescopeService = { record: jest.fn(), }; const mockConfig = { enabled: true, priority: 1, tags: ['query', 'database'], dependencies: ['typeorm'], slowQueryThreshold: 1000, verySlowQueryThreshold: 5000, enableStackTrace: true, enableQueryAnalysis: true, enableOptimizationHints: true, maxQueryLength: 10000, excludeQueries: ['SELECT 1', 'SHOW TABLES'], sampleRate: 100, connectionPoolMonitoring: true }; const module = await testing_1.Test.createTestingModule({ providers: [ query_watcher_service_1.QueryWatcherService, { provide: telescope_service_1.TelescopeService, useValue: mockTelescopeService, }, { provide: 'QUERY_WATCHER_CONFIG', useValue: mockConfig, }, ], }).compile(); service = module.get(query_watcher_service_1.QueryWatcherService); telescopeService = module.get(telescope_service_1.TelescopeService); }); afterEach(() => { jest.clearAllMocks(); service.resetMetrics(); }); describe('initialization', () => { it('should be defined', () => { expect(service).toBeDefined(); }); it('should initialize with correct configuration', () => { const config = service.getConfig(); expect(config.enabled).toBe(true); expect(config.slowQueryThreshold).toBe(1000); expect(config.verySlowQueryThreshold).toBe(5000); expect(config.enableStackTrace).toBe(true); expect(config.excludeQueries).toContain('SELECT 1'); }); it('should initialize with empty metrics', () => { const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(0); expect(metrics.slowQueries).toBe(0); expect(metrics.errorQueries).toBe(0); expect(metrics.averageQueryTime).toBe(0); }); }); describe('query tracking', () => { it('should track fast query', () => { const context = { id: 'query-1', sql: 'SELECT * FROM users WHERE id = $1', parameters: [1], startTime: Date.now(), endTime: Date.now() + 100, duration: 100, operation: 'select', tableName: 'users', resultCount: 1 }; service.trackQuery(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ type: 'query', content: expect.objectContaining({ query: expect.objectContaining({ sql: 'SELECT * FROM users WHERE id = $1', duration: 100, operation: 'select', tableName: 'users', resultCount: 1 }), performance: expect.objectContaining({ duration: 100, slow: false, verySlow: false }) }) })); const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); expect(metrics.slowQueries).toBe(0); }); it('should track slow query', () => { const context = { id: 'query-2', sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), endTime: Date.now() + 2000, duration: 2000, operation: 'select', tableName: 'users', resultCount: 100 }; service.trackQuery(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ type: 'query', content: expect.objectContaining({ performance: expect.objectContaining({ slow: true, verySlow: false }) }), tags: expect.arrayContaining(['query', 'slow']) })); const metrics = service.getMetrics(); expect(metrics.slowQueries).toBeGreaterThan(0); }); it('should track very slow query', () => { const context = { id: 'query-3', sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), endTime: Date.now() + 6000, duration: 6000, operation: 'select', tableName: 'users', resultCount: 1000 }; service.trackQuery(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ performance: expect.objectContaining({ slow: true, verySlow: true }) }), tags: expect.arrayContaining(['very-slow']) })); const metrics = service.getMetrics(); expect(metrics.verySlowQueries).toBe(1); }); it('should track query with error', () => { const error = new Error('Connection lost'); const context = { id: 'query-4', sql: 'SELECT * FROM nonexistent_table', parameters: [], startTime: Date.now(), endTime: Date.now() + 500, duration: 500, operation: 'select', error }; service.trackQuery(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ error: expect.objectContaining({ message: 'Connection lost', name: 'Error' }) }), tags: expect.arrayContaining(['error']) })); const metrics = service.getMetrics(); expect(metrics.errorQueries).toBe(1); }); it('should not track when disabled', async () => { const disabledConfig = { enabled: false, priority: 1, tags: ['query'], dependencies: ['typeorm'], slowQueryThreshold: 1000, verySlowQueryThreshold: 5000, enableStackTrace: true, enableQueryAnalysis: true, enableOptimizationHints: true, maxQueryLength: 10000, excludeQueries: ['SELECT 1'], sampleRate: 100, connectionPoolMonitoring: true }; const module = await testing_1.Test.createTestingModule({ providers: [ query_watcher_service_1.QueryWatcherService, { provide: telescope_service_1.TelescopeService, useValue: { record: jest.fn() }, }, { provide: 'QUERY_WATCHER_CONFIG', useValue: disabledConfig, }, ], }).compile(); const disabledService = module.get(query_watcher_service_1.QueryWatcherService); const context = { id: 'query-5', sql: 'SELECT 1', parameters: [], startTime: Date.now(), operation: 'select' }; disabledService.trackQuery(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); }); describe('query exclusion', () => { it('should exclude configured queries', () => { const context = { id: 'query-6', sql: 'SELECT 1', parameters: [], startTime: Date.now(), operation: 'select' }; service.trackQuery(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should exclude SHOW TABLES queries', () => { const context = { id: 'query-7', sql: 'SHOW TABLES', parameters: [], startTime: Date.now(), operation: 'raw' }; service.trackQuery(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); }); describe('N+1 query detection', () => { it('should detect N+1 query pattern', () => { const baseTime = Date.now(); const traceId = 'trace-123'; for (let i = 0; i < 5; i++) { const context = { id: `query-n-${i}`, sql: 'SELECT * FROM posts WHERE user_id = $1', parameters: [i + 1], startTime: baseTime + i * 100, endTime: baseTime + i * 100 + 50, duration: 50, operation: 'select', entityName: 'Post', tableName: 'posts', traceId }; service.trackQuery(context); } expect(telescopeService.record).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'query', familyHash: 'n_plus_one:Post', content: expect.objectContaining({ nPlusOne: expect.objectContaining({ queryCount: 5, entity: 'Post', sql: 'SELECT * FROM posts WHERE user_id = $1' }) }), tags: expect.arrayContaining(['n-plus-one', 'performance-issue']) })); }); }); describe('metrics collection', () => { beforeEach(() => { service.resetMetrics(); }); it('should collect basic metrics', () => { const queries = [ { duration: 100, operation: 'select', tableName: 'users' }, { duration: 200, operation: 'insert', tableName: 'users' }, { duration: 1500, operation: 'select', tableName: 'posts' }, { duration: 50, operation: 'update', tableName: 'users' }, { duration: 6000, operation: 'select', tableName: 'posts' }, ]; queries.forEach((q, i) => { const context = { id: `query-${i}`, sql: `SELECT * FROM ${q.tableName}`, parameters: [], startTime: Date.now(), endTime: Date.now() + q.duration, duration: q.duration, operation: q.operation, tableName: q.tableName }; service.trackQuery(context); }); const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(5); expect(metrics.slowQueries).toBe(2); expect(metrics.averageQueryTime).toBe((100 + 200 + 1500 + 50 + 6000) / 5); }); it('should track operation distribution', () => { const operations = ['select', 'insert', 'update', 'delete', 'select']; operations.forEach((op, i) => { const context = { id: `query-${i}`, sql: `${op.toUpperCase()} statement`, parameters: [], startTime: Date.now(), duration: 100, operation: op }; service.trackQuery(context); }); const metrics = service.getMetrics(); expect(metrics.operationDistribution.select).toBe(2); expect(metrics.operationDistribution.insert).toBe(1); expect(metrics.operationDistribution.update).toBe(1); expect(metrics.operationDistribution.delete).toBe(1); }); it('should update top slow queries', () => { const slowQueries = [ { sql: 'SELECT * FROM users', duration: 2000 }, { sql: 'SELECT * FROM posts', duration: 3000 }, { sql: 'SELECT * FROM users', duration: 1800 }, { sql: 'SELECT * FROM comments', duration: 1200 }, ]; slowQueries.forEach((q, i) => { const context = { id: `query-${i}`, sql: q.sql, parameters: [], startTime: Date.now(), duration: q.duration, operation: 'select' }; service.trackQuery(context); }); const metrics = service.getMetrics(); expect(metrics.topSlowQueries.length).toBe(3); expect(metrics.topSlowQueries[0].sql).toBe('SELECT * FROM posts'); expect(metrics.topSlowQueries[1].count).toBe(2); }); }); describe('query analysis', () => { it('should analyze query for performance issues', () => { const context = { id: 'query-analyze', sql: 'SELECT * FROM users WHERE UPPER(name) = $1', parameters: ['JOHN'], startTime: Date.now(), duration: 2000, operation: 'select', tableName: 'users' }; service.trackQuery(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ query: expect.objectContaining({ sql: 'SELECT * FROM users WHERE UPPER(name) = $1', duration: 2000 }) }) })); }); it('should detect SELECT * usage', () => { const context = { id: 'query-select-star', sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), duration: 1500, operation: 'select', tableName: 'users' }; service.trackQuery(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ content: expect.objectContaining({ query: expect.objectContaining({ sql: 'SELECT * FROM users', duration: 1500 }) }) })); }); }); describe('utility methods', () => { it('should return recent queries', () => { for (let i = 0; i < 3; i++) { const context = { id: `query-${i}`, sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), operation: 'select' }; service.trackQuery(context); } const recentQueries = service.getRecentQueries(2); expect(recentQueries).toHaveLength(2); expect(recentQueries[0].id).toBe('query-2'); }); it('should return slow queries', () => { const queries = [ { id: 'fast-1', duration: 100 }, { id: 'slow-1', duration: 1500 }, { id: 'fast-2', duration: 200 }, { id: 'slow-2', duration: 2000 }, ]; queries.forEach(q => { const context = { id: q.id, sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), duration: q.duration, operation: 'select' }; service.trackQuery(context); }); const slowQueries = service.getSlowQueries(); expect(slowQueries).toHaveLength(2); expect(slowQueries[0].id).toBe('slow-2'); expect(slowQueries[1].id).toBe('slow-1'); }); it('should reset metrics', () => { const context = { id: 'query-reset', sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), operation: 'select' }; service.trackQuery(context); let metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); service.resetMetrics(); metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(0); expect(metrics.averageQueryTime).toBe(0); expect(service.getRecentQueries()).toHaveLength(0); }); }); }); //# sourceMappingURL=query-watcher.service.spec.js.map