SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
4.1 KB · 125 lines typescript
Raw Blame History
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