1/** 2 * @factory/observability — Prometheus text exposition for the app `/metrics` 3 * endpoints (TASK-0135). 4 * 5 * Every app exposes its spec metrics in Prometheus text format on its OWN 6 * port (the scrape config targets each app's `/metrics`). This module renders 7 * the process-global `MeterProvider` (the one `initOtel` registers, which the 8 * `metrics` singleton instruments live on) into Prometheus text. 9 * 10 * Why not the exporter's own HTTP server? `PrometheusExporter` wants to run 11 * its own `http.Server` on a dedicated port — but each app already owns its 12 * port (api :4000, worker :4003, realtime :4001, web :3000) and Prometheus 13 * must scrape the SAME port the app serves. So the reader is attached to the 14 * MeterProvider with `preventServerStart: true` (no server of its own), and 15 * each app mounts `renderPrometheus()` on a `/metrics` route. When OTel is 16 * disabled there is no MeterProvider → `renderPrometheus()` returns "" and 17 * the app's `/metrics` route serves its fallback (the realtime app's own 18 * registry; the others a 204/503). 19 * 20 * OTel JS 2.x surface (T-190b): `PrometheusExporter extends MetricReader`; 21 * with `preventServerStart` it is a pure pull-reader and its `collect()` 22 * (inherited from `MetricReader`) returns the `CollectionResult` we serialize 23 * with the package's `PrometheusSerializer` (0.223.0: 5-arg constructor 24 * `(prefix?, appendTimestamp?, withResourceConstantLabels?, withoutTargetInfo?, 25 * withoutScopeInfo?)`, `serialize(resourceMetrics)`). We pass 26 * `withoutScopeInfo: true` because the 2.x default inlines `otel_scope_name` 27 * into every data point's labels, which would fragment the bare-metric-name 28 * contract the dashboard-completeness script greps. The leading `target_info` 29 * gauge line is harmless for scrapers, and the dashboards never render it. 30 */ 31import { metrics as metricsApi } from "@opentelemetry/api"; 32import type { MeterProvider as SdkMeterProvider } from "@opentelemetry/sdk-metrics"; 33import type { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; 34 35/** 36 * The Prometheus reader `initOtel` attaches (server-less). Kept so 37 * `renderPrometheus()` can `collect()` from the live MeterProvider. Exported 38 * for the conformance tests; `undefined` when OTel is disabled. 39 */ 40export let prometheusReader: PrometheusExporter | undefined; 41 42/** Set by `initOtel` when enabled (undefined stays when disabled). */ 43export function setPrometheusReader(reader: PrometheusExporter | undefined): void { 44 prometheusReader = reader; 45} 46 47/** 48 * Render the process-global metrics as Prometheus text (version 0.0.4). 49 * 50 * Returns "" when OTel is disabled (no MeterProvider / no reader) — callers 51 * treat that as "nothing to scrape" and serve their own fallback. Never 52 * throws: a render failure degrades to "" (a bad scrape must not take down a 53 * live app's /metrics handler). 54 */ 55export async function renderPrometheus(): Promise<string> { 56 if (!prometheusReader) return ""; 57 try { 58 const { PrometheusSerializer } = await import("@opentelemetry/exporter-prometheus"); 59 const { resourceMetrics } = await prometheusReader.collect(); 60 // 0.223.0 serializer: (prefix?, appendTimestamp?, withResourceConstantLabels?, 61 // withoutTargetInfo?, withoutScopeInfo?). No prefix, no timestamp, target info 62 // kept; withoutScopeInfo=true keeps data points at bare metric names (the 2.x 63 // default would inline otel_scope_name and break the dashboard grep contract). 64 const serializer = new PrometheusSerializer(undefined, false, undefined, false, true); 65 return serializer.serialize(resourceMetrics); 66 } catch { 67 return ""; 68 } 69} 70 71/** 72 * True when the global MeterProvider is the SDK one (i.e. OTel enabled and 73 * metrics will be rendered). Convenience for an app's /metrics route to decide 74 * whether to serve OTel text or its fallback. 75 */ 76export function hasPrometheusReader(): boolean { 77 return prometheusReader !== undefined; 78} 79 80// Re-export for apps that want to build a reader themselves (advanced use). 81export type { SdkMeterProvider, PrometheusExporter }; 82export { metricsApi }; 83