reth_node_metrics/
recorder.rs

1//! Prometheus recorder
2
3use eyre::WrapErr;
4use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
5use metrics_util::layers::{PrefixLayer, Stack};
6use std::sync::{atomic::AtomicBool, LazyLock};
7
8/// Installs the Prometheus recorder as the global recorder.
9///
10/// Note: This must be installed before any metrics are `described`.
11///
12/// Caution: This only configures the global recorder and does not spawn the exporter.
13/// Callers must run [`PrometheusRecorder::spawn_upkeep`] manually.
14pub fn install_prometheus_recorder() -> &'static PrometheusRecorder {
15    &PROMETHEUS_RECORDER_HANDLE
16}
17
18/// The default Prometheus recorder handle. We use a global static to ensure that it is only
19/// installed once.
20static PROMETHEUS_RECORDER_HANDLE: LazyLock<PrometheusRecorder> =
21    LazyLock::new(|| PrometheusRecorder::install().unwrap());
22
23/// A handle to the Prometheus recorder.
24///
25/// This is intended to be used as the global recorder.
26/// Callers must ensure that [`PrometheusRecorder::spawn_upkeep`] is called once.
27#[derive(Debug)]
28pub struct PrometheusRecorder {
29    handle: PrometheusHandle,
30    upkeep: AtomicBool,
31}
32
33impl PrometheusRecorder {
34    const fn new(handle: PrometheusHandle) -> Self {
35        Self { handle, upkeep: AtomicBool::new(false) }
36    }
37
38    /// Returns a reference to the [`PrometheusHandle`].
39    pub const fn handle(&self) -> &PrometheusHandle {
40        &self.handle
41    }
42
43    /// Spawns the upkeep task if there hasn't been one spawned already.
44    ///
45    /// ## Panics
46    ///
47    /// This method must be called from within an existing Tokio runtime or it will panic.
48    ///
49    /// See also [`PrometheusHandle::run_upkeep`]
50    pub fn spawn_upkeep(&self) {
51        if self
52            .upkeep
53            .compare_exchange(
54                false,
55                true,
56                std::sync::atomic::Ordering::SeqCst,
57                std::sync::atomic::Ordering::Acquire,
58            )
59            .is_err()
60        {
61            return;
62        }
63
64        let handle = self.handle.clone();
65        tokio::spawn(async move {
66            loop {
67                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
68                handle.run_upkeep();
69            }
70        });
71    }
72
73    /// Installs Prometheus as the metrics recorder.
74    ///
75    /// Caution: This only configures the global recorder and does not spawn the exporter.
76    /// Callers must run [`Self::spawn_upkeep`] manually.
77    pub fn install() -> eyre::Result<Self> {
78        let recorder = PrometheusBuilder::new().build_recorder();
79        let handle = recorder.handle();
80
81        // Build metrics stack
82        Stack::new(recorder)
83            .push(PrefixLayer::new("reth"))
84            .install()
85            .wrap_err("Couldn't set metrics recorder.")?;
86
87        Ok(Self::new(handle))
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    // Dependencies using different version of the `metrics` crate (to be exact, 0.21 vs 0.22)
95    // may not be able to communicate with each other through the global recorder.
96    //
97    // This test ensures that `metrics-process` dependency plays well with the current
98    // `metrics-exporter-prometheus` dependency version.
99    #[test]
100    fn process_metrics() {
101        // initialize the lazy handle
102        let _ = &*PROMETHEUS_RECORDER_HANDLE;
103
104        let process = metrics_process::Collector::default();
105        process.describe();
106        process.collect();
107
108        let metrics = PROMETHEUS_RECORDER_HANDLE.handle.render();
109        assert!(metrics.contains("process_cpu_seconds_total"), "{metrics:?}");
110    }
111}