use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::TelemetrySink;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecordedCounter {
pub name: String,
pub labels: Vec<(String, String)>,
pub delta: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordedGauge {
pub name: String,
pub labels: Vec<(String, String)>,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecordedEvent {
pub schema: String,
pub fields: HashMap<String, String>,
}
#[derive(Debug, Default)]
struct Inner {
counters: Vec<RecordedCounter>,
gauges: Vec<RecordedGauge>,
events: Vec<RecordedEvent>,
}
#[derive(Debug, Clone)]
pub struct RecordingSink {
inner: Arc<Mutex<Inner>>,
}
impl RecordingSink {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(Inner::default())),
}
}
pub fn clear(&self) {
let mut inner = self.inner.lock().expect("recording sink lock");
inner.counters.clear();
inner.gauges.clear();
inner.events.clear();
}
pub fn counters(&self) -> Vec<RecordedCounter> {
self.inner
.lock()
.expect("recording sink lock")
.counters
.clone()
}
pub fn gauges(&self) -> Vec<RecordedGauge> {
self.inner
.lock()
.expect("recording sink lock")
.gauges
.clone()
}
pub fn events(&self) -> Vec<RecordedEvent> {
self.inner
.lock()
.expect("recording sink lock")
.events
.clone()
}
pub fn recorded_counters_matching(
&self,
name: &str,
label_subset: &[(&str, &str)],
) -> Vec<RecordedCounter> {
self.counters()
.into_iter()
.filter(|c| c.name == name && labels_contain(&c.labels, label_subset))
.collect()
}
pub fn recorded_events_for(&self, schema: &str) -> Vec<RecordedEvent> {
self.events()
.into_iter()
.filter(|e| e.schema == schema)
.collect()
}
}
fn labels_contain(labels: &[(String, String)], subset: &[(&str, &str)]) -> bool {
subset.iter().all(|(k, v)| {
labels
.iter()
.any(|(lk, lv)| lk.as_str() == *k && lv.as_str() == *v)
})
}
impl TelemetrySink for RecordingSink {
fn record_counter(&self, name: &str, labels: &[(&str, &str)], delta: u64) {
let labels: Vec<(String, String)> = labels
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
self.inner
.lock()
.expect("recording sink lock")
.counters
.push(RecordedCounter {
name: name.to_string(),
labels,
delta,
});
}
fn record_gauge(&self, name: &str, labels: &[(&str, &str)], value: f64) {
let labels: Vec<(String, String)> = labels
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
self.inner
.lock()
.expect("recording sink lock")
.gauges
.push(RecordedGauge {
name: name.to_string(),
labels,
value,
});
}
fn log_event(&self, schema: &str, fields: &[(&str, &str)]) {
let fields: HashMap<String, String> = fields
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
self.inner
.lock()
.expect("recording sink lock")
.events
.push(RecordedEvent {
schema: schema.to_string(),
fields,
});
}
}
impl Default for RecordingSink {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn captures_counters_and_events() {
let sink = RecordingSink::new();
sink.record_counter(
"chronon_runs_started",
&[("script", "noop"), ("job", "daily")],
1,
);
sink.log_event("chronon_run_failed", &[("status", "failed")]);
assert_eq!(sink.counters().len(), 1);
assert_eq!(sink.events().len(), 1);
let hits = sink.recorded_counters_matching("chronon_runs_started", &[("job", "daily")]);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].delta, 1);
}
#[test]
fn clear_resets_buffers() {
let sink = RecordingSink::new();
sink.record_gauge("chronon_active_runs", &[], 2.0);
sink.clear();
assert!(sink.gauges().is_empty());
}
}