open_metrics_client/metrics/
histogram.rs

1//! Module implementing an Open Metrics histogram.
2//!
3//! See [`Histogram`] for details.
4
5use super::{MetricType, TypedMetric};
6use owning_ref::OwningRef;
7use std::iter::{self, once};
8use std::sync::{Arc, Mutex, MutexGuard};
9
10/// Open Metrics [`Histogram`] to measure distributions of discrete events.
11///
12/// ```
13/// # use open_metrics_client::metrics::histogram::{Histogram, exponential_buckets};
14/// let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
15/// histogram.observe(4.2);
16/// ```
17// TODO: Consider using atomics. See
18// https://github.com/tikv/rust-prometheus/pull/314.
19pub struct Histogram {
20    inner: Arc<Mutex<Inner>>,
21}
22
23impl Clone for Histogram {
24    fn clone(&self) -> Self {
25        Histogram {
26            inner: self.inner.clone(),
27        }
28    }
29}
30
31pub(crate) struct Inner {
32    // TODO: Consider allowing integer observe values.
33    sum: f64,
34    count: u64,
35    // TODO: Consider being generic over the bucket length.
36    buckets: Vec<(f64, u64)>,
37}
38
39impl Histogram {
40    pub fn new(buckets: impl Iterator<Item = f64>) -> Self {
41        Self {
42            inner: Arc::new(Mutex::new(Inner {
43                sum: Default::default(),
44                count: Default::default(),
45                buckets: buckets
46                    .into_iter()
47                    .chain(once(f64::MAX))
48                    .map(|upper_bound| (upper_bound, 0))
49                    .collect(),
50            })),
51        }
52    }
53
54    pub fn observe(&self, v: f64) {
55        self.observe_and_bucket(v);
56    }
57
58    /// Observes the given value, returning the index of the first bucket the
59    /// value is added to.
60    ///
61    /// Needed in
62    /// [`HistogramWithExemplars`](crate::metrics::exemplar::HistogramWithExemplars).
63    pub(crate) fn observe_and_bucket(&self, v: f64) -> Option<usize> {
64        let mut inner = self.inner.lock().unwrap();
65        inner.sum += v;
66        inner.count += 1;
67
68        let first_bucket = inner
69            .buckets
70            .iter_mut()
71            .enumerate()
72            .find(|(_i, (upper_bound, _value))| upper_bound >= &v);
73
74        match first_bucket {
75            Some((i, (_upper_bound, value))) => {
76                *value += 1;
77                Some(i)
78            }
79            None => None,
80        }
81    }
82
83    pub(crate) fn get(&self) -> (f64, u64, MutexGuardedBuckets) {
84        let inner = self.inner.lock().unwrap();
85        let sum = inner.sum;
86        let count = inner.count;
87        let buckets = OwningRef::new(inner).map(|inner| &inner.buckets);
88        (sum, count, buckets)
89    }
90}
91
92pub(crate) type MutexGuardedBuckets<'a> = OwningRef<MutexGuard<'a, Inner>, Vec<(f64, u64)>>;
93
94impl TypedMetric for Histogram {
95    const TYPE: MetricType = MetricType::Histogram;
96}
97
98pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterator<Item = f64> {
99    iter::repeat(())
100        .enumerate()
101        .map(move |(i, _)| start * factor.powf(i as f64))
102        .take(length.into())
103}
104
105pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> {
106    iter::repeat(())
107        .enumerate()
108        .map(move |(i, _)| start + (width * (i as f64)))
109        .take(length.into())
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn histogram() {
118        let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
119        histogram.observe(1.0);
120    }
121
122    #[test]
123    fn exponential() {
124        assert_eq!(
125            vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0],
126            exponential_buckets(1.0, 2.0, 10).collect::<Vec<_>>()
127        );
128    }
129
130    #[test]
131    fn linear() {
132        assert_eq!(
133            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
134            linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>()
135        );
136    }
137}