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