Skip to main content

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    /// Returns the current sum of all observations.
85    #[cfg(any(test, feature = "test-util"))]
86    pub fn sum(&self) -> f64 {
87        self.inner.read().sum
88    }
89
90    /// Returns the current number of observations.
91    #[cfg(any(test, feature = "test-util"))]
92    pub fn count(&self) -> u64 {
93        self.inner.read().count
94    }
95
96    /// Observes the given value, returning the index of the first bucket the
97    /// value is added to.
98    ///
99    /// Needed in
100    /// [`HistogramWithExemplars`](crate::metrics::exemplar::HistogramWithExemplars).
101    pub(crate) fn observe_and_bucket(&self, v: f64) -> Option<usize> {
102        let mut inner = self.inner.write();
103        inner.sum += v;
104        inner.count += 1;
105
106        let first_bucket = inner
107            .buckets
108            .iter_mut()
109            .enumerate()
110            .find(|(_i, (upper_bound, _value))| upper_bound >= &v);
111
112        match first_bucket {
113            Some((i, (_upper_bound, value))) => {
114                *value += 1;
115                Some(i)
116            }
117            None => None,
118        }
119    }
120
121    pub(crate) fn get(&self) -> (f64, u64, MappedRwLockReadGuard<'_, Vec<(f64, u64)>>) {
122        let inner = self.inner.read();
123        let sum = inner.sum;
124        let count = inner.count;
125        let buckets = RwLockReadGuard::map(inner, |inner| &inner.buckets);
126        (sum, count, buckets)
127    }
128}
129
130impl TypedMetric for Histogram {
131    const TYPE: MetricType = MetricType::Histogram;
132}
133
134/// Exponential bucket distribution.
135pub fn exponential_buckets(start: f64, factor: f64, length: u16) -> impl Iterator<Item = f64> {
136    iter::repeat(())
137        .enumerate()
138        .map(move |(i, _)| start * factor.powf(i as f64))
139        .take(length.into())
140}
141
142/// Exponential bucket distribution within a range
143///
144/// Creates `length` buckets, where the lowest bucket is `min` and the highest bucket is `max`.
145///
146/// If `length` is less than 1, or `min` is less than or equal to 0, an empty iterator is returned.
147pub fn exponential_buckets_range(min: f64, max: f64, length: u16) -> impl Iterator<Item = f64> {
148    let mut len_observed = length;
149    let mut min_bucket = min;
150    // length needs a positive length and min needs to be greater than 0
151    // set len_observed to 0 and min_bucket to 1.0
152    // this will return an empty iterator in the result
153    if length < 1 || min <= 0.0 {
154        len_observed = 0;
155        min_bucket = 1.0;
156    }
157    // We know max/min and highest bucket. Solve for growth_factor.
158    let growth_factor = (max / min_bucket).powf(1.0 / (len_observed as f64 - 1.0));
159
160    iter::repeat(())
161        .enumerate()
162        .map(move |(i, _)| min_bucket * growth_factor.powf(i as f64))
163        .take(len_observed.into())
164}
165
166/// Linear bucket distribution.
167pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item = f64> {
168    iter::repeat(())
169        .enumerate()
170        .map(move |(i, _)| start + (width * (i as f64)))
171        .take(length.into())
172}
173
174impl EncodeMetric for Histogram {
175    fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
176        let (sum, count, buckets) = self.get();
177        encoder.encode_histogram::<NoLabelSet>(sum, count, &buckets, None)
178    }
179
180    fn metric_type(&self) -> MetricType {
181        Self::TYPE
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn histogram() {
191        let histogram = Histogram::new(exponential_buckets(1.0, 2.0, 10));
192        histogram.observe(1.0);
193    }
194
195    #[test]
196    fn exponential() {
197        assert_eq!(
198            vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0],
199            exponential_buckets(1.0, 2.0, 10).collect::<Vec<_>>()
200        );
201    }
202
203    #[test]
204    fn linear() {
205        assert_eq!(
206            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
207            linear_buckets(0.0, 1.0, 10).collect::<Vec<_>>()
208        );
209    }
210
211    #[test]
212    fn exponential_range() {
213        assert_eq!(
214            vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0],
215            exponential_buckets_range(1.0, 32.0, 6).collect::<Vec<_>>()
216        );
217    }
218
219    #[test]
220    fn exponential_range_incorrect() {
221        let res = exponential_buckets_range(1.0, 32.0, 0).collect::<Vec<_>>();
222        assert!(res.is_empty());
223
224        let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>();
225        assert!(res.is_empty());
226    }
227
228    /// Checks that [`Histogram::count()`] works properly.
229    #[test]
230    fn count() {
231        let histogram = Histogram::new([1.0_f64, 2.0, 3.0, 4.0, 5.0]);
232        assert_eq!(
233            histogram.count(),
234            0,
235            "histogram has zero observations when instantiated"
236        );
237
238        histogram.observe(1.0);
239        assert_eq!(histogram.count(), 1, "histogram has one observation");
240
241        histogram.observe(2.5);
242        assert_eq!(histogram.count(), 2, "histogram has two observations");
243
244        histogram.observe(6.0);
245        assert_eq!(histogram.count(), 3, "histogram has three observations");
246    }
247
248    /// Checks that [`Histogram::sum()`] works properly.
249    #[test]
250    fn sum() {
251        const BUCKETS: [f64; 3] = [10.0, 100.0, 1000.0];
252        let histogram = Histogram::new(BUCKETS);
253        assert_eq!(
254            histogram.sum(),
255            0.0,
256            "histogram sum is zero when instantiated"
257        );
258
259        histogram.observe(3.0); // 3 + 4 + 15 + 101 = 123
260        histogram.observe(4.0);
261        histogram.observe(15.0);
262        histogram.observe(101.0);
263        assert_eq!(
264            histogram.sum(),
265            123.0,
266            "histogram sum records accurate sum of observations"
267        );
268
269        histogram.observe(1111.0);
270        assert_eq!(
271            histogram.sum(),
272            1234.0,
273            "histogram sum records accurate sum of observations"
274        );
275    }
276}