import { logInternalError } from "../utils/internal-error.ts"; import { Counter, Gauge, Histogram, type Metric, type MetricLabels, type MetricSnapshot } from "./metrics-primitives.ts"; const METRIC_NAME_PATTERN = /^crew\.[a-z]+\.[a-z][a-z_]*$/; function assertMetricName(name: string): void { if (!METRIC_NAME_PATTERN.test(name)) throw new Error(`Invalid metric name '${name}'. Expected crew...`); } export class MetricRegistry { private metrics = new Map(); registerCounter(name: string, description: string): Counter { assertMetricName(name); if (this.metrics.has(name)) throw new Error(`Metric '${name}' is already registered.`); const metric = new Counter(name, description); this.metrics.set(name, metric); return metric; } registerGauge(name: string, description: string): Gauge { assertMetricName(name); if (this.metrics.has(name)) throw new Error(`Metric '${name}' is already registered.`); const metric = new Gauge(name, description); this.metrics.set(name, metric); return metric; } registerHistogram(name: string, description: string, buckets?: number[]): Histogram { assertMetricName(name); if (this.metrics.has(name)) throw new Error(`Metric '${name}' is already registered.`); const metric = new Histogram(name, description, buckets); this.metrics.set(name, metric); return metric; } counter(name: string, description: string): Counter { const existing = this.metrics.get(name); if (existing instanceof Counter) { if (existing.description !== description) { logInternalError( "metric-registry.counter", new Error("description mismatch"), `name='${name}' original='${existing.description}'`, ); } return existing; } if (existing) throw new Error(`Metric '${name}' is not a counter.`); return this.registerCounter(name, description); } gauge(name: string, description: string): Gauge { const existing = this.metrics.get(name); if (existing instanceof Gauge) { if (existing.description !== description) { logInternalError( "metric-registry.gauge", new Error("description mismatch"), `name='${name}' original='${existing.description}'`, ); } return existing; } if (existing) throw new Error(`Metric '${name}' is not a gauge.`); return this.registerGauge(name, description); } histogram(name: string, description: string, buckets?: number[]): Histogram { const existing = this.metrics.get(name); if (existing instanceof Histogram) { if (existing.description !== description) { logInternalError( "metric-registry.histogram", new Error("description mismatch"), `name='${name}' original='${existing.description}'`, ); } return existing; } if (existing) throw new Error(`Metric '${name}' is not a histogram.`); return this.registerHistogram(name, description, buckets); } get(name: string): Metric | undefined { return this.metrics.get(name); } snapshot(): MetricSnapshot[] { return [...this.metrics.values()].map((metric) => metric.snapshot()); } /** * LABEL-GC: release stored label values matching `predicate` across every * registered metric; returns the total number of series dropped. Dispatches * polymorphically to {@link Metric.releaseLabelValues}, so Histograms (not * run-scoped) are untouched and no private Map is reached into from here. * A release is not an eviction — `cardinalityEvictions` is unaffected. */ releaseLabels(predicate: (labels: MetricLabels) => boolean): number { let released = 0; for (const metric of this.metrics.values()) released += metric.releaseLabelValues(predicate); return released; } dispose(): void { this.metrics.clear(); } } export function createMetricRegistry(): MetricRegistry { return new MetricRegistry(); }