UNPKG

@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
"use strict"; 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