UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

502 lines 21 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 ExceptionWatcherService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExceptionWatcherService = void 0; const common_1 = require("@nestjs/common"); const telescope_service_1 = require("../../core/services/telescope.service"); const exception_watcher_filter_1 = require("./exception-watcher.filter"); const exception_watcher_config_1 = require("./exception-watcher.config"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); let ExceptionWatcherService = ExceptionWatcherService_1 = class ExceptionWatcherService { constructor(telescopeService, exceptionWatcherConfig) { this.telescopeService = telescopeService; this.logger = new common_1.Logger(ExceptionWatcherService_1.name); this.destroy$ = new rxjs_1.Subject(); this.alertsSubject = new rxjs_1.Subject(); this.metricsSubject = new rxjs_1.Subject(); this.exceptionHistory = []; this.exceptionGroups = new Map(); this.maxHistorySize = 10000; this.maxGroupSize = 1000; this.recentExceptions = []; this.alertHistory = []; this.config = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, ...exceptionWatcherConfig }; this.currentMetrics = this.initializeMetrics(); } async onModuleInit() { if (!this.config.enabled) { return; } if (this.config.captureUnhandledRejections) { process.on('unhandledRejection', (reason, promise) => { this.handleUnhandledRejection(reason, promise); }); } if (this.config.captureUncaughtExceptions) { process.on('uncaughtException', (error) => { this.handleUncaughtException(error); }); } this.startPeriodicProcessing(); this.logger.log('Exception watcher initialized'); } initializeMetrics() { return { totalExceptions: 0, uniqueExceptions: 0, errorRate: 0, criticalErrors: 0, highSeverityErrors: 0, mediumSeverityErrors: 0, lowSeverityErrors: 0, exceptionsPerMinute: 0, exceptionsPerHour: 0, errorsByType: {}, errorsByCategory: {}, errorsBySeverity: {}, topErrors: [], averageResponseTime: 0, affectedRequests: 0, trends: { lastHour: { total: 0, change: 0, changePercent: 0, peak: 0, average: 0 }, lastDay: { total: 0, change: 0, changePercent: 0, peak: 0, average: 0 }, lastWeek: { total: 0, change: 0, changePercent: 0, peak: 0, average: 0 } } }; } trackException(context) { if (!this.config.enabled) { return; } try { this.addToHistory(context); this.groupException(context); this.updateMetrics(context); const entry = this.createTelescopeEntry(context); this.telescopeService.record(entry); this.checkAlerts(context); this.correlateException(context); this.logger.debug(`Exception tracked: ${context.id}`); } catch (error) { this.logger.error('Failed to track exception:', error); } } addToHistory(context) { this.exceptionHistory.push(context); if (this.exceptionHistory.length > this.maxHistorySize) { this.exceptionHistory.shift(); } this.recentExceptions.push(context); const tenMinutesAgo = Date.now() - 10 * 60 * 1000; this.recentExceptions = this.recentExceptions.filter(exc => exc.timestamp.getTime() > tenMinutesAgo); } groupException(context) { if (!this.config.groupSimilarErrors || !context.classification) { return; } const groupId = context.classification.groupId; let group = this.exceptionGroups.get(groupId); if (!group) { group = { groupId, fingerprint: context.classification.fingerprint, errorType: context.errorType, errorMessage: context.errorMessage, severity: context.classification.severity, category: context.classification.category, count: 0, firstOccurrence: context.timestamp, lastOccurrence: context.timestamp, affectedUsers: new Set(), affectedRequests: [], stackFrames: [], contexts: [], resolved: false }; this.exceptionGroups.set(groupId, group); } group.count++; group.lastOccurrence = context.timestamp; if (context.userId) { group.affectedUsers.add(context.userId); } if (context.requestId) { group.affectedRequests.push(context.requestId); } if (context.stackFrames) { for (const frame of context.stackFrames) { if (frame.function && frame.file && frame.line) { const existingFrame = group.stackFrames.find(f => f.function === frame.function && f.file === frame.file && f.line === frame.line); if (existingFrame) { existingFrame.count++; } else { group.stackFrames.push({ function: frame.function, file: frame.file, line: frame.line, count: 1 }); } } } } group.contexts.push(context); if (group.contexts.length > this.maxGroupSize) { group.contexts.shift(); } } updateMetrics(context) { this.currentMetrics.totalExceptions++; this.currentMetrics.uniqueExceptions = this.exceptionGroups.size; if (context.classification) { switch (context.classification.severity) { case exception_watcher_filter_1.ErrorSeverity.CRITICAL: this.currentMetrics.criticalErrors++; break; case exception_watcher_filter_1.ErrorSeverity.HIGH: this.currentMetrics.highSeverityErrors++; break; case exception_watcher_filter_1.ErrorSeverity.MEDIUM: this.currentMetrics.mediumSeverityErrors++; break; case exception_watcher_filter_1.ErrorSeverity.LOW: this.currentMetrics.lowSeverityErrors++; break; } this.currentMetrics.errorsByType[context.errorType] = (this.currentMetrics.errorsByType[context.errorType] || 0) + 1; this.currentMetrics.errorsByCategory[context.classification.category] = (this.currentMetrics.errorsByCategory[context.classification.category] || 0) + 1; this.currentMetrics.errorsBySeverity[context.classification.severity] = (this.currentMetrics.errorsBySeverity[context.classification.severity] || 0) + 1; } if (context.response?.duration) { const totalTime = this.currentMetrics.averageResponseTime * this.currentMetrics.affectedRequests; this.currentMetrics.affectedRequests++; this.currentMetrics.averageResponseTime = (totalTime + context.response.duration) / this.currentMetrics.affectedRequests; } this.updateTopErrors(); this.updateRates(); } updateTopErrors() { const topGroups = Array.from(this.exceptionGroups.values()) .sort((a, b) => b.count - a.count) .slice(0, 10); this.currentMetrics.topErrors = topGroups.map(group => ({ groupId: group.groupId, errorType: group.errorType, errorMessage: group.errorMessage, count: group.count, lastOccurrence: group.lastOccurrence, severity: group.severity, category: group.category })); } updateRates() { const now = Date.now(); const oneMinuteAgo = now - 60 * 1000; const oneHourAgo = now - 60 * 60 * 1000; const exceptionsLastMinute = this.exceptionHistory.filter(exc => exc.timestamp.getTime() > oneMinuteAgo).length; const exceptionsLastHour = this.exceptionHistory.filter(exc => exc.timestamp.getTime() > oneHourAgo).length; this.currentMetrics.exceptionsPerMinute = exceptionsLastMinute; this.currentMetrics.exceptionsPerHour = exceptionsLastHour; this.currentMetrics.errorRate = this.calculateErrorRate(); } calculateErrorRate() { const totalRequests = this.currentMetrics.affectedRequests || 1; return (this.currentMetrics.totalExceptions / totalRequests) * 100; } createTelescopeEntry(context) { const entryId = `exception_${context.id}`; const familyHash = context.classification?.groupId || context.id; return { id: entryId, type: 'exception', familyHash, content: { exception: { id: context.id, type: context.errorType, message: context.errorMessage, code: context.errorCode, statusCode: context.statusCode, severity: context.classification?.severity, category: context.classification?.category, fingerprint: context.classification?.fingerprint, groupId: context.classification?.groupId }, stackTrace: { raw: context.stackTrace, frames: context.stackFrames }, request: context.request, response: context.response, environment: context.environment, performance: context.performance, correlation: { traceId: context.traceId, requestId: context.requestId, userId: context.userId, sessionId: context.sessionId } }, tags: this.generateTags(context), timestamp: context.timestamp, sequence: context.timestamp.getTime() }; } generateTags(context) { const tags = ['exception', `type:${context.errorType}`]; if (context.classification) { tags.push(`severity:${context.classification.severity}`); tags.push(`category:${context.classification.category}`); } if (context.statusCode) { tags.push(`status:${context.statusCode}`); } if (context.userId) { tags.push('user-error'); } if (context.request?.method) { tags.push(`method:${context.request.method}`); } return tags; } checkAlerts(context) { if (!this.config.enableRealTimeAlerts) { return; } const now = Date.now(); const timeWindow = this.config.alertThresholds.timeWindow; const windowStart = now - timeWindow; const recentExceptions = this.recentExceptions.filter(exc => exc.timestamp.getTime() > windowStart); const errorRate = recentExceptions.length / (timeWindow / 1000); if (errorRate > this.config.alertThresholds.errorRate) { this.createAlert({ type: 'error_rate', severity: 'high', message: `Error rate exceeded threshold: ${errorRate.toFixed(2)} errors/second`, data: { errorRate, threshold: this.config.alertThresholds.errorRate } }); } const criticalErrors = recentExceptions.filter(exc => exc.classification?.severity === exception_watcher_filter_1.ErrorSeverity.CRITICAL); if (criticalErrors.length >= this.config.alertThresholds.criticalErrors) { this.createAlert({ type: 'critical_errors', severity: 'critical', message: `Critical errors exceeded threshold: ${criticalErrors.length} critical errors`, data: { criticalErrors: criticalErrors.length, threshold: this.config.alertThresholds.criticalErrors } }); } if (context.classification && !this.hasSeenErrorBefore(context)) { this.createAlert({ type: 'new_error', severity: 'medium', message: `New error type detected: ${context.errorType}`, data: { errorType: context.errorType, message: context.errorMessage } }); } } hasSeenErrorBefore(context) { const groupId = context.classification?.groupId; if (!groupId) return false; const group = this.exceptionGroups.get(groupId); return group ? group.count > 1 : false; } createAlert(alert) { const fullAlert = { id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), acknowledged: false, ...alert }; this.alertHistory.push(fullAlert); this.alertsSubject.next(fullAlert); this.logger.warn(`Exception alert: ${fullAlert.message}`, fullAlert.data); } correlateException(context) { if (!this.config.correlateWithRequests && !this.config.correlateWithQueries) { return; } if (this.config.correlateWithRequests && (context.traceId || context.requestId)) { const correlationId = context.traceId || context.requestId; if (correlationId) { this.storeCorrelation('request', correlationId, context); } } if (this.config.correlateWithQueries && context.traceId) { this.storeCorrelation('query', context.traceId, context); } if (context.sessionId) { this.storeCorrelation('session', context.sessionId, context); } } storeCorrelation(type, correlationId, context) { const correlationKey = `${type}:${correlationId}`; if (!context.performance) { context.performance = {}; } context.performance[`${type}CorrelationId`] = correlationId; this.logger.debug(`Correlated exception ${context.id} with ${type} ${correlationId}`); } handleUnhandledRejection(reason, promise) { const error = reason instanceof Error ? reason : new Error(String(reason)); const context = { id: `unhandled_rejection_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), error, errorType: 'UnhandledPromiseRejection', errorMessage: error.message, stackTrace: error.stack, classification: { type: 'system', category: 'system_error', severity: exception_watcher_filter_1.ErrorSeverity.HIGH, fingerprint: this.generateFingerprint(error), groupId: this.generateGroupId(error) } }; this.trackException(context); } handleUncaughtException(error) { const context = { id: `uncaught_exception_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), error, errorType: 'UncaughtException', errorMessage: error.message, stackTrace: error.stack, classification: { type: 'system', category: 'system_error', severity: exception_watcher_filter_1.ErrorSeverity.CRITICAL, fingerprint: this.generateFingerprint(error), groupId: this.generateGroupId(error) } }; this.trackException(context); } generateFingerprint(error) { const components = [ error.constructor.name, error.message?.substring(0, 100), error.stack?.split('\n')[1] ].filter(Boolean); return this.hash(components.join(':')); } generateGroupId(error) { const components = [ error.constructor.name, error.message?.replace(/\d+/g, 'N').substring(0, 50) ].filter(Boolean); return this.hash(components.join(':')); } hash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16); } startPeriodicProcessing() { (0, rxjs_1.interval)(30000) .pipe((0, operators_1.takeUntil)(this.destroy$)) .subscribe(() => { this.updateTrends(); this.cleanupOldData(); this.metricsSubject.next(this.currentMetrics); }); } updateTrends() { const now = Date.now(); const oneHourAgo = now - 60 * 60 * 1000; const oneDayAgo = now - 24 * 60 * 60 * 1000; const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000; this.currentMetrics.trends.lastHour = this.calculateTrend(oneHourAgo); this.currentMetrics.trends.lastDay = this.calculateTrend(oneDayAgo); this.currentMetrics.trends.lastWeek = this.calculateTrend(oneWeekAgo); } calculateTrend(since) { const exceptions = this.exceptionHistory.filter(exc => exc.timestamp.getTime() > since); return { total: exceptions.length, change: 0, changePercent: 0, peak: 0, average: exceptions.length / ((Date.now() - since) / (60 * 60 * 1000)) }; } cleanupOldData() { const now = Date.now(); const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000; this.exceptionHistory = this.exceptionHistory.filter(exc => exc.timestamp.getTime() > oneWeekAgo); this.alertHistory = this.alertHistory.filter(alert => alert.timestamp.getTime() > oneWeekAgo); } getMetrics() { return { ...this.currentMetrics }; } getMetricsStream() { return this.metricsSubject.asObservable().pipe((0, operators_1.shareReplay)(1)); } getAlertsStream() { return this.alertsSubject.asObservable(); } getExceptionGroups() { return Array.from(this.exceptionGroups.values()); } getExceptionGroup(groupId) { return this.exceptionGroups.get(groupId); } getRecentExceptions(limit = 100) { return this.exceptionHistory.slice(-limit).reverse(); } resolveExceptionGroup(groupId, resolvedBy, notes) { const group = this.exceptionGroups.get(groupId); if (!group) return false; group.resolved = true; group.assignedTo = resolvedBy; group.notes = notes; return true; } acknowledgeAlert(alertId) { const alert = this.alertHistory.find(a => a.id === alertId); if (!alert) return false; alert.acknowledged = true; return true; } getConfig() { return { ...this.config }; } onDestroy() { this.destroy$.next(); this.destroy$.complete(); } }; exports.ExceptionWatcherService = ExceptionWatcherService; exports.ExceptionWatcherService = ExceptionWatcherService = ExceptionWatcherService_1 = __decorate([ (0, common_1.Injectable)(), __param(1, (0, common_1.Inject)('EXCEPTION_WATCHER_CONFIG')), __metadata("design:paramtypes", [telescope_service_1.TelescopeService, Object]) ], ExceptionWatcherService); //# sourceMappingURL=exception-watcher.service.js.map