Skip to main content

spectra_core/
storage.rs

1//! Storage backend traits and query filter types for metrics and events.
2//!
3//! # Design notes
4//!
5//! Adapters implement these traits in `spectra-backend-*` crates and inject them through
6//! [`spectra_runtime::SpectraBuilder`]. The default trait methods provide safe stubs so
7//! partial implementations can ship incrementally.
8//!
9//! # Lifecycle example
10//!
11//! ```no_run
12//! # use std::sync::Arc;
13//! # use chrono::Utc;
14//! # use serde_json::json;
15//! # use spectra_core::{
16//! #     EventStorageBackend, MetricsStorageBackend, NoOpEventBackend, NoOpMetricsBackend,
17//! #     MetricsQueryRange, EventsQueryFilter,
18//! # };
19//! # async fn demo() -> spectra_core::Result<()> {
20//! let metrics = Arc::new(NoOpMetricsBackend);
21//! let events = Arc::new(NoOpEventBackend);
22//!
23//! metrics.record_counter("cache_hits", &json!({}), 1, Utc::now()).await?;
24//! events.append_row("request_log", &json!({"ok": true}), Utc::now(), None).await?;
25//!
26//! let points = metrics.query_range(MetricsQueryRange {
27//!     metric_name: "cache_hits".into(),
28//!     start: Utc::now() - chrono::Duration::hours(1),
29//!     end: Utc::now(),
30//!     label_matchers: vec![],
31//! }).await?;
32//! let rows = events.query_rows(EventsQueryFilter {
33//!     table: "request_log".into(),
34//!     ..Default::default()
35//! }).await?;
36//! # let _ = (points, rows);
37//! # Ok(())
38//! # }
39//! ```
40//!
41use std::sync::Arc;
42
43use async_trait::async_trait;
44use chrono::{DateTime, Utc};
45use serde_json::Value;
46
47use crate::error::Result;
48
49/// Storage engine identifier for query routing and UI.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum StorageEngineType {
52    /// No-op backend (discards writes, returns empty reads).
53    NoOp,
54    /// In-memory backend.
55    Mem,
56    /// SQLite embedded backend.
57    Sqlite,
58    /// TensorBase backend.
59    TensorBase,
60    /// ClickHouse backend.
61    ClickHouse,
62}
63
64/// Time-series query parameters.
65#[derive(Debug, Clone)]
66pub struct MetricsQueryRange {
67    /// Metric family name.
68    pub metric_name: String,
69    /// Inclusive range start timestamp.
70    pub start: DateTime<Utc>,
71    /// Inclusive range end timestamp.
72    pub end: DateTime<Utc>,
73    /// Label equality matchers.
74    pub label_matchers: Vec<crate::query::LabelMatcher>,
75}
76
77/// Row filter for event log queries (adapter input).
78#[derive(Debug, Clone, Default)]
79pub struct EventsQueryFilter {
80    /// Event table name.
81    pub table: String,
82    /// Optional inclusive range start.
83    pub start: Option<DateTime<Utc>>,
84    /// Optional inclusive range end.
85    pub end: Option<DateTime<Utc>>,
86    /// Optional partition granularity (`"hourly"` or `"daily"`).
87    pub partition: Option<String>,
88    /// Maximum rows to return.
89    pub limit: Option<u32>,
90    /// Row offset for pagination.
91    pub offset: Option<u32>,
92    /// Column to sort by.
93    pub sort_field: Option<String>,
94    /// Whether sort is descending.
95    pub sort_desc: bool,
96    /// Structured row filter model.
97    pub filter: crate::query::GridFilterModel,
98}
99
100/// Aggregate query for chart views (adapter input).
101#[derive(Debug, Clone)]
102pub struct EventsAggregateFilter {
103    /// Event table name.
104    pub table: String,
105    /// Inclusive range start timestamp.
106    pub start: DateTime<Utc>,
107    /// Inclusive range end timestamp.
108    pub end: DateTime<Utc>,
109    /// Optional partition granularity (`"hourly"` or `"daily"`).
110    pub partition: Option<String>,
111    /// Structured row filter model.
112    pub filter: crate::query::GridFilterModel,
113    /// Aggregation measure.
114    pub measure: crate::query::EventMeasure,
115    /// Field to sum when measure is sum.
116    pub measure_field: Option<String>,
117    /// Time bucket width in seconds.
118    pub time_bucket_secs: Option<u64>,
119    /// Field to group by for slice views.
120    pub group_by_field: Option<String>,
121}
122
123/// Metric point for query results.
124#[derive(Debug, Clone)]
125pub struct MetricPoint {
126    /// Sample timestamp.
127    pub ts: DateTime<Utc>,
128    /// Sample value.
129    pub value: f64,
130    /// Label set as JSON.
131    pub labels: Value,
132}
133
134/// Event row for query results.
135#[derive(Debug, Clone)]
136pub struct EventRow {
137    /// Event timestamp.
138    pub ts: DateTime<Utc>,
139    /// Event field payload.
140    pub fields: Value,
141}
142
143/// One metrics write row for batch insert (counter or gauge).
144#[derive(Debug, Clone)]
145pub struct MetricWriteRow {
146    /// Metric family name.
147    pub name: String,
148    /// `"counter"` or `"gauge"`.
149    pub kind: &'static str,
150    /// Counter delta or gauge value as JSON number.
151    pub value: Value,
152    /// Label set as JSON.
153    pub labels: Value,
154    /// Sample timestamp.
155    pub ts: DateTime<Utc>,
156    /// Optional correlation identifier.
157    pub correlation_id: Option<String>,
158}
159
160/// One event write row for batch insert.
161#[derive(Debug, Clone)]
162pub struct EventWriteRow {
163    /// Event table name.
164    pub table: String,
165    /// Event field payload.
166    pub fields: Value,
167    /// Event timestamp.
168    pub ts: DateTime<Utc>,
169    /// Optional correlation identifier.
170    pub correlation_id: Option<String>,
171}
172
173/// Metrics storage: subscribers call `record_*`; explore UI calls `query_range`.
174#[async_trait]
175pub trait MetricsStorageBackend: Send + Sync {
176    /// Returns the engine type for this backend.
177    fn engine_type(&self) -> StorageEngineType;
178
179    /// Records a counter increment.
180    async fn record_counter(
181        &self,
182        name: &str,
183        labels: &Value,
184        delta: i64,
185        ts: DateTime<Utc>,
186    ) -> Result<()>;
187
188    /// Records a gauge sample.
189    async fn record_gauge(
190        &self,
191        name: &str,
192        labels: &Value,
193        value: f64,
194        ts: DateTime<Utc>,
195    ) -> Result<()>;
196
197    /// Queries a time range of metric points.
198    async fn query_range(&self, _query: MetricsQueryRange) -> Result<Vec<MetricPoint>> {
199        Ok(Vec::new())
200    }
201
202    /// Batch write metrics (default: per-row dispatch).
203    async fn record_metrics_batch(&self, rows: &[MetricWriteRow]) -> Result<()> {
204        for row in rows {
205            if row.kind == "counter" {
206                let delta = row.value.as_i64().unwrap_or(0);
207                self.record_counter(&row.name, &row.labels, delta, row.ts)
208                    .await?;
209            } else {
210                let value = row.value.as_f64().unwrap_or(0.0);
211                self.record_gauge(&row.name, &row.labels, value, row.ts)
212                    .await?;
213            }
214        }
215        Ok(())
216    }
217}
218
219/// Event storage: subscribers call `append_row`; explore UI calls `query_rows`.
220#[async_trait]
221pub trait EventStorageBackend: Send + Sync {
222    /// Returns the engine type for this backend.
223    fn engine_type(&self) -> StorageEngineType;
224
225    /// Appends one event row.
226    async fn append_row(
227        &self,
228        table: &str,
229        fields: &Value,
230        ts: DateTime<Utc>,
231        correlation_id: Option<&str>,
232    ) -> Result<()>;
233
234    /// Queries event rows matching the filter.
235    async fn query_rows(&self, _filter: EventsQueryFilter) -> Result<Vec<EventRow>> {
236        Ok(Vec::new())
237    }
238
239    /// Queries aggregated chart data.
240    async fn query_aggregate(
241        &self,
242        _filter: EventsAggregateFilter,
243    ) -> Result<crate::query::EventAggregateResult> {
244        Ok(crate::query::EventAggregateResult::TimeSeries {
245            series: Vec::new(),
246            headline: Vec::new(),
247        })
248    }
249
250    /// Batch append event rows (default: per-row dispatch).
251    async fn append_rows_batch(&self, rows: &[EventWriteRow]) -> Result<()> {
252        for row in rows {
253            self.append_row(
254                &row.table,
255                &row.fields,
256                row.ts,
257                row.correlation_id.as_deref(),
258            )
259            .await?;
260        }
261        Ok(())
262    }
263}
264
265/// No-op metrics backend (default before host wiring).
266#[derive(Debug, Default, Clone, Copy)]
267pub struct NoOpMetricsBackend;
268
269#[async_trait]
270impl MetricsStorageBackend for NoOpMetricsBackend {
271    fn engine_type(&self) -> StorageEngineType {
272        StorageEngineType::NoOp
273    }
274
275    async fn record_counter(&self, _: &str, _: &Value, _: i64, _: DateTime<Utc>) -> Result<()> {
276        Ok(())
277    }
278
279    async fn record_gauge(&self, _: &str, _: &Value, _: f64, _: DateTime<Utc>) -> Result<()> {
280        Ok(())
281    }
282}
283
284/// No-op event backend (default before host wiring).
285#[derive(Debug, Default, Clone, Copy)]
286pub struct NoOpEventBackend;
287
288#[async_trait]
289impl EventStorageBackend for NoOpEventBackend {
290    fn engine_type(&self) -> StorageEngineType {
291        StorageEngineType::NoOp
292    }
293
294    async fn append_row(
295        &self,
296        _: &str,
297        _: &Value,
298        _: DateTime<Utc>,
299        _: Option<&str>,
300    ) -> Result<()> {
301        Ok(())
302    }
303}
304
305/// Shared handle to a metrics storage backend.
306pub type SharedMetricsBackend = Arc<dyn MetricsStorageBackend>;
307/// Shared handle to an event storage backend.
308pub type SharedEventBackend = Arc<dyn EventStorageBackend>;