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