use std::{
fmt::{self, Debug},
sync::Arc,
time::Duration,
};
use crate::{
encoder::{EncodeMetric, MetricEncoder},
error::Result,
metrics::internal::histogram::{BoundsFilter, HistogramCore},
raw::{MetricLabelSet, MetricType, TypedMetric},
};
pub use crate::{metrics::internal::histogram::HistogramSnapshot, raw::bucket::*};
#[derive(Clone)]
pub struct Histogram {
inner: Arc<HistogramCore>,
created: Option<Duration>,
}
impl Debug for Histogram {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let created = self.created();
self.with_snapshot(|snapshot| {
f.debug_struct("Histogram")
.field("buckets", &snapshot.buckets())
.field("sum", &snapshot.sum())
.field("count", &snapshot.count())
.field("created", &created)
.finish()
})
}
}
impl Default for Histogram {
fn default() -> Self {
Self::new(DEFAULT_BUCKETS)
}
}
impl Histogram {
pub fn new(buckets: impl IntoIterator<Item = f64>) -> Self {
Self {
inner: Arc::new(HistogramCore::from_bounds(buckets, BoundsFilter::RejectNegative)),
created: None,
}
}
pub fn with_created(buckets: impl IntoIterator<Item = f64>, created: Duration) -> Self {
Self {
inner: Arc::new(HistogramCore::from_bounds(buckets, BoundsFilter::RejectNegative)),
created: Some(created),
}
}
pub fn observe(&self, value: f64) {
if value.is_nan() || value.is_sign_negative() {
return;
}
self.inner.observe(value);
}
pub fn with_snapshot<F, R>(&self, func: F) -> R
where
F: FnOnce(&HistogramSnapshot) -> R,
{
let snapshot = self.inner.snapshot();
func(&snapshot)
}
pub const fn created(&self) -> Option<Duration> {
self.created
}
}
impl TypedMetric for Histogram {
const TYPE: MetricType = MetricType::Histogram;
}
impl MetricLabelSet for Histogram {
type LabelSet = ();
}
impl EncodeMetric for Histogram {
fn encode(&self, encoder: &mut dyn MetricEncoder) -> Result<()> {
let created = self.created();
self.with_snapshot(|s| {
let buckets = s.buckets();
let exemplars = None;
encoder.encode_histogram(buckets, exemplars, s.count(), s.sum(), created)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::check_text_encoding;
#[test]
fn test_histogram_initialization() {
let hist = Histogram::default();
hist.with_snapshot(|s| {
let buckets = s.buckets();
assert_eq!(buckets.len(), DEFAULT_BUCKETS.len() + 1); assert_eq!(s.count(), 0);
assert_eq!(s.sum(), 0.0);
});
assert!(hist.created().is_none());
let bounds = vec![1.0, 2.0, 5.0];
let hist = Histogram::new(bounds);
hist.with_snapshot(|s| {
let buckets = s.buckets();
assert_eq!(buckets.len(), 4); assert_eq!(buckets[0].upper_bound(), 1.0);
assert_eq!(buckets[1].upper_bound(), 2.0);
assert_eq!(buckets[2].upper_bound(), 5.0);
assert_eq!(buckets[3].upper_bound(), f64::INFINITY);
});
let created = std::time::SystemTime::UNIX_EPOCH
.elapsed()
.expect("UNIX timestamp when the histogram was created");
let hist = Histogram::with_created(vec![1.0, 2.0], created);
assert!(hist.created().is_some());
}
#[test]
fn test_histogram_observe() {
let hist = Histogram::new(vec![1.0, 2.0, 5.0]);
hist.observe(1.5);
hist.observe(0.5);
hist.observe(3.0);
hist.observe(6.0);
hist.with_snapshot(|s| {
let buckets = s.buckets();
assert_eq!(buckets[0].count(), 1); assert_eq!(buckets[1].count(), 1); assert_eq!(buckets[2].count(), 1); assert_eq!(buckets[3].count(), 1); assert_eq!(s.count(), 4);
assert_eq!(s.sum(), 11.0);
});
}
#[test]
fn test_histogram_invalid_observations() {
let hist = Histogram::default();
hist.observe(-1.0); hist.observe(f64::NAN);
hist.with_snapshot(|s| {
assert_eq!(s.count(), 0);
assert_eq!(s.sum(), 0.0);
});
}
#[test]
fn test_histogram_thread_safe() {
let hist = Histogram::new(vec![1.0, 2.0, 5.0]);
let clone = hist.clone();
let handle = std::thread::spawn(move || {
for i in 1..=100 {
clone.observe(i as f64);
}
});
for i in 1..=100 {
hist.observe(i as f64);
}
handle.join().unwrap();
hist.with_snapshot(|s| {
assert_eq!(s.count(), 200);
assert_eq!(s.sum(), 10100.0);
});
}
#[test]
fn test_text_encoding() {
check_text_encoding(
|registry| {
let hist = Histogram::new(exponential_buckets(1.0, 2.0, 5));
registry.register("my_histogram", "My histogram help", hist.clone()).unwrap();
for i in 1..=100 {
hist.observe(i as f64);
}
},
|output| {
let expected = indoc::indoc! {r#"
# TYPE my_histogram histogram
# HELP my_histogram My histogram help
my_histogram_bucket{le="1.0"} 1
my_histogram_bucket{le="2.0"} 2
my_histogram_bucket{le="4.0"} 4
my_histogram_bucket{le="8.0"} 8
my_histogram_bucket{le="16.0"} 16
my_histogram_bucket{le="+Inf"} 100
my_histogram_count 100
my_histogram_sum 5050.0
# EOF
"#};
assert_eq!(expected, output);
},
);
}
}