export type MetricLabels = Record; export interface MetricPoint { name: string; type: "counter" | "gauge" | "histogram"; value: number; count?: number; sum?: number; min?: number; max?: number; labels: MetricLabels; timestamp: number; } function labelKey(labels: MetricLabels): string { return Object.entries(labels) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .map(([key, value]) => `${key}=${String(value)}`) .join(","); } interface StoredMetric extends MetricPoint { key: string; } export class MetricsRegistry { private points = new Map(); constructor(private readonly clock: () => number = Date.now) {} increment(name: string, value = 1, labels: MetricLabels = {}): void { const key = `counter:${name}:${labelKey(labels)}`; const current = this.points.get(key); this.points.set(key, { key, name, type: "counter", value: (current?.value ?? 0) + value, labels: { ...labels }, timestamp: this.clock(), }); } gauge(name: string, value: number, labels: MetricLabels = {}): void { const key = `gauge:${name}:${labelKey(labels)}`; this.points.set(key, { key, name, type: "gauge", value, labels: { ...labels }, timestamp: this.clock(), }); } observe(name: string, value: number, labels: MetricLabels = {}): void { const key = `histogram:${name}:${labelKey(labels)}`; const current = this.points.get(key); const count = (current?.count ?? 0) + 1; const sum = (current?.sum ?? 0) + value; this.points.set(key, { key, name, type: "histogram", value: sum / count, count, sum, min: current?.min === undefined ? value : Math.min(current.min, value), max: current?.max === undefined ? value : Math.max(current.max, value), labels: { ...labels }, timestamp: this.clock(), }); } snapshot(): MetricPoint[] { return [...this.points.values()].map(({ key: _key, ...point }) => ({ ...point })); } reset(): void { this.points.clear(); } }