UNPKG

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