/** * Unit: the Prometheus text renderer (TASK-0135). * * `renderPrometheus()` renders the process-global MeterProvider to Prometheus * text (version 0.0.4) for the apps' /metrics routes. Asserts: (a) it renders * the metric names + HELP/TYPE lines, (b) it returns "" when there is no * reader (disabled) — the no-op contract. */ import { afterEach, describe, expect, it } from "vitest"; import { metrics as otelMetrics } from "@opentelemetry/api"; import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; import { renderPrometheus, hasPrometheusReader, setPrometheusReader } from "../prometheus.js"; import { m } from "../metrics.js"; describe("renderPrometheus (app /metrics renderer)", () => { afterEach(async () => { // detach the reader + drop the global so the disabled-case test is clean setPrometheusReader(undefined); otelMetrics.disable(); }); it("returns '' when no reader is attached (OTel disabled — the no-op contract)", async () => { setPrometheusReader(undefined); expect(hasPrometheusReader()).toBe(false); expect(await renderPrometheus()).toBe(""); }); it("renders the registered spec metrics as Prometheus text 0.0.4", async () => { otelMetrics.disable(); const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 }); // 0.57.0 ExporterConfig has no withoutScopeInfo/withoutTargetInfo (2.x-only). const prom = new PrometheusExporter({ preventServerStart: true }); const mp = new MeterProvider({ readers: [reader, prom] }); otelMetrics.setGlobalMeterProvider(mp); setPrometheusReader(prom); // exercise a counter + a histogram + a gauge (the three Prometheus types) m.error.record({ service: "factory-api", code: "NOT_FOUND" }); m.http.record({ route: "/api/v1/pages/:id", method: "GET", status: "200" }, 0.012); m.queue.setDepth("webhooks", 7); const text = await renderPrometheus(); expect(text).toContain("# HELP error_total"); expect(text).toContain("# TYPE error_total counter"); // label order is insertion order (toAttrs spreads in the order given) expect(text).toContain('error_total{service="factory-api",code="NOT_FOUND"} 1'); expect(text).toContain("# TYPE http_request_duration_seconds histogram"); expect(text).toContain("http_request_duration_seconds_bucket"); expect(text).toContain('http_request_duration_seconds_sum{route="/api/v1/pages/:id",method="GET",status="200"} 0.012'); // the explicit buckets (in SECONDS — the unit-consistency contract that // makes histogram_quantile correct) expect(text).toContain('http_request_duration_seconds_bucket{route="/api/v1/pages/:id",method="GET",status="200",le="0.005"} 0'); expect(text).toContain('le="+Inf"'); expect(text).toContain("# TYPE queue_depth gauge"); expect(text).toContain('queue_depth{queue="webhooks"} 7'); await mp.shutdown(); }); });