/** * Unit: metric registration CONFORMANCE (the phase-15 conformance contract). * * Dev-side assertion: every metric NAME in the spec list is registered by * the `metrics` singleton with the right instrument kind. (The FULL * conformance — "scraped by Prometheus in staging" — is staging-only: the * staging cluster runs the OTel collector + Prometheus from * `infrastructure/observability/`; this env has no collector, so the * registration IS the locally-runnable assertion, per the task's test * requirement.) */ import { describe, expect, it } from "vitest"; import { METRIC_NAMES, metrics, m, registeredInstruments, setQueueDepth, setWsConnections } from "../metrics.js"; const SPEC_METRICS: { name: string; kind: "counter" | "histogram" | "gauge" }[] = [ { name: "http_request_duration_seconds", kind: "histogram" }, { name: "http_request_count", kind: "counter" }, { name: "db_query_duration_seconds", kind: "histogram" }, { name: "redis_command_duration_seconds", kind: "histogram" }, { name: "queue_depth", kind: "gauge" }, { name: "queue_job_duration_seconds", kind: "histogram" }, { name: "ws_active_connections", kind: "gauge" }, { name: "ws_messages_total", kind: "counter" }, { name: "search_query_duration_seconds", kind: "histogram" }, { name: "ai_tokens_total", kind: "counter" }, { name: "ai_request_duration_seconds", kind: "histogram" }, { name: "storage_ops_total", kind: "counter" }, { name: "storage_op_duration_seconds", kind: "histogram" }, { name: "error_total", kind: "counter" } ]; describe("metrics conformance", () => { it("registers EXACTLY the spec's metric names (no more, no fewer)", () => { const specNames = SPEC_METRICS.map((x) => x.name); expect([...METRIC_NAMES].sort()).toEqual([...specNames].sort()); }); it("every registered instrument matches its spec kind", () => { const reg = registeredInstruments(); for (const { name, kind } of SPEC_METRICS) { const entry = reg[name]; expect(entry, `missing registration for ${name}`).toBeDefined(); expect(entry!.kind).toBe(kind); expect(entry!.instrument).toBeTypeOf("object"); } }); it("the instruments are singletons (stable identity across accessors)", () => { // re-accessing the module singleton returns the same object references expect(metrics.httpRequestDuration).toBe(metrics.httpRequestDuration); expect(registeredInstruments()["http_request_count"]!.instrument).toBe(metrics.httpRequestCount); expect(registeredInstruments()["queue_depth"]!.instrument).toBe(metrics.queueDepthGauge); }); it("instruments created under the no-op global RE-BIND when a real provider is set (the critical no-op-binding guarantee)", async () => { // The apps' real hazard: an instrument accessed BEFORE initOtel sets the // SDK provider would bind to the no-op global and NEVER record. The lazy // registry must re-bind when the global provider is replaced. const { metrics: otelMetrics } = await import("@opentelemetry/api"); const { MeterProvider, InMemoryMetricExporter, PeriodicExportingMetricReader, AggregationTemporality } = await import( "@opentelemetry/sdk-metrics" ); // Reset the global to the no-op so this test starts from the apps' real // hazard state (a fresh process where nothing has booted the SDK yet). otelMetrics.disable(); // (1) early access under the no-op global → no-op instrument const before = metrics.errorTotal; m.error.record({ service: "factory-api", code: "X" }); // (2) the SDK boots (initOtel's setGlobalMeterProvider equivalent) const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 }); const mp = new MeterProvider({ readers: [reader] }); otelMetrics.setGlobalMeterProvider(mp); // (3) a NEW record now lands on the SDK provider — and the accessor // identity CHANGED (rebind), proving the re-creation happened. expect(metrics.errorTotal).not.toBe(before); m.error.record({ service: "factory-api", code: "X" }); m.http.record({ route: "/api/v1/pages/:id", method: "GET", status: "200" }, 0.012); const { resourceMetrics } = await reader.collect(); const all = resourceMetrics.scopeMetrics.flatMap((s) => s.metrics); const err = all.find((x) => x.descriptor.name === "error_total"); const http = all.find((x) => x.descriptor.name === "http_request_count"); expect(err, "error_total must be exported after rebind").toBeDefined(); expect(err!.dataPoints.length).toBe(1); expect(err!.dataPoints[0]!.value).toBe(1); // only the post-rebind record expect(http, "http_request_count must be exported after rebind").toBeDefined(); // (4) gauge values set before the rebind survive it (value sources are // independent of the instruments) — the callbacks re-bind too. setQueueDepth("webhooks", 7); const { resourceMetrics: rm2 } = await reader.collect(); const qd = rm2.scopeMetrics.flatMap((s) => s.metrics).find((x) => x.descriptor.name === "queue_depth"); expect(qd, "queue_depth gauge must re-bind after the provider swap").toBeDefined(); expect(qd!.dataPoints.length).toBe(1); expect(qd!.dataPoints[0]!.value).toBe(7); await mp.shutdown(); // restore the no-op global for the rest of the suite otelMetrics.disable(); }); it("the typed recorders are no-op-safe without a collector (dev contract)", () => { // No SDK registered in this process: every call must be a free no-op. expect(() => { m.http.record({ route: "/api/v1/pages/:id", method: "GET", status: "200" }, 0.012); m.db.record({ model: "block", operation: "findMany" }, 0.003); m.redis.record({ command: "GET" }, 0.001); m.queue.setDepth("webhooks", 42); m.queue.recordJob({ queue: "webhooks" }, 0.2); m.ws.setConnections(1234); m.ws.message(); m.search.record({ type: "bm25" }, 0.05); m.ai.tokens({ direction: "in", model: "gpt-4o-mini" }, 100); m.ai.request({ model: "gpt-4o-mini" }, 1.5); m.storage.op({ op: "put" }, 0.08); m.error.record({ service: "factory-api", code: "INTERNAL_ERROR" }); }).not.toThrow(); }); });