UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

400 lines 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const query_metrics_service_1 = require("./query-metrics.service"); describe('QueryMetricsService', () => { let service; beforeEach(async () => { const module = await testing_1.Test.createTestingModule({ providers: [query_metrics_service_1.QueryMetricsService], }).compile(); service = module.get(query_metrics_service_1.QueryMetricsService); }); afterEach(() => { service.reset(); service.destroy(); }); describe('initialization', () => { it('should be defined', () => { expect(service).toBeDefined(); }); it('should initialize with empty metrics', () => { const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(0); expect(metrics.slowQueries).toBe(0); expect(metrics.averageQueryTime).toBe(0); expect(metrics.topSlowQueries).toHaveLength(0); }); }); describe('query recording', () => { it('should record basic query metrics', () => { const context = { id: 'query-1', sql: 'SELECT * FROM users WHERE id = $1', parameters: [1], startTime: Date.now(), duration: 150, operation: 'select', tableName: 'users' }; service.recordQuery(context); const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); expect(metrics.slowQueries).toBe(0); expect(metrics.averageQueryTime).toBe(150); expect(metrics.operationBreakdown.select.count).toBe(1); expect(metrics.operationBreakdown.select.averageTime).toBe(150); }); it('should record slow query metrics', () => { const context = { id: 'slow-query', sql: 'SELECT * FROM large_table', parameters: [], startTime: Date.now(), duration: 2000, operation: 'select', tableName: 'large_table' }; service.recordQuery(context); const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); expect(metrics.slowQueries).toBe(1); expect(metrics.topSlowQueries).toHaveLength(1); expect(metrics.topSlowQueries[0].sql).toBe('SELECT * FROM large_table'); }); it('should record very slow query metrics', () => { const context = { id: 'very-slow-query', sql: 'SELECT * FROM huge_table', parameters: [], startTime: Date.now(), duration: 6000, operation: 'select', tableName: 'huge_table' }; service.recordQuery(context); const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); expect(metrics.verySlowQueries).toBe(1); }); it('should record query with error', () => { const context = { id: 'error-query', sql: 'SELECT * FROM nonexistent_table', parameters: [], startTime: Date.now(), duration: 100, operation: 'select', error: new Error('Table does not exist') }; service.recordQuery(context); const metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); expect(metrics.errorQueries).toBe(1); expect(metrics.operationBreakdown.select.errorCount).toBe(1); expect(metrics.operationBreakdown.select.errorRate).toBe(100); }); }); describe('operation breakdown', () => { it('should track different operation types', () => { const operations = [ { operation: 'select', duration: 100 }, { operation: 'insert', duration: 200 }, { operation: 'update', duration: 300 }, { operation: 'delete', duration: 400 }, { operation: 'select', duration: 150 } ]; operations.forEach((op, i) => { const context = { id: `query-${i}`, sql: `${op.operation.toUpperCase()} statement`, parameters: [], startTime: Date.now(), duration: op.duration, operation: op.operation }; service.recordQuery(context); }); const metrics = service.getMetrics(); expect(metrics.operationBreakdown.select.count).toBe(2); expect(metrics.operationBreakdown.select.averageTime).toBe(125); expect(metrics.operationBreakdown.insert.count).toBe(1); expect(metrics.operationBreakdown.update.count).toBe(1); expect(metrics.operationBreakdown.delete.count).toBe(1); }); it('should calculate operation error rates', () => { const queries = [ { operation: 'select', duration: 100, hasError: false }, { operation: 'select', duration: 200, hasError: true }, { operation: 'select', duration: 150, hasError: false } ]; queries.forEach((q, i) => { const context = { id: `query-${i}`, sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), duration: q.duration, operation: q.operation, error: q.hasError ? new Error('Query failed') : undefined }; service.recordQuery(context); }); const metrics = service.getMetrics(); expect(metrics.operationBreakdown.select.count).toBe(3); expect(metrics.operationBreakdown.select.errorCount).toBe(1); expect(metrics.operationBreakdown.select.errorRate).toBeCloseTo(33.33, 2); }); }); describe('table metrics', () => { it('should track table-specific metrics', () => { const queries = [ { tableName: 'users', duration: 100 }, { tableName: 'users', duration: 200 }, { tableName: 'posts', duration: 300 }, { tableName: 'users', duration: 1500 } ]; queries.forEach((q, i) => { const context = { id: `query-${i}`, sql: `SELECT * FROM ${q.tableName}`, parameters: [], startTime: Date.now(), duration: q.duration, operation: 'select', tableName: q.tableName }; service.recordQuery(context); }); const metrics = service.getMetrics(); expect(metrics.tableMetrics.users.queryCount).toBe(3); expect(metrics.tableMetrics.users.slowQueryCount).toBe(1); expect(metrics.tableMetrics.users.averageQueryTime).toBeCloseTo(600, 0); expect(metrics.tableMetrics.posts.queryCount).toBe(1); }); it('should track most common operations per table', () => { const queries = [ { tableName: 'users', operation: 'select', duration: 100 }, { tableName: 'users', operation: 'select', duration: 150 }, { tableName: 'users', operation: 'insert', duration: 200 }, { tableName: 'users', operation: 'update', duration: 300 } ]; queries.forEach((q, i) => { const context = { id: `query-${i}`, sql: `${q.operation.toUpperCase()} statement`, parameters: [], startTime: Date.now(), duration: q.duration, operation: q.operation, tableName: q.tableName }; service.recordQuery(context); }); const tableMetrics = service.getTableMetrics('users'); expect(tableMetrics).toBeDefined(); expect(tableMetrics.mostCommonOperations).toHaveLength(3); expect(tableMetrics.mostCommonOperations[0].operation).toBe('select'); expect(tableMetrics.mostCommonOperations[0].count).toBe(2); }); }); describe('percentile calculations', () => { it('should calculate query time percentiles', async () => { const queryTimes = [50, 100, 150, 200, 300, 500, 1000, 2000, 3000, 5000]; queryTimes.forEach((duration, i) => { const context = { id: `query-${i}`, sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), duration, operation: 'select' }; service.recordQuery(context); }); service.updatePercentiles(); const metrics = service.getMetrics(); expect(metrics.queryTimePercentiles.p50).toBeGreaterThan(0); expect(metrics.queryTimePercentiles.p95).toBeGreaterThanOrEqual(metrics.queryTimePercentiles.p50); expect(metrics.queryTimePercentiles.p99).toBeGreaterThanOrEqual(metrics.queryTimePercentiles.p95); }); }); describe('top slow queries', () => { it('should maintain top slow queries list', () => { const slowQueries = [ { sql: 'SELECT * FROM users WHERE name LIKE "%test%"', duration: 2000 }, { sql: 'SELECT * FROM posts JOIN users ON posts.user_id = users.id', duration: 3000 }, { sql: 'SELECT * FROM users WHERE name LIKE "%test%"', duration: 1800 }, { sql: 'SELECT COUNT(*) FROM large_table', duration: 4000 }, { sql: 'SELECT * FROM comments WHERE content LIKE "%search%"', duration: 1200 } ]; slowQueries.forEach((q, i) => { const context = { id: `query-${i}`, sql: q.sql, parameters: [], startTime: Date.now(), duration: q.duration, operation: 'select' }; service.recordQuery(context); }); const metrics = service.getMetrics(); const topSlowQueries = metrics.topSlowQueries; expect(topSlowQueries).toHaveLength(4); expect(topSlowQueries[0].sql).toBe('SELECT COUNT(*) FROM large_table'); const duplicateQuery = topSlowQueries.find(q => q.sql.includes('name LIKE "%test%"')); expect(duplicateQuery).toBeDefined(); expect(duplicateQuery.count).toBe(2); expect(duplicateQuery.averageTime).toBe(1900); }); it('should limit top slow queries to 20', () => { for (let i = 0; i < 25; i++) { const context = { id: `query-${i}`, sql: `SELECT * FROM table_${i}`, parameters: [], startTime: Date.now(), duration: 1000 + i * 100, operation: 'select' }; service.recordQuery(context); } const metrics = service.getMetrics(); expect(metrics.topSlowQueries.length).toBeLessThanOrEqual(20); expect(metrics.topSlowQueries.length).toBeGreaterThan(0); }); }); describe('N+1 detection tracking', () => { it('should record N+1 detections', () => { service.recordNPlusOneDetection(); service.recordNPlusOneDetection(); const metrics = service.getMetrics(); expect(metrics.nPlusOneDetections).toBe(2); }); }); describe('connection pool metrics', () => { it('should update connection pool metrics', () => { const connectionMetrics = { totalConnections: 10, activeConnections: 5, idleConnections: 5, waitingConnections: 0, acquiredConnections: 100, releasedConnections: 95, createdConnections: 10, destroyedConnections: 0, poolSize: 10, maxConnections: 20, minConnections: 5, connectionTimeouts: 0, connectionErrors: 0, averageAcquireTime: 50, averageConnectionLifetime: 300000, healthScore: 85, lastUpdate: new Date() }; service.updateConnectionPoolMetrics(connectionMetrics); const metrics = service.getMetrics(); expect(metrics.connectionPoolMetrics).toEqual(connectionMetrics); }); }); describe('metrics stream', () => { it('should provide metrics stream', (done) => { const metricsStream = service.getMetricsStream(); let updateCount = 0; const subscription = metricsStream.subscribe(metrics => { updateCount++; if (updateCount === 1) { expect(metrics.totalQueries).toBe(1); subscription.unsubscribe(); done(); } }); const context = { id: 'stream-query', sql: 'SELECT 1', parameters: [], startTime: Date.now(), duration: 100, operation: 'select' }; service.recordQuery(context); }); }); describe('utility methods', () => { beforeEach(() => { const queries = [ { sql: 'SELECT * FROM users', duration: 100, tableName: 'users' }, { sql: 'SELECT * FROM posts', duration: 200, tableName: 'posts' }, { sql: 'SELECT * FROM users', duration: 1500, tableName: 'users' }, { sql: 'SELECT * FROM comments', duration: 300, tableName: 'comments' } ]; queries.forEach((q, i) => { const context = { id: `query-${i}`, sql: q.sql, parameters: [], startTime: Date.now(), duration: q.duration, operation: 'select', tableName: q.tableName }; service.recordQuery(context); }); }); it('should return slow queries with limit', () => { const slowQueries = service.getSlowQueries(1); expect(slowQueries).toHaveLength(1); expect(slowQueries[0].sql).toBe('SELECT * FROM users'); }); it('should return table metrics', () => { const userMetrics = service.getTableMetrics('users'); expect(userMetrics).toBeDefined(); expect(userMetrics.queryCount).toBe(2); expect(userMetrics.slowQueryCount).toBe(1); }); it('should return top tables by query count', () => { const topTables = service.getTopTables(5); expect(topTables.length).toBeGreaterThan(0); expect(topTables[0].tableName).toBe('users'); expect(topTables[0].metrics.queryCount).toBe(2); }); it('should return slowest tables', () => { const slowestTables = service.getSlowestTables(1); expect(slowestTables).toHaveLength(1); expect(slowestTables[0].tableName).toBe('users'); }); it('should return recent queries', () => { const recentQueries = service.getRecentQueries(2); expect(recentQueries).toHaveLength(2); expect(recentQueries[0].context.id).toBe('query-3'); }); }); describe('reset and destroy', () => { it('should reset metrics', () => { const context = { id: 'test-query', sql: 'SELECT * FROM users', parameters: [], startTime: Date.now(), duration: 100, operation: 'select' }; service.recordQuery(context); service.recordNPlusOneDetection(); let metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(1); expect(metrics.nPlusOneDetections).toBe(1); service.reset(); metrics = service.getMetrics(); expect(metrics.totalQueries).toBe(0); expect(metrics.nPlusOneDetections).toBe(0); expect(service.getRecentQueries()).toHaveLength(0); }); it('should cleanup resources on destroy', () => { expect(() => service.destroy()).not.toThrow(); }); }); }); //# sourceMappingURL=query-metrics.service.spec.js.map