@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
JavaScript
"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