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