/** * MetricsCollector — single source of truth for all ops metrics. * * Tracks counters (messages, errors, active requests), model call latency/tokens, * event stream, and active request durations. All synchronous, no external deps. */ // ── Types ──────────────────────────────────────────────────────── export interface ModelCallEntry { timestamp: number; provider: string; latency: number; inputTokens: number; outputTokens: number; tokensPerSec: number; error?: string; } export interface EventEntry { timestamp: number; level: 'info' | 'warn' | 'error'; source: string; message: string; context?: Record; } export interface ActiveRequestInfo { id: string; sessionId: string; channel: string; startedAt: number; durationMs: number; } export interface MetricsSnapshot { messagesProcessed: number; errors: number; activeRequests: number; uptime: number; modelCalls: { total: number; avgLatency: number; errorRate: number; recentCalls: ModelCallEntry[]; }; queueDepth: number; } export interface MetricsCollectorConfig { getQueueDepth?: () => number; modelCallBufferSize?: number; eventBufferSize?: number; } // ── Implementation ─────────────────────────────────────────────── interface ActiveRequest { sessionId: string; channel: string; startedAt: number; } export class MetricsCollector { private _messagesProcessed = 0; private _errors = 0; private _activeRequestCount = 0; private _startTime: number; private _modelCalls: ModelCallEntry[] = []; private _modelCallBufferSize: number; private _events: EventEntry[] = []; private _eventBufferSize: number; private _activeRequests: Map = new Map(); private _getQueueDepth: () => number; constructor(config?: MetricsCollectorConfig) { this._startTime = Date.now(); this._getQueueDepth = config?.getQueueDepth ?? (() => 0); this._modelCallBufferSize = config?.modelCallBufferSize ?? 200; this._eventBufferSize = config?.eventBufferSize ?? 500; } // ── Counters ───────────────────────────────────────────────── incrementMessages(): void { this._messagesProcessed++; } incrementErrors(): void { this._errors++; } get messagesProcessed(): number { return this._messagesProcessed; } get errors(): number { return this._errors; } get activeRequestCount(): number { return this._activeRequestCount; } // ── Model call ring buffer ─────────────────────────────────── recordModelCall(entry: ModelCallEntry): void { this._modelCalls.push(entry); if (this._modelCalls.length > this._modelCallBufferSize) { this._modelCalls.shift(); } } getModelMetrics(): ModelCallEntry[] { return [...this._modelCalls]; } // ── Event ring buffer ──────────────────────────────────────── recordEvent(event: EventEntry): void { this._events.push(event); if (this._events.length > this._eventBufferSize) { this._events.shift(); } } getEvents(opts?: { level?: string; limit?: number }): EventEntry[] { let filtered = this._events; if (opts?.level) { filtered = filtered.filter(e => e.level === opts.level); } // Newest first const reversed = [...filtered].reverse(); if (opts?.limit && opts.limit > 0) { return reversed.slice(0, opts.limit); } return reversed; } // ── Active request tracking ────────────────────────────────── startRequest(id: string, info: { sessionId: string; channel: string }): void { this._activeRequests.set(id, { sessionId: info.sessionId, channel: info.channel, startedAt: Date.now(), }); this._activeRequestCount++; } endRequest(id: string): void { if (this._activeRequests.delete(id)) { this._activeRequestCount--; } } getActiveRequests(): ActiveRequestInfo[] { const now = Date.now(); const result: ActiveRequestInfo[] = []; for (const [id, req] of this._activeRequests) { result.push({ id, sessionId: req.sessionId, channel: req.channel, startedAt: req.startedAt, durationMs: now - req.startedAt, }); } return result; } // ── Snapshot ───────────────────────────────────────────────── getSnapshot(): MetricsSnapshot { const calls = this._modelCalls; const totalCalls = calls.length; const errorCalls = calls.filter(c => c.error).length; const avgLatency = totalCalls > 0 ? calls.reduce((sum, c) => sum + c.latency, 0) / totalCalls : 0; const errorRate = totalCalls > 0 ? errorCalls / totalCalls : 0; return { messagesProcessed: this._messagesProcessed, errors: this._errors, activeRequests: this._activeRequestCount, uptime: Math.floor((Date.now() - this._startTime) / 1000), modelCalls: { total: totalCalls, avgLatency: Math.round(avgLatency), errorRate: Math.round(errorRate * 10000) / 10000, recentCalls: calls.slice(-20), }, queueDepth: this._getQueueDepth(), }; } }