Skip to main content

photon_telemetry/
recording.rs

1//! In-memory [`OpsLog`] for tests (`feature = "recording"`).
2
3use std::sync::{Arc, Mutex};
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
53impl RecordingOpsLog {
54    /// Create an empty recording log.
55    #[must_use]
56    pub fn new() -> Self {
57        Self {
58            inner: Arc::new(Mutex::new(Inner::default())),
59        }
60    }
61
62    /// Drop all recorded counters, gauges, and events.
63    ///
64    /// # Panics
65    ///
66    /// Panics if an internal lock is poisoned.
67    pub fn clear(&self) {
68        let mut g = self.inner.lock().expect("recording ops log lock");
69        g.counters.clear();
70        g.gauges.clear();
71        g.events.clear();
72    }
73
74    /// Snapshot of recorded counters.
75    ///
76    /// # Panics
77    ///
78    /// Panics if an internal lock is poisoned.
79    #[must_use]
80    pub fn counters(&self) -> Vec<RecordedCounter> {
81        self.inner
82            .lock()
83            .expect("recording ops log lock")
84            .counters
85            .clone()
86    }
87
88    /// Snapshot of recorded gauges.
89    ///
90    /// # Panics
91    ///
92    /// Panics if an internal lock is poisoned.
93    #[must_use]
94    pub fn gauges(&self) -> Vec<RecordedGauge> {
95        self.inner
96            .lock()
97            .expect("recording ops log lock")
98            .gauges
99            .clone()
100    }
101
102    /// Snapshot of recorded events.
103    ///
104    /// # Panics
105    ///
106    /// Panics if an internal lock is poisoned.
107    #[must_use]
108    pub fn events(&self) -> Vec<RecordedEvent> {
109        self.inner
110            .lock()
111            .expect("recording ops log lock")
112            .events
113            .clone()
114    }
115
116    /// Counters whose name matches and labels contain `label_subset`.
117    #[must_use]
118    pub fn recorded_counters_matching(
119        &self,
120        name: &str,
121        label_subset: &[(&str, &str)],
122    ) -> Vec<RecordedCounter> {
123        self.counters()
124            .into_iter()
125            .filter(|c| c.name == name && labels_contain(&c.labels, label_subset))
126            .collect()
127    }
128
129    /// Events whose name equals `event_name`.
130    #[must_use]
131    pub fn recorded_events_for(&self, event_name: &str) -> Vec<RecordedEvent> {
132        self.events()
133            .into_iter()
134            .filter(|e| e.name == event_name)
135            .collect()
136    }
137}
138
139fn labels_contain(labels: &[(String, String)], subset: &[(&str, &str)]) -> bool {
140    subset.iter().all(|(k, v)| {
141        labels
142            .iter()
143            .any(|(lk, lv)| lk.as_str() == *k && lv.as_str() == *v)
144    })
145}
146
147impl OpsLog for RecordingOpsLog {
148    fn record_counter(&self, name: &str, labels: &[(&str, &str)], value: f64) {
149        let labels: Vec<(String, String)> = labels
150            .iter()
151            .map(|(k, v)| (k.to_string(), v.to_string()))
152            .collect();
153        self.inner
154            .lock()
155            .expect("recording ops log lock")
156            .counters
157            .push(RecordedCounter {
158                name: name.to_string(),
159                labels,
160                value,
161            });
162    }
163
164    fn record_gauge(&self, name: &str, labels: &[(&str, &str)], value: f64) {
165        let labels: Vec<(String, String)> = labels
166            .iter()
167            .map(|(k, v)| (k.to_string(), v.to_string()))
168            .collect();
169        self.inner
170            .lock()
171            .expect("recording ops log lock")
172            .gauges
173            .push(RecordedGauge {
174                name: name.to_string(),
175                labels,
176                value,
177            });
178    }
179
180    fn log_event(&self, name: &str, payload: &Value) {
181        self.inner
182            .lock()
183            .expect("recording ops log lock")
184            .events
185            .push(RecordedEvent {
186                name: name.to_string(),
187                payload: payload.clone(),
188            });
189    }
190}
191
192impl Default for RecordingOpsLog {
193    fn default() -> Self {
194        Self::new()
195    }
196}