import { EventEmitter } from 'events';
import { Logger } from '../utils/logger';
export interface Metric {
name: string;
value: number;
timestamp: Date;
tags?: Record<string, string>;
type: 'counter' | 'gauge' | 'histogram' | 'timer';
}
export interface MetricsSummary {
totalRequests: number;
averageResponseTime: number;
errorRate: number;
toolCallsPerMinute: number;
cacheHitRate: number;
databaseConnections: number;
memoryUsage: number;
uptime: number;
}
export class MetricsCollector extends EventEmitter {
private metrics: Map<string, Metric[]> = new Map();
private counters: Map<string, number> = new Map();
private gauges: Map<string, number> = new Map();
private timers: Map<string, number[]> = new Map();
private startTime: Date = new Date();
constructor() {
super();
this.setupPeriodicCollection();
}
// Counter metrics (increment only)
incrementCounter(name: string, value: number = 1, tags?: Record<string, string>): void {
const currentValue = this.counters.get(name) || 0;
this.counters.set(name, currentValue + value);
this.recordMetric({
name,
value: currentValue + value,
timestamp: new Date(),
tags,
type: 'counter'
});
}
// Gauge metrics (can go up or down)
setGauge(name: string, value: number, tags?: Record<string, string>): void {
this.gauges.set(name, value);
this.recordMetric({
name,
value,
timestamp: new Date(),
tags,
type: 'gauge'
});
}
// Timer metrics (for measuring duration)
startTimer(name: string): () => void {
const startTime = Date.now();
return () => {
const duration = Date.now() - startTime;
this.recordTimer(name, duration);
};
}
recordTimer(name: string, duration: number, tags?: Record<string, string>): void {
const timers = this.timers.get(name) || [];
timers.push(duration);
this.timers.set(name, timers);
// Keep only last 1000 measurements
if (timers.length > 1000) {
timers.shift();
}
this.recordMetric({
name,
value: duration,
timestamp: new Date(),
tags,
type: 'timer'
});
}
// Histogram metrics (for measuring distributions)
recordHistogram(name: string, value: number, tags?: Record<string, string>): void {
this.recordMetric({
name,
value,
timestamp: new Date(),
tags,
type: 'histogram'
});
}
// Get metrics summary
getMetricsSummary(): MetricsSummary {
const totalRequests = this.counters.get('http_requests_total') || 0;
const errors = this.counters.get('http_errors_total') || 0;
const toolCalls = this.counters.get('tool_calls_total') || 0;
const cacheHits = this.counters.get('cache_hits_total') || 0;
const cacheMisses = this.counters.get('cache_misses_total') || 0;
const responseTimers = this.timers.get('http_request_duration') || [];
const averageResponseTime = responseTimers.length > 0
? responseTimers.reduce((a, b) => a + b, 0) / responseTimers.length
: 0;
const errorRate = totalRequests > 0 ? (errors / totalRequests) * 100 : 0;
const cacheHitRate = (cacheHits + cacheMisses) > 0
? (cacheHits / (cacheHits + cacheMisses)) * 100
: 0;
const uptime = (Date.now() - this.startTime.getTime()) / 1000;
// Get last minute of tool calls
const oneMinuteAgo = Date.now() - 60000;
const recentToolCalls = this.getMetricsSince('tool_calls_total', oneMinuteAgo).length;
return {
totalRequests,
averageResponseTime,
errorRate,
toolCallsPerMinute: recentToolCalls,
cacheHitRate,
databaseConnections: this.gauges.get('database_connections') || 0,
memoryUsage: this.gauges.get('memory_usage_mb') || 0,
uptime
};
}
// Get specific metric
getMetric(name: string): Metric[] {
return this.metrics.get(name) || [];
}
// Get metrics since a specific time
getMetricsSince(name: string, timestamp: number): Metric[] {
const metrics = this.metrics.get(name) || [];
return metrics.filter(metric => metric.timestamp.getTime() >= timestamp);
}
// Get all counter values
getCounters(): Map<string, number> {
return new Map(this.counters);
}
// Get all gauge values
getGauges(): Map<string, number> {
return new Map(this.gauges);
}
// Get timer statistics
getTimerStats(name: string): {
count: number;
min: number;
max: number;
avg: number;
p50: number;
p95: number;
p99: number;
} | null {
const timers = this.timers.get(name);
if (!timers || timers.length === 0) {
return null;
}
const sorted = [...timers].sort((a, b) => a - b);
const count = sorted.length;
const min = sorted[0];
const max = sorted[count - 1];
const avg = sorted.reduce((a, b) => a + b, 0) / count;
const p50 = sorted[Math.floor(count * 0.5)];
const p95 = sorted[Math.floor(count * 0.95)];
const p99 = sorted[Math.floor(count * 0.99)];
return { count, min, max, avg, p50, p95, p99 };
}
// Export metrics in Prometheus format
exportPrometheusFormat(): string {
let output = '';
// Export counters
this.counters.forEach((value, name) => {
output += `# TYPE ${name} counter\n`;
output += `${name} ${value}\n\n`;
});
// Export gauges
this.gauges.forEach((value, name) => {
output += `# TYPE ${name} gauge\n`;
output += `${name} ${value}\n\n`;
});
// Export timer statistics
this.timers.forEach((values, name) => {
if (values.length > 0) {
const stats = this.getTimerStats(name);
if (stats) {
output += `# TYPE ${name}_duration_seconds summary\n`;
output += `${name}_duration_seconds_count ${stats.count}\n`;
output += `${name}_duration_seconds_sum ${(stats.avg * stats.count) / 1000}\n`;
output += `${name}_duration_seconds{quantile="0.5"} ${stats.p50 / 1000}\n`;
output += `${name}_duration_seconds{quantile="0.95"} ${stats.p95 / 1000}\n`;
output += `${name}_duration_seconds{quantile="0.99"} ${stats.p99 / 1000}\n\n`;
}
}
});
return output;
}
// Record a metric
private recordMetric(metric: Metric): void {
const metrics = this.metrics.get(metric.name) || [];
metrics.push(metric);
// Keep only last 10000 metrics per name
if (metrics.length > 10000) {
metrics.shift();
}
this.metrics.set(metric.name, metrics);
// Emit metric event for real-time monitoring
this.emit('metric', metric);
// Log high-level metrics
if (metric.type === 'counter' && metric.value % 100 === 0) {
Logger.info(`Metric milestone: ${metric.name} = ${metric.value}`);
}
}
// Setup periodic collection of system metrics
private setupPeriodicCollection(): void {
setInterval(() => {
this.collectSystemMetrics();
}, 30000); // Every 30 seconds
setInterval(() => {
this.cleanupOldMetrics();
}, 300000); // Every 5 minutes
}
// Collect system metrics
private collectSystemMetrics(): void {
// Memory usage
const memUsage = process.memoryUsage();
this.setGauge('memory_usage_mb', Math.round(memUsage.heapUsed / 1024 / 1024));
this.setGauge('memory_total_mb', Math.round(memUsage.heapTotal / 1024 / 1024));
this.setGauge('memory_external_mb', Math.round(memUsage.external / 1024 / 1024));
// CPU usage (approximate)
const cpuUsage = process.cpuUsage();
this.setGauge('cpu_user_microseconds', cpuUsage.user);
this.setGauge('cpu_system_microseconds', cpuUsage.system);
// Event loop lag
const start = process.hrtime.bigint();
setImmediate(() => {
const lag = Number(process.hrtime.bigint() - start) / 1000000; // Convert to milliseconds
this.setGauge('event_loop_lag_ms', lag);
});
// Uptime
this.setGauge('uptime_seconds', process.uptime());
}
// Cleanup old metrics to prevent memory leaks
private cleanupOldMetrics(): void {
const oneHourAgo = Date.now() - 3600000; // 1 hour ago
this.metrics.forEach((metrics, name) => {
const filtered = metrics.filter(metric => metric.timestamp.getTime() > oneHourAgo);
if (filtered.length !== metrics.length) {
this.metrics.set(name, filtered);
}
});
Logger.debug('Cleaned up old metrics');
}
// Reset all metrics (useful for testing)
reset(): void {
this.metrics.clear();
this.counters.clear();
this.gauges.clear();
this.timers.clear();
this.startTime = new Date();
}
}
// Singleton instance
export const metricsCollector = new MetricsCollector();
// Middleware for Express to collect HTTP metrics
export const metricsMiddleware = (req: any, res: any, next: any): void => {
const startTime = Date.now();
// Increment request counter
metricsCollector.incrementCounter('http_requests_total', 1, {
method: req.method,
route: req.route?.path || req.path
});
// Override res.end to collect response metrics
const originalEnd = res.end;
res.end = function(this: any, ...args: any[]) {
const duration = Date.now() - startTime;
// Record response time
metricsCollector.recordTimer('http_request_duration', duration, {
method: req.method,
status_code: res.statusCode.toString()
});
// Count errors
if (res.statusCode >= 400) {
metricsCollector.incrementCounter('http_errors_total', 1, {
method: req.method,
status_code: res.statusCode.toString()
});
}
// Call original end method
return originalEnd.apply(this, args);
};
next();
};
// Function to create tool call metrics
export const recordToolCall = (toolName: string, duration: number, success: boolean, userId?: string): void => {
metricsCollector.incrementCounter('tool_calls_total', 1, {
tool_name: toolName,
success: success.toString(),
user_id: userId || 'unknown'
});
metricsCollector.recordTimer('tool_call_duration', duration, {
tool_name: toolName
});
if (!success) {
metricsCollector.incrementCounter('tool_call_errors_total', 1, {
tool_name: toolName
});
}
};
// Function to record cache metrics
export const recordCacheMetrics = (operation: 'hit' | 'miss' | 'set' | 'delete', key: string): void => {
metricsCollector.incrementCounter(`cache_${operation}s_total`, 1, {
operation,
key_type: key.split(':')[0] || 'unknown'
});
};
// Function to record database metrics
export const recordDatabaseMetrics = (operation: string, duration: number, success: boolean): void => {
metricsCollector.incrementCounter('database_operations_total', 1, {
operation,
success: success.toString()
});
metricsCollector.recordTimer('database_operation_duration', duration, {
operation
});
if (!success) {
metricsCollector.incrementCounter('database_errors_total', 1, {
operation
});
}
};