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 => { // Per-IP tracking not yet available from Rust return new Map(); }, topIPs: (_limit?: number): Array<{ ip: string; count: number }> => { // Per-IP tracking not yet available from Rust return []; }, }; 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 => { // Throughput history not yet available from Rust return []; }, 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 => { return new Map(); }, }; public requests = { perSecond: (): number => { // Rust tracks connections, not HTTP requests (TCP-level proxy) return 0; }, perMinute: (): number => { return 0; }, total: (): number => { // Use total connections as a proxy for total requests return 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 }, }; }, }; }