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}