UNPKG

tryaii-mcp-server

Version:

TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence

343 lines 14.1 kB
import { EventEmitter } from 'events'; import { logger } from '../utils/logger.js'; export class MetricsService extends EventEmitter { static instance; metrics = new Map(); performanceData = []; counters = new Map(); gauges = new Map(); histograms = new Map(); startTime = Date.now(); // Retention settings maxMetricsAge = 24 * 60 * 60 * 1000; // 24 hours maxDataPoints = 10000; constructor() { super(); this.initializeMetrics(); this.startCleanupTimer(); } static getInstance() { if (!MetricsService.instance) { MetricsService.instance = new MetricsService(); } return MetricsService.instance; } initializeMetrics() { // Initialize basic counters this.counters.set('requests.total', 0); this.counters.set('requests.successful', 0); this.counters.set('requests.failed', 0); this.counters.set('ai.total_cost', 0); this.counters.set('ai.total_tokens', 0); this.counters.set('users.rate_limit_violations', 0); // Initialize gauges this.gauges.set('users.active', 0); this.gauges.set('system.uptime', 0); logger.info('Metrics service initialized'); } // ============================================================================================ // METRIC RECORDING METHODS // ============================================================================================ recordCounter(name, value = 1, labels = {}) { const current = this.counters.get(name) || 0; this.counters.set(name, current + value); this.recordMetric({ name, value: current + value, timestamp: Date.now(), labels, unit: 'count' }); this.emit('counter', { name, value, labels }); } recordGauge(name, value, labels = {}) { this.gauges.set(name, value); this.recordMetric({ name, value, timestamp: Date.now(), labels, unit: 'gauge' }); this.emit('gauge', { name, value, labels }); } recordHistogram(name, value, labels = {}) { const values = this.histograms.get(name) || []; values.push(value); // Keep only recent values if (values.length > 1000) { values.splice(0, values.length - 1000); } this.histograms.set(name, values); this.recordMetric({ name, value, timestamp: Date.now(), labels, unit: 'histogram' }); this.emit('histogram', { name, value, labels }); } recordMetric(metric) { const metricArray = this.metrics.get(metric.name) || []; metricArray.push(metric); // Keep only recent data points if (metricArray.length > this.maxDataPoints) { metricArray.splice(0, metricArray.length - this.maxDataPoints); } this.metrics.set(metric.name, metricArray); } // ============================================================================================ // SPECIALIZED RECORDING METHODS // ============================================================================================ recordRequest(success, responseTime, endpoint, userId) { this.recordCounter('requests.total'); this.recordCounter(success ? 'requests.successful' : 'requests.failed'); this.recordHistogram('response_time', responseTime, { endpoint }); this.performanceData.push({ timestamp: Date.now(), responseTime, statusCode: success ? 200 : 500, endpoint, userId }); // Keep performance data manageable if (this.performanceData.length > this.maxDataPoints) { this.performanceData.splice(0, this.performanceData.length - this.maxDataPoints); } logger.debug('Request recorded', { success, responseTime, endpoint, userId }); } recordAIUsage(modelId, provider, cost, tokens, userId) { this.recordCounter('ai.total_cost', cost); this.recordCounter('ai.total_tokens', tokens); this.recordCounter(`ai.model.${modelId}`, 1); this.recordCounter(`ai.provider.${provider}`, 1); this.recordMetric({ name: 'ai.usage', value: cost, timestamp: Date.now(), labels: { modelId, provider, userId: userId || 'anonymous' }, unit: 'cost' }); logger.debug('AI usage recorded', { modelId, provider, cost, tokens, userId }); } recordUserActivity(userId, action) { this.recordCounter(`user.activity.${action}`, 1, { userId }); this.recordMetric({ name: 'user.activity', value: 1, timestamp: Date.now(), labels: { userId, action }, unit: 'count' }); } recordRateLimitViolation(userId, tier, reason) { this.recordCounter('users.rate_limit_violations'); this.recordCounter(`rate_limit.violations.${tier}`, 1); this.recordMetric({ name: 'rate_limit.violation', value: 1, timestamp: Date.now(), labels: { userId, tier, reason }, unit: 'count' }); logger.warn('Rate limit violation recorded', { userId, tier, reason }); } // ============================================================================================ // SYSTEM METRICS // ============================================================================================ updateSystemMetrics() { const memUsage = process.memoryUsage(); const uptime = Date.now() - this.startTime; this.recordGauge('system.uptime', uptime); this.recordGauge('system.memory.rss', memUsage.rss); this.recordGauge('system.memory.heap_used', memUsage.heapUsed); this.recordGauge('system.memory.heap_total', memUsage.heapTotal); this.recordGauge('system.memory.external', memUsage.external); // CPU usage (simplified - in production, use proper CPU monitoring) this.recordGauge('system.cpu_usage', Math.random() * 100); // Mock for now } // ============================================================================================ // AGGREGATION AND ANALYSIS // ============================================================================================ getSystemMetrics() { const now = Date.now(); const oneMinuteAgo = now - 60 * 1000; // Calculate request rate (requests per minute) const recentRequests = this.performanceData.filter(p => p.timestamp > oneMinuteAgo); const requestRate = recentRequests.length; // Calculate percentiles for response time const responseTimes = this.histograms.get('response_time') || []; const sortedTimes = responseTimes.sort((a, b) => a - b); const p95Index = Math.floor(sortedTimes.length * 0.95); const p99Index = Math.floor(sortedTimes.length * 0.99); return { requests: { total: this.counters.get('requests.total') || 0, successful: this.counters.get('requests.successful') || 0, failed: this.counters.get('requests.failed') || 0, rate: requestRate }, performance: { averageResponseTime: responseTimes.length > 0 ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length : 0, p95ResponseTime: sortedTimes[p95Index] || 0, p99ResponseTime: sortedTimes[p99Index] || 0 }, ai: { totalCost: this.counters.get('ai.total_cost') || 0, totalTokens: this.counters.get('ai.total_tokens') || 0, modelUsage: this.getModelUsage(), providerUsage: this.getProviderUsage() }, users: { activeUsers: this.gauges.get('users.active') || 0, totalUsers: this.getUniqueUsersCount(), rateLimitViolations: this.counters.get('users.rate_limit_violations') || 0 }, system: { uptime: now - this.startTime, memoryUsage: this.gauges.get('system.memory.heap_used') || 0, cpuUsage: this.gauges.get('system.cpu_usage') || 0 } }; } getModelUsage() { const modelUsage = {}; for (const [key, value] of this.counters.entries()) { if (key.startsWith('ai.model.')) { const modelId = key.replace('ai.model.', ''); modelUsage[modelId] = value; } } return modelUsage; } getProviderUsage() { const providerUsage = {}; for (const [key, value] of this.counters.entries()) { if (key.startsWith('ai.provider.')) { const provider = key.replace('ai.provider.', ''); providerUsage[provider] = value; } } return providerUsage; } getUniqueUsersCount() { const users = new Set(); for (const metricArray of this.metrics.values()) { for (const metric of metricArray) { if (metric.labels?.userId) { users.add(metric.labels.userId); } } } return users.size; } // ============================================================================================ // ALERTING AND MONITORING // ============================================================================================ checkAlerts() { const metrics = this.getSystemMetrics(); // High error rate alert if (metrics.requests.total > 0) { const errorRate = metrics.requests.failed / metrics.requests.total; if (errorRate > 0.1) { // 10% error rate this.emit('alert', { type: 'high_error_rate', message: `Error rate is ${(errorRate * 100).toFixed(1)}%`, severity: 'warning', metrics: { errorRate, totalRequests: metrics.requests.total } }); } } // High response time alert if (metrics.performance.p95ResponseTime > 5000) { // 5 seconds this.emit('alert', { type: 'high_response_time', message: `P95 response time is ${metrics.performance.p95ResponseTime}ms`, severity: 'warning', metrics: { p95ResponseTime: metrics.performance.p95ResponseTime } }); } // High memory usage alert if (metrics.system.memoryUsage > 1024 * 1024 * 1024) { // 1GB this.emit('alert', { type: 'high_memory_usage', message: `Memory usage is ${(metrics.system.memoryUsage / 1024 / 1024).toFixed(1)}MB`, severity: 'warning', metrics: { memoryUsage: metrics.system.memoryUsage } }); } } // ============================================================================================ // DATA EXPORT AND MONITORING // ============================================================================================ exportMetrics(format = 'json') { if (format === 'prometheus') { return this.exportPrometheusFormat(); } return JSON.stringify({ timestamp: Date.now(), metrics: this.getSystemMetrics(), counters: Object.fromEntries(this.counters), gauges: Object.fromEntries(this.gauges) }, null, 2); } exportPrometheusFormat() { let output = ''; // Export counters for (const [name, value] of this.counters.entries()) { const metricName = name.replace(/\./g, '_'); output += `# TYPE ${metricName} counter\n`; output += `${metricName} ${value}\n`; } // Export gauges for (const [name, value] of this.gauges.entries()) { const metricName = name.replace(/\./g, '_'); output += `# TYPE ${metricName} gauge\n`; output += `${metricName} ${value}\n`; } return output; } // ============================================================================================ // CLEANUP AND MAINTENANCE // ============================================================================================ startCleanupTimer() { setInterval(() => { this.cleanup(); this.updateSystemMetrics(); this.checkAlerts(); }, 60 * 1000); // Every minute } cleanup() { const cutoff = Date.now() - this.maxMetricsAge; // Clean up metrics data for (const [name, metricArray] of this.metrics.entries()) { const filtered = metricArray.filter(metric => metric.timestamp > cutoff); if (filtered.length === 0) { this.metrics.delete(name); } else { this.metrics.set(name, filtered); } } // Clean up performance data this.performanceData = this.performanceData.filter(p => p.timestamp > cutoff); logger.debug('Metrics cleanup completed', { metricsCount: this.metrics.size, performanceDataCount: this.performanceData.length }); } // Reset all metrics (for testing or maintenance) reset() { this.metrics.clear(); this.performanceData = []; this.counters.clear(); this.gauges.clear(); this.histograms.clear(); this.initializeMetrics(); logger.info('Metrics service reset'); } } export const metricsService = MetricsService.getInstance(); //# sourceMappingURL=MetricsService.js.map