@ahmedhegazee/nestjs-telescope
Version:
Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling
388 lines • 15.5 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var QueryWatcherService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryWatcherService = void 0;
const common_1 = require("@nestjs/common");
const telescope_service_1 = require("../../core/services/telescope.service");
const query_watcher_config_1 = require("./query-watcher.config");
let QueryWatcherService = QueryWatcherService_1 = class QueryWatcherService {
constructor(telescopeService, queryWatcherConfig) {
this.telescopeService = telescopeService;
this.logger = new common_1.Logger(QueryWatcherService_1.name);
this.queryHistory = [];
this.maxHistorySize = 1000;
this.recentQueries = new Map();
this.config = { ...query_watcher_config_1.defaultQueryWatcherConfig, ...queryWatcherConfig };
this.queryMetrics = this.initializeMetrics();
}
initializeMetrics() {
return {
totalQueries: 0,
slowQueries: 0,
verySlowQueries: 0,
errorQueries: 0,
averageQueryTime: 0,
queriesPerSecond: 0,
queryTimeDistribution: {
fast: 0,
normal: 0,
slow: 0,
verySlow: 0
},
operationDistribution: {
select: 0,
insert: 0,
update: 0,
delete: 0,
raw: 0
},
topSlowQueries: []
};
}
trackQuery(context) {
if (!this.config.enabled) {
return;
}
try {
if (this.shouldExcludeQuery(context.sql)) {
return;
}
if (Math.random() * 100 > this.config.sampleRate) {
return;
}
this.addToHistory(context);
this.updateMetrics(context);
const entry = this.createTelescopeEntry(context);
this.telescopeService.record(entry);
if (context.duration && context.duration > this.config.slowQueryThreshold) {
this.analyzeSlowQuery(context);
}
this.trackForNPlusOneDetection(context);
}
catch (error) {
this.logger.error('Failed to track query:', error);
}
}
shouldExcludeQuery(sql) {
const normalizedSql = sql.trim().toUpperCase();
return this.config.excludeQueries.some(excludePattern => normalizedSql.startsWith(excludePattern.toUpperCase()));
}
addToHistory(context) {
this.queryHistory.push(context);
if (this.queryHistory.length > this.maxHistorySize) {
this.queryHistory.shift();
}
}
updateMetrics(context) {
this.queryMetrics.totalQueries++;
if (context.error) {
this.queryMetrics.errorQueries++;
}
if (context.duration) {
this.queryMetrics.averageQueryTime =
((this.queryMetrics.averageQueryTime * (this.queryMetrics.totalQueries - 1)) + context.duration) /
this.queryMetrics.totalQueries;
if (context.duration < 50) {
this.queryMetrics.queryTimeDistribution.fast++;
}
else if (context.duration < 500) {
this.queryMetrics.queryTimeDistribution.normal++;
}
else if (context.duration < 2000) {
this.queryMetrics.queryTimeDistribution.slow++;
this.queryMetrics.slowQueries++;
}
else {
this.queryMetrics.queryTimeDistribution.verySlow++;
this.queryMetrics.verySlowQueries++;
}
this.updateTopSlowQueries(context);
}
this.queryMetrics.operationDistribution[context.operation]++;
this.updateQueriesPerSecond();
}
updateTopSlowQueries(context) {
if (!context.duration || context.duration < this.config.slowQueryThreshold) {
return;
}
const existingQuery = this.queryMetrics.topSlowQueries.find(q => q.sql === context.sql);
if (existingQuery) {
existingQuery.count++;
existingQuery.lastExecuted = new Date();
if (context.duration > existingQuery.duration) {
existingQuery.duration = context.duration;
}
}
else {
this.queryMetrics.topSlowQueries.push({
sql: context.sql,
duration: context.duration,
count: 1,
lastExecuted: new Date()
});
}
this.queryMetrics.topSlowQueries.sort((a, b) => b.duration - a.duration);
if (this.queryMetrics.topSlowQueries.length > 10) {
this.queryMetrics.topSlowQueries = this.queryMetrics.topSlowQueries.slice(0, 10);
}
}
updateQueriesPerSecond() {
const now = Date.now();
const oneMinuteAgo = now - 60000;
const recentQueries = this.queryHistory.filter(q => q.startTime > oneMinuteAgo);
this.queryMetrics.queriesPerSecond = recentQueries.length / 60;
}
createTelescopeEntry(context) {
const entryId = `query_${context.id}`;
const familyHash = this.generateFamilyHash(context);
return {
id: entryId,
type: 'query',
familyHash,
content: {
query: {
id: context.id,
sql: this.truncateQuery(context.sql),
parameters: context.parameters,
duration: context.duration,
operation: context.operation,
entityName: context.entityName,
tableName: context.tableName,
affectedRows: context.affectedRows,
resultCount: context.resultCount,
connectionId: context.connectionId,
traceId: context.traceId,
timestamp: new Date(context.startTime).toISOString()
},
performance: {
duration: context.duration,
slow: context.duration ? context.duration > this.config.slowQueryThreshold : false,
verySlow: context.duration ? context.duration > this.config.verySlowQueryThreshold : false
},
error: context.error ? {
message: context.error.message,
stack: context.error.stack,
name: context.error.name
} : null,
stack: this.config.enableStackTrace ? context.stack : null,
analysis: context.duration && context.duration > this.config.slowQueryThreshold ?
this.getQueryAnalysis(context) : null
},
tags: this.generateTags(context),
timestamp: new Date(context.startTime),
sequence: context.startTime
};
}
generateFamilyHash(context) {
const normalizedSql = this.normalizeQuery(context.sql);
return `${context.operation}:${normalizedSql}`;
}
normalizeQuery(sql) {
return sql
.replace(/\$\d+/g, '?')
.replace(/\?/g, '?')
.replace(/\s+/g, ' ')
.replace(/\d+/g, 'N')
.replace(/'[^']*'/g, "'?'")
.trim()
.substring(0, 100);
}
truncateQuery(sql) {
if (sql.length <= this.config.maxQueryLength) {
return sql;
}
return sql.substring(0, this.config.maxQueryLength) + '... [truncated]';
}
generateTags(context) {
const tags = ['query', `operation:${context.operation}`];
if (context.entityName) {
tags.push(`entity:${context.entityName}`);
}
if (context.tableName) {
tags.push(`table:${context.tableName}`);
}
if (context.duration) {
if (context.duration > this.config.verySlowQueryThreshold) {
tags.push('very-slow');
}
else if (context.duration > this.config.slowQueryThreshold) {
tags.push('slow');
}
else if (context.duration < 50) {
tags.push('fast');
}
}
if (context.error) {
tags.push('error');
}
if (context.userId) {
tags.push('user-query');
}
return tags;
}
analyzeSlowQuery(context) {
if (!this.config.enableQueryAnalysis || !context.duration) {
return;
}
try {
const analysis = this.performQueryAnalysis(context);
if (analysis.issues.length > 0) {
this.logger.warn(`Slow query detected: ${context.id}`, {
sql: context.sql,
duration: context.duration,
issues: analysis.issues.map(i => i.description)
});
}
}
catch (error) {
this.logger.error('Failed to analyze slow query:', error);
}
}
performQueryAnalysis(context) {
const issues = [];
const optimizationHints = [];
const sql = context.sql.toLowerCase();
if (sql.includes('select') && !sql.includes('where') && !sql.includes('limit')) {
issues.push({
type: 'full_table_scan',
severity: 'high',
description: 'Query may be performing a full table scan',
suggestion: 'Add appropriate WHERE clause or LIMIT to reduce scanned rows',
affectedTable: context.tableName
});
}
const joinCount = (sql.match(/join/g) || []).length;
if (joinCount > 5) {
issues.push({
type: 'excessive_joins',
severity: 'medium',
description: `Query has ${joinCount} joins which may impact performance`,
suggestion: 'Consider denormalizing data or using separate queries',
affectedTable: context.tableName
});
}
if (sql.includes('(select') || sql.includes('exists (')) {
issues.push({
type: 'subquery_performance',
severity: 'medium',
description: 'Query contains subqueries that may be optimized',
suggestion: 'Consider rewriting subqueries as JOINs or using CTEs',
affectedTable: context.tableName
});
}
if (issues.length > 0) {
optimizationHints.push({
type: 'index_suggestion',
priority: 'high',
description: 'Consider adding database indexes',
implementation: 'Analyze query execution plan and add indexes on filtered columns',
estimatedImpact: 'Could reduce query time by 50-90%'
});
}
return {
queryId: context.id,
sql: context.sql,
duration: context.duration,
severity: context.duration > this.config.verySlowQueryThreshold ? 'critical' : 'slow',
issues,
optimizationHints,
affectedRows: context.affectedRows || 0
};
}
trackForNPlusOneDetection(context) {
if (context.operation !== 'select' || !context.entityName) {
return;
}
const key = `${context.entityName}_${context.traceId}`;
if (!this.recentQueries.has(key)) {
this.recentQueries.set(key, []);
}
const queries = this.recentQueries.get(key);
queries.push(context);
const tenSecondsAgo = Date.now() - 10000;
const recentQueries = queries.filter(q => q.startTime > tenSecondsAgo);
this.recentQueries.set(key, recentQueries);
if (recentQueries.length > 3) {
this.detectNPlusOnePattern(key, recentQueries);
}
}
detectNPlusOnePattern(key, queries) {
const normalizedQueries = queries.map(q => this.normalizeQuery(q.sql));
const uniqueQueries = new Set(normalizedQueries);
if (uniqueQueries.size === 1 && queries.length > 3) {
this.logger.warn(`N+1 query pattern detected: ${key}`, {
queryCount: queries.length,
sql: queries[0].sql,
entity: queries[0].entityName
});
this.createNPlusOneEntry(queries);
}
}
createNPlusOneEntry(queries) {
const firstQuery = queries[0];
const entry = {
id: `n_plus_one_${firstQuery.id}`,
type: 'query',
familyHash: `n_plus_one:${firstQuery.entityName}`,
content: {
nPlusOne: {
queryCount: queries.length,
entity: firstQuery.entityName,
sql: firstQuery.sql,
totalDuration: queries.reduce((sum, q) => sum + (q.duration || 0), 0),
queries: queries.map(q => ({
id: q.id,
duration: q.duration,
parameters: q.parameters
}))
}
},
tags: ['query', 'n-plus-one', 'performance-issue', `entity:${firstQuery.entityName}`],
timestamp: new Date(firstQuery.startTime),
sequence: firstQuery.startTime
};
this.telescopeService.record(entry);
}
getQueryAnalysis(context) {
return this.performQueryAnalysis(context);
}
getMetrics() {
return { ...this.queryMetrics };
}
getConfig() {
return { ...this.config };
}
getRecentQueries(limit = 50) {
return this.queryHistory.slice(-limit);
}
getSlowQueries(limit = 20) {
return this.queryHistory
.filter(q => q.duration && q.duration > this.config.slowQueryThreshold)
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
.slice(0, limit);
}
resetMetrics() {
Object.assign(this.queryMetrics, this.initializeMetrics());
this.queryHistory.length = 0;
this.recentQueries.clear();
}
};
exports.QueryWatcherService = QueryWatcherService;
exports.QueryWatcherService = QueryWatcherService = QueryWatcherService_1 = __decorate([
(0, common_1.Injectable)(),
__param(1, (0, common_1.Inject)('QUERY_WATCHER_CONFIG')),
__metadata("design:paramtypes", [telescope_service_1.TelescopeService, Object])
], QueryWatcherService);
//# sourceMappingURL=query-watcher.service.js.map