soma_observability/metrics.rs
1//! Prometheus metrics recorder and renderer.
2//!
3//! The server installs a single global Prometheus recorder at startup via
4//! [`init`], then exposes the gathered metrics over a `/metrics` route that
5//! calls [`render`]. Emitting code anywhere in the workspace uses the
6//! lightweight `metrics` facade (`metrics::counter!`, `metrics::histogram!`);
7//! when no recorder is installed (stdio mode, CLI, tests) those macros are
8//! cheap no-ops, so emitting metrics is always safe.
9//!
10//! Everything here is fail-soft: a metrics problem must never take the server
11//! down, so install failures are logged and swallowed and `/metrics` simply
12//! reports "not initialized" until a recorder exists.
13
14use std::sync::OnceLock;
15
16use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
17
18static HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();
19
20/// Install the global Prometheus recorder. Idempotent: a second call is a no-op,
21/// and a failed install is logged rather than propagated so startup continues.
22pub fn init() {
23 if HANDLE.get().is_some() {
24 return;
25 }
26 match PrometheusBuilder::new().install_recorder() {
27 Ok(handle) => {
28 // Ignore a race where another thread set it first — either handle works.
29 let _ = HANDLE.set(handle);
30 }
31 Err(error) => {
32 tracing::warn!(%error, "failed to install Prometheus recorder; /metrics will be empty");
33 }
34 }
35}
36
37/// Render the current metrics in Prometheus text exposition format, or `None`
38/// if no recorder has been installed (so the route can answer 503).
39pub fn render() -> Option<String> {
40 HANDLE.get().map(PrometheusHandle::render)
41}
42
43/// Whether a recorder has been installed. Primarily for tests and diagnostics.
44pub fn is_installed() -> bool {
45 HANDLE.get().is_some()
46}
47
48#[cfg(test)]
49#[path = "metrics_tests.rs"]
50mod tests;