use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
pub struct Counter {
value: AtomicU64,
name: String,
labels: Vec<(String, String)>,
}
impl Counter {
pub fn new(name: impl Into<String>, labels: Vec<(String, String)>) -> Self {
Self {
value: AtomicU64::new(0),
name: name.into(),
labels,
}
}
pub fn add(&self, value: u64) {
self.value.fetch_add(value, Ordering::Relaxed);
}
pub fn inc(&self) {
self.add(1);
}
pub fn get(&self) -> u64 {
self.value.load(Ordering::Relaxed)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn labels(&self) -> &[(String, String)] {
&self.labels
}
}
impl std::fmt::Debug for Counter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Counter")
.field("name", &self.name)
.field("value", &self.get())
.finish()
}
}
pub struct Gauge {
bits: AtomicU64,
name: String,
}
impl Gauge {
pub fn new(name: impl Into<String>) -> Self {
Self {
bits: AtomicU64::new(0f64.to_bits()),
name: name.into(),
}
}
pub fn set(&self, v: f64) {
self.bits.store(v.to_bits(), Ordering::Relaxed);
}
pub fn get(&self) -> f64 {
f64::from_bits(self.bits.load(Ordering::Relaxed))
}
pub fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Debug for Gauge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gauge")
.field("name", &self.name)
.field("value", &self.get())
.finish()
}
}
pub struct Histogram {
boundaries: Vec<f64>,
counts: Mutex<Vec<u64>>,
sum: Mutex<f64>,
count: AtomicU64,
name: String,
samples: Mutex<Vec<f64>>,
}
impl Histogram {
pub fn new(name: impl Into<String>, boundaries: &[f64]) -> Self {
let mut bounds: Vec<f64> = boundaries.to_vec();
bounds.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
bounds.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
let n = bounds.len() + 1; Self {
boundaries: bounds,
counts: Mutex::new(vec![0u64; n]),
sum: Mutex::new(0.0),
count: AtomicU64::new(0),
name: name.into(),
samples: Mutex::new(Vec::with_capacity(1024)),
}
}
pub fn record(&self, value: f64) {
if let Ok(mut s) = self.sum.lock() {
*s += value;
}
self.count.fetch_add(1, Ordering::Relaxed);
if let Ok(mut counts) = self.counts.lock() {
let mut placed = false;
for (i, &bound) in self.boundaries.iter().enumerate() {
if value <= bound {
for j in i..counts.len() {
counts[j] += 1;
}
placed = true;
break;
}
}
if !placed {
if let Some(last) = counts.last_mut() {
*last += 1;
}
}
}
if let Ok(mut s) = self.samples.lock() {
if s.len() < 10_000 {
s.push(value);
}
}
}
pub fn buckets(&self) -> Vec<(f64, u64)> {
let counts = self.counts.lock().map(|g| g.clone()).unwrap_or_default();
let mut result = Vec::with_capacity(counts.len());
for (i, count) in counts.iter().enumerate() {
let bound = self.boundaries.get(i).copied().unwrap_or(f64::INFINITY);
result.push((bound, *count));
}
result
}
pub fn count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
pub fn sum(&self) -> f64 {
self.sum.lock().map(|g| *g).unwrap_or(0.0)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn percentile(&self, p: f64) -> f64 {
let mut samples = match self.samples.lock() {
Ok(g) => g.clone(),
Err(_) => return 0.0,
};
if samples.is_empty() {
return 0.0;
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((p / 100.0) * (samples.len() as f64 - 1.0))
.round()
.clamp(0.0, (samples.len() - 1) as f64) as usize;
samples[idx]
}
pub fn p50(&self) -> f64 {
self.percentile(50.0)
}
pub fn p95(&self) -> f64 {
self.percentile(95.0)
}
pub fn p99(&self) -> f64 {
self.percentile(99.0)
}
}
impl std::fmt::Debug for Histogram {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Histogram")
.field("name", &self.name)
.field("count", &self.count())
.field("sum", &self.sum())
.finish()
}
}
pub struct MeterRegistry {
counters: Mutex<HashMap<String, Arc<Counter>>>,
gauges: Mutex<HashMap<String, Arc<Gauge>>>,
histograms: Mutex<HashMap<String, Arc<Histogram>>>,
}
impl MeterRegistry {
pub fn new() -> Self {
Self {
counters: Mutex::new(HashMap::new()),
gauges: Mutex::new(HashMap::new()),
histograms: Mutex::new(HashMap::new()),
}
}
pub fn counter(&self, name: &str, labels: &[(&str, &str)]) -> Arc<Counter> {
let key = Self::make_key(name, labels);
let mut map = self.counters.lock().unwrap_or_else(|e| e.into_inner());
map.entry(key)
.or_insert_with(|| {
Arc::new(Counter::new(
name,
labels
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
))
})
.clone()
}
pub fn gauge(&self, name: &str) -> Arc<Gauge> {
let mut map = self.gauges.lock().unwrap_or_else(|e| e.into_inner());
map.entry(name.to_owned())
.or_insert_with(|| Arc::new(Gauge::new(name)))
.clone()
}
pub fn histogram(&self, name: &str, boundaries: &[f64]) -> Arc<Histogram> {
let mut map = self.histograms.lock().unwrap_or_else(|e| e.into_inner());
map.entry(name.to_owned())
.or_insert_with(|| Arc::new(Histogram::new(name, boundaries)))
.clone()
}
fn make_key(name: &str, labels: &[(&str, &str)]) -> String {
if labels.is_empty() {
return name.to_owned();
}
let mut parts: Vec<String> = labels.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
parts.sort();
format!("{}{{{}}}", name, parts.join(","))
}
}
impl Default for MeterRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn test_counter_atomic() {
let counter = Arc::new(Counter::new("requests", vec![]));
let handles: Vec<_> = (0..4)
.map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..250 {
c.add(1);
}
})
})
.collect();
for h in handles {
h.join().expect("thread panicked");
}
assert_eq!(counter.get(), 1000);
}
#[test]
fn test_histogram_buckets() {
let h = Histogram::new("latency", &[1.0, 5.0, 10.0]);
h.record(0.5);
h.record(3.0);
h.record(7.0);
h.record(20.0);
assert_eq!(h.count(), 4);
let buckets = h.buckets();
assert_eq!(buckets[0].1, 1);
assert_eq!(buckets[1].1, 2);
assert_eq!(buckets[2].1, 3);
assert_eq!(buckets[3].1, 4);
}
#[test]
fn test_histogram_percentiles() {
let h = Histogram::new("rt", &[1.0, 10.0, 100.0]);
for i in 1u64..=100 {
h.record(i as f64);
}
let p50 = h.p50();
let p95 = h.p95();
let p99 = h.p99();
assert!(p50 <= p95, "p50={} p95={}", p50, p95);
assert!(p95 <= p99, "p95={} p99={}", p95, p99);
}
#[test]
fn test_meter_registry_same_counter() {
let reg = MeterRegistry::new();
let c1 = reg.counter("reqs", &[("method", "GET")]);
let c2 = reg.counter("reqs", &[("method", "GET")]);
c1.add(5);
assert_eq!(c2.get(), 5);
}
#[test]
fn test_gauge_set_get() {
let g = Gauge::new("cpu");
g.set(0.75);
assert!((g.get() - 0.75).abs() < 1e-9);
}
}