Skip to main content

linera_base/
prometheus_util.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This module defines utility functions for interacting with Prometheus (logging metrics, etc)
5
6use prometheus::{
7    core::{MetricVec, MetricVecBuilder},
8    exponential_buckets, histogram_opts, linear_buckets, register_gauge, register_gauge_vec,
9    register_histogram, register_histogram_vec, register_int_counter, register_int_counter_vec,
10    register_int_gauge, register_int_gauge_vec, Gauge, GaugeVec, Histogram, HistogramVec,
11    IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts,
12};
13
14use crate::time::Instant;
15
16const LINERA_NAMESPACE: &str = "linera";
17
18/// The message reported when registering a metric fails.
19///
20/// In practice the only cause is a name already taken, and this panic is the sole report of
21/// it, so it has to say which metric rather than only that one could not be created.
22fn registration_failure(name: &str, error: impl std::fmt::Display) -> String {
23    format!("cannot register metric {name}: {error}")
24}
25
26/// Instantiates the sole child of a metric vector that has no labels.
27///
28/// A `MetricVec` exports one series per child, so registering it is not enough to make it
29/// appear in `/metrics`: with no child it emits nothing, which is indistinguishable from the
30/// metric having been renamed or removed. Creating the child up front exports it at zero
31/// until the code path that observes it first runs. Vectors that do have labels cannot get
32/// this treatment, because their label values are not known ahead of time.
33fn materialize_unlabeled<Builder: MetricVecBuilder>(
34    metric: MetricVec<Builder>,
35    label_names: &[&str],
36) -> MetricVec<Builder> {
37    if label_names.is_empty() {
38        metric.with_label_values(&[]);
39    }
40    metric
41}
42
43/// Declares Prometheus metrics as statics, along with an `init_metrics` that forces all of them.
44///
45/// A metric is only exported once its `LazyLock` has been forced, so one that is touched only
46/// on a rare code path disappears whenever its process is replaced. Generating the initializer
47/// from the declarations themselves means a newly added metric cannot be left out of it.
48#[macro_export]
49macro_rules! declare_metrics {
50    ($(
51        $(#[$attribute:meta])*
52        $visibility:vis static $name:ident: $metric_type:ty = $registration:expr;
53    )*) => {
54        $(
55            $(#[$attribute])*
56            $visibility static $name: ::std::sync::LazyLock<$metric_type> =
57                ::std::sync::LazyLock::new(|| $registration);
58        )*
59
60        /// Registers every metric declared in this module.
61        pub fn init_metrics() {
62            $( ::std::sync::LazyLock::force(&$name); )*
63        }
64    };
65}
66
67/// Wrapper around Prometheus `register_int_counter_vec!` macro which also sets the `linera` namespace
68pub fn register_int_counter_vec(
69    name: &str,
70    description: &str,
71    label_names: &[&str],
72) -> IntCounterVec {
73    let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
74    materialize_unlabeled(
75        register_int_counter_vec!(counter_opts, label_names)
76            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
77        label_names,
78    )
79}
80
81/// Wrapper around Prometheus `register_int_counter_vec!` macro with `linera` namespace and a subsystem.
82/// Results in metrics named `linera_<subsystem>_<name>`.
83pub fn register_int_counter_vec_with_subsystem(
84    subsystem: &str,
85    name: &str,
86    description: &str,
87    label_names: &[&str],
88) -> IntCounterVec {
89    let counter_opts = Opts::new(name, description)
90        .namespace(LINERA_NAMESPACE)
91        .subsystem(subsystem);
92    materialize_unlabeled(
93        register_int_counter_vec!(counter_opts, label_names)
94            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
95        label_names,
96    )
97}
98
99/// Wrapper around Prometheus `register_int_counter!` macro which also sets the `linera` namespace
100pub fn register_int_counter(name: &str, description: &str) -> IntCounter {
101    let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
102    register_int_counter!(counter_opts)
103        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
104}
105
106/// Wrapper around Prometheus `register_histogram_vec!` macro which also sets the `linera` namespace
107pub fn register_histogram_vec(
108    name: &str,
109    description: &str,
110    label_names: &[&str],
111    buckets: Option<Vec<f64>>,
112) -> HistogramVec {
113    let histogram_opts = if let Some(buckets) = buckets {
114        histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
115    } else {
116        histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
117    };
118
119    materialize_unlabeled(
120        register_histogram_vec!(histogram_opts, label_names)
121            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
122        label_names,
123    )
124}
125
126/// Wrapper around Prometheus `register_histogram_vec!` macro with `linera` namespace and a subsystem.
127/// Results in metrics named `linera_<subsystem>_<name>`.
128pub fn register_histogram_vec_with_subsystem(
129    subsystem: &str,
130    name: &str,
131    description: &str,
132    label_names: &[&str],
133    buckets: Option<Vec<f64>>,
134) -> HistogramVec {
135    let histogram_opts = if let Some(buckets) = buckets {
136        histogram_opts!(name, description, buckets)
137            .namespace(LINERA_NAMESPACE)
138            .subsystem(subsystem)
139    } else {
140        histogram_opts!(name, description)
141            .namespace(LINERA_NAMESPACE)
142            .subsystem(subsystem)
143    };
144
145    materialize_unlabeled(
146        register_histogram_vec!(histogram_opts, label_names)
147            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
148        label_names,
149    )
150}
151
152/// Wrapper around Prometheus `register_histogram!` macro which also sets the `linera` namespace
153pub fn register_histogram(name: &str, description: &str, buckets: Option<Vec<f64>>) -> Histogram {
154    let histogram_opts = if let Some(buckets) = buckets {
155        histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
156    } else {
157        histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
158    };
159
160    register_histogram!(histogram_opts)
161        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
162}
163
164/// Wrapper around Prometheus `register_histogram!` macro with `linera` namespace and a subsystem.
165/// Results in metrics named `linera_<subsystem>_<name>`.
166pub fn register_histogram_with_subsystem(
167    subsystem: &str,
168    name: &str,
169    description: &str,
170    buckets: Option<Vec<f64>>,
171) -> Histogram {
172    let histogram_opts = if let Some(buckets) = buckets {
173        histogram_opts!(name, description, buckets)
174            .namespace(LINERA_NAMESPACE)
175            .subsystem(subsystem)
176    } else {
177        histogram_opts!(name, description)
178            .namespace(LINERA_NAMESPACE)
179            .subsystem(subsystem)
180    };
181
182    register_histogram!(histogram_opts)
183        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
184}
185
186/// Wrapper around Prometheus `register_int_gauge!` macro which also sets the `linera` namespace
187pub fn register_int_gauge(name: &str, description: &str) -> IntGauge {
188    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
189    register_int_gauge!(gauge_opts)
190        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
191}
192
193/// Wrapper around Prometheus `register_int_gauge!` macro with `linera` namespace and a subsystem.
194/// Results in metrics named `linera_<subsystem>_<name>`.
195pub fn register_int_gauge_with_subsystem(
196    subsystem: &str,
197    name: &str,
198    description: &str,
199) -> IntGauge {
200    let gauge_opts = Opts::new(name, description)
201        .namespace(LINERA_NAMESPACE)
202        .subsystem(subsystem);
203    register_int_gauge!(gauge_opts)
204        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
205}
206
207/// Wrapper around Prometheus `register_int_gauge_vec!` macro which also sets the `linera` namespace
208pub fn register_int_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> IntGaugeVec {
209    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
210    materialize_unlabeled(
211        register_int_gauge_vec!(gauge_opts, label_names)
212            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
213        label_names,
214    )
215}
216
217/// Wrapper around Prometheus `register_gauge!` macro (floating-point gauge) which also sets the
218/// `linera` namespace.
219pub fn register_gauge(name: &str, description: &str) -> Gauge {
220    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
221    register_gauge!(gauge_opts)
222        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
223}
224
225/// Wrapper around Prometheus `register_gauge!` macro with `linera` namespace and a subsystem.
226/// Results in metrics named `linera_<subsystem>_<name>`.
227pub fn register_gauge_with_subsystem(subsystem: &str, name: &str, description: &str) -> Gauge {
228    let gauge_opts = Opts::new(name, description)
229        .namespace(LINERA_NAMESPACE)
230        .subsystem(subsystem);
231    register_gauge!(gauge_opts)
232        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
233}
234
235/// Wrapper around Prometheus `register_int_counter!` macro with `linera` namespace and a
236/// subsystem. Results in metrics named `linera_<subsystem>_<name>`.
237pub fn register_int_counter_with_subsystem(
238    subsystem: &str,
239    name: &str,
240    description: &str,
241) -> IntCounter {
242    let counter_opts = Opts::new(name, description)
243        .namespace(LINERA_NAMESPACE)
244        .subsystem(subsystem);
245    register_int_counter!(counter_opts)
246        .unwrap_or_else(|error| panic!("{}", registration_failure(name, error)))
247}
248
249/// Wrapper around Prometheus `register_gauge_vec!` macro (floating-point gauge) which also sets
250/// the `linera` namespace. Use this for quantities with a fractional part (e.g. token balances).
251pub fn register_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> GaugeVec {
252    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
253    materialize_unlabeled(
254        register_gauge_vec!(gauge_opts, label_names)
255            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
256        label_names,
257    )
258}
259
260/// Wrapper around Prometheus `register_int_gauge_vec!` macro with `linera` namespace and a subsystem.
261/// Results in metrics named `linera_<subsystem>_<name>`.
262pub fn register_int_gauge_vec_with_subsystem(
263    subsystem: &str,
264    name: &str,
265    description: &str,
266    label_names: &[&str],
267) -> IntGaugeVec {
268    let gauge_opts = Opts::new(name, description)
269        .namespace(LINERA_NAMESPACE)
270        .subsystem(subsystem);
271    materialize_unlabeled(
272        register_int_gauge_vec!(gauge_opts, label_names)
273            .unwrap_or_else(|error| panic!("{}", registration_failure(name, error))),
274        label_names,
275    )
276}
277
278/// Construct the bucket interval exponentially starting from a value and an ending value.
279#[expect(
280    clippy::cast_possible_truncation,
281    clippy::cast_sign_loss,
282    reason = "histogram bucket count; loss of precision is acceptable"
283)]
284pub fn exponential_bucket_interval(start_value: f64, end_value: f64) -> Option<Vec<f64>> {
285    let quot = end_value / start_value;
286    let factor = 3.0_f64;
287    let count_approx = quot.ln() / factor.ln();
288    let count = count_approx.round() as usize;
289    let mut buckets = exponential_buckets(start_value, factor, count)
290        .expect("Exponential buckets creation should not fail!");
291    if let Some(last) = buckets.last() {
292        if *last < end_value {
293            buckets.push(end_value);
294        }
295    }
296    Some(buckets)
297}
298
299/// Construct the latencies exponentially starting from 0.001 and ending at the maximum latency
300pub fn exponential_bucket_latencies(max_latency: f64) -> Option<Vec<f64>> {
301    exponential_bucket_interval(0.001_f64, max_latency)
302}
303
304/// Construct the bucket interval linearly starting from a value and an ending value.
305#[expect(
306    clippy::cast_possible_truncation,
307    clippy::cast_sign_loss,
308    reason = "histogram bucket count; loss of precision is acceptable"
309)]
310pub fn linear_bucket_interval(start_value: f64, width: f64, end_value: f64) -> Option<Vec<f64>> {
311    let count = (end_value - start_value) / width;
312    let count = count.round() as usize;
313    let mut buckets = linear_buckets(start_value, width, count)
314        .expect("Linear buckets creation should not fail!");
315    buckets.push(end_value);
316    Some(buckets)
317}
318
319/// The unit of measurement for latency metrics.
320enum MeasurementUnit {
321    /// Measure latency in milliseconds.
322    Milliseconds,
323    /// Measure latency in microseconds.
324    Microseconds,
325}
326
327/// A guard for an active latency measurement.
328///
329/// Finishes the measurement when dropped, and then updates the `Metric`.
330pub struct ActiveMeasurementGuard<'metric, Metric>
331where
332    Metric: MeasureLatency,
333{
334    start: Instant,
335    metric: Option<&'metric Metric>,
336    unit: MeasurementUnit,
337}
338
339impl<Metric> ActiveMeasurementGuard<'_, Metric>
340where
341    Metric: MeasureLatency,
342{
343    /// Finishes the measurement, updates the `Metric` and returns the measured latency in
344    /// the unit specified when the measurement was started.
345    pub fn finish(mut self) -> f64 {
346        self.finish_by_ref()
347    }
348
349    /// Finishes the measurement without taking ownership of this [`ActiveMeasurementGuard`],
350    /// updates the `Metric` and returns the measured latency in the unit specified when
351    /// the measurement was started.
352    fn finish_by_ref(&mut self) -> f64 {
353        match self.metric.take() {
354            Some(metric) => {
355                let latency = match self.unit {
356                    MeasurementUnit::Milliseconds => self.start.elapsed().as_secs_f64() * 1000.0,
357                    MeasurementUnit::Microseconds => {
358                        self.start.elapsed().as_secs_f64() * 1_000_000.0
359                    }
360                };
361                metric.finish_measurement(latency);
362                latency
363            }
364            None => {
365                // This is getting called from `Drop` after `finish` has already been
366                // executed
367                f64::NAN
368            }
369        }
370    }
371}
372
373impl<Metric> Drop for ActiveMeasurementGuard<'_, Metric>
374where
375    Metric: MeasureLatency,
376{
377    fn drop(&mut self) {
378        self.finish_by_ref();
379    }
380}
381
382/// An extension trait for metrics that can be used to measure latencies.
383pub trait MeasureLatency: Sized {
384    /// Starts measuring the latency in milliseconds, finishing when the returned
385    /// [`ActiveMeasurementGuard`] is dropped.
386    fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self>;
387
388    /// Starts measuring the latency in microseconds, finishing when the returned
389    /// [`ActiveMeasurementGuard`] is dropped.
390    fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self>;
391
392    /// Updates the metric with measured latency in `milliseconds`.
393    fn finish_measurement(&self, milliseconds: f64);
394}
395
396impl MeasureLatency for HistogramVec {
397    fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
398        ActiveMeasurementGuard {
399            start: Instant::now(),
400            metric: Some(self),
401            unit: MeasurementUnit::Milliseconds,
402        }
403    }
404
405    fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
406        ActiveMeasurementGuard {
407            start: Instant::now(),
408            metric: Some(self),
409            unit: MeasurementUnit::Microseconds,
410        }
411    }
412
413    fn finish_measurement(&self, milliseconds: f64) {
414        self.with_label_values(&[]).observe(milliseconds);
415    }
416}
417
418impl MeasureLatency for Histogram {
419    fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
420        ActiveMeasurementGuard {
421            start: Instant::now(),
422            metric: Some(self),
423            unit: MeasurementUnit::Milliseconds,
424        }
425    }
426
427    fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
428        ActiveMeasurementGuard {
429            start: Instant::now(),
430            metric: Some(self),
431            unit: MeasurementUnit::Microseconds,
432        }
433    }
434
435    fn finish_measurement(&self, milliseconds: f64) {
436        self.observe(milliseconds);
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    /// The panic is the only report of a duplicate registration, so it has to name the
445    /// metric. Asserted on the message directly: panicking here would run the global hook
446    /// whose invocations `panic_hook`'s own test counts.
447    #[test]
448    fn a_failed_registration_names_the_metric() {
449        let message = registration_failure("some_metric", "Duplicate metrics collector");
450        assert!(message.contains("some_metric"), "got: {message}");
451    }
452
453    /// Pins the naming contract the `_with_subsystem` helpers document, so callers can drop a
454    /// hand-written prefix from every metric name and rely on the subsystem instead.
455    #[test]
456    fn subsystem_is_inserted_between_namespace_and_name() {
457        register_int_counter_with_subsystem(
458            "testsubsystem",
459            "testname",
460            "Pins the namespace/subsystem/name composition",
461        );
462
463        assert!(prometheus::gather()
464            .iter()
465            .any(|family| family.get_name() == "linera_testsubsystem_testname"));
466    }
467
468    // Helper function for approximate floating point comparison
469    fn assert_float_vec_eq(left: &[f64], right: &[f64]) {
470        const EPSILON: f64 = 1e-10;
471
472        assert_eq!(left.len(), right.len(), "Vectors have different lengths");
473        for (i, (l, r)) in left.iter().zip(right.iter()).enumerate() {
474            assert!(
475                (l - r).abs() < EPSILON,
476                "Vectors differ at index {i}: {l} != {r}"
477            );
478        }
479    }
480
481    #[test]
482    fn test_linear_bucket_interval() {
483        // Case 1: Width divides range evenly - small values
484        let buckets = linear_bucket_interval(0.05, 0.01, 0.1).unwrap();
485        assert_float_vec_eq(&buckets, &[0.05, 0.06, 0.07, 0.08, 0.09, 0.1]);
486
487        // Case 2: Width divides range evenly - large values
488        let buckets = linear_bucket_interval(100.0, 50.0, 500.0).unwrap();
489        assert_float_vec_eq(
490            &buckets,
491            &[
492                100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0, 450.0, 500.0,
493            ],
494        );
495
496        // Case 3: Width doesn't divide range evenly - small values
497        let buckets = linear_bucket_interval(0.05, 0.12, 0.5).unwrap();
498        assert_float_vec_eq(&buckets, &[0.05, 0.17, 0.29, 0.41, 0.5]);
499
500        // Case 4: Width doesn't divide range evenly - large values
501        let buckets = linear_bucket_interval(100.0, 150.0, 500.0).unwrap();
502        assert_float_vec_eq(&buckets, &[100.0, 250.0, 400.0, 500.0]);
503    }
504}