Skip to main content

spectra_core/
registry.rs

1//! Schema registry for compile-time registered Spectra event and metric metadata.
2
3use std::collections::BTreeSet;
4use std::sync::OnceLock;
5
6use quark::inventory;
7
8use crate::classification::FieldClassification;
9
10/// Verbosity / sampling tier for a Spectra schema (more verbose = greater ordinal).
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
12pub enum SpectraLevel {
13    /// Error-tier emits (always on).
14    Error,
15    /// Warning-tier emits (always on).
16    Warn,
17    /// Informational emits.
18    #[default]
19    Info,
20    /// Debug-tier emits.
21    Debug,
22    /// Trace-tier emits.
23    Trace,
24}
25
26impl SpectraLevel {
27    /// Parse `SPECTRA_LEVEL` / DSL values (`error`, `warn`, `info`, `debug`, `trace`).
28    pub fn parse(s: &str) -> Option<Self> {
29        match s.trim().to_ascii_lowercase().as_str() {
30            "error" => Some(Self::Error),
31            "warn" | "warning" => Some(Self::Warn),
32            "info" => Some(Self::Info),
33            "debug" => Some(Self::Debug),
34            "trace" => Some(Self::Trace),
35            _ => None,
36        }
37    }
38
39    /// True for tiers that must never be level-gated or statistically sampled away.
40    pub fn is_always_on(self) -> bool {
41        matches!(self, Self::Error | Self::Warn)
42    }
43}
44
45/// Event vs metric schema registration.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum LoggingKind {
48    /// Structured event table schema.
49    Event,
50    /// Metric family schema.
51    Metric,
52}
53
54/// Field metadata for registry / UI (macros populate this).
55#[derive(Debug, Clone)]
56pub struct SchemaFieldMetadata {
57    /// Field name.
58    pub name: String,
59    /// Rust type name as registered.
60    pub rust_type: String,
61    /// GDPR-oriented classification metadata.
62    pub classification: FieldClassification,
63}
64
65/// Runtime metadata for a Spectra schema (event table or metric family).
66#[derive(Debug, Clone)]
67pub struct SchemaMetadata {
68    /// Table or metric name (registry key).
69    pub table_or_metric: String,
70    /// Logical store name from the schema DSL (`store:` field); used for registry grouping and routing.
71    pub store: String,
72    /// Schema version string.
73    pub version: String,
74    /// Optional human-readable description.
75    pub description: Option<String>,
76    /// Whether this schema is an event table or metric family.
77    pub logging_kind: LoggingKind,
78    /// Registered field metadata.
79    pub fields: Vec<SchemaFieldMetadata>,
80    /// Compile-time default verbosity tier (see [`SpectraLevel`]).
81    pub default_level: SpectraLevel,
82    /// `1.0` = always emit when level passes; `0.0` = never (except forced errors/warns).
83    pub default_sample_rate: f64,
84    /// Gauges only: minimum interval between emits (last-write-wins coalesce window).
85    pub gauge_coalesce_ms: Option<u64>,
86}
87
88impl Default for SchemaMetadata {
89    fn default() -> Self {
90        Self {
91            table_or_metric: String::new(),
92            store: "default".to_string(),
93            version: String::new(),
94            description: None,
95            logging_kind: LoggingKind::Metric,
96            fields: Vec::new(),
97            default_level: SpectraLevel::Info,
98            default_sample_rate: 1.0,
99            gauge_coalesce_ms: None,
100        }
101    }
102}
103
104impl SchemaMetadata {
105    /// Returns the table or metric name.
106    pub fn table_name(&self) -> &str {
107        &self.table_or_metric
108    }
109}
110
111impl quark::Registrable for SchemaMetadata {
112    fn registry_key(&self) -> &str {
113        &self.table_or_metric
114    }
115}
116
117/// Function-pointer wrapper collected by `inventory`.
118pub struct SchemaMetadataInit(pub fn() -> SchemaMetadata);
119
120inventory::collect!(SchemaMetadataInit);
121
122/// Registry of metric and event schema metadata linked into the current binary.
123///
124/// [`global`](Self::global) lazily discovers every `spectra_schema!` and `spectra_metric!`
125/// registration submitted through `inventory`. Schema modules must be compiled into the binary
126/// (linked with `mod`); declaring macros alone in unlinked files does not register them.
127///
128/// # Examples
129///
130/// ```
131/// use spectra_core::SchemaRegistry;
132///
133/// let registry = SchemaRegistry::global();
134/// for name in registry.list_schemas() {
135///     let schema = registry.get_schema(name).expect("listed schema");
136///     println!("{} uses store {}", schema.table_name(), schema.store);
137/// }
138///
139/// // "default" is always available even when this binary declares no schemas.
140/// assert!(registry.distinct_store_names().iter().any(|name| name == "default"));
141/// ```
142#[derive(Debug)]
143pub struct SchemaRegistry {
144    inner: quark::Registry<SchemaMetadata>,
145}
146
147impl SchemaRegistry {
148    /// Creates an empty registry.
149    pub fn new() -> Self {
150        Self {
151            inner: quark::Registry::new(),
152        }
153    }
154
155    /// Populate from all `inventory::submit!`-ed [`SchemaMetadataInit`] items.
156    pub fn auto_discover() -> Self {
157        let mut registry = Self::new();
158        for init in inventory::iter::<SchemaMetadataInit> {
159            let metadata = (init.0)();
160            registry.register(Box::leak(Box::new(metadata)));
161        }
162        registry
163    }
164
165    /// Installs a custom registry as the process-global instance (call once).
166    ///
167    /// Panics if called more than once. Prefer letting [`Self::global`] auto-discover
168    /// schemas unless the host needs a custom registry.
169    pub fn set_global(registry: SchemaRegistry) {
170        // Process invariant: double install indicates a host boot bug.
171        #[allow(clippy::expect_used)]
172        GLOBAL_REGISTRY
173            .set(registry)
174            .expect("SchemaRegistry::set_global called more than once");
175    }
176
177    /// Returns the process-global registry, auto-discovering on first access.
178    pub fn global() -> &'static Self {
179        GLOBAL_REGISTRY.get_or_init(Self::auto_discover)
180    }
181
182    /// Registers a leaked schema metadata entry.
183    pub fn register(&mut self, metadata: &'static SchemaMetadata) {
184        self.inner.register(metadata);
185    }
186
187    /// Looks up schema metadata by table or metric name.
188    pub fn get_schema(&self, table_or_metric: &str) -> Option<&'static SchemaMetadata> {
189        self.inner.get(table_or_metric)
190    }
191
192    /// Lists all registered table and metric names.
193    pub fn list_schemas(&self) -> Vec<&str> {
194        self.inner.list()
195    }
196
197    /// Returns whether a schema is registered.
198    pub fn has_schema(&self, table_or_metric: &str) -> bool {
199        self.inner.get(table_or_metric).is_some()
200    }
201
202    /// Distinct `store` names from all registered schemas, always including `"default"`.
203    pub fn distinct_store_names(&self) -> Vec<String> {
204        let mut names: BTreeSet<String> = BTreeSet::new();
205        names.insert("default".to_string());
206        for name in self.list_schemas() {
207            if let Some(meta) = self.get_schema(name) {
208                if !meta.store.is_empty() {
209                    names.insert(meta.store.clone());
210                }
211            }
212        }
213        names.into_iter().collect()
214    }
215}
216
217static GLOBAL_REGISTRY: OnceLock<SchemaRegistry> = OnceLock::new();
218
219/// Distinct Spectra store names from schema inventory (always includes `"default"`).
220pub fn collect_distinct_spectra_store_names() -> Vec<String> {
221    SchemaRegistry::global().distinct_store_names()
222}
223
224impl Default for SchemaRegistry {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230impl std::ops::Deref for SchemaRegistry {
231    type Target = quark::Registry<SchemaMetadata>;
232
233    fn deref(&self) -> &Self::Target {
234        &self.inner
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn registry_empty_when_no_submissions_in_test_crate() {
244        let reg = SchemaRegistry::new();
245        assert!(reg.list_schemas().is_empty());
246    }
247}