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}