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}