metrics_util/storage/
histogram.rs

1//! Helper functions and types related to histogram data.
2
3/// A bucketed histogram.
4///
5/// This histogram tracks the number of samples that fall into pre-defined buckets,
6/// rather than exposing any sort of quantiles.
7///
8/// This type is most useful with systems that prefer bucketed data, such as Prometheus'
9/// histogram type, as opposed to its summary type, which deals with quantiles.
10#[derive(Debug, Clone)]
11pub struct Histogram {
12    count: u64,
13    bounds: Vec<f64>,
14    buckets: Vec<u64>,
15    sum: f64,
16}
17
18impl Histogram {
19    /// Creates a new `Histogram`.
20    ///
21    /// If `bounds` is empty, returns `None`.
22    pub fn new(bounds: &[f64]) -> Option<Histogram> {
23        if bounds.is_empty() {
24            return None;
25        }
26
27        let buckets = vec![0u64; bounds.len()];
28
29        Some(Histogram { count: 0, bounds: Vec::from(bounds), buckets, sum: 0.0 })
30    }
31
32    /// Gets the sum of all samples.
33    pub fn sum(&self) -> f64 {
34        self.sum
35    }
36
37    /// Gets the sample count.
38    pub fn count(&self) -> u64 {
39        self.count
40    }
41
42    /// Gets the buckets.
43    ///
44    /// Buckets are tuples, where the first element is the bucket limit itself, and the second
45    /// element is the count of samples in that bucket.
46    pub fn buckets(&self) -> Vec<(f64, u64)> {
47        self.bounds.iter().cloned().zip(self.buckets.iter().cloned()).collect()
48    }
49
50    /// Records a single sample.
51    pub fn record(&mut self, sample: f64) {
52        self.sum += sample;
53        self.count += 1;
54
55        // Add the sample to every bucket where the value is less than the bound.
56        for (idx, bucket) in self.bounds.iter().enumerate() {
57            if sample <= *bucket {
58                self.buckets[idx] += 1;
59            }
60        }
61    }
62
63    /// Records multiple samples.
64    pub fn record_many<'a, S>(&mut self, samples: S)
65    where
66        S: IntoIterator<Item = &'a f64> + 'a,
67    {
68        let mut bucketed = vec![0u64; self.buckets.len()];
69
70        let mut sum = 0.0;
71        let mut count = 0;
72        for sample in samples.into_iter() {
73            sum += *sample;
74            count += 1;
75
76            for (idx, bucket) in self.bounds.iter().enumerate() {
77                if sample <= bucket {
78                    bucketed[idx] += 1;
79                    break;
80                }
81            }
82        }
83
84        // Add each bucket to the next bucket to satisfy the "less than or equal to"
85        // behavior of the buckets.
86        if bucketed.len() >= 2 {
87            for idx in 0..(bucketed.len() - 1) {
88                bucketed[idx + 1] += bucketed[idx];
89            }
90        }
91
92        // Merge our temporary buckets to our main buckets.
93        for (idx, local) in bucketed.iter().enumerate() {
94            self.buckets[idx] += local;
95        }
96        self.sum += sum;
97        self.count += count;
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::Histogram;
104
105    #[test]
106    fn test_histogram() {
107        // No buckets, can't do shit.
108        let histogram = Histogram::new(&[]);
109        assert!(histogram.is_none());
110
111        let buckets = &[10.0, 25.0, 100.0];
112        let values = vec![3.0, 2.0, 6.0, 12.0, 56.0, 82.0, 202.0, 100.0, 29.0];
113
114        let mut histogram = Histogram::new(buckets).expect("histogram should have been created");
115
116        histogram.record_many(&values);
117        histogram.record(89.0);
118
119        let result = histogram.buckets();
120        assert_eq!(result.len(), 3);
121
122        let (_, first) = result[0];
123        assert_eq!(first, 3);
124        let (_, second) = result[1];
125        assert_eq!(second, 4);
126        let (_, third) = result[2];
127        assert_eq!(third, 9);
128
129        assert_eq!(histogram.count(), values.len() as u64 + 1);
130        assert_eq!(histogram.sum(), 581.0);
131    }
132}