1use std::collections::BTreeSet;
4use std::sync::OnceLock;
5
6use quark::inventory;
7
8use crate::classification::FieldClassification;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
12pub enum SpectraLevel {
13 Error,
15 Warn,
17 #[default]
19 Info,
20 Debug,
22 Trace,
24}
25
26impl SpectraLevel {
27 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 pub fn is_always_on(self) -> bool {
41 matches!(self, Self::Error | Self::Warn)
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum LoggingKind {
48 Event,
50 Metric,
52}
53
54#[derive(Debug, Clone)]
56pub struct SchemaFieldMetadata {
57 pub name: String,
59 pub rust_type: String,
61 pub classification: FieldClassification,
63}
64
65#[derive(Debug, Clone)]
67pub struct SchemaMetadata {
68 pub table_or_metric: String,
70 pub store: String,
72 pub version: String,
74 pub description: Option<String>,
76 pub logging_kind: LoggingKind,
78 pub fields: Vec<SchemaFieldMetadata>,
80 pub default_level: SpectraLevel,
82 pub default_sample_rate: f64,
84 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 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
117pub struct SchemaMetadataInit(pub fn() -> SchemaMetadata);
119
120inventory::collect!(SchemaMetadataInit);
121
122#[derive(Debug)]
143pub struct SchemaRegistry {
144 inner: quark::Registry<SchemaMetadata>,
145}
146
147impl SchemaRegistry {
148 pub fn new() -> Self {
150 Self {
151 inner: quark::Registry::new(),
152 }
153 }
154
155 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 pub fn set_global(registry: SchemaRegistry) {
167 GLOBAL_REGISTRY
168 .set(registry)
169 .expect("SchemaRegistry::set_global called more than once");
170 }
171
172 pub fn global() -> &'static SchemaRegistry {
174 GLOBAL_REGISTRY.get_or_init(SchemaRegistry::auto_discover)
175 }
176
177 pub fn register(&mut self, metadata: &'static SchemaMetadata) {
179 self.inner.register(metadata);
180 }
181
182 pub fn get_schema(&self, table_or_metric: &str) -> Option<&'static SchemaMetadata> {
184 self.inner.get(table_or_metric)
185 }
186
187 pub fn list_schemas(&self) -> Vec<&str> {
189 self.inner.list()
190 }
191
192 pub fn has_schema(&self, table_or_metric: &str) -> bool {
194 self.inner.get(table_or_metric).is_some()
195 }
196
197 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
214pub 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}