scirs2_core/structured_logging/
metrics.rs1use std::collections::HashMap;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10
11pub struct Counter {
19 value: AtomicU64,
20 name: String,
21 labels: Vec<(String, String)>,
22}
23
24impl Counter {
25 pub fn new(name: impl Into<String>, labels: Vec<(String, String)>) -> Self {
27 Self {
28 value: AtomicU64::new(0),
29 name: name.into(),
30 labels,
31 }
32 }
33
34 pub fn add(&self, value: u64) {
36 self.value.fetch_add(value, Ordering::Relaxed);
37 }
38
39 pub fn inc(&self) {
41 self.add(1);
42 }
43
44 pub fn get(&self) -> u64 {
46 self.value.load(Ordering::Relaxed)
47 }
48
49 pub fn name(&self) -> &str {
51 &self.name
52 }
53
54 pub fn labels(&self) -> &[(String, String)] {
56 &self.labels
57 }
58}
59
60impl std::fmt::Debug for Counter {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("Counter")
63 .field("name", &self.name)
64 .field("value", &self.get())
65 .finish()
66 }
67}
68
69pub struct Gauge {
78 bits: AtomicU64,
80 name: String,
81}
82
83impl Gauge {
84 pub fn new(name: impl Into<String>) -> Self {
86 Self {
87 bits: AtomicU64::new(0f64.to_bits()),
88 name: name.into(),
89 }
90 }
91
92 pub fn set(&self, v: f64) {
94 self.bits.store(v.to_bits(), Ordering::Relaxed);
95 }
96
97 pub fn get(&self) -> f64 {
99 f64::from_bits(self.bits.load(Ordering::Relaxed))
100 }
101
102 pub fn name(&self) -> &str {
104 &self.name
105 }
106}
107
108impl std::fmt::Debug for Gauge {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("Gauge")
111 .field("name", &self.name)
112 .field("value", &self.get())
113 .finish()
114 }
115}
116
117pub struct Histogram {
128 boundaries: Vec<f64>,
130 counts: Mutex<Vec<u64>>,
133 sum: Mutex<f64>,
135 count: AtomicU64,
137 name: String,
138 samples: Mutex<Vec<f64>>,
140}
141
142impl Histogram {
143 pub fn new(name: impl Into<String>, boundaries: &[f64]) -> Self {
147 let mut bounds: Vec<f64> = boundaries.to_vec();
148 bounds.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
149 bounds.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
150 let n = bounds.len() + 1; Self {
152 boundaries: bounds,
153 counts: Mutex::new(vec![0u64; n]),
154 sum: Mutex::new(0.0),
155 count: AtomicU64::new(0),
156 name: name.into(),
157 samples: Mutex::new(Vec::with_capacity(1024)),
158 }
159 }
160
161 pub fn record(&self, value: f64) {
163 if let Ok(mut s) = self.sum.lock() {
165 *s += value;
166 }
167 self.count.fetch_add(1, Ordering::Relaxed);
169
170 if let Ok(mut counts) = self.counts.lock() {
172 let mut placed = false;
173 for (i, &bound) in self.boundaries.iter().enumerate() {
174 if value <= bound {
175 for j in i..counts.len() {
177 counts[j] += 1;
178 }
179 placed = true;
180 break;
181 }
182 }
183 if !placed {
184 if let Some(last) = counts.last_mut() {
186 *last += 1;
187 }
188 }
189 }
190
191 if let Ok(mut s) = self.samples.lock() {
193 if s.len() < 10_000 {
194 s.push(value);
195 }
196 }
197 }
198
199 pub fn buckets(&self) -> Vec<(f64, u64)> {
203 let counts = self.counts.lock().map(|g| g.clone()).unwrap_or_default();
204 let mut result = Vec::with_capacity(counts.len());
205 for (i, count) in counts.iter().enumerate() {
206 let bound = self.boundaries.get(i).copied().unwrap_or(f64::INFINITY);
207 result.push((bound, *count));
208 }
209 result
210 }
211
212 pub fn count(&self) -> u64 {
214 self.count.load(Ordering::Relaxed)
215 }
216
217 pub fn sum(&self) -> f64 {
219 self.sum.lock().map(|g| *g).unwrap_or(0.0)
220 }
221
222 pub fn name(&self) -> &str {
224 &self.name
225 }
226
227 pub fn percentile(&self, p: f64) -> f64 {
234 let mut samples = match self.samples.lock() {
235 Ok(g) => g.clone(),
236 Err(_) => return 0.0,
237 };
238 if samples.is_empty() {
239 return 0.0;
240 }
241 samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
242 let idx = ((p / 100.0) * (samples.len() as f64 - 1.0))
243 .round()
244 .clamp(0.0, (samples.len() - 1) as f64) as usize;
245 samples[idx]
246 }
247
248 pub fn p50(&self) -> f64 {
250 self.percentile(50.0)
251 }
252
253 pub fn p95(&self) -> f64 {
255 self.percentile(95.0)
256 }
257
258 pub fn p99(&self) -> f64 {
260 self.percentile(99.0)
261 }
262}
263
264impl std::fmt::Debug for Histogram {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 f.debug_struct("Histogram")
267 .field("name", &self.name)
268 .field("count", &self.count())
269 .field("sum", &self.sum())
270 .finish()
271 }
272}
273
274pub struct MeterRegistry {
283 counters: Mutex<HashMap<String, Arc<Counter>>>,
284 gauges: Mutex<HashMap<String, Arc<Gauge>>>,
285 histograms: Mutex<HashMap<String, Arc<Histogram>>>,
286}
287
288impl MeterRegistry {
289 pub fn new() -> Self {
291 Self {
292 counters: Mutex::new(HashMap::new()),
293 gauges: Mutex::new(HashMap::new()),
294 histograms: Mutex::new(HashMap::new()),
295 }
296 }
297
298 pub fn counter(&self, name: &str, labels: &[(&str, &str)]) -> Arc<Counter> {
300 let key = Self::make_key(name, labels);
301 let mut map = self.counters.lock().unwrap_or_else(|e| e.into_inner());
302 map.entry(key)
303 .or_insert_with(|| {
304 Arc::new(Counter::new(
305 name,
306 labels
307 .iter()
308 .map(|(k, v)| (k.to_string(), v.to_string()))
309 .collect(),
310 ))
311 })
312 .clone()
313 }
314
315 pub fn gauge(&self, name: &str) -> Arc<Gauge> {
317 let mut map = self.gauges.lock().unwrap_or_else(|e| e.into_inner());
318 map.entry(name.to_owned())
319 .or_insert_with(|| Arc::new(Gauge::new(name)))
320 .clone()
321 }
322
323 pub fn histogram(&self, name: &str, boundaries: &[f64]) -> Arc<Histogram> {
328 let mut map = self.histograms.lock().unwrap_or_else(|e| e.into_inner());
329 map.entry(name.to_owned())
330 .or_insert_with(|| Arc::new(Histogram::new(name, boundaries)))
331 .clone()
332 }
333
334 fn make_key(name: &str, labels: &[(&str, &str)]) -> String {
335 if labels.is_empty() {
336 return name.to_owned();
337 }
338 let mut parts: Vec<String> = labels.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
339 parts.sort();
340 format!("{}{{{}}}", name, parts.join(","))
341 }
342}
343
344impl Default for MeterRegistry {
345 fn default() -> Self {
346 Self::new()
347 }
348}
349
350#[cfg(test)]
355mod tests {
356 use super::*;
357 use std::sync::Arc;
358 use std::thread;
359
360 #[test]
361 fn test_counter_atomic() {
362 let counter = Arc::new(Counter::new("requests", vec![]));
363 let handles: Vec<_> = (0..4)
364 .map(|_| {
365 let c = Arc::clone(&counter);
366 thread::spawn(move || {
367 for _ in 0..250 {
368 c.add(1);
369 }
370 })
371 })
372 .collect();
373 for h in handles {
374 h.join().expect("thread panicked");
375 }
376 assert_eq!(counter.get(), 1000);
377 }
378
379 #[test]
380 fn test_histogram_buckets() {
381 let h = Histogram::new("latency", &[1.0, 5.0, 10.0]);
382 h.record(0.5);
383 h.record(3.0);
384 h.record(7.0);
385 h.record(20.0);
386 assert_eq!(h.count(), 4);
387
388 let buckets = h.buckets();
389 assert_eq!(buckets[0].1, 1);
391 assert_eq!(buckets[1].1, 2);
393 assert_eq!(buckets[2].1, 3);
395 assert_eq!(buckets[3].1, 4);
397 }
398
399 #[test]
400 fn test_histogram_percentiles() {
401 let h = Histogram::new("rt", &[1.0, 10.0, 100.0]);
402 for i in 1u64..=100 {
403 h.record(i as f64);
404 }
405 let p50 = h.p50();
406 let p95 = h.p95();
407 let p99 = h.p99();
408 assert!(p50 <= p95, "p50={} p95={}", p50, p95);
409 assert!(p95 <= p99, "p95={} p99={}", p95, p99);
410 }
411
412 #[test]
413 fn test_meter_registry_same_counter() {
414 let reg = MeterRegistry::new();
415 let c1 = reg.counter("reqs", &[("method", "GET")]);
416 let c2 = reg.counter("reqs", &[("method", "GET")]);
417 c1.add(5);
418 assert_eq!(c2.get(), 5);
420 }
421
422 #[test]
423 fn test_gauge_set_get() {
424 let g = Gauge::new("cpu");
425 g.set(0.75);
426 assert!((g.get() - 0.75).abs() < 1e-9);
427 }
428}