Skip to main content

spectra_core/
router.rs

1//! Resolves event tables and metric names to storage backends for reads and writes.
2
3use std::collections::HashMap;
4use std::sync::{Arc, OnceLock, RwLock};
5
6use crate::error::Result;
7use crate::query::EventAggregateResult;
8use crate::storage::{
9    EventsAggregateFilter, EventsQueryFilter, MetricsQueryRange, NoOpEventBackend,
10    NoOpMetricsBackend, SharedEventBackend, SharedMetricsBackend,
11};
12
13static GLOBAL_ROUTER: OnceLock<Arc<SpectraRouter>> = OnceLock::new();
14
15/// Resolves event tables and metric names to storage backends.
16///
17/// A runtime installs default metrics and events backends, then registers schema-specific
18/// routes. Queries use a named route when present and otherwise fall back to the corresponding
19/// default backend. Most applications access this through `Spectra::router()`.
20///
21/// # Examples
22///
23/// ```no_run
24/// use chrono::{Duration, Utc};
25/// use spectra_core::{MetricsQueryRange, SpectraRouter};
26///
27/// # async fn example() -> spectra_core::Result<()> {
28/// let router = SpectraRouter::new();
29/// let now = Utc::now();
30/// let points = router.query_metrics(MetricsQueryRange {
31///     metric_name: "cache_hits".into(),
32///     start: now - Duration::minutes(5),
33///     end: now,
34///     label_matchers: vec![],
35/// }).await?;
36///
37/// // A new router uses no-op defaults, so no rows are returned.
38/// assert!(points.is_empty());
39/// # Ok(())
40/// # }
41/// ```
42pub struct SpectraRouter {
43    events: RwLock<HashMap<String, SharedEventBackend>>,
44    metrics: RwLock<HashMap<String, SharedMetricsBackend>>,
45    default_events: SharedEventBackend,
46    default_metrics: SharedMetricsBackend,
47}
48
49impl Default for SpectraRouter {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl SpectraRouter {
56    /// Creates a router with no-op default backends.
57    pub fn new() -> Self {
58        Self {
59            events: RwLock::new(HashMap::new()),
60            metrics: RwLock::new(HashMap::new()),
61            default_events: Arc::new(NoOpEventBackend),
62            default_metrics: Arc::new(NoOpMetricsBackend),
63        }
64    }
65
66    /// Create a router with default backends for unregistered schema names.
67    pub fn with_defaults(
68        default_metrics: SharedMetricsBackend,
69        default_events: SharedEventBackend,
70    ) -> Self {
71        Self {
72            events: RwLock::new(HashMap::new()),
73            metrics: RwLock::new(HashMap::new()),
74            default_events,
75            default_metrics,
76        }
77    }
78
79    /// Registers a storage backend for an event table.
80    pub fn register_event_backend(&self, table: impl Into<String>, backend: SharedEventBackend) {
81        if let Ok(mut guard) = self.events.write() {
82            guard.insert(table.into(), backend);
83        }
84    }
85
86    /// Registers a storage backend for a metric family.
87    pub fn register_metrics_backend(&self, name: impl Into<String>, backend: SharedMetricsBackend) {
88        if let Ok(mut guard) = self.metrics.write() {
89            guard.insert(name.into(), backend);
90        }
91    }
92
93    /// Resolves the backend for an event table, falling back to the default.
94    pub fn resolve_event(&self, table: &str) -> SharedEventBackend {
95        self.events
96            .read()
97            .ok()
98            .and_then(|g| g.get(table).cloned())
99            .unwrap_or_else(|| Arc::clone(&self.default_events))
100    }
101
102    /// Resolves the backend for a metric family, falling back to the default.
103    pub fn resolve_metrics(&self, name: &str) -> SharedMetricsBackend {
104        self.metrics
105            .read()
106            .ok()
107            .and_then(|g| g.get(name).cloned())
108            .unwrap_or_else(|| Arc::clone(&self.default_metrics))
109    }
110
111    /// Queries event rows through the resolved backend.
112    pub async fn query_events(
113        &self,
114        filter: EventsQueryFilter,
115    ) -> Result<Vec<crate::storage::EventRow>> {
116        let backend = self.resolve_event(&filter.table);
117        backend.query_rows(filter).await
118    }
119
120    /// Queries metric points through the resolved backend.
121    pub async fn query_metrics(
122        &self,
123        query: MetricsQueryRange,
124    ) -> Result<Vec<crate::storage::MetricPoint>> {
125        let backend = self.resolve_metrics(&query.metric_name);
126        backend.query_range(query).await
127    }
128
129    /// Queries aggregated chart data through the resolved backend.
130    pub async fn query_event_aggregate(
131        &self,
132        filter: EventsAggregateFilter,
133    ) -> Result<EventAggregateResult> {
134        let table = filter.table.clone();
135        let backend = self.resolve_event(&table);
136        backend.query_aggregate(filter).await
137    }
138
139    /// Installs a router as the process-global instance (call once).
140    pub fn set_global(router: Arc<Self>) {
141        let _ = GLOBAL_ROUTER.set(router);
142    }
143
144    /// Returns the process-global router (panics if not installed).
145    pub fn global() -> Arc<SpectraRouter> {
146        GLOBAL_ROUTER
147            .get()
148            .cloned()
149            .expect("SpectraRouter::set_global was not called")
150    }
151
152    /// Returns the process-global router if installed.
153    pub fn try_global() -> Option<Arc<SpectraRouter>> {
154        GLOBAL_ROUTER.get().cloned()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::storage::{EventStorageBackend, EventsQueryFilter};
162    use async_trait::async_trait;
163    use chrono::Utc;
164    use serde_json::json;
165    use std::sync::atomic::{AtomicU32, Ordering};
166
167    struct CountingEventBackend {
168        appends: AtomicU32,
169    }
170
171    #[async_trait]
172    impl EventStorageBackend for CountingEventBackend {
173        fn engine_type(&self) -> crate::storage::StorageEngineType {
174            crate::storage::StorageEngineType::NoOp
175        }
176
177        async fn append_row(
178            &self,
179            _: &str,
180            _: &serde_json::Value,
181            _: chrono::DateTime<Utc>,
182            _: Option<&str>,
183        ) -> crate::error::Result<()> {
184            self.appends.fetch_add(1, Ordering::SeqCst);
185            Ok(())
186        }
187    }
188
189    #[test]
190    fn router_noop_default() {
191        let router = SpectraRouter::new();
192        let rt = tokio::runtime::Runtime::new().expect("runtime");
193        let rows = rt
194            .block_on(router.query_events(EventsQueryFilter {
195                table: "missing".into(),
196                ..Default::default()
197            }))
198            .expect("query");
199        assert!(rows.is_empty());
200    }
201
202    #[test]
203    fn router_resolve_event_backend() {
204        let router = SpectraRouter::new();
205        let counting = Arc::new(CountingEventBackend {
206            appends: AtomicU32::new(0),
207        });
208        let backend: SharedEventBackend = Arc::clone(&counting) as SharedEventBackend;
209        router.register_event_backend("t1", backend);
210        let resolved = router.resolve_event("t1");
211        let rt = tokio::runtime::Runtime::new().expect("runtime");
212        rt.block_on(async {
213            resolved
214                .append_row("t1", &json!({}), Utc::now(), None)
215                .await
216                .expect("append");
217        });
218        assert_eq!(counting.appends.load(Ordering::SeqCst), 1);
219    }
220}