spectra_core/sinks/
recording.rs1use std::sync::{Arc, Mutex};
4
5use serde_json::Value;
6
7use crate::sink::SpectraSink;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct RecordedCounter {
12 pub name: String,
13 pub labels: Vec<(String, String)>,
14 pub delta: i64,
15}
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct RecordedGauge {
20 pub name: String,
21 pub labels: Vec<(String, String)>,
22 pub value: f64,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct RecordedEvent {
28 pub table: String,
29 pub fields: Value,
30}
31
32#[derive(Debug, Default)]
33struct Inner {
34 counters: Vec<RecordedCounter>,
35 gauges: Vec<RecordedGauge>,
36 events: Vec<RecordedEvent>,
37}
38
39#[derive(Debug, Clone)]
60pub struct RecordingSink {
61 inner: Arc<Mutex<Inner>>,
62}
63
64impl Default for RecordingSink {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl RecordingSink {
71 pub fn new() -> Self {
73 Self {
74 inner: Arc::new(Mutex::new(Inner::default())),
75 }
76 }
77
78 pub fn clear(&self) {
80 let mut g = self.inner.lock().expect("recording sink lock");
81 g.counters.clear();
82 g.gauges.clear();
83 g.events.clear();
84 }
85
86 pub fn counters(&self) -> Vec<RecordedCounter> {
88 self.inner
89 .lock()
90 .expect("recording sink lock")
91 .counters
92 .clone()
93 }
94
95 pub fn gauges(&self) -> Vec<RecordedGauge> {
97 self.inner
98 .lock()
99 .expect("recording sink lock")
100 .gauges
101 .clone()
102 }
103
104 pub fn events(&self) -> Vec<RecordedEvent> {
106 self.inner
107 .lock()
108 .expect("recording sink lock")
109 .events
110 .clone()
111 }
112
113 pub fn recorded_counters_matching(
115 &self,
116 name: &str,
117 label_subset: &[(&str, &str)],
118 ) -> Vec<RecordedCounter> {
119 self.counters()
120 .into_iter()
121 .filter(|c| c.name == name && labels_contain(&c.labels, label_subset))
122 .collect()
123 }
124
125 pub fn recorded_events_for(&self, table: &str) -> Vec<RecordedEvent> {
127 self.events()
128 .into_iter()
129 .filter(|e| e.table == table)
130 .collect()
131 }
132}
133
134fn labels_contain(labels: &[(String, String)], subset: &[(&str, &str)]) -> bool {
135 subset.iter().all(|(k, v)| {
136 labels
137 .iter()
138 .any(|(lk, lv)| lk.as_str() == *k && lv.as_str() == *v)
139 })
140}
141
142impl SpectraSink for RecordingSink {
143 fn record_counter(&self, name: &str, labels: &[(&str, &str)], delta: i64) {
144 let labels: Vec<(String, String)> = labels
145 .iter()
146 .map(|(k, v)| (k.to_string(), v.to_string()))
147 .collect();
148 self.inner
149 .lock()
150 .expect("recording sink lock")
151 .counters
152 .push(RecordedCounter {
153 name: name.to_string(),
154 labels,
155 delta,
156 });
157 }
158
159 fn record_gauge(&self, name: &str, labels: &[(&str, &str)], value: f64) {
160 let labels: Vec<(String, String)> = labels
161 .iter()
162 .map(|(k, v)| (k.to_string(), v.to_string()))
163 .collect();
164 self.inner
165 .lock()
166 .expect("recording sink lock")
167 .gauges
168 .push(RecordedGauge {
169 name: name.to_string(),
170 labels,
171 value,
172 });
173 }
174
175 fn log_event(&self, table: &str, fields: &Value) {
176 self.inner
177 .lock()
178 .expect("recording sink lock")
179 .events
180 .push(RecordedEvent {
181 table: table.to_string(),
182 fields: fields.clone(),
183 });
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use serde_json::json;
191
192 #[test]
193 fn captures_counters_and_events() {
194 let sink = RecordingSink::new();
195 sink.record_counter("example_db_reads", &[("table", "t"), ("op", "get")], 1);
196 sink.log_event("example_error_log", &json!({"source": "database"}));
197
198 assert_eq!(sink.counters().len(), 1);
199 assert_eq!(sink.events().len(), 1);
200 let hits = sink.recorded_counters_matching("example_db_reads", &[("op", "get")]);
201 assert_eq!(hits.len(), 1);
202 }
203}