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    pub fn set_global(registry: SchemaRegistry) {
167        GLOBAL_REGISTRY
168            .set(registry)
169            .expect("SchemaRegistry::set_global called more than once");
170    }
171
172    /// Returns the process-global registry, auto-discovering on first access.
173    pub fn global() -> &'static SchemaRegistry {
174        GLOBAL_REGISTRY.get_or_init(SchemaRegistry::auto_discover)
175    }
176
177    /// Registers a leaked schema metadata entry.
178    pub fn register(&mut self, metadata: &'static SchemaMetadata) {
179        self.inner.register(metadata);
180    }
181
182    /// Looks up schema metadata by table or metric name.
183    pub fn get_schema(&self, table_or_metric: &str) -> Option<&'static SchemaMetadata> {
184        self.inner.get(table_or_metric)
185    }
186
187    /// Lists all registered table and metric names.
188    pub fn list_schemas(&self) -> Vec<&str> {
189        self.inner.list()
190    }
191
192    /// Returns whether a schema is registered.
193    pub fn has_schema(&self, table_or_metric: &str) -> bool {
194        self.inner.get(table_or_metric).is_some()
195    }
196
197    /// Distinct `store` names from all registered schemas, always including `"default"`.
198    pub fn distinct_store_names(&self) -> Vec<String> {
199        let mut names: BTreeSet<String> = BTreeSet::new();
200        names.insert("default".to_string());
201        for name in self.list_schemas() {
202            if let Some(meta) = self.get_schema(name) {
203                if !meta.store.is_empty() {
204                    names.insert(meta.store.clone());
205                }
206            }
207        }
208        names.into_iter().collect()
209    }
210}
211
212static GLOBAL_REGISTRY: OnceLock<SchemaRegistry> = OnceLock::new();
213
214/// Distinct Spectra store names from schema inventory (always includes `"default"`).
215pub fn collect_distinct_spectra_store_names() -> Vec<String> {
216    SchemaRegistry::global().distinct_store_names()
217}
218
219impl Default for SchemaRegistry {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225impl std::ops::Deref for SchemaRegistry {
226    type Target = quark::Registry<SchemaMetadata>;
227
228    fn deref(&self) -> &Self::Target {
229        &self.inner
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn registry_empty_when_no_submissions_in_test_crate() {
239        let reg = SchemaRegistry::new();
240        assert!(reg.list_schemas().is_empty());
241    }
242}