Skip to main content

photon_telemetry/
recording.rs

1//! In-memory [`OpsLog`] for tests (`feature = "recording"`).
2
3use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
4
5use serde_json::Value;
6
7use super::OpsLog;
8
9/// Captured counter increment.
10#[derive(Debug, Clone, PartialEq)]
11pub struct RecordedCounter {
12    /// Metric name.
13    pub name: String,
14    /// Label key/value pairs.
15    pub labels: Vec<(String, String)>,
16    /// Increment amount.
17    pub value: f64,
18}
19
20/// Captured gauge sample.
21#[derive(Debug, Clone, PartialEq)]
22pub struct RecordedGauge {
23    /// Metric name.
24    pub name: String,
25    /// Label key/value pairs.
26    pub labels: Vec<(String, String)>,
27    /// Gauge value.
28    pub value: f64,
29}
30
31/// Captured structured event.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RecordedEvent {
34    /// Event name.
35    pub name: String,
36    /// Event payload.
37    pub payload: Value,
38}
39
40#[derive(Debug, Default)]
41struct Inner {
42    counters: Vec<RecordedCounter>,
43    gauges: Vec<RecordedGauge>,
44    events: Vec<RecordedEvent>,
45}
46
47/// Append-only in-memory ops log for assertions in unit/integration tests.
48#[derive(Debug, Clone)]
49pub struct RecordingOpsLog {
50    inner: Arc<Mutex<Inner>>,
51}
52
53fn lock_inner(inner: &Mutex<Inner>) -> MutexGuard<'_, Inner> {
54    inner.lock().unwrap_or_else(PoisonError::into_inner)
55}
56
57impl RecordingOpsLog {
58    /// Create an empty recording log.
59    #[must_use]
60    pub fn new() -> Self {
61        Self {
62            inner: Arc::new(Mutex::new(Inner::default())),
63        }
64    }
65
66    /// Drop all recorded counters, gauges, and events.
67    pub fn clear(&self) {
68        let mut g = lock_inner(&self.inner);
69        g.counters.clear();
70        g.gauges.clear();
71        g.events.clear();
72    }
73
74    /// Snapshot of recorded counters.
75    #[must_use]
76    pub fn counters(&self) -> Vec<RecordedCounter> {
77        lock_inner(&self.inner).counters.clone()
78    }
79
80    /// Snapshot of recorded gauges.
81    #[must_use]
82    pub fn gauges(&self) -> Vec<RecordedGauge> {
83        lock_inner(&self.inner).gauges.clone()
84    }
85
86    /// Snapshot of recorded events.
87    #[must_use]
88    pub fn events(&self) -> Vec<RecordedEvent> {
89        lock_inner(&self.inner).events.clone()
90    }
91
92    /// Counters whose name matches and labels contain `label_subset`.
93    #[must_use]
94    pub fn recorded_counters_matching(
95        &self,
96        name: &str,
97        label_subset: &[(&str, &str)],
98    ) -> Vec<RecordedCounter> {
99        self.counters()
100            .into_iter()
101            .filter(|c| c.name == name && labels_contain(&c.labels, label_subset))
102            .collect()
103    }
104
105    /// Events whose name equals `event_name`.
106    #[must_use]
107    pub fn recorded_events_for(&self, event_name: &str) -> Vec<RecordedEvent> {
108        self.events()
109            .into_iter()
110            .filter(|e| e.name == event_name)
111            .collect()
112    }
113}
114
115fn labels_contain(labels: &[(String, String)], subset: &[(&str, &str)]) -> bool {
116    subset.iter().all(|(k, v)| {
117        labels
118            .iter()
119            .any(|(lk, lv)| lk.as_str() == *k && lv.as_str() == *v)
120    })
121}
122
123impl OpsLog for RecordingOpsLog {
124    fn record_counter(&self, name: &str, labels: &[(&str, &str)], value: f64) {
125        let labels: Vec<(String, String)> = labels
126            .iter()
127            .map(|(k, v)| (k.to_string(), v.to_string()))
128            .collect();
129        lock_inner(&self.inner).counters.push(RecordedCounter {
130            name: name.to_string(),
131            labels,
132            value,
133        });
134    }
135
136    fn record_gauge(&self, name: &str, labels: &[(&str, &str)], value: f64) {
137        let labels: Vec<(String, String)> = labels
138            .iter()
139            .map(|(k, v)| (k.to_string(), v.to_string()))
140            .collect();
141        lock_inner(&self.inner).gauges.push(RecordedGauge {
142            name: name.to_string(),
143            labels,
144            value,
145        });
146    }
147
148    fn log_event(&self, name: &str, payload: &Value) {
149        lock_inner(&self.inner).events.push(RecordedEvent {
150            name: name.to_string(),
151            payload: payload.clone(),
152        });
153    }
154}
155
156impl Default for RecordingOpsLog {
157    fn default() -> Self {
158        Self::new()
159    }
160}