import type { IMetrics, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js'; import type { RustProxyBridge } from './rust-proxy-bridge.js'; /** * Adapts Rust JSON metrics to the IMetrics interface. * * Polls the Rust binary periodically via the bridge and caches the result. * All IMetrics getters read from the cache synchronously. * * Rust Metrics JSON fields (camelCase via serde): * activeConnections, totalConnections, bytesIn, bytesOut, * throughputInBytesPerSec, throughputOutBytesPerSec, * routes: { [routeName]: { activeConnections, totalConnections, bytesIn, bytesOut, ... } } */ export class RustMetricsAdapter implements IMetrics { private bridge: RustProxyBridge; private cache: any = null; private pollTimer: ReturnType | null = null; private pollIntervalMs: number; constructor(bridge: RustProxyBridge, pollIntervalMs = 1000) { this.bridge = bridge; this.pollIntervalMs = pollIntervalMs; } /** * Poll Rust for metrics once. Can be awaited to ensure cache is fresh. */ public async poll(): Promise { try { this.cache = await this.bridge.getMetrics(); } catch { // Ignore poll errors (bridge may be shutting down) } } public startPolling(): void { if (this.pollTimer) return; // Immediate first poll so cache is populated ASAP this.poll(); this.pollTimer = setInterval(() => { this.poll(); }, this.pollIntervalMs); if (this.pollTimer.unref) { this.pollTimer.unref(); } } public stopPolling(): void { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } } // --- IMetrics implementation --- public connections = { active: (): number => { return this.cache?.activeConnections ?? 0; }, total: (): number => { return this.cache?.totalConnections ?? 0; }, byRoute: (): Map => { const result = new Map(); if (this.cache?.routes) { for (const [name, rm] of Object.entries(this.cache.routes)) { result.set(name, (rm as any).activeConnections ?? 0); } } return result; }, byIP: (): Map => { const result = new Map(); if (this.cache?.ips) { for (const [ip, im] of Object.entries(this.cache.ips)) { result.set(ip, (im as any).activeConnections ?? 0); } } return result; }, topIPs: (limit: number = 10): Array<{ ip: string; count: number }> => { const result: Array<{ ip: string; count: number }> = []; if (this.cache?.ips) { for (const [ip, im] of Object.entries(this.cache.ips)) { result.push({ ip, count: (im as any).activeConnections ?? 0 }); } } result.sort((a, b) => b.count - a.count); return result.slice(0, limit); }, }; public throughput = { instant: (): IThroughputData => { return { in: this.cache?.throughputInBytesPerSec ?? 0, out: this.cache?.throughputOutBytesPerSec ?? 0, }; }, recent: (): IThroughputData => { return { in: this.cache?.throughputRecentInBytesPerSec ?? 0, out: this.cache?.throughputRecentOutBytesPerSec ?? 0, }; }, average: (): IThroughputData => { return this.throughput.instant(); }, custom: (_seconds: number): IThroughputData => { return this.throughput.instant(); }, history: (seconds: number): Array => { if (!this.cache?.throughputHistory) return []; return this.cache.throughputHistory.slice(-seconds).map((p: any) => ({ timestamp: p.timestampMs, in: p.bytesIn, out: p.bytesOut, })); }, byRoute: (_windowSeconds?: number): Map => { const result = new Map(); if (this.cache?.routes) { for (const [name, rm] of Object.entries(this.cache.routes)) { result.set(name, { in: (rm as any).throughputInBytesPerSec ?? 0, out: (rm as any).throughputOutBytesPerSec ?? 0, }); } } return result; }, byIP: (_windowSeconds?: number): Map => { const result = new Map(); if (this.cache?.ips) { for (const [ip, im] of Object.entries(this.cache.ips)) { result.set(ip, { in: (im as any).throughputInBytesPerSec ?? 0, out: (im as any).throughputOutBytesPerSec ?? 0, }); } } return result; }, }; public requests = { perSecond: (): number => { return this.cache?.httpRequestsPerSec ?? 0; }, perMinute: (): number => { return (this.cache?.httpRequestsPerSecRecent ?? 0) * 60; }, total: (): number => { return this.cache?.totalHttpRequests ?? this.cache?.totalConnections ?? 0; }, }; public totals = { bytesIn: (): number => { return this.cache?.bytesIn ?? 0; }, bytesOut: (): number => { return this.cache?.bytesOut ?? 0; }, connections: (): number => { return this.cache?.totalConnections ?? 0; }, }; public percentiles = { connectionDuration: (): { p50: number; p95: number; p99: number } => { return { p50: 0, p95: 0, p99: 0 }; }, bytesTransferred: (): { in: { p50: number; p95: number; p99: number }; out: { p50: number; p95: number; p99: number }; } => { return { in: { p50: 0, p95: 0, p99: 0 }, out: { p50: 0, p95: 0, p99: 0 }, }; }, }; }