UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

346 lines 15.2 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 RequestWatcherService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.RequestWatcherService = void 0; const common_1 = require("@nestjs/common"); const telescope_service_1 = require("../../core/services/telescope.service"); const data_masking_service_1 = require("../../core/services/data-masking.service"); const enhanced_circuit_breaker_service_1 = require("../../core/services/enhanced-circuit-breaker.service"); const enhanced_memory_manager_service_1 = require("../../core/services/enhanced-memory-manager.service"); const adaptive_sampling_service_1 = require("../../core/services/adaptive-sampling.service"); let RequestWatcherService = RequestWatcherService_1 = class RequestWatcherService { constructor(telescopeService, telescopeConfig, dataMaskingService, circuitBreakerService, memoryManagerService, adaptiveSamplingService) { this.telescopeService = telescopeService; this.telescopeConfig = telescopeConfig; this.dataMaskingService = dataMaskingService; this.circuitBreakerService = circuitBreakerService; this.memoryManagerService = memoryManagerService; this.adaptiveSamplingService = adaptiveSamplingService; this.logger = new common_1.Logger(RequestWatcherService_1.name); this.config = this.buildConfig(telescopeConfig); this.requestMetrics = this.initializeMetrics(); } buildConfig(config) { const defaultConfig = { enabled: true, excludePaths: [ '/health', '/metrics', '/telescope', '/favicon.ico', '/robots.txt', '/_next', '/static', '/assets', ], sampling: { enabled: true, rate: 100, rules: [ { path: '/api/health', rate: 10, priority: 1 }, { path: '/api/metrics', rate: 10, priority: 1 }, { path: '/api', method: 'GET', rate: 50, priority: 2 }, { path: '/api', method: 'POST', rate: 100, priority: 3 }, { path: '/api', statusCode: 500, rate: 100, priority: 4 }, ], }, security: { maskSensitiveData: true, logResponseBodies: false, logSuccessfulResponseBodies: false, sensitiveKeys: [ 'password', 'token', 'secret', 'key', 'auth', 'authorization', 'credential', 'pin', 'ssn', 'social', 'card', 'account', ], }, performance: { slowRequestThreshold: 1000, collectMetrics: true, }, filters: { methods: [], statusCodes: [], contentTypes: [], }, }; const mergedConfig = { ...defaultConfig, ...config.watchers?.request, }; if (config.watchers?.request?.sampling && !config.watchers.request.sampling.rules) { mergedConfig.sampling.rules = defaultConfig.sampling.rules; } return mergedConfig; } initializeMetrics() { return { totalRequests: 0, slowRequests: 0, errorRequests: 0, averageResponseTime: 0, requestsPerSecond: 0, statusCodeDistribution: {}, methodDistribution: {}, }; } async trackRequest(requestContext, responseContext, error) { if (!this.config.enabled) { return; } await this.circuitBreakerService.executeWithFallback('trackRequest', async () => { if (!this.adaptiveSamplingService.shouldSample(requestContext, error || undefined)) { return; } const entry = await this.createTelescopeEntry(requestContext, responseContext, error); await this.telescopeService.record(entry); if (this.config.performance.collectMetrics) { await this.updateMetrics(requestContext, responseContext, error); } if (responseContext.duration > this.config.performance.slowRequestThreshold) { this.logger.warn(`Slow request detected: ${requestContext.method} ${requestContext.url} (${responseContext.duration}ms)`); } if (error || responseContext.statusCode >= 400) { const safeSummary = this.dataMaskingService.createSafeSummary({ method: requestContext.method, url: requestContext.url, statusCode: responseContext.statusCode, error: error?.message, }); this.logger.warn(`Request error: ${safeSummary}`); } }, async () => { this.logger.warn(`Request tracking failed, using fallback for: ${requestContext.method} ${requestContext.url}`); this.memoryManagerService.addTimelineEntry('request-tracking-failures', { method: requestContext.method, url: requestContext.url, timestamp: Date.now(), reason: 'circuit-breaker-fallback', }); }); } async createTelescopeEntry(requestContext, responseContext, error) { const entryId = `req_${requestContext.id}`; const familyHash = this.generateFamilyHash(requestContext); const maskedRequestData = this.config.security.maskSensitiveData ? this.dataMaskingService.maskSensitiveData({ headers: requestContext.headers, query: requestContext.query, body: requestContext.body, }) : { headers: requestContext.headers, query: requestContext.query, body: requestContext.body, }; const maskedResponseData = this.config.security.maskSensitiveData ? this.dataMaskingService.maskSensitiveData({ headers: responseContext.headers, body: responseContext.body, }) : { headers: responseContext.headers, body: responseContext.body, }; return { id: entryId, type: 'request', familyHash, content: { request: { id: requestContext.id, method: requestContext.method, url: requestContext.url, headers: maskedRequestData.headers, query: maskedRequestData.query, body: maskedRequestData.body, userAgent: requestContext.userAgent, ip: requestContext.ip, sessionId: requestContext.sessionId, userId: requestContext.userId, traceId: requestContext.traceId, timestamp: new Date(requestContext.startTime).toISOString(), }, response: { statusCode: responseContext.statusCode, headers: maskedResponseData.headers, body: maskedResponseData.body, size: responseContext.size, duration: responseContext.duration, timestamp: new Date(responseContext.endTime).toISOString(), }, error: error ? { message: error.message, stack: error.stack, name: error.name, } : null, performance: { duration: responseContext.duration, slow: responseContext.duration > this.config.performance.slowRequestThreshold, }, security: { masked: this.config.security.maskSensitiveData, sensitiveDetected: this.dataMaskingService.detectSensitiveData({ request: requestContext, response: responseContext, }), }, }, tags: this.generateTags(requestContext, responseContext, error), timestamp: new Date(requestContext.startTime), sequence: Date.now(), }; } generateFamilyHash(requestContext) { const path = requestContext.url.split('?')[0]; const normalizedPath = path.replace(/\/\d+/g, '/{id}'); return `${requestContext.method}:${normalizedPath}`; } generateTags(requestContext, responseContext, error) { const tags = ['request']; tags.push(`method:${requestContext.method.toLowerCase()}`); tags.push(`status:${responseContext.statusCode}`); if (responseContext.statusCode >= 200 && responseContext.statusCode < 300) { tags.push('success'); } else if (responseContext.statusCode >= 400 && responseContext.statusCode < 500) { tags.push('client-error'); } else if (responseContext.statusCode >= 500) { tags.push('server-error'); } if (responseContext.duration > this.config.performance.slowRequestThreshold) { tags.push('slow'); } if (responseContext.duration < 100) { tags.push('fast'); } if (requestContext.userId) { tags.push('authenticated'); } else { tags.push('anonymous'); } if (requestContext.sessionId) { tags.push('session'); } if (error) { tags.push('error'); tags.push(`error-type:${error.name}`); } if (this.dataMaskingService.detectSensitiveData({ request: requestContext, response: responseContext, })) { tags.push('sensitive-data'); } return tags; } async updateMetrics(requestContext, responseContext, error) { try { this.requestMetrics.totalRequests++; if (responseContext.duration > this.config.performance.slowRequestThreshold) { this.requestMetrics.slowRequests++; } if (error || responseContext.statusCode >= 400) { this.requestMetrics.errorRequests++; } this.requestMetrics.statusCodeDistribution[responseContext.statusCode] = (this.requestMetrics.statusCodeDistribution[responseContext.statusCode] || 0) + 1; this.requestMetrics.methodDistribution[requestContext.method] = (this.requestMetrics.methodDistribution[requestContext.method] || 0) + 1; const totalTime = this.requestMetrics.averageResponseTime * (this.requestMetrics.totalRequests - 1); this.requestMetrics.averageResponseTime = (totalTime + responseContext.duration) / this.requestMetrics.totalRequests; this.memoryManagerService.addMetricsEntry('request-metrics', { url: requestContext.url, method: requestContext.method, statusCode: responseContext.statusCode, duration: responseContext.duration, error: error ? error.message : null, }, this.config.performance.collectMetrics ? 300000 : undefined); this.memoryManagerService.addTimelineEntry('requests', { method: requestContext.method, url: requestContext.url, duration: responseContext.duration, statusCode: responseContext.statusCode, timestamp: Date.now(), }); } catch (metricsError) { this.logger.error('Failed to update request metrics:', metricsError); } } getMetrics() { return { ...this.requestMetrics, samplingStats: this.adaptiveSamplingService.getStats(), memoryStats: this.memoryManagerService.getMemoryStats(), circuitBreakerStats: this.circuitBreakerService.getAllStats(), }; } async clearMetrics() { Object.assign(this.requestMetrics, this.initializeMetrics()); this.memoryManagerService.clearAll(); this.adaptiveSamplingService.resetStats(); this.logger.debug('Request metrics cleared'); } getExcludedPaths() { return this.config.excludePaths; } shouldSampleRequest(request) { if (!this.config.sampling.enabled) { return true; } const matchingRule = this.config.sampling.rules.find((rule) => { if (rule.path && !request.url.includes(rule.path)) return false; if (rule.method && rule.method !== request.method) return false; return true; }); if (matchingRule) { return Math.random() * 100 < matchingRule.rate; } return Math.random() * 100 < this.config.sampling.rate; } shouldMaskBody(body) { return this.config.security.maskSensitiveData; } shouldLogSuccessfulResponseBodies() { return this.config.security.logSuccessfulResponseBodies; } }; exports.RequestWatcherService = RequestWatcherService; exports.RequestWatcherService = RequestWatcherService = RequestWatcherService_1 = __decorate([ (0, common_1.Injectable)(), __param(1, (0, common_1.Inject)('TELESCOPE_CONFIG')), __metadata("design:paramtypes", [telescope_service_1.TelescopeService, Object, data_masking_service_1.DataMaskingService, enhanced_circuit_breaker_service_1.EnhancedCircuitBreakerService, enhanced_memory_manager_service_1.EnhancedMemoryManagerService, adaptive_sampling_service_1.AdaptiveSamplingService]) ], RequestWatcherService); //# sourceMappingURL=request-watcher.service.js.map