tryaii-mcp-server
Version:
TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence
343 lines • 14.1 kB
JavaScript
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