1pub mod params;
13pub mod results;
14pub mod traits;
15
16use std::collections::BTreeMap;
17use std::time::Duration;
18
19use crate::model::error::{ErrorCode, ObzError};
20
21pub use params::{
23 ExtensionParams, LabelValuesParams, LogSearchParams, MetricInfoParams, MetricMetadataParams,
24 MetricQueryParams, TraceGetParams, TraceSearchParams,
25};
26pub use results::{
27 ExtensionResult, LogSearchResult, MetricQueryResult, MetricResultType, ProviderResult,
28 TraceSearchResult,
29};
30pub use traits::{ExtensionProvider, LogProvider, MetricProvider, TraceProvider};
31
32#[derive(Clone)]
75pub struct ProviderConfig {
76 values: BTreeMap<String, String>,
78 auth: BTreeMap<String, String>,
80 headers: BTreeMap<String, String>,
82 verbose: bool,
84 timeout: Option<Duration>,
86}
87
88impl ProviderConfig {
89 pub fn new() -> Self {
91 Self {
92 values: BTreeMap::new(),
93 auth: BTreeMap::new(),
94 headers: BTreeMap::new(),
95 verbose: false,
96 timeout: None,
97 }
98 }
99
100 pub fn set(&mut self, key: &str, value: impl Into<String>) -> &mut Self {
106 self.values.insert(key.to_string(), value.into());
107 self
108 }
109
110 pub fn get(&self, key: &str) -> Option<&str> {
114 self.values.get(key).map(String::as_str)
115 }
116
117 pub fn get_owned(&self, key: &str) -> Option<String> {
122 self.values.get(key).cloned()
123 }
124
125 pub fn require(&self, key: &str) -> Result<&str, ObzError> {
133 self.get(key).ok_or_else(|| ObzError::InvalidArgument {
134 code: ErrorCode::MissingRequired,
135 message: format!("--{key} is required"),
136 suggestion: None,
137 })
138 }
139
140 pub fn require_config(&self, key: &str) -> Result<&str, ObzError> {
152 self.get(key).ok_or_else(|| ObzError::InvalidArgument {
153 code: ErrorCode::MissingRequired,
154 message: format!(
155 "--{key} is required. Set it in config.yaml under your provider's config block"
156 ),
157 suggestion: None,
158 })
159 }
160
161 pub fn set_auth(&mut self, key: &str, value: impl Into<String>) -> &mut Self {
168 self.auth.insert(key.to_string(), value.into());
169 self
170 }
171
172 pub fn auth_get(&self, key: &str) -> Option<&str> {
178 self.auth
179 .get(key)
180 .map(String::as_str)
181 .filter(|v| !v.is_empty())
182 }
183
184 pub fn auth_get_owned(&self, key: &str) -> Option<String> {
188 self.auth_get(key).map(str::to_string)
189 }
190
191 pub fn auth_require(&self, key: &str) -> Result<&str, ObzError> {
199 self.auth_get(key)
200 .ok_or_else(|| auth_missing_error(key, ""))
201 }
202
203 pub fn bearer_token(&self) -> Option<String> {
207 self.auth_get_owned("token")
208 }
209
210 pub fn basic_auth(&self) -> Option<(String, String)> {
217 match (self.auth_get("username"), self.auth_get("password")) {
218 (Some(u), Some(p)) => Some((u.to_string(), p.to_string())),
219 _ => None,
220 }
221 }
222
223 pub fn set_header(&mut self, key: &str, value: impl Into<String>) -> &mut Self {
227 self.headers.insert(key.to_ascii_lowercase(), value.into());
228 self
229 }
230
231 pub fn custom_headers(&self) -> &BTreeMap<String, String> {
233 &self.headers
234 }
235
236 pub fn set_verbose(&mut self, verbose: bool) {
240 self.verbose = verbose;
241 }
242
243 pub fn verbose(&self) -> bool {
245 self.verbose
246 }
247
248 pub fn set_timeout(&mut self, timeout: Duration) {
250 self.timeout = Some(timeout);
251 }
252
253 pub fn timeout(&self) -> Option<Duration> {
255 self.timeout
256 }
257}
258
259impl Default for ProviderConfig {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264
265pub fn is_sensitive_key(key: &str) -> bool {
271 let k = key.to_ascii_lowercase();
272 k.contains("token")
273 || k.contains("password")
274 || k.contains("secret")
275 || k == "key"
276 || k.ends_with("-key")
277}
278
279pub fn auth_missing_error(key: &str, provider_type: &str) -> ObzError {
284 let hint = if provider_type.is_empty() {
285 format!("Set it in config.yaml: providers.<name>.auth.{key}")
286 } else {
287 format!(
288 "Set it in config.yaml: providers.<name>.auth.{key} (provider type: {provider_type})"
289 )
290 };
291 ObzError::InvalidArgument {
292 code: ErrorCode::MissingRequired,
293 message: format!("{key} is required. {hint}"),
294 suggestion: None,
295 }
296}
297
298impl std::fmt::Debug for ProviderConfig {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 let mut s = f.debug_struct("ProviderConfig");
301
302 let redacted_values: BTreeMap<&str, &str> = self
304 .values
305 .iter()
306 .map(|(k, v)| {
307 if is_sensitive_key(k) {
308 (k.as_str(), "[REDACTED]")
309 } else {
310 (k.as_str(), v.as_str())
311 }
312 })
313 .collect();
314 s.field("values", &redacted_values);
315
316 if self.auth.is_empty() {
318 s.field("auth", &"{}");
319 } else {
320 s.field("auth", &"[REDACTED]");
321 }
322
323 let redacted_headers: BTreeMap<&str, &str> = self
325 .headers
326 .iter()
327 .map(|(k, v)| {
328 if is_sensitive_key(k) {
329 (k.as_str(), "[REDACTED]")
330 } else {
331 (k.as_str(), v.as_str())
332 }
333 })
334 .collect();
335 s.field("headers", &redacted_headers);
336
337 s.field("verbose", &self.verbose);
338 s.field("timeout", &self.timeout);
339 s.finish()
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum Signal {
346 Metric,
348 Log,
350 Trace,
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
361 fn test_get_returns_value_when_key_exists() {
362 let mut config = ProviderConfig::new();
363 config.set("endpoint", "http://localhost:8428");
364 assert_eq!(config.get("endpoint"), Some("http://localhost:8428"));
365 }
366
367 #[test]
368 fn test_get_returns_none_when_key_missing() {
369 let config = ProviderConfig::new();
370 assert_eq!(config.get("endpoint"), None);
371 }
372
373 #[test]
374 fn test_get_owned_returns_cloned_value() {
375 let mut config = ProviderConfig::new();
376 config.set("project", "abc");
377 assert_eq!(config.get_owned("project"), Some("abc".to_string()));
378 assert_eq!(config.get_owned("missing"), None);
379 }
380
381 #[test]
382 fn test_require_returns_value_when_key_exists() {
383 let mut config = ProviderConfig::new();
384 config.set("endpoint", "http://localhost:8428");
385 assert_eq!(config.require("endpoint").unwrap(), "http://localhost:8428");
386 }
387
388 #[test]
389 fn test_require_returns_error_when_key_missing() {
390 let config = ProviderConfig::new();
391 let err = config.require("endpoint").unwrap_err();
392 match err {
393 ObzError::InvalidArgument { code, message, .. } => {
394 assert_eq!(code, ErrorCode::MissingRequired);
395 assert!(message.contains("--endpoint"));
396 }
397 _ => panic!("expected InvalidArgument, got {err:?}"),
398 }
399 }
400
401 #[test]
402 fn test_require_config_returns_value_when_key_exists() {
403 let mut config = ProviderConfig::new();
404 config.set("endpoint", "http://localhost:8428");
405 assert_eq!(
406 config.require_config("endpoint").unwrap(),
407 "http://localhost:8428"
408 );
409 }
410
411 #[test]
412 fn test_require_config_error_includes_config_hint() {
413 let config = ProviderConfig::new();
414 let err = config.require_config("endpoint").unwrap_err();
415 match err {
416 ObzError::InvalidArgument { code, message, .. } => {
417 assert_eq!(code, ErrorCode::MissingRequired);
418 assert!(message.contains("--endpoint"));
419 assert!(
420 message.contains("config.yaml"),
421 "require_config error should mention config.yaml, got: {message}"
422 );
423 }
424 _ => panic!("expected InvalidArgument, got {err:?}"),
425 }
426 }
427
428 #[test]
429 fn test_require_error_does_not_mention_config() {
430 let config = ProviderConfig::new();
431 let err = config.require("query").unwrap_err();
432 match err {
433 ObzError::InvalidArgument { message, .. } => {
434 assert!(
435 !message.contains("config.yaml"),
436 "require() should not mention config.yaml, got: {message}"
437 );
438 }
439 _ => panic!("expected InvalidArgument, got {err:?}"),
440 }
441 }
442
443 #[test]
444 fn test_set_overwrites_existing_value() {
445 let mut config = ProviderConfig::new();
446 config.set("endpoint", "http://old");
447 config.set("endpoint", "http://new");
448 assert_eq!(config.get("endpoint"), Some("http://new"));
449 }
450
451 #[test]
452 fn test_set_supports_chaining() {
453 let mut config = ProviderConfig::new();
454 config
455 .set("endpoint", "http://localhost")
456 .set("project", "abc");
457 assert_eq!(config.get("endpoint"), Some("http://localhost"));
458 assert_eq!(config.get("project"), Some("abc"));
459 }
460
461 #[test]
464 fn test_auth_get_returns_value() {
465 let mut config = ProviderConfig::new();
466 config.set_auth("token", "my-token");
467 assert_eq!(config.auth_get("token"), Some("my-token"));
468 }
469
470 #[test]
471 fn test_auth_get_returns_none_when_missing() {
472 let config = ProviderConfig::new();
473 assert_eq!(config.auth_get("token"), None);
474 }
475
476 #[test]
477 fn test_auth_get_returns_none_for_empty_string() {
478 let mut config = ProviderConfig::new();
479 config.set_auth("token", "");
480 assert_eq!(config.auth_get("token"), None);
481 }
482
483 #[test]
484 fn test_auth_get_owned_returns_cloned_value() {
485 let mut config = ProviderConfig::new();
486 config.set_auth("api-key", "abc123");
487 assert_eq!(config.auth_get_owned("api-key"), Some("abc123".to_string()));
488 assert_eq!(config.auth_get_owned("missing"), None);
489 }
490
491 #[test]
492 fn test_auth_require_returns_value() {
493 let mut config = ProviderConfig::new();
494 config.set_auth("token", "secret");
495 assert_eq!(config.auth_require("token").unwrap(), "secret");
496 }
497
498 #[test]
499 fn test_auth_require_returns_error_when_missing() {
500 let config = ProviderConfig::new();
501 let err = config.auth_require("token").unwrap_err();
502 match err {
503 ObzError::InvalidArgument { code, message, .. } => {
504 assert_eq!(code, ErrorCode::MissingRequired);
505 assert!(message.contains("token"));
506 assert!(message.contains("config.yaml"));
507 }
508 _ => panic!("expected InvalidArgument, got {err:?}"),
509 }
510 }
511
512 #[test]
513 fn test_auth_require_returns_error_for_empty_string() {
514 let mut config = ProviderConfig::new();
515 config.set_auth("token", "");
516 assert!(config.auth_require("token").is_err());
517 }
518
519 #[test]
520 fn test_bearer_token_returns_token_from_auth() {
521 let mut config = ProviderConfig::new();
522 config.set_auth("token", "bearer-secret");
523 assert_eq!(config.bearer_token(), Some("bearer-secret".to_string()));
524 }
525
526 #[test]
527 fn test_bearer_token_returns_none_when_missing() {
528 let config = ProviderConfig::new();
529 assert_eq!(config.bearer_token(), None);
530 }
531
532 #[test]
533 fn test_basic_auth_returns_credentials_when_both_present() {
534 let mut config = ProviderConfig::new();
535 config.set_auth("username", "admin");
536 config.set_auth("password", "secret");
537 assert_eq!(
538 config.basic_auth(),
539 Some(("admin".to_string(), "secret".to_string()))
540 );
541 }
542
543 #[test]
544 fn test_basic_auth_returns_none_when_username_missing() {
545 let mut config = ProviderConfig::new();
546 config.set_auth("password", "secret");
547 assert_eq!(config.basic_auth(), None);
548 }
549
550 #[test]
551 fn test_basic_auth_returns_none_when_password_missing() {
552 let mut config = ProviderConfig::new();
553 config.set_auth("username", "admin");
554 assert_eq!(config.basic_auth(), None);
555 }
556
557 #[test]
558 fn test_basic_auth_returns_none_when_both_missing() {
559 let config = ProviderConfig::new();
560 assert_eq!(config.basic_auth(), None);
561 }
562
563 #[test]
564 fn test_basic_auth_returns_none_when_empty_strings() {
565 let mut config = ProviderConfig::new();
566 config.set_auth("username", "");
567 config.set_auth("password", "secret");
568 assert_eq!(config.basic_auth(), None);
569 }
570
571 #[test]
574 fn test_set_header_lowercases_key() {
575 let mut config = ProviderConfig::new();
576 config.set_header("X-Scope-OrgID", "my-tenant");
577 assert_eq!(
578 config.custom_headers().get("x-scope-orgid"),
579 Some(&"my-tenant".to_string())
580 );
581 }
582
583 #[test]
584 fn test_custom_headers_returns_all_headers() {
585 let mut config = ProviderConfig::new();
586 config.set_header("x-custom", "value1");
587 config.set_header("x-other", "value2");
588 assert_eq!(config.custom_headers().len(), 2);
589 }
590
591 #[test]
594 fn test_verbose_returns_true_when_set() {
595 let mut config = ProviderConfig::new();
596 config.set_verbose(true);
597 assert!(config.verbose());
598 }
599
600 #[test]
601 fn test_verbose_returns_false_by_default() {
602 let config = ProviderConfig::new();
603 assert!(!config.verbose());
604 }
605
606 #[test]
607 fn test_timeout_returns_none_by_default() {
608 let config = ProviderConfig::new();
609 assert_eq!(config.timeout(), None);
610 }
611
612 #[test]
613 fn test_timeout_returns_value_when_set() {
614 let mut config = ProviderConfig::new();
615 config.set_timeout(Duration::from_secs(30));
616 assert_eq!(config.timeout(), Some(Duration::from_secs(30)));
617 }
618
619 #[test]
622 fn test_debug_redacts_auth_entirely() {
623 let mut config = ProviderConfig::new();
624 config.set("endpoint", "http://localhost");
625 config.set_auth("token", "super-secret-token");
626 config.set_auth("password", "hunter2");
627
628 let debug = format!("{config:?}");
629
630 assert!(!debug.contains("super-secret-token"));
631 assert!(!debug.contains("hunter2"));
632 assert!(debug.contains("[REDACTED]"));
633 assert!(debug.contains("http://localhost"));
634 }
635
636 #[test]
637 fn test_debug_redacts_sensitive_value_keys() {
638 let mut config = ProviderConfig::new();
639 config.set("endpoint", "http://localhost");
640 config.set("access-key-secret", "should-be-redacted");
641
642 let debug = format!("{config:?}");
643 assert!(!debug.contains("should-be-redacted"));
644 assert!(debug.contains("[REDACTED]"));
645 }
646
647 #[test]
648 fn test_debug_preserves_non_sensitive_values() {
649 let mut config = ProviderConfig::new();
650 config.set("endpoint", "http://localhost");
651 config.set("project", "my-project");
652 config.set("region", "cn-hangzhou");
653
654 let debug = format!("{config:?}");
655 assert!(debug.contains("http://localhost"));
656 assert!(debug.contains("my-project"));
657 assert!(debug.contains("cn-hangzhou"));
658 }
659
660 #[test]
661 fn test_debug_redacts_sensitive_header_values() {
662 let mut config = ProviderConfig::new();
663 config.set_header("x-auth-token", "secret-header-val");
664 config.set_header("x-custom", "visible-val");
665
666 let debug = format!("{config:?}");
667 assert!(!debug.contains("secret-header-val"));
668 assert!(debug.contains("visible-val"));
669 }
670
671 #[test]
672 fn test_sensitive_key_detection_avoids_false_positives() {
673 let mut config = ProviderConfig::new();
674 config.set("monkey", "banana");
675 config.set("hotkey-binding", "ctrl+c");
676 config.set("keyboard", "us-layout");
677
678 let debug = format!("{config:?}");
679 assert!(debug.contains("banana"));
680 assert!(debug.contains("ctrl+c"));
681 assert!(debug.contains("us-layout"));
682 }
683
684 #[test]
685 fn test_default() {
686 let config = ProviderConfig::default();
687 assert_eq!(config.get("anything"), None);
688 assert!(!config.verbose());
689 assert_eq!(config.timeout(), None);
690 }
691
692 #[test]
695 fn test_auth_missing_error_without_provider_type() {
696 let err = auth_missing_error("token", "");
697 match err {
698 ObzError::InvalidArgument { code, message, .. } => {
699 assert_eq!(code, ErrorCode::MissingRequired);
700 assert!(message.contains("token"));
701 assert!(message.contains("config.yaml"));
702 }
703 _ => panic!("expected InvalidArgument"),
704 }
705 }
706
707 #[test]
708 fn test_auth_missing_error_with_provider_type() {
709 let err = auth_missing_error("api-key", "dd");
710 match err {
711 ObzError::InvalidArgument { message, .. } => {
712 assert!(message.contains("api-key"));
713 assert!(message.contains("dd"));
714 }
715 _ => panic!("expected InvalidArgument"),
716 }
717 }
718}