@ahmedhegazee/nestjs-telescope
Version:
Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling
245 lines • 10.6 kB
JavaScript
"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 RequestMetricsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestMetricsService = void 0;
const common_1 = require("@nestjs/common");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
let RequestMetricsService = RequestMetricsService_1 = class RequestMetricsService {
constructor() {
this.logger = new common_1.Logger(RequestMetricsService_1.name);
this.dataSubject = new rxjs_1.Subject();
this.requestHistory = [];
this.maxHistorySize = 10000;
this.responseTimes = [];
this.maxResponseTimesSamples = 1000;
this.currentMetrics = {
totalRequests: 0,
requestsPerSecond: 0,
averageResponseTime: 0,
medianResponseTime: 0,
p95ResponseTime: 0,
p99ResponseTime: 0,
slowRequestCount: 0,
errorCount: 0,
errorRate: 0,
byMethod: {},
byStatusCode: {},
byEndpoint: {},
timeWindow: {
last5Minutes: 0,
last15Minutes: 0,
last30Minutes: 0,
last60Minutes: 0
}
};
this.setupMetricsProcessing();
this.startPeriodicCalculations();
}
setupMetricsProcessing() {
this.dataSubject
.pipe((0, operators_1.scan)((metrics, dataPoint) => this.updateMetrics(metrics, dataPoint), this.currentMetrics), (0, operators_1.shareReplay)(1))
.subscribe(metrics => {
this.currentMetrics = metrics;
});
}
startPeriodicCalculations() {
(0, rxjs_1.interval)(30000).subscribe(() => {
this.updateTimeWindowMetrics();
this.updateRequestsPerSecond();
});
}
recordRequest(requestContext, responseContext, error) {
const dataPoint = {
requestContext,
responseContext,
error,
timestamp: new Date()
};
this.requestHistory.push(dataPoint);
if (this.requestHistory.length > this.maxHistorySize) {
this.requestHistory.shift();
}
this.responseTimes.push(responseContext.duration);
if (this.responseTimes.length > this.maxResponseTimesSamples) {
this.responseTimes.shift();
}
this.dataSubject.next(dataPoint);
}
updateMetrics(current, dataPoint) {
const { requestContext, responseContext, error } = dataPoint;
const updated = { ...current };
updated.totalRequests++;
if (error || responseContext.statusCode >= 400) {
updated.errorCount++;
}
if (responseContext.duration > 1000) {
updated.slowRequestCount++;
}
updated.errorRate = (updated.errorCount / updated.totalRequests) * 100;
updated.averageResponseTime = this.calculateNewAverage(current.averageResponseTime, responseContext.duration, updated.totalRequests);
updated.medianResponseTime = this.calculatePercentile(50);
updated.p95ResponseTime = this.calculatePercentile(95);
updated.p99ResponseTime = this.calculatePercentile(99);
updated.byMethod = this.updateMethodMetrics(updated.byMethod, requestContext.method, responseContext, error);
updated.byStatusCode = this.updateStatusCodeMetrics(updated.byStatusCode, responseContext.statusCode);
updated.byEndpoint = this.updateEndpointMetrics(updated.byEndpoint, requestContext, responseContext, error);
return updated;
}
calculateNewAverage(currentAverage, newValue, totalCount) {
return ((currentAverage * (totalCount - 1)) + newValue) / totalCount;
}
calculatePercentile(percentile) {
if (this.responseTimes.length === 0)
return 0;
const sorted = [...this.responseTimes].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
updateMethodMetrics(current, method, responseContext, error) {
const updated = { ...current };
if (!updated[method]) {
updated[method] = {
count: 0,
averageResponseTime: 0,
errorCount: 0,
errorRate: 0
};
}
const methodMetrics = updated[method];
methodMetrics.count++;
if (error || responseContext.statusCode >= 400) {
methodMetrics.errorCount++;
}
methodMetrics.averageResponseTime = this.calculateNewAverage(methodMetrics.averageResponseTime, responseContext.duration, methodMetrics.count);
methodMetrics.errorRate = (methodMetrics.errorCount / methodMetrics.count) * 100;
return updated;
}
updateStatusCodeMetrics(current, statusCode) {
const updated = { ...current };
updated[statusCode] = (updated[statusCode] || 0) + 1;
return updated;
}
updateEndpointMetrics(current, requestContext, responseContext, error) {
const updated = { ...current };
const endpoint = this.normalizeEndpoint(requestContext.method, requestContext.url);
if (!updated[endpoint]) {
updated[endpoint] = {
count: 0,
averageResponseTime: 0,
errorCount: 0,
errorRate: 0,
lastAccessed: new Date()
};
}
const endpointMetrics = updated[endpoint];
endpointMetrics.count++;
endpointMetrics.lastAccessed = new Date();
if (error || responseContext.statusCode >= 400) {
endpointMetrics.errorCount++;
}
endpointMetrics.averageResponseTime = this.calculateNewAverage(endpointMetrics.averageResponseTime, responseContext.duration, endpointMetrics.count);
endpointMetrics.errorRate = (endpointMetrics.errorCount / endpointMetrics.count) * 100;
return updated;
}
normalizeEndpoint(method, url) {
const path = url.split('?')[0];
const normalizedPath = path.replace(/\/\d+/g, '/{id}');
const uuidPattern = /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
const finalPath = normalizedPath.replace(uuidPattern, '/{uuid}');
return `${method} ${finalPath}`;
}
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 count = this.requestHistory.filter(dataPoint => dataPoint.timestamp.getTime() > cutoff).length;
this.currentMetrics.timeWindow[window.key] = count;
}
}
updateRequestsPerSecond() {
const now = Date.now();
const oneMinuteAgo = now - 60000;
const recentRequests = this.requestHistory.filter(dataPoint => dataPoint.timestamp.getTime() > oneMinuteAgo);
this.currentMetrics.requestsPerSecond = recentRequests.length / 60;
}
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));
}
getTopEndpoints(limit = 10) {
return Object.entries(this.currentMetrics.byEndpoint)
.map(([endpoint, metrics]) => ({ endpoint, metrics }))
.sort((a, b) => b.metrics.count - a.metrics.count)
.slice(0, limit);
}
getSlowestEndpoints(limit = 10) {
return Object.entries(this.currentMetrics.byEndpoint)
.map(([endpoint, metrics]) => ({ endpoint, metrics }))
.sort((a, b) => b.metrics.averageResponseTime - a.metrics.averageResponseTime)
.slice(0, limit);
}
getErrorProneEndpoints(limit = 10) {
return Object.entries(this.currentMetrics.byEndpoint)
.map(([endpoint, metrics]) => ({ endpoint, metrics }))
.filter(({ metrics }) => metrics.errorRate > 0)
.sort((a, b) => b.metrics.errorRate - a.metrics.errorRate)
.slice(0, limit);
}
getRecentRequests(limit = 100) {
return this.requestHistory
.slice(-limit)
.reverse();
}
getRequestsInTimeWindow(windowMs) {
const cutoff = Date.now() - windowMs;
return this.requestHistory.filter(dataPoint => dataPoint.timestamp.getTime() > cutoff);
}
reset() {
this.currentMetrics = {
totalRequests: 0,
requestsPerSecond: 0,
averageResponseTime: 0,
medianResponseTime: 0,
p95ResponseTime: 0,
p99ResponseTime: 0,
slowRequestCount: 0,
errorCount: 0,
errorRate: 0,
byMethod: {},
byStatusCode: {},
byEndpoint: {},
timeWindow: {
last5Minutes: 0,
last15Minutes: 0,
last30Minutes: 0,
last60Minutes: 0
}
};
this.requestHistory.length = 0;
this.responseTimes.length = 0;
}
};
exports.RequestMetricsService = RequestMetricsService;
exports.RequestMetricsService = RequestMetricsService = RequestMetricsService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [])
], RequestMetricsService);
//# sourceMappingURL=request-metrics.service.js.map