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    exponential_buckets, histogram_opts, linear_buckets, register_gauge_vec, register_histogram,
8    register_histogram_vec, register_int_counter, register_int_counter_vec, register_int_gauge,
9    register_int_gauge_vec, GaugeVec, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge,
10    IntGaugeVec, Opts,
11};
12
13use crate::time::Instant;
14
15const LINERA_NAMESPACE: &str = "linera";
16
17/// Wrapper around Prometheus `register_int_counter_vec!` macro which also sets the `linera` namespace
18pub fn register_int_counter_vec(
19    name: &str,
20    description: &str,
21    label_names: &[&str],
22) -> IntCounterVec {
23    let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
24    register_int_counter_vec!(counter_opts, label_names).expect("IntCounter can be created")
25}
26
27/// Wrapper around Prometheus `register_int_counter_vec!` macro with `linera` namespace and a subsystem.
28/// Results in metrics named `linera_<subsystem>_<name>`.
29pub fn register_int_counter_vec_with_subsystem(
30    subsystem: &str,
31    name: &str,
32    description: &str,
33    label_names: &[&str],
34) -> IntCounterVec {
35    let counter_opts = Opts::new(name, description)
36        .namespace(LINERA_NAMESPACE)
37        .subsystem(subsystem);
38    register_int_counter_vec!(counter_opts, label_names).expect("IntCounter can be created")
39}
40
41/// Wrapper around Prometheus `register_int_counter!` macro which also sets the `linera` namespace
42pub fn register_int_counter(name: &str, description: &str) -> IntCounter {
43    let counter_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
44    register_int_counter!(counter_opts).expect("IntCounter can be created")
45}
46
47/// Wrapper around Prometheus `register_histogram_vec!` macro which also sets the `linera` namespace
48pub fn register_histogram_vec(
49    name: &str,
50    description: &str,
51    label_names: &[&str],
52    buckets: Option<Vec<f64>>,
53) -> HistogramVec {
54    let histogram_opts = if let Some(buckets) = buckets {
55        histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
56    } else {
57        histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
58    };
59
60    register_histogram_vec!(histogram_opts, label_names).expect("Histogram can be created")
61}
62
63/// Wrapper around Prometheus `register_histogram_vec!` macro with `linera` namespace and a subsystem.
64/// Results in metrics named `linera_<subsystem>_<name>`.
65pub fn register_histogram_vec_with_subsystem(
66    subsystem: &str,
67    name: &str,
68    description: &str,
69    label_names: &[&str],
70    buckets: Option<Vec<f64>>,
71) -> HistogramVec {
72    let histogram_opts = if let Some(buckets) = buckets {
73        histogram_opts!(name, description, buckets)
74            .namespace(LINERA_NAMESPACE)
75            .subsystem(subsystem)
76    } else {
77        histogram_opts!(name, description)
78            .namespace(LINERA_NAMESPACE)
79            .subsystem(subsystem)
80    };
81
82    register_histogram_vec!(histogram_opts, label_names).expect("Histogram can be created")
83}
84
85/// Wrapper around Prometheus `register_histogram!` macro which also sets the `linera` namespace
86pub fn register_histogram(name: &str, description: &str, buckets: Option<Vec<f64>>) -> Histogram {
87    let histogram_opts = if let Some(buckets) = buckets {
88        histogram_opts!(name, description, buckets).namespace(LINERA_NAMESPACE)
89    } else {
90        histogram_opts!(name, description).namespace(LINERA_NAMESPACE)
91    };
92
93    register_histogram!(histogram_opts).expect("Histogram can be created")
94}
95
96/// Wrapper around Prometheus `register_histogram!` macro with `linera` namespace and a subsystem.
97/// Results in metrics named `linera_<subsystem>_<name>`.
98pub fn register_histogram_with_subsystem(
99    subsystem: &str,
100    name: &str,
101    description: &str,
102    buckets: Option<Vec<f64>>,
103) -> Histogram {
104    let histogram_opts = if let Some(buckets) = buckets {
105        histogram_opts!(name, description, buckets)
106            .namespace(LINERA_NAMESPACE)
107            .subsystem(subsystem)
108    } else {
109        histogram_opts!(name, description)
110            .namespace(LINERA_NAMESPACE)
111            .subsystem(subsystem)
112    };
113
114    register_histogram!(histogram_opts).expect("Histogram can be created")
115}
116
117/// Wrapper around Prometheus `register_int_gauge!` macro which also sets the `linera` namespace
118pub fn register_int_gauge(name: &str, description: &str) -> IntGauge {
119    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
120    register_int_gauge!(gauge_opts).expect("IntGauge can be created")
121}
122
123/// Wrapper around Prometheus `register_int_gauge!` macro with `linera` namespace and a subsystem.
124/// Results in metrics named `linera_<subsystem>_<name>`.
125pub fn register_int_gauge_with_subsystem(
126    subsystem: &str,
127    name: &str,
128    description: &str,
129) -> IntGauge {
130    let gauge_opts = Opts::new(name, description)
131        .namespace(LINERA_NAMESPACE)
132        .subsystem(subsystem);
133    register_int_gauge!(gauge_opts).expect("IntGauge can be created")
134}
135
136/// Wrapper around Prometheus `register_int_gauge_vec!` macro which also sets the `linera` namespace
137pub fn register_int_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> IntGaugeVec {
138    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
139    register_int_gauge_vec!(gauge_opts, label_names).expect("IntGauge can be created")
140}
141
142/// Wrapper around Prometheus `register_gauge_vec!` macro (floating-point gauge) which also sets
143/// the `linera` namespace. Use this for quantities with a fractional part (e.g. token balances).
144pub fn register_gauge_vec(name: &str, description: &str, label_names: &[&str]) -> GaugeVec {
145    let gauge_opts = Opts::new(name, description).namespace(LINERA_NAMESPACE);
146    register_gauge_vec!(gauge_opts, label_names).expect("Gauge can be created")
147}
148
149/// Wrapper around Prometheus `register_int_gauge_vec!` macro with `linera` namespace and a subsystem.
150/// Results in metrics named `linera_<subsystem>_<name>`.
151pub fn register_int_gauge_vec_with_subsystem(
152    subsystem: &str,
153    name: &str,
154    description: &str,
155    label_names: &[&str],
156) -> IntGaugeVec {
157    let gauge_opts = Opts::new(name, description)
158        .namespace(LINERA_NAMESPACE)
159        .subsystem(subsystem);
160    register_int_gauge_vec!(gauge_opts, label_names).expect("IntGauge can be created")
161}
162
163/// Construct the bucket interval exponentially starting from a value and an ending value.
164#[expect(
165    clippy::cast_possible_truncation,
166    clippy::cast_sign_loss,
167    reason = "histogram bucket count; loss of precision is acceptable"
168)]
169pub fn exponential_bucket_interval(start_value: f64, end_value: f64) -> Option<Vec<f64>> {
170    let quot = end_value / start_value;
171    let factor = 3.0_f64;
172    let count_approx = quot.ln() / factor.ln();
173    let count = count_approx.round() as usize;
174    let mut buckets = exponential_buckets(start_value, factor, count)
175        .expect("Exponential buckets creation should not fail!");
176    if let Some(last) = buckets.last() {
177        if *last < end_value {
178            buckets.push(end_value);
179        }
180    }
181    Some(buckets)
182}
183
184/// Construct the latencies exponentially starting from 0.001 and ending at the maximum latency
185pub fn exponential_bucket_latencies(max_latency: f64) -> Option<Vec<f64>> {
186    exponential_bucket_interval(0.001_f64, max_latency)
187}
188
189/// Construct the bucket interval linearly starting from a value and an ending value.
190#[expect(
191    clippy::cast_possible_truncation,
192    clippy::cast_sign_loss,
193    reason = "histogram bucket count; loss of precision is acceptable"
194)]
195pub fn linear_bucket_interval(start_value: f64, width: f64, end_value: f64) -> Option<Vec<f64>> {
196    let count = (end_value - start_value) / width;
197    let count = count.round() as usize;
198    let mut buckets = linear_buckets(start_value, width, count)
199        .expect("Linear buckets creation should not fail!");
200    buckets.push(end_value);
201    Some(buckets)
202}
203
204/// The unit of measurement for latency metrics.
205enum MeasurementUnit {
206    /// Measure latency in milliseconds.
207    Milliseconds,
208    /// Measure latency in microseconds.
209    Microseconds,
210}
211
212/// A guard for an active latency measurement.
213///
214/// Finishes the measurement when dropped, and then updates the `Metric`.
215pub struct ActiveMeasurementGuard<'metric, Metric>
216where
217    Metric: MeasureLatency,
218{
219    start: Instant,
220    metric: Option<&'metric Metric>,
221    unit: MeasurementUnit,
222}
223
224impl<Metric> ActiveMeasurementGuard<'_, Metric>
225where
226    Metric: MeasureLatency,
227{
228    /// Finishes the measurement, updates the `Metric` and returns the measured latency in
229    /// the unit specified when the measurement was started.
230    pub fn finish(mut self) -> f64 {
231        self.finish_by_ref()
232    }
233
234    /// Finishes the measurement without taking ownership of this [`ActiveMeasurementGuard`],
235    /// updates the `Metric` and returns the measured latency in the unit specified when
236    /// the measurement was started.
237    fn finish_by_ref(&mut self) -> f64 {
238        match self.metric.take() {
239            Some(metric) => {
240                let latency = match self.unit {
241                    MeasurementUnit::Milliseconds => self.start.elapsed().as_secs_f64() * 1000.0,
242                    MeasurementUnit::Microseconds => {
243                        self.start.elapsed().as_secs_f64() * 1_000_000.0
244                    }
245                };
246                metric.finish_measurement(latency);
247                latency
248            }
249            None => {
250                // This is getting called from `Drop` after `finish` has already been
251                // executed
252                f64::NAN
253            }
254        }
255    }
256}
257
258impl<Metric> Drop for ActiveMeasurementGuard<'_, Metric>
259where
260    Metric: MeasureLatency,
261{
262    fn drop(&mut self) {
263        self.finish_by_ref();
264    }
265}
266
267/// An extension trait for metrics that can be used to measure latencies.
268pub trait MeasureLatency: Sized {
269    /// Starts measuring the latency in milliseconds, finishing when the returned
270    /// [`ActiveMeasurementGuard`] is dropped.
271    fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self>;
272
273    /// Starts measuring the latency in microseconds, finishing when the returned
274    /// [`ActiveMeasurementGuard`] is dropped.
275    fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self>;
276
277    /// Updates the metric with measured latency in `milliseconds`.
278    fn finish_measurement(&self, milliseconds: f64);
279}
280
281impl MeasureLatency for HistogramVec {
282    fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
283        ActiveMeasurementGuard {
284            start: Instant::now(),
285            metric: Some(self),
286            unit: MeasurementUnit::Milliseconds,
287        }
288    }
289
290    fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
291        ActiveMeasurementGuard {
292            start: Instant::now(),
293            metric: Some(self),
294            unit: MeasurementUnit::Microseconds,
295        }
296    }
297
298    fn finish_measurement(&self, milliseconds: f64) {
299        self.with_label_values(&[]).observe(milliseconds);
300    }
301}
302
303impl MeasureLatency for Histogram {
304    fn measure_latency(&self) -> ActiveMeasurementGuard<'_, Self> {
305        ActiveMeasurementGuard {
306            start: Instant::now(),
307            metric: Some(self),
308            unit: MeasurementUnit::Milliseconds,
309        }
310    }
311
312    fn measure_latency_us(&self) -> ActiveMeasurementGuard<'_, Self> {
313        ActiveMeasurementGuard {
314            start: Instant::now(),
315            metric: Some(self),
316            unit: MeasurementUnit::Microseconds,
317        }
318    }
319
320    fn finish_measurement(&self, milliseconds: f64) {
321        self.observe(milliseconds);
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    // Helper function for approximate floating point comparison
330    fn assert_float_vec_eq(left: &[f64], right: &[f64]) {
331        const EPSILON: f64 = 1e-10;
332
333        assert_eq!(left.len(), right.len(), "Vectors have different lengths");
334        for (i, (l, r)) in left.iter().zip(right.iter()).enumerate() {
335            assert!(
336                (l - r).abs() < EPSILON,
337                "Vectors differ at index {i}: {l} != {r}"
338            );
339        }
340    }
341
342    #[test]
343    fn test_linear_bucket_interval() {
344        // Case 1: Width divides range evenly - small values
345        let buckets = linear_bucket_interval(0.05, 0.01, 0.1).unwrap();
346        assert_float_vec_eq(&buckets, &[0.05, 0.06, 0.07, 0.08, 0.09, 0.1]);
347
348        // Case 2: Width divides range evenly - large values
349        let buckets = linear_bucket_interval(100.0, 50.0, 500.0).unwrap();
350        assert_float_vec_eq(
351            &buckets,
352            &[
353                100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0, 450.0, 500.0,
354            ],
355        );
356
357        // Case 3: Width doesn't divide range evenly - small values
358        let buckets = linear_bucket_interval(0.05, 0.12, 0.5).unwrap();
359        assert_float_vec_eq(&buckets, &[0.05, 0.17, 0.29, 0.41, 0.5]);
360
361        // Case 4: Width doesn't divide range evenly - large values
362        let buckets = linear_bucket_interval(100.0, 150.0, 500.0).unwrap();
363        assert_float_vec_eq(&buckets, &[100.0, 250.0, 400.0, 500.0]);
364    }
365}