rust_expect/metrics/
core.rs

1//! Metrics collection and reporting.
2//!
3//! This module provides metrics collection for monitoring session
4//! performance and behavior.
5
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11/// A counter metric.
12#[derive(Debug, Default)]
13pub struct Counter {
14    value: AtomicU64,
15}
16
17impl Counter {
18    /// Create a new counter.
19    #[must_use]
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Increment by 1.
25    pub fn inc(&self) {
26        self.value.fetch_add(1, Ordering::Relaxed);
27    }
28
29    /// Increment by n.
30    pub fn add(&self, n: u64) {
31        self.value.fetch_add(n, Ordering::Relaxed);
32    }
33
34    /// Get current value.
35    #[must_use]
36    pub fn get(&self) -> u64 {
37        self.value.load(Ordering::Relaxed)
38    }
39
40    /// Reset to zero.
41    pub fn reset(&self) {
42        self.value.store(0, Ordering::Relaxed);
43    }
44}
45
46/// A gauge metric.
47#[derive(Debug, Default)]
48pub struct Gauge {
49    value: AtomicU64,
50}
51
52impl Gauge {
53    /// Create a new gauge.
54    #[must_use]
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Set the value.
60    pub fn set(&self, value: u64) {
61        self.value.store(value, Ordering::Relaxed);
62    }
63
64    /// Increment by 1.
65    pub fn inc(&self) {
66        self.value.fetch_add(1, Ordering::Relaxed);
67    }
68
69    /// Decrement by 1.
70    pub fn dec(&self) {
71        self.value.fetch_sub(1, Ordering::Relaxed);
72    }
73
74    /// Get current value.
75    #[must_use]
76    pub fn get(&self) -> u64 {
77        self.value.load(Ordering::Relaxed)
78    }
79}
80
81/// A histogram for measuring distributions.
82#[derive(Debug)]
83pub struct Histogram {
84    /// Bucket boundaries.
85    buckets: Vec<f64>,
86    /// Counts per bucket.
87    counts: Vec<AtomicU64>,
88    /// Sum of all values.
89    sum: AtomicU64,
90    /// Total count.
91    count: AtomicU64,
92}
93
94impl Histogram {
95    /// Create with default buckets.
96    #[must_use]
97    pub fn new() -> Self {
98        Self::with_buckets(vec![
99            0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
100        ])
101    }
102
103    /// Create with custom buckets.
104    #[must_use]
105    pub fn with_buckets(buckets: Vec<f64>) -> Self {
106        let counts = (0..=buckets.len()).map(|_| AtomicU64::new(0)).collect();
107        Self {
108            buckets,
109            counts,
110            sum: AtomicU64::new(0),
111            count: AtomicU64::new(0),
112        }
113    }
114
115    /// Observe a value.
116    pub fn observe(&self, value: f64) {
117        // Find bucket and increment
118        let idx = self
119            .buckets
120            .iter()
121            .position(|&b| value <= b)
122            .unwrap_or(self.buckets.len());
123        self.counts[idx].fetch_add(1, Ordering::Relaxed);
124
125        // Update sum (as bits for f64 storage)
126        let bits = value.to_bits();
127        self.sum.fetch_add(bits, Ordering::Relaxed);
128        self.count.fetch_add(1, Ordering::Relaxed);
129    }
130
131    /// Get the count.
132    #[must_use]
133    pub fn count(&self) -> u64 {
134        self.count.load(Ordering::Relaxed)
135    }
136
137    /// Get bucket counts.
138    #[must_use]
139    pub fn bucket_counts(&self) -> Vec<u64> {
140        self.counts
141            .iter()
142            .map(|c| c.load(Ordering::Relaxed))
143            .collect()
144    }
145}
146
147impl Default for Histogram {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153/// Timer for measuring durations.
154#[derive(Debug)]
155pub struct Timer {
156    start: Instant,
157}
158
159impl Timer {
160    /// Start a new timer.
161    #[must_use]
162    pub fn start() -> Self {
163        Self {
164            start: Instant::now(),
165        }
166    }
167
168    /// Get elapsed time.
169    #[must_use]
170    pub fn elapsed(&self) -> Duration {
171        self.start.elapsed()
172    }
173
174    /// Stop and return duration in seconds.
175    #[must_use]
176    pub fn stop(self) -> f64 {
177        self.start.elapsed().as_secs_f64()
178    }
179
180    /// Stop and record to histogram.
181    pub fn record_to(self, histogram: &Histogram) {
182        histogram.observe(self.stop());
183    }
184}
185
186/// Session metrics.
187#[derive(Debug, Default)]
188pub struct SessionMetrics {
189    /// Bytes sent.
190    pub bytes_sent: Counter,
191    /// Bytes received.
192    pub bytes_received: Counter,
193    /// Commands executed.
194    pub commands_executed: Counter,
195    /// Pattern matches.
196    pub pattern_matches: Counter,
197    /// Timeouts.
198    pub timeouts: Counter,
199    /// Errors.
200    pub errors: Counter,
201    /// Active sessions.
202    pub active_sessions: Gauge,
203    /// Command duration histogram.
204    pub command_duration: Histogram,
205    /// Expect duration histogram.
206    pub expect_duration: Histogram,
207}
208
209impl SessionMetrics {
210    /// Create new session metrics.
211    #[must_use]
212    pub fn new() -> Self {
213        Self::default()
214    }
215
216    /// Report a snapshot.
217    #[must_use]
218    pub fn snapshot(&self) -> MetricsSnapshot {
219        MetricsSnapshot {
220            bytes_sent: self.bytes_sent.get(),
221            bytes_received: self.bytes_received.get(),
222            commands_executed: self.commands_executed.get(),
223            pattern_matches: self.pattern_matches.get(),
224            timeouts: self.timeouts.get(),
225            errors: self.errors.get(),
226            active_sessions: self.active_sessions.get(),
227        }
228    }
229}
230
231/// Snapshot of metrics.
232#[derive(Debug, Clone)]
233pub struct MetricsSnapshot {
234    /// Bytes sent.
235    pub bytes_sent: u64,
236    /// Bytes received.
237    pub bytes_received: u64,
238    /// Commands executed.
239    pub commands_executed: u64,
240    /// Pattern matches.
241    pub pattern_matches: u64,
242    /// Timeouts.
243    pub timeouts: u64,
244    /// Errors.
245    pub errors: u64,
246    /// Active sessions.
247    pub active_sessions: u64,
248}
249
250/// Global metrics registry.
251#[derive(Debug, Default)]
252pub struct MetricsRegistry {
253    counters: Arc<Mutex<HashMap<String, Arc<Counter>>>>,
254    gauges: Arc<Mutex<HashMap<String, Arc<Gauge>>>>,
255    histograms: Arc<Mutex<HashMap<String, Arc<Histogram>>>>,
256}
257
258impl MetricsRegistry {
259    /// Create a new registry.
260    #[must_use]
261    pub fn new() -> Self {
262        Self::default()
263    }
264
265    /// Get or create a counter.
266    #[must_use]
267    pub fn counter(&self, name: &str) -> Arc<Counter> {
268        let mut counters = self
269            .counters
270            .lock()
271            .unwrap_or_else(std::sync::PoisonError::into_inner);
272        counters
273            .entry(name.to_string())
274            .or_insert_with(|| Arc::new(Counter::new()))
275            .clone()
276    }
277
278    /// Get or create a gauge.
279    #[must_use]
280    pub fn gauge(&self, name: &str) -> Arc<Gauge> {
281        let mut gauges = self
282            .gauges
283            .lock()
284            .unwrap_or_else(std::sync::PoisonError::into_inner);
285        gauges
286            .entry(name.to_string())
287            .or_insert_with(|| Arc::new(Gauge::new()))
288            .clone()
289    }
290
291    /// Get or create a histogram.
292    #[must_use]
293    pub fn histogram(&self, name: &str) -> Arc<Histogram> {
294        let mut histograms = self
295            .histograms
296            .lock()
297            .unwrap_or_else(std::sync::PoisonError::into_inner);
298        histograms
299            .entry(name.to_string())
300            .or_insert_with(|| Arc::new(Histogram::new()))
301            .clone()
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn counter_basic() {
311        let counter = Counter::new();
312        assert_eq!(counter.get(), 0);
313
314        counter.inc();
315        assert_eq!(counter.get(), 1);
316
317        counter.add(5);
318        assert_eq!(counter.get(), 6);
319    }
320
321    #[test]
322    fn gauge_basic() {
323        let gauge = Gauge::new();
324        assert_eq!(gauge.get(), 0);
325
326        gauge.set(10);
327        assert_eq!(gauge.get(), 10);
328
329        gauge.inc();
330        assert_eq!(gauge.get(), 11);
331
332        gauge.dec();
333        assert_eq!(gauge.get(), 10);
334    }
335
336    #[test]
337    fn histogram_basic() {
338        let histogram = Histogram::new();
339        histogram.observe(0.1);
340        histogram.observe(0.5);
341        histogram.observe(1.0);
342
343        assert_eq!(histogram.count(), 3);
344    }
345
346    #[test]
347    fn timer_basic() {
348        let timer = Timer::start();
349        std::thread::sleep(Duration::from_millis(10));
350        let elapsed = timer.stop();
351
352        assert!(elapsed >= 0.01);
353    }
354
355    #[test]
356    fn session_metrics() {
357        let metrics = SessionMetrics::new();
358        metrics.bytes_sent.add(100);
359        metrics.commands_executed.inc();
360
361        let snapshot = metrics.snapshot();
362        assert_eq!(snapshot.bytes_sent, 100);
363        assert_eq!(snapshot.commands_executed, 1);
364    }
365}