1use std::collections::BTreeMap;
22use std::sync::{
23 Mutex, OnceLock,
24 atomic::{AtomicU64, Ordering},
25};
26
27#[derive(Debug, Default)]
30pub struct Counter {
31 value: AtomicU64,
32}
33
34impl Counter {
35 pub const fn new() -> Self {
36 Self {
37 value: AtomicU64::new(0),
38 }
39 }
40 #[inline(always)]
44 pub fn inc(&self) {
45 self.value.fetch_add(1, Ordering::Relaxed);
46 }
47 #[inline(always)]
48 pub fn add(&self, delta: u64) {
49 self.value.fetch_add(delta, Ordering::Relaxed);
50 }
51 #[inline(always)]
52 pub fn get(&self) -> u64 {
53 self.value.load(Ordering::Relaxed)
54 }
55 pub fn reset(&self) {
56 self.value.store(0, Ordering::Relaxed);
57 }
58}
59
60#[derive(Debug)]
64pub struct Histogram {
65 buckets: [AtomicU64; 16],
66 sum: AtomicU64,
67 count: AtomicU64,
68}
69
70impl Default for Histogram {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl Histogram {
77 pub const fn new() -> Self {
78 #[allow(clippy::declare_interior_mutable_const)]
82 const Z: AtomicU64 = AtomicU64::new(0);
83 Self {
84 buckets: [Z; 16],
85 sum: Z,
86 count: Z,
87 }
88 }
89
90 pub fn record(&self, value: u64) {
91 let bucket = (value.checked_ilog2().unwrap_or(0) as usize).min(15);
92 self.buckets[bucket].fetch_add(1, Ordering::Relaxed);
93 self.sum.fetch_add(value, Ordering::Relaxed);
94 self.count.fetch_add(1, Ordering::Relaxed);
95 }
96
97 pub fn count(&self) -> u64 {
98 self.count.load(Ordering::Relaxed)
99 }
100 pub fn sum(&self) -> u64 {
101 self.sum.load(Ordering::Relaxed)
102 }
103 pub fn mean(&self) -> Option<f64> {
104 let c = self.count();
105 if c == 0 {
106 None
107 } else {
108 Some(self.sum() as f64 / c as f64)
109 }
110 }
111 pub fn bucket_counts(&self) -> [u64; 16] {
112 let mut out = [0u64; 16];
113 for (i, b) in self.buckets.iter().enumerate() {
114 out[i] = b.load(Ordering::Relaxed);
115 }
116 out
117 }
118}
119
120pub struct MetricsRegistry {
124 counters: Mutex<BTreeMap<&'static str, &'static Counter>>,
125 histograms: Mutex<BTreeMap<&'static str, &'static Histogram>>,
126}
127
128impl MetricsRegistry {
129 pub fn global() -> &'static Self {
131 static R: OnceLock<MetricsRegistry> = OnceLock::new();
132 R.get_or_init(|| MetricsRegistry {
133 counters: Mutex::new(BTreeMap::new()),
134 histograms: Mutex::new(BTreeMap::new()),
135 })
136 }
137
138 pub fn register_counter(&self, name: &'static str, c: &'static Counter) {
142 self.counters
143 .lock()
144 .expect("registry poisoned")
145 .insert(name, c);
146 }
147
148 pub fn register_histogram(&self, name: &'static str, h: &'static Histogram) {
149 self.histograms
150 .lock()
151 .expect("registry poisoned")
152 .insert(name, h);
153 }
154
155 pub fn snapshot(&self) -> Snapshot {
158 let counters = self
159 .counters
160 .lock()
161 .unwrap()
162 .iter()
163 .map(|(&n, c)| (n.to_string(), c.get()))
164 .collect();
165 let histograms = self
166 .histograms
167 .lock()
168 .unwrap()
169 .iter()
170 .map(|(&n, h)| {
171 (
172 n.to_string(),
173 HistogramSnapshot {
174 count: h.count(),
175 sum: h.sum(),
176 buckets: h.bucket_counts(),
177 },
178 )
179 })
180 .collect();
181 Snapshot {
182 counters,
183 histograms,
184 }
185 }
186}
187
188#[derive(Debug)]
189pub struct HistogramSnapshot {
190 pub count: u64,
191 pub sum: u64,
192 pub buckets: [u64; 16],
193}
194
195#[derive(Debug, Default)]
196pub struct Snapshot {
197 pub counters: BTreeMap<String, u64>,
198 pub histograms: BTreeMap<String, HistogramSnapshot>,
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn counter_basic() {
207 let c = Counter::new();
208 c.inc();
209 c.inc();
210 c.add(10);
211 assert_eq!(c.get(), 12);
212 }
213
214 #[test]
215 fn histogram_records_in_bucket() {
216 let h = Histogram::new();
217 h.record(0); h.record(1); h.record(7); h.record(1024); let b = h.bucket_counts();
222 assert_eq!(h.count(), 4);
223 assert_eq!(b[0] + b[2] + b[10], 4);
224 }
225
226 #[test]
227 fn registry_round_trip() {
228 static C: Counter = Counter::new();
229 static H: Histogram = Histogram::new();
230 let r = MetricsRegistry::global();
231 r.register_counter("test_count", &C);
232 r.register_histogram("test_hist", &H);
233 C.inc();
234 H.record(42);
235 let snap = r.snapshot();
236 assert!(snap.counters.contains_key("test_count"));
237 assert!(snap.histograms.contains_key("test_hist"));
238 }
239}