Skip to main content

mentedb_core/
metrics.rs

1//! Observability metrics for MenteDB.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4
5/// Atomic counters for all MenteDB operations.
6pub struct Metrics {
7    /// Total write operations.
8    pub writes_total: AtomicU64,
9    /// Total read operations.
10    pub reads_total: AtomicU64,
11    /// Total delete operations.
12    pub deletes_total: AtomicU64,
13    /// Cache hits.
14    pub cache_hits: AtomicU64,
15    /// Cache misses.
16    pub cache_misses: AtomicU64,
17    /// WAL sync operations.
18    pub wal_syncs: AtomicU64,
19    /// Context assembly operations.
20    pub context_assemblies: AtomicU64,
21    /// MQL queries parsed.
22    pub mql_queries_parsed: AtomicU64,
23    /// Contradictions detected.
24    pub contradictions_detected: AtomicU64,
25    /// Beliefs propagated.
26    pub beliefs_propagated: AtomicU64,
27    /// Speculative cache hits.
28    pub speculative_hits: AtomicU64,
29    /// Speculative cache misses.
30    pub speculative_misses: AtomicU64,
31    /// Sum of write latencies in microseconds.
32    pub write_latency_us_sum: AtomicU64,
33    /// Number of write latency samples.
34    pub write_latency_count: AtomicU64,
35    /// Sum of read latencies in microseconds.
36    pub read_latency_us_sum: AtomicU64,
37    /// Number of read latency samples.
38    pub read_latency_count: AtomicU64,
39}
40
41/// A point-in-time snapshot of metrics.
42#[derive(Debug, Clone)]
43pub struct MetricsSnapshot {
44    /// Total write operations.
45    pub writes_total: u64,
46    /// Total read operations.
47    pub reads_total: u64,
48    /// Total delete operations.
49    pub deletes_total: u64,
50    /// Cache hit rate (0.0–1.0).
51    pub cache_hit_rate: f64,
52    /// Average write latency in microseconds.
53    pub avg_write_latency_us: f64,
54    /// Average read latency in microseconds.
55    pub avg_read_latency_us: f64,
56    /// Total contradictions detected.
57    pub contradictions_detected: u64,
58    /// Speculative cache hit rate (0.0–1.0).
59    pub speculative_hit_rate: f64,
60}
61
62impl Metrics {
63    /// Create a new zeroed metrics instance.
64    pub fn new() -> Self {
65        Self {
66            writes_total: AtomicU64::new(0),
67            reads_total: AtomicU64::new(0),
68            deletes_total: AtomicU64::new(0),
69            cache_hits: AtomicU64::new(0),
70            cache_misses: AtomicU64::new(0),
71            wal_syncs: AtomicU64::new(0),
72            context_assemblies: AtomicU64::new(0),
73            mql_queries_parsed: AtomicU64::new(0),
74            contradictions_detected: AtomicU64::new(0),
75            beliefs_propagated: AtomicU64::new(0),
76            speculative_hits: AtomicU64::new(0),
77            speculative_misses: AtomicU64::new(0),
78            write_latency_us_sum: AtomicU64::new(0),
79            write_latency_count: AtomicU64::new(0),
80            read_latency_us_sum: AtomicU64::new(0),
81            read_latency_count: AtomicU64::new(0),
82        }
83    }
84
85    /// Increment writes counter.
86    pub fn inc_writes(&self) {
87        self.writes_total.fetch_add(1, Ordering::Relaxed);
88    }
89
90    /// Increment reads counter.
91    pub fn inc_reads(&self) {
92        self.reads_total.fetch_add(1, Ordering::Relaxed);
93    }
94
95    /// Increment deletes counter.
96    pub fn inc_deletes(&self) {
97        self.deletes_total.fetch_add(1, Ordering::Relaxed);
98    }
99
100    /// Increment cache hits counter.
101    pub fn inc_cache_hits(&self) {
102        self.cache_hits.fetch_add(1, Ordering::Relaxed);
103    }
104
105    /// Increment cache misses counter.
106    pub fn inc_cache_misses(&self) {
107        self.cache_misses.fetch_add(1, Ordering::Relaxed);
108    }
109
110    /// Record a write latency sample.
111    pub fn record_write_latency(&self, microseconds: u64) {
112        self.write_latency_us_sum
113            .fetch_add(microseconds, Ordering::Relaxed);
114        self.write_latency_count.fetch_add(1, Ordering::Relaxed);
115    }
116
117    /// Record a read latency sample.
118    pub fn record_read_latency(&self, microseconds: u64) {
119        self.read_latency_us_sum
120            .fetch_add(microseconds, Ordering::Relaxed);
121        self.read_latency_count.fetch_add(1, Ordering::Relaxed);
122    }
123
124    /// Export metrics in Prometheus text exposition format.
125    pub fn export_prometheus(&self) -> String {
126        let mut out = String::with_capacity(2048);
127
128        let counters = [
129            (
130                "mentedb_writes_total",
131                "Total write operations",
132                &self.writes_total,
133            ),
134            (
135                "mentedb_reads_total",
136                "Total read operations",
137                &self.reads_total,
138            ),
139            (
140                "mentedb_deletes_total",
141                "Total delete operations",
142                &self.deletes_total,
143            ),
144            (
145                "mentedb_cache_hits_total",
146                "Total cache hits",
147                &self.cache_hits,
148            ),
149            (
150                "mentedb_cache_misses_total",
151                "Total cache misses",
152                &self.cache_misses,
153            ),
154            (
155                "mentedb_wal_syncs_total",
156                "Total WAL sync operations",
157                &self.wal_syncs,
158            ),
159            (
160                "mentedb_context_assemblies_total",
161                "Total context assemblies",
162                &self.context_assemblies,
163            ),
164            (
165                "mentedb_mql_queries_parsed_total",
166                "Total MQL queries parsed",
167                &self.mql_queries_parsed,
168            ),
169            (
170                "mentedb_contradictions_detected_total",
171                "Total contradictions detected",
172                &self.contradictions_detected,
173            ),
174            (
175                "mentedb_beliefs_propagated_total",
176                "Total beliefs propagated",
177                &self.beliefs_propagated,
178            ),
179            (
180                "mentedb_speculative_hits_total",
181                "Speculative cache hits",
182                &self.speculative_hits,
183            ),
184            (
185                "mentedb_speculative_misses_total",
186                "Speculative cache misses",
187                &self.speculative_misses,
188            ),
189        ];
190
191        for (name, help, counter) in &counters {
192            out.push_str(&format!(
193                "# HELP {name} {help}\n# TYPE {name} counter\n{name} {}\n",
194                counter.load(Ordering::Relaxed)
195            ));
196        }
197
198        // Latency summaries
199        let wl_sum = self.write_latency_us_sum.load(Ordering::Relaxed);
200        let wl_count = self.write_latency_count.load(Ordering::Relaxed);
201        out.push_str(&format!(
202            "# HELP mentedb_write_latency_us Write latency in microseconds\n\
203             # TYPE mentedb_write_latency_us summary\n\
204             mentedb_write_latency_us_sum {wl_sum}\n\
205             mentedb_write_latency_us_count {wl_count}\n"
206        ));
207
208        let rl_sum = self.read_latency_us_sum.load(Ordering::Relaxed);
209        let rl_count = self.read_latency_count.load(Ordering::Relaxed);
210        out.push_str(&format!(
211            "# HELP mentedb_read_latency_us Read latency in microseconds\n\
212             # TYPE mentedb_read_latency_us summary\n\
213             mentedb_read_latency_us_sum {rl_sum}\n\
214             mentedb_read_latency_us_count {rl_count}\n"
215        ));
216
217        out
218    }
219
220    /// Export metrics as a JSON string.
221    pub fn export_json(&self) -> String {
222        let snap = self.snapshot();
223        format!(
224            concat!(
225                "{{",
226                "\"writes_total\":{},",
227                "\"reads_total\":{},",
228                "\"deletes_total\":{},",
229                "\"cache_hit_rate\":{:.4},",
230                "\"avg_write_latency_us\":{:.2},",
231                "\"avg_read_latency_us\":{:.2},",
232                "\"contradictions_detected\":{},",
233                "\"speculative_hit_rate\":{:.4},",
234                "\"wal_syncs\":{},",
235                "\"context_assemblies\":{},",
236                "\"mql_queries_parsed\":{},",
237                "\"beliefs_propagated\":{}",
238                "}}"
239            ),
240            snap.writes_total,
241            snap.reads_total,
242            snap.deletes_total,
243            snap.cache_hit_rate,
244            snap.avg_write_latency_us,
245            snap.avg_read_latency_us,
246            snap.contradictions_detected,
247            snap.speculative_hit_rate,
248            self.wal_syncs.load(Ordering::Relaxed),
249            self.context_assemblies.load(Ordering::Relaxed),
250            self.mql_queries_parsed.load(Ordering::Relaxed),
251            self.beliefs_propagated.load(Ordering::Relaxed),
252        )
253    }
254
255    /// Take a point-in-time snapshot of all metrics.
256    pub fn snapshot(&self) -> MetricsSnapshot {
257        let hits = self.cache_hits.load(Ordering::Relaxed);
258        let misses = self.cache_misses.load(Ordering::Relaxed);
259        let cache_total = hits + misses;
260        let cache_hit_rate = if cache_total > 0 {
261            hits as f64 / cache_total as f64
262        } else {
263            0.0
264        };
265
266        let wl_sum = self.write_latency_us_sum.load(Ordering::Relaxed);
267        let wl_count = self.write_latency_count.load(Ordering::Relaxed);
268        let avg_write = if wl_count > 0 {
269            wl_sum as f64 / wl_count as f64
270        } else {
271            0.0
272        };
273
274        let rl_sum = self.read_latency_us_sum.load(Ordering::Relaxed);
275        let rl_count = self.read_latency_count.load(Ordering::Relaxed);
276        let avg_read = if rl_count > 0 {
277            rl_sum as f64 / rl_count as f64
278        } else {
279            0.0
280        };
281
282        let spec_hits = self.speculative_hits.load(Ordering::Relaxed);
283        let spec_misses = self.speculative_misses.load(Ordering::Relaxed);
284        let spec_total = spec_hits + spec_misses;
285        let speculative_hit_rate = if spec_total > 0 {
286            spec_hits as f64 / spec_total as f64
287        } else {
288            0.0
289        };
290
291        MetricsSnapshot {
292            writes_total: self.writes_total.load(Ordering::Relaxed),
293            reads_total: self.reads_total.load(Ordering::Relaxed),
294            deletes_total: self.deletes_total.load(Ordering::Relaxed),
295            cache_hit_rate,
296            avg_write_latency_us: avg_write,
297            avg_read_latency_us: avg_read,
298            contradictions_detected: self.contradictions_detected.load(Ordering::Relaxed),
299            speculative_hit_rate,
300        }
301    }
302}
303
304impl Default for Metrics {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn increment_and_read() {
316        let m = Metrics::new();
317        m.inc_writes();
318        m.inc_writes();
319        m.inc_reads();
320        m.inc_cache_hits();
321        m.inc_cache_hits();
322        m.inc_cache_misses();
323        m.record_write_latency(100);
324        m.record_write_latency(200);
325
326        let snap = m.snapshot();
327        assert_eq!(snap.writes_total, 2);
328        assert_eq!(snap.reads_total, 1);
329        assert!((snap.cache_hit_rate - 2.0 / 3.0).abs() < 0.01);
330        assert!((snap.avg_write_latency_us - 150.0).abs() < 0.01);
331    }
332
333    #[test]
334    fn prometheus_format() {
335        let m = Metrics::new();
336        m.inc_writes();
337        m.inc_reads();
338        m.inc_reads();
339
340        let prom = m.export_prometheus();
341        assert!(prom.contains("# HELP mentedb_writes_total Total write operations"));
342        assert!(prom.contains("# TYPE mentedb_writes_total counter"));
343        assert!(prom.contains("mentedb_writes_total 1"));
344        assert!(prom.contains("mentedb_reads_total 2"));
345    }
346
347    #[test]
348    fn json_format() {
349        let m = Metrics::new();
350        m.inc_writes();
351        m.inc_deletes();
352        m.record_read_latency(500);
353
354        let json = m.export_json();
355        // Verify it's valid JSON by checking structure
356        assert!(json.starts_with('{'));
357        assert!(json.ends_with('}'));
358        assert!(json.contains("\"writes_total\":1"));
359        assert!(json.contains("\"deletes_total\":1"));
360        assert!(json.contains("\"avg_read_latency_us\":500.00"));
361    }
362}