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