pub mod memory;
pub mod postgres;
pub mod retention;
pub mod sqlite;
use crate::bus::EventBus;
use crate::event::TelemetryEvent;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::broadcast::error::RecvError;
use tokio::task::JoinHandle;
use tracing::warn;
#[derive(Debug, Clone, Default)]
pub struct QueryFilter {
pub topic: Option<String>,
pub class: Option<String>,
pub id: Option<String>,
pub since: Option<u64>,
pub until: Option<u64>,
pub limit: Option<usize>,
}
impl QueryFilter {
pub fn matches(&self, ev: &TelemetryEvent) -> bool {
if let Some(t) = &self.topic {
if &ev.topic != t {
return false;
}
}
if let Some(c) = &self.class {
if &ev.class != c {
return false;
}
}
if let Some(i) = &self.id {
if &ev.id != i {
return false;
}
}
if let Some(s) = self.since {
if ev.ingress_ts < s {
return false;
}
}
if let Some(u) = self.until {
if ev.ingress_ts > u {
return false;
}
}
true
}
}
#[async_trait]
pub trait StorageEngine: Send + Sync {
async fn append(&self, event: &TelemetryEvent) -> anyhow::Result<()>;
async fn query(&self, filter: &QueryFilter) -> anyhow::Result<Vec<TelemetryEvent>>;
async fn cleanup(&self, older_than_ts: u64) -> anyhow::Result<usize>;
}
pub fn spawn_pipeline(bus: &EventBus, engine: Arc<dyn StorageEngine>) -> JoinHandle<()> {
let mut rx = bus.subscribe();
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(ev) => {
if let Err(e) = engine.append(&ev).await {
warn!(error = %e, "storage append failed");
}
}
Err(RecvError::Lagged(n)) => {
warn!(skipped = n, "storage pipeline lagged");
}
Err(RecvError::Closed) => break,
}
}
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::event::TelemetryEvent;
use serde_json::json;
pub fn ev(topic: &str, ingress_ts: u64) -> TelemetryEvent {
TelemetryEvent {
topic: topic.into(),
id: "i".into(),
class: "c".into(),
ts: 1,
payload: json!({"k": "v"}),
ingress_ts,
}
}
pub async fn conformance(engine: &dyn StorageEngine) {
engine.append(&ev("a", 10)).await.unwrap();
engine.append(&ev("b", 20)).await.unwrap();
engine.append(&ev("a", 30)).await.unwrap();
let all = engine.query(&QueryFilter::default()).await.unwrap();
assert_eq!(all.len(), 3);
assert_eq!(all[0].ingress_ts, 10);
assert_eq!(all[2].ingress_ts, 30);
let f = QueryFilter {
topic: Some("a".into()),
since: Some(15),
until: Some(35),
limit: None,
..Default::default()
};
let hits = engine.query(&f).await.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].ingress_ts, 30);
let f = QueryFilter {
limit: Some(2),
..Default::default()
};
assert_eq!(engine.query(&f).await.unwrap().len(), 2);
let removed = engine.cleanup(20).await.unwrap();
assert_eq!(removed, 1);
let left = engine.query(&QueryFilter::default()).await.unwrap();
assert_eq!(left.len(), 2);
assert!(left.iter().all(|e| e.ingress_ts >= 20));
}
}