1use serde::Serialize;
13use std::collections::BTreeMap;
14use std::sync::Mutex;
15use std::sync::atomic::{AtomicU64, Ordering};
16
17#[derive(Default, Debug)]
21pub struct MetricsCounters {
22 pub accepted_connections: AtomicU64,
24 pub dispatched_requests: AtomicU64,
27 pub dispatch_errors_by_code: Mutex<BTreeMap<u16, u64>>,
31 pub audit_events_total: AtomicU64,
33 pub audit_drops_total: AtomicU64,
36}
37
38impl MetricsCounters {
39 pub fn record_error(&self, code: u16) {
43 if let Ok(mut map) = self.dispatch_errors_by_code.lock() {
44 *map.entry(code).or_insert(0) += 1;
45 }
46 }
47
48 pub fn snapshot(&self) -> MetricsSnapshot {
51 let errors_by_code = self
52 .dispatch_errors_by_code
53 .lock()
54 .map(|m| m.clone())
55 .unwrap_or_default();
56 MetricsSnapshot {
57 accepted_connections: self.accepted_connections.load(Ordering::Relaxed),
58 dispatched_requests: self.dispatched_requests.load(Ordering::Relaxed),
59 dispatch_errors_by_code: errors_by_code,
60 audit_events_total: self.audit_events_total.load(Ordering::Relaxed),
61 audit_drops_total: self.audit_drops_total.load(Ordering::Relaxed),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Serialize)]
70pub struct MetricsSnapshot {
71 pub accepted_connections: u64,
72 pub dispatched_requests: u64,
73 pub dispatch_errors_by_code: BTreeMap<u16, u64>,
74 pub audit_events_total: u64,
75 pub audit_drops_total: u64,
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn defaults_zero() {
84 let m = MetricsCounters::default();
85 let s = m.snapshot();
86 assert_eq!(s.accepted_connections, 0);
87 assert_eq!(s.dispatched_requests, 0);
88 assert_eq!(s.audit_events_total, 0);
89 assert_eq!(s.audit_drops_total, 0);
90 assert!(s.dispatch_errors_by_code.is_empty());
91 }
92
93 #[test]
94 fn atomic_increments_visible_in_snapshot() {
95 let m = MetricsCounters::default();
96 m.accepted_connections.fetch_add(3, Ordering::Relaxed);
97 m.dispatched_requests.fetch_add(7, Ordering::Relaxed);
98 m.audit_events_total.fetch_add(11, Ordering::Relaxed);
99 m.audit_drops_total.fetch_add(1, Ordering::Relaxed);
100 let s = m.snapshot();
101 assert_eq!(s.accepted_connections, 3);
102 assert_eq!(s.dispatched_requests, 7);
103 assert_eq!(s.audit_events_total, 11);
104 assert_eq!(s.audit_drops_total, 1);
105 }
106
107 #[test]
108 fn record_error_accumulates_per_code() {
109 let m = MetricsCounters::default();
110 m.record_error(1001); m.record_error(1001);
112 m.record_error(1002); let s = m.snapshot();
114 assert_eq!(s.dispatch_errors_by_code.get(&1001), Some(&2));
115 assert_eq!(s.dispatch_errors_by_code.get(&1002), Some(&1));
116 assert_eq!(s.dispatch_errors_by_code.get(&9999), None);
117 }
118
119 #[test]
120 fn snapshot_is_serialisable_to_json() {
121 let m = MetricsCounters::default();
122 m.accepted_connections.fetch_add(5, Ordering::Relaxed);
123 m.record_error(1010);
124 let s = m.snapshot();
125 let j = serde_json::to_value(&s).expect("serialize snapshot");
126 assert_eq!(j["accepted_connections"], 5);
127 assert_eq!(j["dispatch_errors_by_code"]["1010"], 1);
128 }
129}