metrics/metrics-util/src/histogram.rs

148 lines
3.9 KiB
Rust

//! Helper functions and types related to histogram data.
/// A bucketed histogram.
///
/// This histogram tracks the number of samples that fall into pre-defined buckets,
/// rather than exposing any sort of quantiles.
///
/// This type is most useful with systems that prefer bucketed data, such as Prometheus'
/// histogram type, as opposed to its summary type, which deals with quantiles.
#[derive(Debug, Clone)]
pub struct Histogram {
count: u64,
bounds: Vec<u64>,
buckets: Vec<u64>,
sum: u64,
}
impl Histogram {
/// Creates a new `Histogram`.
///
/// If `bounds` is empty, returns `None`.
pub fn new(bounds: &[u64]) -> Option<Histogram> {
if bounds.len() == 0 {
return None;
}
let mut buckets = Vec::with_capacity(bounds.len());
for _ in bounds {
buckets.push(0);
}
Some(Histogram {
count: 0,
bounds: Vec::from(bounds),
buckets,
sum: 0,
})
}
/// Gets the sum of all samples.
pub fn sum(&self) -> u64 {
self.sum
}
/// Gets the sample count.
pub fn count(&self) -> u64 {
self.count
}
/// Gets the buckets.
///
/// Buckets are tuples, where the first element is the bucket limit itself, and the second
/// element is the count of samples in that bucket.
pub fn buckets(&self) -> Vec<(u64, u64)> {
self.bounds
.iter()
.cloned()
.zip(self.buckets.iter().cloned())
.collect()
}
/// Records a single sample.
pub fn record(&mut self, sample: u64) {
self.sum += sample;
self.count += 1;
// Add the sample to every bucket where the value is less than the bound.
for (idx, bucket) in self.bounds.iter().enumerate() {
if sample <= *bucket {
self.buckets[idx] += 1;
}
}
}
/// Records multiple samples.
pub fn record_many<'a, S>(&mut self, samples: S)
where
S: IntoIterator<Item = &'a u64> + 'a,
{
let mut bucketed = Vec::with_capacity(self.buckets.len());
for _ in 0..self.buckets.len() {
bucketed.push(0);
}
let mut sum = 0;
let mut count = 0;
for sample in samples.into_iter() {
sum += *sample;
count += 1;
for (idx, bucket) in self.bounds.iter().enumerate() {
if sample <= bucket {
bucketed[idx] += 1;
break;
}
}
}
// Add each bucket to the next bucket to satisfy the "less than or equal to"
// behavior of the buckets.
if bucketed.len() >= 2 {
for idx in 0..(bucketed.len() - 1) {
bucketed[idx + 1] += bucketed[idx];
}
}
// Merge our temporary buckets to our main buckets.
for (idx, local) in bucketed.iter().enumerate() {
self.buckets[idx] += local;
}
self.sum += sum;
self.count += count;
}
}
#[cfg(test)]
mod tests {
use super::Histogram;
#[test]
fn test_histogram() {
// No buckets, can't do shit.
let histogram = Histogram::new(&[]);
assert!(histogram.is_none());
let buckets = &[10, 25, 100];
let values = vec![3, 2, 6, 12, 56, 82, 202, 100, 29];
let mut histogram = Histogram::new(buckets).expect("histogram should have been created");
histogram.record_many(&values);
histogram.record(89);
let result = histogram.buckets();
assert_eq!(result.len(), 3);
let (_, first) = result[0];
assert_eq!(first, 3);
let (_, second) = result[1];
assert_eq!(second, 4);
let (_, third) = result[2];
assert_eq!(third, 9);
assert_eq!(histogram.count(), values.len() as u64 + 1);
assert_eq!(histogram.sum(), 581);
}
}