obz_core/registry.rs
1//! Provider registry.
2//!
3//! The registry is the runtime catalog of available providers.
4//! It is analogous to `DuckDB`'s catalog — providers register their
5//! metadata and factory functions here at startup. The obz shell then
6//! looks up a provider by name and instantiates it on demand with the
7//! user-supplied endpoint and credentials.
8//!
9//! # Why lazy instantiation?
10//!
11//! Different providers use different default ports and authentication
12//! schemes. The endpoint is supplied by the user at invocation time via
13//! `--endpoint`, so providers cannot be instantiated until after argument
14//! parsing. The registry stores factory functions; the shell calls the
15//! right one after resolving `--provider` and `--endpoint`.
16//!
17//! # Registration flow (performed by the obz shell at startup)
18//!
19//! ```text
20//! obz main()
21//! → build ProviderRegistry
22//! → registry.register(ProviderMeta { name: "<provider>", aliases: &[...], ... })
23//! → ... (repeat for each built-in provider)
24//! → clap parses args
25//! → registry.get("<alias>")? → &ProviderMeta
26//! → meta.build(config)? → BuiltProvider { metric: Some(Box<dyn MetricProvider>), ... }
27//! ```
28
29use std::collections::BTreeMap;
30use std::future::Future;
31use std::pin::Pin;
32use std::time::Duration;
33
34use crate::cmd_path::StandardCommand;
35use crate::descriptor::{CommandDescriptor, FlagDescriptor};
36use crate::model::error::{ErrorCode, ObzError};
37use crate::provider::traits::{ExtensionProvider, LogProvider, MetricProvider, TraceProvider};
38use crate::provider::ProviderConfig;
39
40/// A fully instantiated provider ready to execute commands.
41///
42/// Built by calling [`ProviderMeta::build`] after the user's endpoint
43/// and credentials are known. Each signal capability is `None` if the
44/// provider does not support that signal.
45pub struct BuiltProvider {
46 /// The canonical provider name (for use in response metadata).
47 pub name: &'static str,
48 /// Query language for metric queries (e.g. `"MetricsQL"`).
49 pub metric_query_language: Option<&'static str>,
50 /// Query language for log queries (e.g. `"LogsQL"`).
51 pub log_query_language: Option<&'static str>,
52 /// Metric provider implementation.
53 pub metric: Option<Box<dyn MetricProvider>>,
54 /// Log provider implementation.
55 pub log: Option<Box<dyn LogProvider>>,
56 /// Trace provider implementation.
57 pub trace: Option<Box<dyn TraceProvider>>,
58 /// Extension command provider implementation.
59 pub extension: Option<Box<dyn ExtensionProvider>>,
60}
61
62/// Type alias for the provider factory function.
63///
64/// Takes connection config and returns a fully built provider or an error
65/// if the HTTP client cannot be constructed (e.g. invalid TLS config).
66pub type ProviderFactory = fn(&ProviderConfig) -> Result<BuiltProvider, ObzError>;
67
68/// Result of a provider-specific health check.
69#[derive(Debug, Clone)]
70pub struct CheckResult {
71 /// Severity level of the check outcome.
72 pub severity: CheckSeverity,
73 /// Human-readable status message.
74 pub message: String,
75 /// What the check verified.
76 pub scope: CheckScope,
77 /// End-to-end latency for the live check when a request was attempted.
78 pub latency: Option<Duration>,
79}
80
81/// Severity level for a provider check result.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum CheckSeverity {
84 /// Check passed successfully.
85 Ok,
86 /// Check completed but with a warning (e.g. server error, degraded).
87 Warn,
88 /// Check failed.
89 Fail,
90}
91
92/// What a provider check verified.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum CheckScope {
95 /// Only network connectivity was verified.
96 Connectivity,
97 /// Both connectivity and authentication were verified.
98 ConnectivityAndAuth,
99 /// Credentials are configured but the check could not verify them.
100 ConfiguredNotVerifiable,
101}
102
103/// Type alias for an async provider check function.
104///
105/// The function takes the provider's resolved config and returns a `CheckResult`.
106pub type ProviderCheckFn =
107 fn(&ProviderConfig) -> Pin<Box<dyn Future<Output = CheckResult> + Send + '_>>;
108
109/// Which standard (core) commands a provider supports.
110///
111/// Used by the obz shell to generate accurate `--help` output showing
112/// provider support tags next to each core command.
113/// Set a field to `true` if the provider implements that operation.
114#[derive(Debug, Clone, Copy, Default)]
115pub struct SupportedCommands {
116 // --- metric ---
117 pub metric_query: bool,
118 pub metric_list: bool,
119 pub metric_info: bool,
120 pub metric_labels: bool,
121 pub metric_label_values: bool,
122 pub metric_series: bool,
123 // --- log ---
124 pub log_search: bool,
125 // --- trace ---
126 pub trace_search: bool,
127 pub trace_get: bool,
128}
129
130/// Static metadata and factory for a single provider.
131///
132/// Stored in the registry; does not hold any live connections or state.
133pub struct ProviderMeta {
134 /// Canonical name used as the primary `--provider` value.
135 pub name: &'static str,
136 /// Human-readable display name shown in help and diagnostics.
137 pub display_name: &'static str,
138 /// All accepted `--provider` aliases, including the canonical name.
139 /// Insertion order is preserved for deterministic `--help` output.
140 pub aliases: &'static [&'static str],
141 /// Which standard core commands this provider supports.
142 /// Used to generate provider support tags in `--help` output.
143 pub supported_commands: SupportedCommands,
144 /// Factory function — instantiates the provider from a `ProviderConfig`.
145 pub build: ProviderFactory,
146 /// Optional health check function registered by the provider.
147 ///
148 /// When `Some`, `provider check` calls this to verify connectivity
149 /// and (where possible) authentication. When `None`, the check is skipped.
150 pub check: Option<ProviderCheckFn>,
151 /// Provider-specific flags per standard command.
152 /// Key is the standard command identifier for the target core command.
153 pub command_flags: &'static [(StandardCommand, &'static [FlagDescriptor])],
154 /// Extension commands registered by this provider.
155 ///
156 /// Each entry is a `(signal, descriptor)` pair where `signal` is the
157 /// signal group the command belongs to (e.g. `"trace"`, `"metric"`).
158 /// Unlike `command_flags` (which uses [`StandardCommand`] identifiers), extension commands
159 /// use a bare signal name because the command name itself lives in
160 /// the descriptor.
161 pub extension_commands: &'static [(&'static str, CommandDescriptor)],
162}
163
164/// Runtime catalog of available providers.
165///
166/// Built once at startup by the obz shell. Providers are not instantiated
167/// until the user's `--provider` and `--endpoint` are known.
168#[derive(Default)]
169pub struct ProviderRegistry {
170 /// Alias → index into `metas`. `BTreeMap` for deterministic ordering.
171 by_alias: BTreeMap<String, usize>,
172 /// Insertion-ordered list of provider metadata.
173 metas: Vec<ProviderMeta>,
174}
175
176impl ProviderRegistry {
177 /// Create an empty registry.
178 pub fn new() -> Self {
179 Self::default()
180 }
181
182 /// Register a provider.
183 ///
184 /// # Panics
185 ///
186 /// Panics if any alias conflicts with an already-registered provider.
187 /// This is a programming error caught at startup, not a runtime error.
188 pub fn register(&mut self, meta: ProviderMeta) {
189 let idx = self.metas.len();
190 for &alias in meta.aliases {
191 assert!(
192 self.by_alias.insert(alias.to_string(), idx).is_none(),
193 "provider alias '{}' is already registered",
194 alias
195 );
196 }
197 self.metas.push(meta);
198 }
199
200 /// Look up provider metadata by name or alias.
201 ///
202 /// # Errors
203 ///
204 /// Returns [`ObzError::InvalidArgument`] if the name is not registered.
205 pub fn get(&self, name: &str) -> Result<&ProviderMeta, ObzError> {
206 self.by_alias
207 .get(name)
208 .map(|&i| &self.metas[i])
209 .ok_or_else(|| ObzError::InvalidArgument {
210 code: ErrorCode::InvalidFlag,
211 message: format!(
212 "unknown provider '{}' — run `obz provider list` to see available providers",
213 name
214 ),
215 suggestion: None,
216 })
217 }
218
219 /// All registered provider metadata in insertion order.
220 pub fn all(&self) -> &[ProviderMeta] {
221 &self.metas
222 }
223
224 /// All accepted alias strings in sorted order (for clap validation).
225 pub fn all_aliases(&self) -> Vec<&str> {
226 self.by_alias.keys().map(String::as_str).collect()
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 /// Build a minimal dummy `ProviderMeta` for testing.
235 ///
236 /// Uses `SupportedCommands::default()` and no extension commands — the
237 /// registry tests only care about name/alias resolution, not capabilities.
238 fn dummy_meta(name: &'static str, aliases: &'static [&'static str]) -> ProviderMeta {
239 fn dummy_build(_: &ProviderConfig) -> Result<BuiltProvider, ObzError> {
240 Ok(BuiltProvider {
241 name: "dummy",
242 metric_query_language: None,
243 log_query_language: None,
244 metric: None,
245 log: None,
246 trace: None,
247 extension: None,
248 })
249 }
250
251 ProviderMeta {
252 name,
253 display_name: name,
254 aliases,
255 supported_commands: SupportedCommands::default(),
256 build: dummy_build,
257 check: None,
258 command_flags: &[],
259 extension_commands: &[],
260 }
261 }
262
263 #[test]
264 fn test_get_resolves_provider_by_name_and_alias() {
265 let mut registry = ProviderRegistry::new();
266 registry.register(dummy_meta("victoriametrics", &["vm", "victoriametrics"]));
267
268 let meta = registry.get("vm").unwrap();
269 assert_eq!(meta.name, "victoriametrics");
270
271 let meta2 = registry.get("victoriametrics").unwrap();
272 assert_eq!(meta2.name, "victoriametrics");
273 }
274
275 #[test]
276 fn test_get_returns_error_for_unknown_provider() {
277 let registry = ProviderRegistry::new();
278 match registry.get("nonexistent") {
279 Err(ObzError::InvalidArgument { code, message, .. }) => {
280 assert_eq!(code, ErrorCode::InvalidFlag);
281 assert!(message.contains("nonexistent"));
282 }
283 Err(other) => panic!("expected InvalidArgument, got {other:?}"),
284 Ok(_) => panic!("expected error, got Ok"),
285 }
286 }
287
288 #[test]
289 fn test_all_preserves_insertion_order() {
290 let mut registry = ProviderRegistry::new();
291 registry.register(dummy_meta("alpha", &["alpha"]));
292 registry.register(dummy_meta("beta", &["beta"]));
293 registry.register(dummy_meta("gamma", &["gamma"]));
294
295 let names: Vec<&str> = registry.all().iter().map(|m| m.name).collect();
296 assert_eq!(names, vec!["alpha", "beta", "gamma"]);
297 }
298
299 #[test]
300 fn test_all_aliases_returns_sorted_list() {
301 let mut registry = ProviderRegistry::new();
302 registry.register(dummy_meta("victoriametrics", &["vm", "victoriametrics"]));
303 registry.register(dummy_meta("victorialogs", &["vl", "victorialogs"]));
304
305 let aliases = registry.all_aliases();
306 // BTreeMap is sorted, so aliases should be in alphabetical order.
307 assert_eq!(aliases, vec!["victorialogs", "victoriametrics", "vl", "vm"]);
308 }
309
310 #[test]
311 #[should_panic(expected = "provider alias 'vm' is already registered")]
312 fn test_register_panics_on_duplicate_alias() {
313 let mut registry = ProviderRegistry::new();
314 registry.register(dummy_meta("provider1", &["vm"]));
315 registry.register(dummy_meta("provider2", &["vm"])); // should panic
316 }
317
318 #[test]
319 fn test_new_registry_is_empty() {
320 let registry = ProviderRegistry::new();
321 assert!(registry.all().is_empty());
322 assert!(registry.all_aliases().is_empty());
323 }
324
325 #[test]
326 fn test_registry_with_extension_commands() {
327 static EXT_CMDS: &[(&str, CommandDescriptor)] = &[
328 (
329 "trace",
330 CommandDescriptor {
331 name: "services",
332 description: "List services",
333 flags: &[],
334 },
335 ),
336 (
337 "metric",
338 CommandDescriptor {
339 name: "top-queries",
340 description: "Top queries",
341 flags: &[],
342 },
343 ),
344 ];
345
346 fn build_with_ext(_: &ProviderConfig) -> Result<BuiltProvider, ObzError> {
347 Ok(BuiltProvider {
348 name: "test",
349 metric_query_language: None,
350 log_query_language: None,
351 metric: None,
352 log: None,
353 trace: None,
354 extension: None,
355 })
356 }
357
358 let meta = ProviderMeta {
359 name: "testprov",
360 display_name: "TestProvider",
361 aliases: &["tp"],
362 supported_commands: SupportedCommands::default(),
363 build: build_with_ext,
364 check: None,
365 command_flags: &[],
366 extension_commands: EXT_CMDS,
367 };
368
369 let mut registry = ProviderRegistry::new();
370 registry.register(meta);
371
372 let retrieved = registry.get("tp").unwrap();
373 assert_eq!(retrieved.extension_commands.len(), 2);
374 assert_eq!(retrieved.extension_commands[0].0, "trace");
375 assert_eq!(retrieved.extension_commands[0].1.name, "services");
376 assert_eq!(retrieved.extension_commands[1].0, "metric");
377 assert_eq!(retrieved.extension_commands[1].1.name, "top-queries");
378 }
379}