turbovault_core/
metrics.rs

1//! Lock-free metrics infrastructure for high-performance observability
2//!
3//! This module provides minimal metrics collection using atomic operations
4//! with zero locking overhead. For comprehensive observability (tracing, OpenTelemetry),
5//! use the observability infrastructure in turbomcp-server.
6//!
7//! Design:
8//! - Counter: Monotonically increasing atomic u64 values
9//! - Histogram: Atomic-backed distribution tracking
10//! - HistogramTimer: RAII timer for automatic duration recording
11//! - MetricsContext: Global registry for named metrics (rarely used)
12
13use parking_lot::RwLock;
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::Instant;
18
19/// A lock-free counter metric (monotonically increasing)
20#[derive(Debug, Clone)]
21pub struct Counter {
22    value: Arc<AtomicU64>,
23    name: String,
24}
25
26impl Counter {
27    /// Create a new counter with the given name
28    pub fn new(name: impl Into<String>) -> Self {
29        Self {
30            value: Arc::new(AtomicU64::new(0)),
31            name: name.into(),
32        }
33    }
34
35    /// Increment counter by 1 (lock-free operation)
36    pub fn increment(&self) {
37        self.add(1);
38    }
39
40    /// Add value to counter (lock-free, uses saturating arithmetic)
41    pub fn add(&self, value: u64) {
42        let mut current = self.value.load(Ordering::Relaxed);
43        loop {
44            let new_value = current.saturating_add(value);
45            match self.value.compare_exchange_weak(
46                current,
47                new_value,
48                Ordering::Release,
49                Ordering::Relaxed,
50            ) {
51                Ok(_) => break,
52                Err(actual) => current = actual,
53            }
54        }
55    }
56
57    /// Get current value (lock-free read)
58    pub fn value(&self) -> u64 {
59        self.value.load(Ordering::Acquire)
60    }
61
62    /// Get metric name
63    pub fn name(&self) -> &str {
64        &self.name
65    }
66
67    /// Reset counter to zero
68    pub fn reset(&self) {
69        self.value.store(0, Ordering::Release);
70    }
71}
72
73/// A histogram for tracking value distributions
74#[derive(Debug, Clone)]
75pub struct Histogram {
76    values: Arc<RwLock<Vec<f64>>>,
77    name: String,
78}
79
80impl Histogram {
81    /// Create a new histogram
82    pub fn new(name: impl Into<String>) -> Self {
83        Self {
84            values: Arc::new(RwLock::new(Vec::new())),
85            name: name.into(),
86        }
87    }
88
89    /// Record a value in the histogram
90    pub fn record(&self, value: f64) {
91        let mut v = self.values.write();
92        v.push(value);
93    }
94
95    /// Create a timer for automatic duration recording
96    pub fn timer(&self) -> HistogramTimer {
97        HistogramTimer {
98            histogram: self.clone(),
99            start: Instant::now(),
100        }
101    }
102
103    /// Get statistics for recorded values
104    pub fn stats(&self) -> HistogramStats {
105        let v = self.values.read();
106        if v.is_empty() {
107            return HistogramStats {
108                count: 0,
109                sum: 0.0,
110                min: 0.0,
111                max: 0.0,
112                mean: 0.0,
113            };
114        }
115
116        let sum: f64 = v.iter().sum();
117        let count = v.len();
118        let mean = sum / count as f64;
119        let min = v.iter().copied().fold(f64::INFINITY, f64::min);
120        let max = v.iter().copied().fold(f64::NEG_INFINITY, f64::max);
121
122        HistogramStats {
123            count,
124            sum,
125            min,
126            max,
127            mean,
128        }
129    }
130
131    /// Get metric name
132    pub fn name(&self) -> &str {
133        &self.name
134    }
135
136    /// Clear all recorded values
137    pub fn reset(&self) {
138        let mut v = self.values.write();
139        v.clear();
140    }
141}
142
143/// Statistics computed from histogram values
144#[derive(Debug, Clone)]
145pub struct HistogramStats {
146    /// Number of samples
147    pub count: usize,
148    /// Sum of all values
149    pub sum: f64,
150    /// Minimum value observed
151    pub min: f64,
152    /// Maximum value observed
153    pub max: f64,
154    /// Mean of all values
155    pub mean: f64,
156}
157
158/// RAII timer that records duration to histogram on drop
159#[derive(Debug)]
160pub struct HistogramTimer {
161    histogram: Histogram,
162    start: Instant,
163}
164
165impl Drop for HistogramTimer {
166    fn drop(&mut self) {
167        let duration_ms = self.start.elapsed().as_secs_f64() * 1000.0;
168        self.histogram.record(duration_ms);
169    }
170}
171
172/// Global metrics context registry (rarely used)
173///
174/// For most use cases, use turbomcp's ServerMetrics directly.
175/// This is provided for backward compatibility and simple metric collection.
176#[derive(Debug)]
177pub struct MetricsContext {
178    enabled: bool,
179    counters: Arc<RwLock<HashMap<String, Counter>>>,
180    histograms: Arc<RwLock<HashMap<String, Histogram>>>,
181}
182
183impl MetricsContext {
184    /// Create new metrics context
185    pub fn new(enabled: bool) -> Self {
186        Self {
187            enabled,
188            counters: Arc::new(RwLock::new(HashMap::new())),
189            histograms: Arc::new(RwLock::new(HashMap::new())),
190        }
191    }
192
193    /// Get or create a named counter
194    pub fn counter(&self, name: &str) -> Counter {
195        if !self.enabled {
196            return Counter::new(name);
197        }
198
199        let mut counters = self.counters.write();
200        if let Some(counter) = counters.get(name) {
201            counter.clone()
202        } else {
203            let counter = Counter::new(name);
204            counters.insert(name.to_string(), counter.clone());
205            counter
206        }
207    }
208
209    /// Get or create a named histogram
210    pub fn histogram(&self, name: &str) -> Histogram {
211        if !self.enabled {
212            return Histogram::new(name);
213        }
214
215        let mut histograms = self.histograms.write();
216        if let Some(histogram) = histograms.get(name) {
217            histogram.clone()
218        } else {
219            let histogram = Histogram::new(name);
220            histograms.insert(name.to_string(), histogram.clone());
221            histogram
222        }
223    }
224
225    /// Get all counter statistics
226    pub fn get_counters(&self) -> HashMap<String, u64> {
227        self.counters
228            .read()
229            .iter()
230            .map(|(k, v)| (k.clone(), v.value()))
231            .collect()
232    }
233
234    /// Get all histogram statistics
235    pub fn get_histograms(&self) -> HashMap<String, HistogramStats> {
236        self.histograms
237            .read()
238            .iter()
239            .map(|(k, v)| (k.clone(), v.stats()))
240            .collect()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_counter_increment() {
250        let counter = Counter::new("test");
251        assert_eq!(counter.value(), 0);
252        counter.increment();
253        assert_eq!(counter.value(), 1);
254        counter.add(5);
255        assert_eq!(counter.value(), 6);
256    }
257
258    #[test]
259    fn test_counter_saturation() {
260        let counter = Counter::new("test");
261        counter.value.store(u64::MAX, Ordering::Release);
262        counter.add(100); // Should saturate, not overflow
263        assert_eq!(counter.value(), u64::MAX);
264    }
265
266    #[test]
267    fn test_histogram_stats() {
268        let histogram = Histogram::new("test");
269        histogram.record(1.0);
270        histogram.record(2.0);
271        histogram.record(3.0);
272
273        let stats = histogram.stats();
274        assert_eq!(stats.count, 3);
275        assert_eq!(stats.sum, 6.0);
276        assert_eq!(stats.min, 1.0);
277        assert_eq!(stats.max, 3.0);
278        assert_eq!(stats.mean, 2.0);
279    }
280
281    #[test]
282    fn test_histogram_timer() {
283        let histogram = Histogram::new("test");
284        {
285            let _timer = histogram.timer();
286            std::thread::sleep(std::time::Duration::from_millis(10));
287        }
288
289        let stats = histogram.stats();
290        assert_eq!(stats.count, 1);
291        assert!(stats.sum > 10.0); // At least 10ms
292    }
293
294    #[test]
295    fn test_metrics_context() {
296        let ctx = MetricsContext::new(true);
297        let counter = ctx.counter("requests");
298        counter.add(5);
299
300        let counters = ctx.get_counters();
301        assert_eq!(counters.get("requests"), Some(&5));
302    }
303
304    #[test]
305    fn test_metrics_context_disabled() {
306        let ctx = MetricsContext::new(false);
307        let counter = ctx.counter("requests");
308        counter.add(100);
309        // Stats should still be empty since context is disabled
310        let counters = ctx.get_counters();
311        assert!(counters.is_empty());
312    }
313}