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, OnceLock};
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.
14///
15/// Use [`init_prometheus_recorder`] to install a custom recorder.
16pub fn install_prometheus_recorder() -> &'static PrometheusRecorder {
17    PROMETHEUS_RECORDER_HANDLE.get_or_init(|| {
18        PrometheusRecorder::install().expect("Failed to install Prometheus recorder")
19    })
20}
21
22/// Installs the provided recorder as the global recorder.
23///
24/// To customize the builder, first construct a recorder with
25/// [`PrometheusRecorder::install_with_builder`], then pass it here.
26///
27/// # Panics
28///
29/// Panics if a recorder has already been installed.
30pub fn init_prometheus_recorder(recorder: PrometheusRecorder) -> &'static PrometheusRecorder {
31    PROMETHEUS_RECORDER_HANDLE.set(recorder).expect("Prometheus recorder already installed");
32    PROMETHEUS_RECORDER_HANDLE.get().expect("Prometheus recorder is set")
33}
34
35/// The default Prometheus recorder handle. We use a global static to ensure that it is only
36/// installed once.
37static PROMETHEUS_RECORDER_HANDLE: OnceLock<PrometheusRecorder> = OnceLock::new();
38
39/// Installs the Prometheus recorder with a custom builder.
40///
41/// Returns an error if a recorder has already been installed.
42pub fn try_install_prometheus_recorder_with_builder(
43    builder: PrometheusBuilder,
44) -> eyre::Result<&'static PrometheusRecorder> {
45    let recorder = PrometheusRecorder::install_with_builder(builder)?;
46    PROMETHEUS_RECORDER_HANDLE
47        .set(recorder)
48        .map_err(|_| eyre::eyre!("Prometheus recorder already installed"))?;
49    Ok(PROMETHEUS_RECORDER_HANDLE.get().expect("recorder is set"))
50}
51
52/// A handle to the Prometheus recorder.
53///
54/// This is intended to be used as the global recorder.
55/// Callers must ensure that [`PrometheusRecorder::spawn_upkeep`] is called once.
56#[derive(Debug)]
57pub struct PrometheusRecorder {
58    handle: PrometheusHandle,
59    upkeep: AtomicBool,
60}
61
62impl PrometheusRecorder {
63    const fn new(handle: PrometheusHandle) -> Self {
64        Self { handle, upkeep: AtomicBool::new(false) }
65    }
66
67    /// Returns a reference to the [`PrometheusHandle`].
68    pub const fn handle(&self) -> &PrometheusHandle {
69        &self.handle
70    }
71
72    /// Spawns the upkeep task if there hasn't been one spawned already.
73    ///
74    /// ## Panics
75    ///
76    /// This method must be called from within an existing Tokio runtime or it will panic.
77    ///
78    /// See also [`PrometheusHandle::run_upkeep`]
79    pub fn spawn_upkeep(&self) {
80        if self
81            .upkeep
82            .compare_exchange(
83                false,
84                true,
85                std::sync::atomic::Ordering::SeqCst,
86                std::sync::atomic::Ordering::Acquire,
87            )
88            .is_err()
89        {
90            return;
91        }
92
93        let handle = self.handle.clone();
94        tokio::spawn(async move {
95            loop {
96                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
97                handle.run_upkeep();
98            }
99        });
100    }
101
102    /// Installs Prometheus as the metrics recorder.
103    ///
104    /// Caution: This only configures the global recorder and does not spawn the exporter.
105    /// Callers must run [`Self::spawn_upkeep`] manually.
106    pub fn install() -> eyre::Result<Self> {
107        Self::install_with_builder(PrometheusBuilder::new())
108    }
109
110    /// Installs Prometheus as the metrics recorder with a custom builder.
111    ///
112    /// Caution: This only configures the global recorder and does not spawn the exporter.
113    /// Callers must run [`Self::spawn_upkeep`] manually.
114    pub fn install_with_builder(builder: PrometheusBuilder) -> eyre::Result<Self> {
115        let recorder = builder.build_recorder();
116        let handle = recorder.handle();
117
118        // Build metrics stack
119        Stack::new(recorder)
120            .push(PrefixLayer::new("reth"))
121            .install()
122            .wrap_err("Couldn't set metrics recorder.")?;
123
124        Ok(Self::new(handle))
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    // Dependencies using different version of the `metrics` crate (to be exact, 0.21 vs 0.22)
132    // may not be able to communicate with each other through the global recorder.
133    //
134    // This test ensures that `metrics-process` dependency plays well with the current
135    // `metrics-exporter-prometheus` dependency version.
136    #[test]
137    fn process_metrics() {
138        let recorder = install_prometheus_recorder();
139
140        let process = metrics_process::Collector::default();
141        process.describe();
142        process.collect();
143
144        let metrics = recorder.handle().render();
145        assert!(metrics.contains("process_cpu_seconds_total"), "{metrics:?}");
146    }
147}