use std::sync::Mutex;
const BUCKET_BOUNDS_US: &[u64] = &[
10, 50, 100, 500, 1_000, 5_000, 10_000, 50_000, 100_000, 500_000, 1_000_000,
];
const NUM_BUCKETS: usize = 12;
struct HistogramInner {
buckets: [u64; NUM_BUCKETS],
sum_us: u64,
count: u64,
}
impl HistogramInner {
fn new() -> Self {
Self {
buckets: [0; NUM_BUCKETS],
sum_us: 0,
count: 0,
}
}
fn observe(&mut self, value_us: u64) {
self.sum_us = self.sum_us.saturating_add(value_us);
self.count += 1;
for (i, &bound) in BUCKET_BOUNDS_US.iter().enumerate() {
if value_us <= bound {
self.buckets[i] += 1;
}
}
self.buckets[NUM_BUCKETS - 1] += 1;
}
fn reset(&mut self) {
self.buckets = [0; NUM_BUCKETS];
self.sum_us = 0;
self.count = 0;
}
}
pub struct Histogram {
inner: Mutex<HistogramInner>,
pub name: &'static str,
pub help: &'static str,
}
impl Histogram {
pub fn new(name: &'static str, help: &'static str) -> Self {
Self {
inner: Mutex::new(HistogramInner::new()),
name,
help,
}
}
pub fn observe(&self, value_us: u64) {
if let Ok(mut inner) = self.inner.lock() {
inner.observe(value_us);
}
}
pub fn reset(&self) {
if let Ok(mut inner) = self.inner.lock() {
inner.reset();
}
}
pub fn export_prometheus(&self, out: &mut String) {
let inner = match self.inner.lock() {
Ok(inner) => inner,
Err(_) => return,
};
out.push_str(&format!("# HELP {} {}\n", self.name, self.help));
out.push_str(&format!("# TYPE {} histogram\n", self.name));
for (i, &bound) in BUCKET_BOUNDS_US.iter().enumerate() {
let label = if bound < 1_000 {
format!("{}µs", bound)
} else if bound < 1_000_000 {
format!("{}ms", bound / 1_000)
} else {
format!("{}s", bound / 1_000_000)
};
out.push_str(&format!(
"{}_bucket{{le=\"{}\"}} {}\n",
self.name, label, inner.buckets[i]
));
}
out.push_str(&format!(
"{}_bucket{{le=\"+Inf\"}} {}\n",
self.name,
inner.buckets[NUM_BUCKETS - 1]
));
out.push_str(&format!("{}_sum {}\n", self.name, inner.sum_us));
out.push_str(&format!("{}_count {}\n", self.name, inner.count));
}
pub fn snapshot(&self) -> (u64, u64) {
self.inner
.lock()
.map(|inner| (inner.sum_us, inner.count))
.unwrap_or((0, 0))
}
pub fn bucket_snapshot(&self) -> ([u64; NUM_BUCKETS], u64) {
self.inner
.lock()
.map(|inner| (inner.buckets, inner.count))
.unwrap_or(([0; NUM_BUCKETS], 0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_observe_buckets() {
let h = Histogram::new("test", "test histogram");
h.observe(5); h.observe(100); h.observe(2_000_000);
let (buckets, count) = h.bucket_snapshot();
assert_eq!(count, 3);
assert_eq!(buckets[0], 1);
assert_eq!(buckets[NUM_BUCKETS - 1], 3);
}
#[test]
fn test_reset() {
let h = Histogram::new("test", "test histogram");
h.observe(100);
h.reset();
let (sum, count) = h.snapshot();
assert_eq!(sum, 0);
assert_eq!(count, 0);
}
#[test]
fn test_prometheus_export() {
let h = Histogram::new("dbx_query_latency_us", "Query latency");
h.observe(80);
let mut out = String::new();
h.export_prometheus(&mut out);
assert!(out.contains("dbx_query_latency_us_count 1"));
assert!(out.contains("# TYPE dbx_query_latency_us histogram"));
}
}