Skip to main content

codemem_engine/
metrics.rs

1//! In-memory metrics collector for operational metrics.
2
3use codemem_core::{LatencyStats, Metrics, MetricsSnapshot};
4use std::collections::{HashMap, VecDeque};
5use std::sync::Mutex;
6
7/// Maximum number of latency samples to retain per operation.
8const MAX_SAMPLES: usize = 10_000;
9
10/// In-memory metrics collector.
11///
12/// Collects latency samples, counter increments, and gauge values.
13/// Thread-safe via internal `Mutex`.
14pub struct InMemoryMetrics {
15    inner: Mutex<Inner>,
16}
17
18struct Inner {
19    /// Raw latency samples per operation, capped at `MAX_SAMPLES` per key.
20    latency_samples: HashMap<String, VecDeque<f64>>,
21    /// Cumulative counters.
22    counters: HashMap<String, u64>,
23    /// Point-in-time gauges.
24    gauges: HashMap<String, f64>,
25}
26
27impl InMemoryMetrics {
28    pub fn new() -> Self {
29        Self {
30            inner: Mutex::new(Inner {
31                latency_samples: HashMap::new(),
32                counters: HashMap::new(),
33                gauges: HashMap::new(),
34            }),
35        }
36    }
37
38    /// Take a snapshot of all collected metrics.
39    pub fn snapshot(&self) -> MetricsSnapshot {
40        let inner = match self.inner.lock() {
41            Ok(guard) => guard,
42            Err(e) => {
43                tracing::warn!("Metrics lock poisoned: {e}");
44                return MetricsSnapshot::default();
45            }
46        };
47
48        let latencies: HashMap<String, LatencyStats> = inner
49            .latency_samples
50            .iter()
51            .map(|(name, samples)| {
52                let stats = compute_latency_stats(samples);
53                (name.clone(), stats)
54            })
55            .collect();
56
57        MetricsSnapshot {
58            latencies,
59            counters: inner.counters.clone(),
60            gauges: inner.gauges.clone(),
61        }
62    }
63}
64
65impl Default for InMemoryMetrics {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl Metrics for InMemoryMetrics {
72    fn record_latency(&self, operation: &str, duration_ms: f64) {
73        if let Ok(mut inner) = self.inner.lock() {
74            let samples = inner
75                .latency_samples
76                .entry(operation.to_string())
77                .or_default();
78            if samples.len() >= MAX_SAMPLES {
79                samples.pop_front();
80            }
81            samples.push_back(duration_ms);
82        }
83    }
84
85    fn increment_counter(&self, name: &str, delta: u64) {
86        if let Ok(mut inner) = self.inner.lock() {
87            *inner.counters.entry(name.to_string()).or_insert(0) += delta;
88        }
89    }
90
91    fn record_gauge(&self, name: &str, value: f64) {
92        if let Ok(mut inner) = self.inner.lock() {
93            inner.gauges.insert(name.to_string(), value);
94        }
95    }
96}
97
98/// Compute percentile-based statistics from a deque of samples.
99/// The deque is capped at `MAX_SAMPLES`, so the sort is bounded.
100fn compute_latency_stats(samples: &VecDeque<f64>) -> LatencyStats {
101    if samples.is_empty() {
102        return LatencyStats::default();
103    }
104
105    let mut sorted: Vec<f64> = samples.iter().copied().collect();
106    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
107
108    let count = sorted.len() as u64;
109    let total: f64 = sorted.iter().sum();
110    let min = sorted[0];
111    let max = sorted[sorted.len() - 1];
112
113    let p50 = percentile(&sorted, 50.0);
114    let p95 = percentile(&sorted, 95.0);
115    let p99 = percentile(&sorted, 99.0);
116
117    LatencyStats {
118        count,
119        total_ms: total,
120        min_ms: min,
121        max_ms: max,
122        p50_ms: p50,
123        p95_ms: p95,
124        p99_ms: p99,
125    }
126}
127
128fn percentile(sorted: &[f64], pct: f64) -> f64 {
129    if sorted.is_empty() {
130        return 0.0;
131    }
132    let idx = (pct / 100.0 * (sorted.len() as f64 - 1.0)).round() as usize;
133    sorted[idx.min(sorted.len() - 1)]
134}
135
136#[cfg(test)]
137#[path = "tests/metrics_tests.rs"]
138mod tests;