/** Lightweight in-process metrics: counters, gauges, sliding-window rates, latency reservoirs. Prometheus text export. */ class Window { private buckets: number[]; private idx = 0; private last: number; constructor( private seconds: number, now = Date.now(), ) { this.buckets = new Array(seconds).fill(0); this.last = Math.floor(now / 1000); } private roll(now: number) { const sec = Math.floor(now / 1000); let steps = sec - this.last; if (steps <= 0) return; if (steps > this.seconds) steps = this.seconds; for (let i = 0; i < steps; i++) { this.idx = (this.idx + 1) % this.seconds; this.buckets[this.idx] = 0; } this.last = sec; } add(n: number, now = Date.now()) { this.roll(now); this.buckets[this.idx]! += n; } sum(now = Date.now()): number { this.roll(now); return this.buckets.reduce((a, b) => a + b, 0); } perSecond(now = Date.now()): number { return this.sum(now) / this.seconds; } } export class Reservoir { private values: number[] = []; constructor(private size = 512) {} add(v: number) { if (this.values.length < this.size) this.values.push(v); else this.values[Math.floor(Math.random() * this.size)] = v; } quantile(q: number): number | null { if (!this.values.length) return null; const s = [...this.values].sort((a, b) => a - b); return s[Math.min(s.length - 1, Math.floor(q * s.length))] ?? null; } count() { return this.values.length; } } export class Telemetry { private counters = new Map(); private gauges = new Map(); private windows = new Map(); private reservoirs = new Map(); readonly startedAt = Date.now(); inc(name: string, n = 1, labels?: Record) { const key = this.key(name, labels); this.counters.set(key, (this.counters.get(key) ?? 0) + n); let w = this.windows.get(key); if (!w) { w = new Window(60); this.windows.set(key, w); } w.add(n); } gauge(name: string, v: number, labels?: Record) { this.gauges.set(this.key(name, labels), v); } observe(name: string, v: number, labels?: Record) { const key = this.key(name, labels); let r = this.reservoirs.get(key); if (!r) { r = new Reservoir(); this.reservoirs.set(key, r); } r.add(v); } counter(name: string, labels?: Record): number { return this.counters.get(this.key(name, labels)) ?? 0; } rate(name: string, labels?: Record): number { return this.windows.get(this.key(name, labels))?.perSecond() ?? 0; } lastMinute(name: string, labels?: Record): number { return this.windows.get(this.key(name, labels))?.sum() ?? 0; } quantile(name: string, q: number, labels?: Record): number | null { return this.reservoirs.get(this.key(name, labels))?.quantile(q) ?? null; } private key(name: string, labels?: Record) { if (!labels) return name; const l = Object.entries(labels) .sort() .map(([k, v]) => `${k}="${v.replace(/"/g, '\\"')}"`) .join(","); return `${name}{${l}}`; } /** Prometheus exposition format. */ prometheus(): string { const lines: string[] = []; for (const [k, v] of this.counters) lines.push(`ma_${k} ${v}`); for (const [k, v] of this.gauges) lines.push(`ma_${k} ${v}`); for (const [k, r] of this.reservoirs) { const base = k.includes("{") ? k.replace("{", "_seconds{") : `${k}_seconds`; for (const q of [0.5, 0.95, 0.99]) { const val = r.quantile(q); if (val != null) lines.push(`ma_${base.includes("{") ? base.replace("}", `,quantile="${q}"}`) : `${base}{quantile="${q}"}`} ${val / 1000}`); } } lines.push(`ma_uptime_seconds ${(Date.now() - this.startedAt) / 1000}`); const mem = process.memoryUsage(); lines.push(`ma_process_rss_bytes ${mem.rss}`, `ma_process_heap_used_bytes ${mem.heapUsed}`); return lines.join("\n") + "\n"; } } export const telemetry = new Telemetry();