use super::{histogram::HistogramSnapshot, Percentile};
use std::{collections::HashMap, fmt::Display};
#[derive(Debug, PartialEq, Eq)]
pub enum TypedMeasurement {
Counter(String, i64),
Gauge(String, u64),
TimingHistogram(String, SummarizedHistogram),
ValueHistogram(String, SummarizedHistogram),
}
#[derive(Default, Debug)]
pub struct Snapshot {
measurements: Vec<TypedMeasurement>,
}
impl Snapshot {
pub(crate) fn set_count<T>(&mut self, key: T, value: i64)
where
T: Display,
{
self.measurements
.push(TypedMeasurement::Counter(key.to_string(), value));
}
pub(crate) fn set_gauge<T>(&mut self, key: T, value: u64)
where
T: Display,
{
self.measurements.push(TypedMeasurement::Gauge(key.to_string(), value));
}
pub(crate) fn set_timing_histogram<T>(&mut self, key: T, h: HistogramSnapshot, percentiles: &[Percentile])
where
T: Display,
{
let summarized = SummarizedHistogram::from_histogram(h, percentiles);
self.measurements
.push(TypedMeasurement::TimingHistogram(key.to_string(), summarized));
}
pub(crate) fn set_value_histogram<T>(&mut self, key: T, h: HistogramSnapshot, percentiles: &[Percentile])
where
T: Display,
{
let summarized = SummarizedHistogram::from_histogram(h, percentiles);
self.measurements
.push(TypedMeasurement::ValueHistogram(key.to_string(), summarized));
}
pub fn into_simple(self) -> SimpleSnapshot { SimpleSnapshot::from_snapshot(self) }
pub fn into_vec(self) -> Vec<TypedMeasurement> { self.measurements }
}
#[derive(Default)]
pub struct SimpleSnapshot {
pub(crate) counters: HashMap<String, i64>,
pub(crate) gauges: HashMap<String, u64>,
pub(crate) timings: HashMap<String, SummarizedHistogram>,
pub(crate) values: HashMap<String, SummarizedHistogram>,
}
impl SimpleSnapshot {
pub(crate) fn from_snapshot(s: Snapshot) -> Self {
let mut ss = SimpleSnapshot::default();
for metric in s.into_vec() {
match metric {
TypedMeasurement::Counter(key, value) => {
ss.counters.insert(key, value);
},
TypedMeasurement::Gauge(key, value) => {
ss.gauges.insert(key, value);
},
TypedMeasurement::TimingHistogram(key, value) => {
ss.timings.insert(key, value);
},
TypedMeasurement::ValueHistogram(key, value) => {
ss.values.insert(key, value);
},
}
}
ss
}
pub fn count(&self, key: &str) -> Option<i64> { self.counters.get(key).cloned() }
pub fn gauge(&self, key: &str) -> Option<u64> { self.gauges.get(key).cloned() }
pub fn timing_histogram(&self, key: &str, percentile: f64) -> Option<u64> {
let p = Percentile::from(percentile);
self.timings.get(key).and_then(|s| s.measurements().get(&p)).cloned()
}
pub fn value_histogram(&self, key: &str, percentile: f64) -> Option<u64> {
let p = Percentile::from(percentile);
self.values.get(key).and_then(|s| s.measurements().get(&p)).cloned()
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct SummarizedHistogram {
count: u64,
sum: u64,
measurements: HashMap<Percentile, u64>,
}
impl SummarizedHistogram {
pub(crate) fn from_histogram(histogram: HistogramSnapshot, percentiles: &[Percentile]) -> Self {
let mut measurements = HashMap::default();
let count = histogram.count();
let sum = histogram.sum();
for percentile in percentiles {
let value = histogram.histogram().value_at_percentile(percentile.value);
measurements.insert(percentile.clone(), value);
}
SummarizedHistogram {
count,
sum,
measurements,
}
}
pub fn count(&self) -> u64 { self.count }
pub fn sum(&self) -> u64 { self.sum }
pub fn measurements(&self) -> &HashMap<Percentile, u64> { &self.measurements }
}
#[cfg(test)]
mod tests {
use super::{HistogramSnapshot, Percentile, Snapshot, TypedMeasurement};
use hdrhistogram::Histogram;
#[test]
fn test_snapshot_simple_set_and_get() {
let key = "ok".to_owned();
let mut snapshot = Snapshot::default();
snapshot.set_count(key.clone(), 1);
snapshot.set_gauge(key.clone(), 42);
let values = snapshot.into_vec();
assert_eq!(values[0], TypedMeasurement::Counter("ok".to_owned(), 1));
assert_eq!(values[1], TypedMeasurement::Gauge("ok".to_owned(), 42));
}
#[test]
fn test_snapshot_percentiles() {
{
let mut snapshot = Snapshot::default();
let mut h1 = Histogram::<u64>::new_with_bounds(1, u64::max_value(), 3).unwrap();
let mut sum = 0;
h1.saturating_record(500_000);
sum += 500_000;
h1.saturating_record(750_000);
sum += 750_000;
h1.saturating_record(1_000_000);
sum += 1_000_000;
h1.saturating_record(1_250_000);
sum += 1_250_000;
let tkey = "ok".to_owned();
let mut tpercentiles = Vec::new();
tpercentiles.push(Percentile::from(0.0));
tpercentiles.push(Percentile::from(50.0));
tpercentiles.push(Percentile::from(99.0));
tpercentiles.push(Percentile::from(100.0));
let fake = Percentile::from(63.0);
snapshot.set_timing_histogram(tkey.clone(), HistogramSnapshot::new(h1, sum), &tpercentiles);
let values = snapshot.into_vec();
match values.get(0) {
Some(TypedMeasurement::TimingHistogram(key, summary)) => {
assert_eq!(key, "ok");
assert_eq!(summary.count(), 4);
assert_eq!(summary.sum(), 3_500_000);
let min_tpercentile = summary.measurements().get(&tpercentiles[0]);
let p50_tpercentile = summary.measurements().get(&tpercentiles[1]);
let p99_tpercentile = summary.measurements().get(&tpercentiles[2]);
let max_tpercentile = summary.measurements().get(&tpercentiles[3]);
let fake_tpercentile = summary.measurements().get(&fake);
assert!(min_tpercentile.is_some());
assert!(p50_tpercentile.is_some());
assert!(p99_tpercentile.is_some());
assert!(max_tpercentile.is_some());
assert!(fake_tpercentile.is_none());
},
_ => panic!("expected timing histogram value! actual: {:?}", values[0]),
}
}
{
let mut snapshot = Snapshot::default();
let mut h1 = Histogram::<u64>::new_with_bounds(1, u64::max_value(), 3).unwrap();
let mut sum = 0;
h1.saturating_record(500_000);
sum += 500_000;
h1.saturating_record(750_000);
sum += 750_000;
h1.saturating_record(1_000_000);
sum += 1_000_000;
h1.saturating_record(1_250_000);
sum += 1_250_000;
let tkey = "ok".to_owned();
let mut tpercentiles = Vec::new();
tpercentiles.push(Percentile::from(0.0));
tpercentiles.push(Percentile::from(50.0));
tpercentiles.push(Percentile::from(99.0));
tpercentiles.push(Percentile::from(100.0));
let fake = Percentile::from(63.0);
snapshot.set_value_histogram(tkey.clone(), HistogramSnapshot::new(h1, sum), &tpercentiles);
let values = snapshot.into_vec();
match values.get(0) {
Some(TypedMeasurement::ValueHistogram(key, summary)) => {
assert_eq!(key, "ok");
assert_eq!(summary.count(), 4);
assert_eq!(summary.sum(), 3_500_000);
let min_tpercentile = summary.measurements().get(&tpercentiles[0]);
let p50_tpercentile = summary.measurements().get(&tpercentiles[1]);
let p99_tpercentile = summary.measurements().get(&tpercentiles[2]);
let max_tpercentile = summary.measurements().get(&tpercentiles[3]);
let fake_tpercentile = summary.measurements().get(&fake);
assert!(min_tpercentile.is_some());
assert!(p50_tpercentile.is_some());
assert!(p99_tpercentile.is_some());
assert!(max_tpercentile.is_some());
assert!(fake_tpercentile.is_none());
},
_ => panic!("expected value histogram value! actual: {:?}", values[0]),
}
}
}
#[test]
fn test_percentiles() {
let min_p = Percentile::from(0.0);
assert_eq!(min_p.label(), "min");
let max_p = Percentile::from(100.0);
assert_eq!(max_p.label(), "max");
let clamped_min_p = Percentile::from(-20.0);
assert_eq!(clamped_min_p.label(), "min");
assert_eq!(clamped_min_p.percentile(), 0.0);
let clamped_max_p = Percentile::from(1442.0);
assert_eq!(clamped_max_p.label(), "max");
assert_eq!(clamped_max_p.percentile(), 100.0);
let p99_p = Percentile::from(99.0);
assert_eq!(p99_p.label(), "p99");
let p999_p = Percentile::from(99.9);
assert_eq!(p999_p.label(), "p999");
let p9999_p = Percentile::from(99.99);
assert_eq!(p9999_p.label(), "p9999");
}
}