UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

377 lines 15.1 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 QueryMetricsService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryMetricsService = void 0; const common_1 = require("@nestjs/common"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); let QueryMetricsService = QueryMetricsService_1 = class QueryMetricsService { constructor() { this.logger = new common_1.Logger(QueryMetricsService_1.name); this.destroy$ = new rxjs_1.Subject(); this.dataSubject = new rxjs_1.Subject(); this.queryHistory = []; this.queryTimes = []; this.maxHistorySize = 10000; this.maxTimesSamples = 1000; this.currentMetrics = { totalQueries: 0, slowQueries: 0, verySlowQueries: 0, errorQueries: 0, averageQueryTime: 0, medianQueryTime: 0, p95QueryTime: 0, p99QueryTime: 0, queriesPerSecond: 0, queryTimePercentiles: { p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }, operationBreakdown: { select: this.initializeOperationMetrics(), insert: this.initializeOperationMetrics(), update: this.initializeOperationMetrics(), delete: this.initializeOperationMetrics(), raw: this.initializeOperationMetrics() }, tableMetrics: {}, timeWindowMetrics: { last5Minutes: this.initializeWindowMetrics(), last15Minutes: this.initializeWindowMetrics(), last30Minutes: this.initializeWindowMetrics(), last60Minutes: this.initializeWindowMetrics() }, topSlowQueries: [], nPlusOneDetections: 0 }; this.setupMetricsProcessing(); this.startPeriodicCalculations(); } initializeOperationMetrics() { return { count: 0, averageTime: 0, errorCount: 0, errorRate: 0, slowCount: 0, slowRate: 0 }; } initializeWindowMetrics() { return { queryCount: 0, averageTime: 0, slowQueryCount: 0, errorCount: 0, queriesPerSecond: 0 }; } setupMetricsProcessing() { this.dataSubject .pipe((0, operators_1.scan)((metrics, dataPoint) => this.updateMetrics(metrics, dataPoint), this.currentMetrics), (0, operators_1.shareReplay)(1), (0, operators_1.takeUntil)(this.destroy$)) .subscribe(metrics => { this.currentMetrics = metrics; }); } startPeriodicCalculations() { (0, rxjs_1.interval)(30000) .pipe((0, operators_1.takeUntil)(this.destroy$)) .subscribe(() => { this.updateTimeWindowMetrics(); this.updateQueriesPerSecond(); this.updatePercentiles(); this.cleanupOldData(); }); } recordQuery(context) { const dataPoint = { context, timestamp: new Date() }; this.queryHistory.push(dataPoint); if (this.queryHistory.length > this.maxHistorySize) { this.queryHistory.shift(); } if (context.duration) { this.queryTimes.push(context.duration); if (this.queryTimes.length > this.maxTimesSamples) { this.queryTimes.shift(); } } this.dataSubject.next(dataPoint); } updateMetrics(current, dataPoint) { const { context } = dataPoint; const updated = { ...current }; updated.totalQueries++; if (context.error) { updated.errorQueries++; } if (context.duration) { updated.averageQueryTime = this.calculateNewAverage(current.averageQueryTime, context.duration, updated.totalQueries); if (context.duration > 5000) { updated.verySlowQueries++; } else if (context.duration > 1000) { updated.slowQueries++; } } updated.operationBreakdown = this.updateOperationMetrics(updated.operationBreakdown, context); if (context.tableName) { updated.tableMetrics = this.updateTableMetrics(updated.tableMetrics, context); } if (context.duration && context.duration > 1000) { updated.topSlowQueries = this.updateTopSlowQueries(updated.topSlowQueries, context); } return updated; } calculateNewAverage(currentAverage, newValue, totalCount) { return ((currentAverage * (totalCount - 1)) + newValue) / totalCount; } updateOperationMetrics(current, context) { const updated = { ...current }; const operation = context.operation; const metrics = { ...updated[operation] }; metrics.count++; if (context.error) { metrics.errorCount++; } if (context.duration) { metrics.averageTime = this.calculateNewAverage(metrics.averageTime, context.duration, metrics.count); if (context.duration > 1000) { metrics.slowCount++; } } metrics.errorRate = (metrics.errorCount / metrics.count) * 100; metrics.slowRate = (metrics.slowCount / metrics.count) * 100; updated[operation] = metrics; return updated; } updateTableMetrics(current, context) { const updated = { ...current }; const tableName = context.tableName; if (!updated[tableName]) { updated[tableName] = { queryCount: 0, averageQueryTime: 0, slowQueryCount: 0, errorCount: 0, mostCommonOperations: [], lastAccessed: new Date() }; } const metrics = updated[tableName]; metrics.queryCount++; metrics.lastAccessed = new Date(); if (context.error) { metrics.errorCount++; } if (context.duration) { metrics.averageQueryTime = this.calculateNewAverage(metrics.averageQueryTime, context.duration, metrics.queryCount); if (context.duration > 1000) { metrics.slowQueryCount++; } } const existingOp = metrics.mostCommonOperations.find(op => op.operation === context.operation); if (existingOp) { existingOp.count++; existingOp.averageTime = this.calculateNewAverage(existingOp.averageTime, context.duration || 0, existingOp.count); } else { metrics.mostCommonOperations.push({ operation: context.operation, count: 1, averageTime: context.duration || 0 }); } metrics.mostCommonOperations.sort((a, b) => b.count - a.count); metrics.mostCommonOperations = metrics.mostCommonOperations.slice(0, 5); return updated; } updateTopSlowQueries(current, context) { const updated = [...current]; const sql = this.normalizeQuery(context.sql); const existingQuery = updated.find(q => this.normalizeQuery(q.sql) === sql); if (existingQuery) { existingQuery.count++; existingQuery.lastExecuted = new Date(); existingQuery.averageTime = this.calculateNewAverage(existingQuery.averageTime, context.duration || 0, existingQuery.count); } else { updated.push({ sql: context.sql, averageTime: context.duration || 0, count: 1, lastExecuted: new Date(), tableName: context.tableName, operation: context.operation }); } updated.sort((a, b) => b.averageTime - a.averageTime); return updated.slice(0, 20); } normalizeQuery(sql) { return sql .replace(/\$\d+/g, '?') .replace(/\?/g, '?') .replace(/\s+/g, ' ') .replace(/\d+/g, 'N') .replace(/'[^']*'/g, "'?'") .trim() .substring(0, 200); } updateTimeWindowMetrics() { const now = Date.now(); const windows = [ { key: 'last5Minutes', milliseconds: 5 * 60 * 1000 }, { key: 'last15Minutes', milliseconds: 15 * 60 * 1000 }, { key: 'last30Minutes', milliseconds: 30 * 60 * 1000 }, { key: 'last60Minutes', milliseconds: 60 * 60 * 1000 } ]; for (const window of windows) { const cutoff = now - window.milliseconds; const windowQueries = this.queryHistory.filter(dataPoint => dataPoint.timestamp.getTime() > cutoff); const windowMetrics = { queryCount: windowQueries.length, averageTime: 0, slowQueryCount: 0, errorCount: 0, queriesPerSecond: windowQueries.length / (window.milliseconds / 1000) }; if (windowQueries.length > 0) { const totalTime = windowQueries.reduce((sum, dp) => sum + (dp.context.duration || 0), 0); windowMetrics.averageTime = totalTime / windowQueries.length; windowMetrics.slowQueryCount = windowQueries.filter(dp => dp.context.duration && dp.context.duration > 1000).length; windowMetrics.errorCount = windowQueries.filter(dp => dp.context.error).length; } this.currentMetrics.timeWindowMetrics[window.key] = windowMetrics; } } updateQueriesPerSecond() { const now = Date.now(); const oneMinuteAgo = now - 60000; const recentQueries = this.queryHistory.filter(dataPoint => dataPoint.timestamp.getTime() > oneMinuteAgo); this.currentMetrics.queriesPerSecond = recentQueries.length / 60; } updatePercentiles() { if (this.queryTimes.length === 0) return; const sortedTimes = [...this.queryTimes].sort((a, b) => a - b); this.currentMetrics.medianQueryTime = this.calculatePercentile(sortedTimes, 50); this.currentMetrics.p95QueryTime = this.calculatePercentile(sortedTimes, 95); this.currentMetrics.p99QueryTime = this.calculatePercentile(sortedTimes, 99); this.currentMetrics.queryTimePercentiles = { p50: this.calculatePercentile(sortedTimes, 50), p75: this.calculatePercentile(sortedTimes, 75), p90: this.calculatePercentile(sortedTimes, 90), p95: this.calculatePercentile(sortedTimes, 95), p99: this.calculatePercentile(sortedTimes, 99) }; } calculatePercentile(sortedArray, percentile) { const index = Math.ceil((percentile / 100) * sortedArray.length) - 1; return sortedArray[Math.max(0, index)] || 0; } cleanupOldData() { const now = Date.now(); const twoHoursAgo = now - 2 * 60 * 60 * 1000; while (this.queryHistory.length > 0 && this.queryHistory[0].timestamp.getTime() < twoHoursAgo) { this.queryHistory.shift(); } } recordNPlusOneDetection() { this.currentMetrics.nPlusOneDetections++; } updateConnectionPoolMetrics(connectionMetrics) { this.currentMetrics.connectionPoolMetrics = connectionMetrics; } getMetrics() { return { ...this.currentMetrics }; } getMetricsStream() { return this.dataSubject.pipe((0, operators_1.scan)((metrics, dataPoint) => this.updateMetrics(metrics, dataPoint), this.currentMetrics), (0, operators_1.shareReplay)(1)); } getSlowQueries(limit = 10) { return this.currentMetrics.topSlowQueries.slice(0, limit); } getTableMetrics(tableName) { return this.currentMetrics.tableMetrics[tableName]; } getTopTables(limit = 10) { return Object.entries(this.currentMetrics.tableMetrics) .map(([tableName, metrics]) => ({ tableName, metrics })) .sort((a, b) => b.metrics.queryCount - a.metrics.queryCount) .slice(0, limit); } getSlowestTables(limit = 10) { return Object.entries(this.currentMetrics.tableMetrics) .map(([tableName, metrics]) => ({ tableName, metrics })) .sort((a, b) => b.metrics.averageQueryTime - a.metrics.averageQueryTime) .slice(0, limit); } getRecentQueries(limit = 100) { return this.queryHistory.slice(-limit).reverse(); } reset() { this.currentMetrics = { totalQueries: 0, slowQueries: 0, verySlowQueries: 0, errorQueries: 0, averageQueryTime: 0, medianQueryTime: 0, p95QueryTime: 0, p99QueryTime: 0, queriesPerSecond: 0, queryTimePercentiles: { p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }, operationBreakdown: { select: this.initializeOperationMetrics(), insert: this.initializeOperationMetrics(), update: this.initializeOperationMetrics(), delete: this.initializeOperationMetrics(), raw: this.initializeOperationMetrics() }, tableMetrics: {}, timeWindowMetrics: { last5Minutes: this.initializeWindowMetrics(), last15Minutes: this.initializeWindowMetrics(), last30Minutes: this.initializeWindowMetrics(), last60Minutes: this.initializeWindowMetrics() }, topSlowQueries: [], nPlusOneDetections: 0 }; this.queryHistory.length = 0; this.queryTimes.length = 0; } destroy() { this.destroy$.next(); this.destroy$.complete(); } }; exports.QueryMetricsService = QueryMetricsService; exports.QueryMetricsService = QueryMetricsService = QueryMetricsService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", []) ], QueryMetricsService); //# sourceMappingURL=query-metrics.service.js.map