Skip to main content

hypercall/observability/
prometheus.rs

1//! Prometheus metrics exposition.
2//!
3//! Installs a global Prometheus recorder and exposes a render function
4//! for the `/metrics` endpoint.
5
6use metrics::{describe_gauge, gauge};
7use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::OnceLock;
10use std::time::Duration;
11
12static PROM_HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();
13static UPKEEP_STARTED: AtomicBool = AtomicBool::new(false);
14
15/// Initialize the Prometheus metrics recorder.
16///
17/// This is idempotent - calling it multiple times returns the same handle.
18/// Must be called early in server startup to avoid losing metrics.
19///
20/// Note: This does NOT spawn the upkeep task. Call `start_upkeep_task()`
21/// separately after the tokio runtime is running to enable metric cleanup.
22pub fn init_prometheus() -> &'static PrometheusHandle {
23    PROM_HANDLE.get_or_init(|| {
24        let handle = PrometheusBuilder::new()
25            .install_recorder()
26            .expect("failed to install Prometheus recorder");
27
28        tracing::info!("Prometheus metrics recorder initialized");
29        handle
30    })
31}
32
33/// Start the background upkeep task for Prometheus metrics.
34///
35/// This must be called from within a tokio runtime context.
36/// It's safe to call multiple times - only the first call spawns the task.
37pub fn start_upkeep_task() {
38    if UPKEEP_STARTED
39        .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
40        .is_ok()
41    {
42        // Register memory metrics
43        describe_gauge!(
44            "ht_process_memory_rss_bytes",
45            "Resident set size (physical memory) in bytes"
46        );
47        describe_gauge!(
48            "ht_process_memory_virtual_bytes",
49            "Virtual memory size in bytes"
50        );
51
52        let handle = init_prometheus().clone();
53        tokio::spawn(async move {
54            let mut interval = tokio::time::interval(Duration::from_secs(5));
55            loop {
56                interval.tick().await;
57                handle.run_upkeep();
58
59                // Update memory metrics
60                if let Some(usage) = memory_stats::memory_stats() {
61                    gauge!("ht_process_memory_rss_bytes").set(usage.physical_mem as f64);
62                    gauge!("ht_process_memory_virtual_bytes").set(usage.virtual_mem as f64);
63                }
64            }
65        });
66        tracing::debug!("Prometheus upkeep task started");
67    }
68}
69
70/// Render current metrics in Prometheus exposition format.
71pub fn render() -> String {
72    init_prometheus().render()
73}