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