Skip to main content

machi_obs/
recording.rs

1//! In-memory metrics capture for tests and local debugging.
2
3use std::sync::Mutex;
4
5use crate::metrics::MetricsSink;
6
7/// Thread-safe capture of all metric events.
8#[derive(Debug, Default)]
9pub struct RecordingMetrics {
10    counters: Mutex<Vec<CounterEvent>>,
11    histograms: Mutex<Vec<HistogramEvent>>,
12    gauges: Mutex<Vec<GaugeEvent>>,
13}
14
15/// Captured counter sample.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct CounterEvent {
18    /// Series name.
19    pub name: String,
20    /// Increment.
21    pub value: u64,
22    /// Labels as `k=v` pairs.
23    pub labels: Vec<(String, String)>,
24}
25
26/// Captured histogram sample.
27#[derive(Debug, Clone, PartialEq)]
28pub struct HistogramEvent {
29    /// Series name.
30    pub name: String,
31    /// Observed value.
32    pub value: f64,
33    /// Labels.
34    pub labels: Vec<(String, String)>,
35}
36
37/// Captured gauge sample.
38#[derive(Debug, Clone, PartialEq)]
39pub struct GaugeEvent {
40    /// Series name.
41    pub name: String,
42    /// Gauge value.
43    pub value: f64,
44    /// Labels.
45    pub labels: Vec<(String, String)>,
46}
47
48impl RecordingMetrics {
49    /// Empty recorder.
50    #[must_use]
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Snapshot of counter events.
56    #[must_use]
57    pub fn counters(&self) -> Vec<CounterEvent> {
58        self.counters
59            .lock()
60            .unwrap_or_else(std::sync::PoisonError::into_inner)
61            .clone()
62    }
63
64    /// Snapshot of histogram events.
65    #[must_use]
66    pub fn histograms(&self) -> Vec<HistogramEvent> {
67        self.histograms
68            .lock()
69            .unwrap_or_else(std::sync::PoisonError::into_inner)
70            .clone()
71    }
72
73    /// Sum of counter values for a series name.
74    #[must_use]
75    pub fn counter_sum(&self, name: &str) -> u64 {
76        self.counters()
77            .into_iter()
78            .filter(|e| e.name == name)
79            .map(|e| e.value)
80            .sum()
81    }
82
83    /// Whether any event used this series name.
84    #[must_use]
85    pub fn saw(&self, name: &str) -> bool {
86        self.counters().iter().any(|e| e.name == name)
87            || self.histograms().iter().any(|e| e.name == name)
88            || self
89                .gauges
90                .lock()
91                .unwrap_or_else(std::sync::PoisonError::into_inner)
92                .iter()
93                .any(|e| e.name == name)
94    }
95}
96
97impl MetricsSink for RecordingMetrics {
98    fn counter(&self, name: &str, value: u64, labels: &[(&str, &str)]) {
99        self.counters
100            .lock()
101            .unwrap_or_else(std::sync::PoisonError::into_inner)
102            .push(CounterEvent {
103                name: name.to_owned(),
104                value,
105                labels: owned_labels(labels),
106            });
107    }
108
109    fn histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
110        self.histograms
111            .lock()
112            .unwrap_or_else(std::sync::PoisonError::into_inner)
113            .push(HistogramEvent {
114                name: name.to_owned(),
115                value,
116                labels: owned_labels(labels),
117            });
118    }
119
120    fn gauge(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
121        self.gauges
122            .lock()
123            .unwrap_or_else(std::sync::PoisonError::into_inner)
124            .push(GaugeEvent {
125                name: name.to_owned(),
126                value,
127                labels: owned_labels(labels),
128            });
129    }
130}
131
132fn owned_labels(labels: &[(&str, &str)]) -> Vec<(String, String)> {
133    labels
134        .iter()
135        .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
136        .collect()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::metrics::{METRIC_TURNS_TOTAL, record_turn};
143
144    #[test]
145    fn captures_turn() {
146        let rec = RecordingMetrics::new();
147        record_turn(&rec, "ok", 2, 5.0);
148        assert!(rec.saw(METRIC_TURNS_TOTAL));
149        assert_eq!(rec.counter_sum(METRIC_TURNS_TOTAL), 1);
150        assert!(!rec.histograms().is_empty());
151    }
152}