Skip to main content

firstpass_proxy/
metrics.rs

1//! Prometheus metrics: install the global recorder once per process and serve `GET /metrics`
2//! for scraping. Real signals only — latency, escalations, what got served, what dropped.
3
4use std::sync::OnceLock;
5
6use axum::http::{StatusCode, header};
7use axum::response::{IntoResponse, Response};
8use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
9
10use crate::error::ProxyError;
11
12/// Caches the install outcome so every caller (the real server, every test that builds an
13/// [`crate::proxy::app`]) observes the same result without racing to install the recorder twice.
14static HANDLE: OnceLock<Result<PrometheusHandle, String>> = OnceLock::new();
15
16/// Install the global Prometheus recorder, once per process, and return a handle that renders
17/// the scrape payload. Safe to call more than once: `OnceLock::get_or_init` runs the install
18/// exactly once even under concurrent callers, so repeat calls (e.g. from every test that builds
19/// an [`crate::proxy::app`]) just reuse the cached handle instead of re-installing.
20///
21/// # Errors
22/// Returns [`ProxyError::Internal`] if the first install fails (e.g. a different metrics
23/// recorder is already installed in-process).
24pub fn install() -> Result<PrometheusHandle, ProxyError> {
25    HANDLE
26        .get_or_init(|| {
27            PrometheusBuilder::new()
28                .install_recorder()
29                .map_err(|e| e.to_string())
30        })
31        .clone()
32        .map_err(ProxyError::Internal)
33}
34
35/// `GET /metrics` — render the current Prometheus scrape payload.
36pub async fn handler() -> Response {
37    match install() {
38        Ok(handle) => (
39            StatusCode::OK,
40            [(header::CONTENT_TYPE, "text/plain; version=0.0.4")],
41            handle.render(),
42        )
43            .into_response(),
44        Err(err) => err.into_response(),
45    }
46}