prometheus_client/metrics/
histogram.rs

1//! Module implementing an Open Metrics histogram.
2//!
3//! See [`Histogram`] for details.
4
5use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet};
6
7use super::{MetricType, TypedMetric};
8use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
9use std::iter::{self, once};
10use std::sync::Arc;
11
12/// Open Metrics [`Histogram`] to measure distributions of discrete events.
13///
14/// ```
15/// # use prometheus_client::metrics::histogram::{Histogram, exponential_buckets};
16/// let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
17/// histogram.observe(4.2);
18/// ```
19///
20/// [`Histogram`] does not implement [`Default`], given that the choice of
21/// bucket values depends on the situation [`Histogram`] is used in. As an
22/// example, to measure HTTP request latency, the values suggested in the
23/// Golang implementation might work for you:
24///
25/// ```
26/// # use prometheus_client::metrics::histogram::Histogram;
27/// // Default values from go client(https://github.com/prometheus/client_golang/blob/5d584e2717ef525673736d72cd1d12e304f243d7/prometheus/histogram.go#L68)
28/// let custom_buckets = [
29///    0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
30/// ];
31/// let histogram = Histogram::new(custom_buckets);
32/// histogram.observe(4.2);
33/// ```
34// TODO: Consider using atomics. See
35// https://github.com/tikv/rust-prometheus/pull/314.
36#[derive(Debug)]
37pub struct Histogram {
38    inner: Arc<RwLock<Inner>>,
39}
40
41impl Clone for Histogram {
42    fn clone(&self) -> Self {
43        Histogram {
44            inner: self.inner.clone(),
45        }
46    }
47}
48
49#[derive(Debug)]
50pub(crate) struct Inner {
51    // TODO: Consider allowing integer observe values.
52    sum: f64,
53    count: u64,
54    // TODO: Consider being generic over the bucket length.
55    buckets: Vec<(f64, u64)>,
56}
57
58impl Histogram {
59    /// Create a new [`Histogram`].
60    ///
61    /// ```rust
62    /// # use prometheus_client::metrics::histogram::Histogram;
63    /// let histogram = Histogram::new([10.0, 100.0, 1_000.0]);
64    /// ```
65    pub fn new(buckets: impl IntoIterator<Item = f64>) -> Self {
66        Self {
67            inner: Arc::new(RwLock::new(Inner {
68                sum: Default::default(),
69                count: Default::default(),
70                buckets: buckets
71                    .into_iter()
72                    .chain(once(f64::MAX))
73                    .map(|upper_bound| (upper_bound, 0))
74                    .collect(),
75            })),
76        }
77    }
78
79    /// Observe the given value.
80    pub fn observe(&self, v: f64) {
81        self.observe_and_bucket(v);
82    }
83
84    /// Observes the given value, returning the index of the first bucket the
85    /// value is added to.
86    ///
87    /// Needed in
88    /// [`HistogramWithExemplars`](crate::metrics::exemplar::HistogramWithExemplars).
89    pub(crate) fn observe_and_bucket(&self, v: f64) -> Option<usize> {
90        let mut inner = self.inner.write();
91        inner.sum += v;
92        inner.count += 1;
93
94        let first_bucket = inner
95            .buckets
96            .iter_mut()
97            .enumerate()
98            .find(|(_i, (upper_bound, _value))| upper_bound >= &v);
99
100        match first_bucket {
101            Some((i, (_upper_bound, value))) => {
102                *value += 1;
103                Some(i)
104            }
105            None => None,
106        }
107    }
108
109    pub(crate) fn get(&self) -> (f64, u64, MappedRwLockReadGuard<Vec<(f64, u64)>>) {
110        let inner = self.inner.read();
111        let sum = inner.sum;
112        let count = inner.count;
113        let buckets = RwLockReadGuard::map(inner, |inner| &inner.buckets);
114        (sum, count, buckets)
115    }
116}
117
118impl TypedMetric for Histogram {
119    const TYPE: MetricType = MetricType::Histogram;
120}
121
122/// Exponential bucket distribution.
123pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterator<Item = f64> {
124    iter::repeat(())
125        .enumerate()
126        .map(move |(i, _)| start * factor.powf(i as f64))
127        .take(length.into())
128}
129
130/// Exponential bucket distribution within a range
131///
132/// Creates `length` buckets, where the lowest bucket is `min` and the highest bucket is `max`.
133///
134/// If `length` is less than 1, or `min` is less than or equal to 0, an empty iterator is returned.
135pub fn exponential_buckets_range(min: f64, max: f64, length: u16) -> impl Iterator<Item = f64> {
136    let mut len_observed = length;
137    let mut min_bucket = min;
138    // length needs a positive length and min needs to be greater than 0
139    // set len_observed to 0 and min_bucket to 1.0
140    // this will return an empty iterator in the result
141    if length < 1 || min <= 0.0 {
142        len_observed = 0;
143        min_bucket = 1.0;
144    }
145    // We know max/min and highest bucket. Solve for growth_factor.
146    let growth_factor = (max / min_bucket).powf(1.0 / (len_observed as f64 - 1.0));
147
148    iter::repeat(())
149        .enumerate()
150        .map(move |(i, _)| min_bucket * growth_factor.powf(i as f64))
151        .take(len_observed.into())
152}
153
154/// Linear bucket distribution.
155pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> {
156    iter::repeat(())
157        .enumerate()
158        .map(move |(i, _)| start + (width * (i as f64)))
159        .take(length.into())
160}
161
162impl EncodeMetric for Histogram {
163    fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
164        let (sum, count, buckets) = self.get();
165        encoder.encode_histogram::<NoLabelSet>(sum, count, &buckets, None)
166    }
167
168    fn metric_type(&self) -> MetricType {
169        Self::TYPE
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn histogram() {
179        let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
180        histogram.observe(1.0);
181    }
182
183    #[test]
184    fn exponential() {
185        assert_eq!(
186            vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0],
187            exponential_buckets(1.0, 2.0, 10).collect::<Vec<_>>()
188        );
189    }
190
191    #[test]
192    fn linear() {
193        assert_eq!(
194            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
195            linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>()
196        );
197    }
198
199    #[test]
200    fn exponential_range() {
201        assert_eq!(
202            vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0],
203            exponential_buckets_range(1.0, 32.0, 6).collect::<Vec<_>>()
204        );
205    }
206
207    #[test]
208    fn exponential_range_incorrect() {
209        let res = exponential_buckets_range(1.0, 32.0, 0).collect::<Vec<_>>();
210        assert!(res.is_empty());
211
212        let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>();
213        assert!(res.is_empty());
214    }
215}