UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

365 lines 13.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 AdaptiveSamplingService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AdaptiveSamplingService = void 0; const common_1 = require("@nestjs/common"); let AdaptiveSamplingService = AdaptiveSamplingService_1 = class AdaptiveSamplingService { constructor(config = {}) { this.logger = new common_1.Logger(AdaptiveSamplingService_1.name); this.samplingRules = []; this.loadHistory = []; this.maxHistorySize = 100; this.loadCalculationWindow = 60000; this.config = { enabled: true, baseSampleRate: 100, adaptiveEnabled: true, loadBasedSampling: true, errorSamplingMultiplier: 2.0, healthCheckSampleRate: 1, maxSampleRate: 100, minSampleRate: 1, ...config }; this.stats = { totalRequests: 0, sampledRequests: 0, currentSampleRate: this.config.baseSampleRate, effectiveSampleRate: this.config.baseSampleRate, loadFactor: 0, errorRate: 0, lastAdaptation: Date.now() }; this.initializeDefaultRules(); this.logger.debug('Adaptive Sampling Service initialized', this.config); } shouldSample(context, error) { if (!this.config.enabled) { return true; } this.stats.totalRequests++; try { const sampleRate = this.calculateSampleRate(context, error); const shouldSample = Math.random() * 100 < sampleRate; if (shouldSample) { this.stats.sampledRequests++; } this.updateLoadMetrics(context, error); if (this.config.adaptiveEnabled) { this.adaptSamplingRate(); } return shouldSample; } catch (error) { this.logger.error('Error in sampling decision:', error); return true; } } calculateSampleRate(context, error) { let sampleRate = this.config.baseSampleRate; try { const ruleRate = this.applyRules(context); if (ruleRate !== null) { sampleRate = ruleRate; } if (error) { sampleRate *= this.config.errorSamplingMultiplier; } if (this.config.loadBasedSampling) { const loadFactor = this.getCurrentLoadFactor(); sampleRate = this.adjustForLoad(sampleRate, loadFactor); } if (this.config.adaptiveEnabled) { sampleRate = this.applyAdaptiveAdjustments(sampleRate, context); } sampleRate = Math.max(this.config.minSampleRate, Math.min(this.config.maxSampleRate, sampleRate)); return sampleRate; } catch (error) { this.logger.error('Error calculating sample rate:', error); return this.config.baseSampleRate; } } addRule(rule) { this.samplingRules.push(rule); this.samplingRules.sort((a, b) => b.priority - a.priority); this.logger.debug('Sampling rule added:', rule); } removeRule(path, method) { const index = this.samplingRules.findIndex(rule => rule.path === path && rule.method === method); if (index >= 0) { this.samplingRules.splice(index, 1); this.logger.debug('Sampling rule removed:', { path, method }); } } getStats() { this.updateEffectiveSampleRate(); return { ...this.stats }; } getRules() { return [...this.samplingRules]; } resetStats() { this.stats.totalRequests = 0; this.stats.sampledRequests = 0; this.stats.effectiveSampleRate = this.config.baseSampleRate; this.stats.lastAdaptation = Date.now(); this.loadHistory.splice(0); this.logger.debug('Sampling statistics reset'); } initializeDefaultRules() { const defaultRules = [ { path: '/health', rate: this.config.healthCheckSampleRate, priority: 10 }, { path: '/metrics', rate: this.config.healthCheckSampleRate, priority: 10 }, { path: '/api/health', rate: this.config.healthCheckSampleRate, priority: 10 }, { path: '/favicon.ico', rate: 0, priority: 9 }, { path: '/robots.txt', rate: 0, priority: 9 }, { path: '/api', method: 'GET', rate: 50, priority: 5 }, { path: '/api', method: 'POST', rate: 100, priority: 6 }, { path: '/api', method: 'PUT', rate: 100, priority: 6 }, { path: '/api', method: 'DELETE', rate: 100, priority: 7 } ]; this.samplingRules.push(...defaultRules); } applyRules(context) { for (const rule of this.samplingRules) { if (this.matchesRule(context, rule)) { return rule.rate; } } return null; } matchesRule(context, rule) { if (!this.matchesPath(context.url, rule.path)) { return false; } if (rule.method && context.method !== rule.method) { return false; } if (rule.conditions) { return rule.conditions.every(condition => this.matchesCondition(context, condition)); } return true; } matchesPath(url, pattern) { const path = url.split('?')[0]; if (pattern.includes('*')) { const regexPattern = pattern.replace(/\*/g, '.*'); return new RegExp(`^${regexPattern}`).test(path); } return path.startsWith(pattern); } matchesCondition(context, condition) { try { let value; switch (condition.type) { case 'header': value = context.headers[condition.key || '']; break; case 'query': value = context.query[condition.key || '']; break; case 'user': value = context.userId; break; case 'time': value = Date.now(); break; case 'load': value = this.getCurrentLoadFactor(); break; default: return false; } return this.evaluateCondition(value, condition.value, condition.operator); } catch (error) { return false; } } evaluateCondition(actual, expected, operator) { switch (operator) { case 'equals': return actual === expected; case 'contains': return String(actual).includes(String(expected)); case 'greater': return Number(actual) > Number(expected); case 'less': return Number(actual) < Number(expected); case 'regex': return new RegExp(expected).test(String(actual)); default: return false; } } adjustForLoad(sampleRate, loadFactor) { if (loadFactor > 0.8) { return sampleRate * 0.5; } else if (loadFactor > 0.6) { return sampleRate * 0.7; } else if (loadFactor > 0.4) { return sampleRate * 0.9; } return sampleRate; } applyAdaptiveAdjustments(sampleRate, context) { const errorRate = this.getErrorRate(); if (errorRate > 0.05) { sampleRate *= 1.5; } const avgResponseTime = this.getAverageResponseTime(); if (avgResponseTime > 1000) { sampleRate *= 1.2; } return sampleRate; } updateLoadMetrics(context, error) { const now = Date.now(); const metrics = { requestCount: 1, errorCount: error ? 1 : 0, averageResponseTime: 0, timestamp: now }; this.loadHistory.push(metrics); const cutoff = now - this.loadCalculationWindow; while (this.loadHistory.length > 0 && this.loadHistory[0].timestamp < cutoff) { this.loadHistory.shift(); } if (this.loadHistory.length > this.maxHistorySize) { this.loadHistory.splice(0, this.loadHistory.length - this.maxHistorySize); } } getCurrentLoadFactor() { if (this.loadHistory.length === 0) { return 0; } const now = Date.now(); const recentMetrics = this.loadHistory.filter(m => now - m.timestamp < this.loadCalculationWindow); if (recentMetrics.length === 0) { return 0; } const totalRequests = recentMetrics.reduce((sum, m) => sum + m.requestCount, 0); const timeWindow = this.loadCalculationWindow / 1000; const requestsPerSecond = totalRequests / timeWindow; return Math.min(1, requestsPerSecond / 100); } getErrorRate() { if (this.loadHistory.length === 0) { return 0; } const now = Date.now(); const recentMetrics = this.loadHistory.filter(m => now - m.timestamp < this.loadCalculationWindow); if (recentMetrics.length === 0) { return 0; } const totalRequests = recentMetrics.reduce((sum, m) => sum + m.requestCount, 0); const totalErrors = recentMetrics.reduce((sum, m) => sum + m.errorCount, 0); return totalRequests > 0 ? totalErrors / totalRequests : 0; } getAverageResponseTime() { if (this.loadHistory.length === 0) { return 0; } const now = Date.now(); const recentMetrics = this.loadHistory.filter(m => now - m.timestamp < this.loadCalculationWindow); if (recentMetrics.length === 0) { return 0; } const totalResponseTime = recentMetrics.reduce((sum, m) => sum + m.averageResponseTime, 0); return totalResponseTime / recentMetrics.length; } adaptSamplingRate() { const now = Date.now(); const timeSinceLastAdaptation = now - this.stats.lastAdaptation; if (timeSinceLastAdaptation < 30000) { return; } const loadFactor = this.getCurrentLoadFactor(); const errorRate = this.getErrorRate(); let newRate = this.config.baseSampleRate; if (loadFactor > 0.8) { newRate *= 0.5; } else if (loadFactor > 0.6) { newRate *= 0.7; } if (errorRate > 0.1) { newRate *= 1.5; } else if (errorRate > 0.05) { newRate *= 1.2; } newRate = Math.max(this.config.minSampleRate, Math.min(this.config.maxSampleRate, newRate)); if (Math.abs(newRate - this.stats.currentSampleRate) > 5) { this.stats.currentSampleRate = newRate; this.stats.lastAdaptation = now; this.stats.loadFactor = loadFactor; this.stats.errorRate = errorRate; this.logger.debug(`Adaptive sampling rate adjusted to ${newRate}% (load: ${loadFactor.toFixed(2)}, error: ${errorRate.toFixed(2)})`); } } updateEffectiveSampleRate() { if (this.stats.totalRequests > 0) { this.stats.effectiveSampleRate = (this.stats.sampledRequests / this.stats.totalRequests) * 100; } } }; exports.AdaptiveSamplingService = AdaptiveSamplingService; exports.AdaptiveSamplingService = AdaptiveSamplingService = AdaptiveSamplingService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [Object]) ], AdaptiveSamplingService); //# sourceMappingURL=adaptive-sampling.service.js.map