spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1/** Lightweight in-process metrics: counters, gauges, sliding-window rates, latency reservoirs. Prometheus text export. */23class Window {4 private buckets: number[];5 private idx = 0;6 private last: number;7 constructor(8 private seconds: number,9 now = Date.now(),10 ) {11 this.buckets = new Array(seconds).fill(0);12 this.last = Math.floor(now / 1000);13 }14 private roll(now: number) {15 const sec = Math.floor(now / 1000);16 let steps = sec - this.last;17 if (steps <= 0) return;18 if (steps > this.seconds) steps = this.seconds;19 for (let i = 0; i < steps; i++) {20 this.idx = (this.idx + 1) % this.seconds;21 this.buckets[this.idx] = 0;22 }23 this.last = sec;24 }25 add(n: number, now = Date.now()) {26 this.roll(now);27 this.buckets[this.idx]! += n;28 }29 sum(now = Date.now()): number {30 this.roll(now);31 return this.buckets.reduce((a, b) => a + b, 0);32 }33 perSecond(now = Date.now()): number {34 return this.sum(now) / this.seconds;35 }36}3738export class Reservoir {39 private values: number[] = [];40 constructor(private size = 512) {}41 add(v: number) {42 if (this.values.length < this.size) this.values.push(v);43 else this.values[Math.floor(Math.random() * this.size)] = v;44 }45 quantile(q: number): number | null {46 if (!this.values.length) return null;47 const s = [...this.values].sort((a, b) => a - b);48 return s[Math.min(s.length - 1, Math.floor(q * s.length))] ?? null;49 }50 count() {51 return this.values.length;52 }53}5455export class Telemetry {56 private counters = new Map<string, number>();57 private gauges = new Map<string, number>();58 private windows = new Map<string, Window>();59 private reservoirs = new Map<string, Reservoir>();60 readonly startedAt = Date.now();6162 inc(name: string, n = 1, labels?: Record<string, string>) {63 const key = this.key(name, labels);64 this.counters.set(key, (this.counters.get(key) ?? 0) + n);65 let w = this.windows.get(key);66 if (!w) {67 w = new Window(60);68 this.windows.set(key, w);69 }70 w.add(n);71 }72 gauge(name: string, v: number, labels?: Record<string, string>) {73 this.gauges.set(this.key(name, labels), v);74 }75 observe(name: string, v: number, labels?: Record<string, string>) {76 const key = this.key(name, labels);77 let r = this.reservoirs.get(key);78 if (!r) {79 r = new Reservoir();80 this.reservoirs.set(key, r);81 }82 r.add(v);83 }84 counter(name: string, labels?: Record<string, string>): number {85 return this.counters.get(this.key(name, labels)) ?? 0;86 }87 rate(name: string, labels?: Record<string, string>): number {88 return this.windows.get(this.key(name, labels))?.perSecond() ?? 0;89 }90 lastMinute(name: string, labels?: Record<string, string>): number {91 return this.windows.get(this.key(name, labels))?.sum() ?? 0;92 }93 quantile(name: string, q: number, labels?: Record<string, string>): number | null {94 return this.reservoirs.get(this.key(name, labels))?.quantile(q) ?? null;95 }96 private key(name: string, labels?: Record<string, string>) {97 if (!labels) return name;98 const l = Object.entries(labels)99 .sort()100 .map(([k, v]) => `${k}="${v.replace(/"/g, '\\"')}"`)101 .join(",");102 return `${name}{${l}}`;103 }104105 /** Prometheus exposition format. */106 prometheus(): string {107 const lines: string[] = [];108 for (const [k, v] of this.counters) lines.push(`ma_${k} ${v}`);109 for (const [k, v] of this.gauges) lines.push(`ma_${k} ${v}`);110 for (const [k, r] of this.reservoirs) {111 const base = k.includes("{") ? k.replace("{", "_seconds{") : `${k}_seconds`;112 for (const q of [0.5, 0.95, 0.99]) {113 const val = r.quantile(q);114 if (val != null) lines.push(`ma_${base.includes("{") ? base.replace("}", `,quantile="${q}"}`) : `${base}{quantile="${q}"}`} ${val / 1000}`);115 }116 }117 lines.push(`ma_uptime_seconds ${(Date.now() - this.startedAt) / 1000}`);118 const mem = process.memoryUsage();119 lines.push(`ma_process_rss_bytes ${mem.rss}`, `ma_process_heap_used_bytes ${mem.heapUsed}`);120 return lines.join("\n") + "\n";121 }122}123124export const telemetry = new Telemetry();125