Skip to main content

otel_rs/
config.rs

1//! Configuration for the observability stack.
2//!
3//! Provides composable sub-configurations with builder patterns for
4//! fine-grained control over exporter, tracing, and metrics settings.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use otel_rs::config::*;
10//!
11//! let _guard = OtelConfig::builder()
12//!     .service_name("my-service")
13//!     .service_version("1.0.0")
14//!     .exporter(ExporterConfig::builder()
15//!         .endpoint("https://otel.example.com:4317")
16//!         .bearer_token("your-api-key")
17//!         .build())
18//!     .tracing(TracingConfig::builder()
19//!         .sampling(SamplingStrategy::TraceIdRatio(0.1))
20//!         .build())
21//!     .allow_crate("my_service")
22//!     .log_level(LogLevel::Info)
23//!     .output_format(OutputFormat::Json)
24//!     .init()
25//!     .await?;
26//! ```
27
28use std::time::Duration;
29
30// ── Enums ──────────────────────────────────────────────────────────
31
32/// OTLP protocol for exporting telemetry data.
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub enum OtlpProtocol {
35    /// gRPC protocol (default, more efficient for high-volume telemetry).
36    #[default]
37    Grpc,
38    /// HTTP/protobuf protocol (better firewall compatibility).
39    Http,
40}
41
42impl OtlpProtocol {
43    /// Returns the string representation.
44    pub const fn as_str(&self) -> &'static str {
45        match self {
46            Self::Grpc => "grpc",
47            Self::Http => "http",
48        }
49    }
50}
51
52impl std::str::FromStr for OtlpProtocol {
53    type Err = String;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        match s.to_lowercase().as_str() {
57            "grpc" => Ok(Self::Grpc),
58            "http" | "http/protobuf" => Ok(Self::Http),
59            other => Err(format!("unknown OTLP protocol: {other}")),
60        }
61    }
62}
63
64impl std::fmt::Display for OtlpProtocol {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.write_str(self.as_str())
67    }
68}
69
70/// Sampling strategy for traces.
71#[derive(Debug, Clone, Copy, Default)]
72pub enum SamplingStrategy {
73    /// Always sample all traces (default).
74    #[default]
75    AlwaysOn,
76    /// Never sample.
77    AlwaysOff,
78    /// Sample based on trace ID ratio (0.0 to 1.0).
79    TraceIdRatio(f64),
80    /// Sample based on parent span decision.
81    ParentBased,
82}
83
84/// Log level for filtering output.
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
86pub enum LogLevel {
87    Trace,
88    Debug,
89    #[default]
90    Info,
91    Warn,
92    Error,
93}
94
95impl LogLevel {
96    /// Returns the string representation.
97    pub const fn as_str(&self) -> &'static str {
98        match self {
99            Self::Trace => "trace",
100            Self::Debug => "debug",
101            Self::Info => "info",
102            Self::Warn => "warn",
103            Self::Error => "error",
104        }
105    }
106}
107
108impl std::str::FromStr for LogLevel {
109    type Err = String;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        match s.to_lowercase().as_str() {
113            "trace" => Ok(Self::Trace),
114            "debug" => Ok(Self::Debug),
115            "info" => Ok(Self::Info),
116            "warn" | "warning" => Ok(Self::Warn),
117            "error" => Ok(Self::Error),
118            other => Err(format!("unknown log level: {other}")),
119        }
120    }
121}
122
123impl std::fmt::Display for LogLevel {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.write_str(self.as_str())
126    }
127}
128
129/// Output format for console/stdout logs.
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub enum OutputFormat {
132    /// Human-readable pretty format (default).
133    #[default]
134    Pretty,
135    /// Compact single-line format.
136    Compact,
137    /// JSON format for structured logging.
138    Json,
139}
140
141/// OTLP authentication credentials.
142#[derive(Debug, Clone, Default)]
143pub enum OtlpCredentials {
144    /// No authentication (default).
145    #[default]
146    None,
147    /// Bearer token (`Authorization: Bearer <token>`).
148    Bearer(String),
149    /// Basic auth (`Authorization: Basic base64(user:pass)`).
150    Basic {
151        /// Username.
152        username: String,
153        /// Password.
154        password: String,
155    },
156    /// Custom headers.
157    Headers(std::collections::HashMap<String, String>),
158}
159
160// ── Sub-configs ────────────────────────────────────────────────────
161
162/// OTLP exporter connection configuration.
163#[derive(Debug, Clone)]
164pub struct ExporterConfig {
165    pub(crate) endpoint: String,
166    pub(crate) protocol: OtlpProtocol,
167    pub(crate) timeout: Duration,
168    pub(crate) credentials: OtlpCredentials,
169}
170
171impl Default for ExporterConfig {
172    fn default() -> Self {
173        Self {
174            endpoint: "http://localhost:4317".to_string(),
175            protocol: OtlpProtocol::default(),
176            timeout: Duration::from_secs(10),
177            credentials: OtlpCredentials::None,
178        }
179    }
180}
181
182impl ExporterConfig {
183    /// Create a builder.
184    #[must_use]
185    pub fn builder() -> ExporterConfigBuilder {
186        ExporterConfigBuilder::new()
187    }
188}
189
190/// Builder for [`ExporterConfig`].
191#[derive(Debug, Clone)]
192pub struct ExporterConfigBuilder {
193    config: ExporterConfig,
194}
195
196impl Default for ExporterConfigBuilder {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202impl ExporterConfigBuilder {
203    /// Create a new builder with defaults.
204    #[must_use]
205    pub fn new() -> Self {
206        Self {
207            config: ExporterConfig::default(),
208        }
209    }
210
211    /// Set the OTLP collector endpoint.
212    #[must_use]
213    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
214        self.config.endpoint = endpoint.into();
215        self
216    }
217
218    /// Set the OTLP protocol.
219    #[must_use]
220    pub const fn protocol(mut self, protocol: OtlpProtocol) -> Self {
221        self.config.protocol = protocol;
222        self
223    }
224
225    /// Set the export timeout.
226    #[must_use]
227    pub const fn timeout(mut self, timeout: Duration) -> Self {
228        self.config.timeout = timeout;
229        self
230    }
231
232    /// Set bearer token authentication.
233    #[must_use]
234    pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
235        self.config.credentials = OtlpCredentials::Bearer(token.into());
236        self
237    }
238
239    /// Set basic authentication.
240    #[must_use]
241    pub fn basic_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
242        self.config.credentials = OtlpCredentials::Basic {
243            username: username.into(),
244            password: password.into(),
245        };
246        self
247    }
248
249    /// Add a custom header. Can be called multiple times.
250    #[must_use]
251    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
252        match &mut self.config.credentials {
253            OtlpCredentials::Headers(h) => {
254                h.insert(key.into(), value.into());
255            }
256            _ => {
257                let mut h = std::collections::HashMap::new();
258                h.insert(key.into(), value.into());
259                self.config.credentials = OtlpCredentials::Headers(h);
260            }
261        }
262        self
263    }
264
265    /// Set custom headers, replacing any existing credentials.
266    #[must_use]
267    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
268        self.config.credentials = OtlpCredentials::Headers(headers);
269        self
270    }
271
272    /// Build the exporter configuration.
273    #[must_use]
274    pub fn build(self) -> ExporterConfig {
275        self.config
276    }
277}
278
279/// Tracing-specific configuration.
280#[derive(Debug, Clone)]
281pub struct TracingConfig {
282    pub(crate) sampling: SamplingStrategy,
283    pub(crate) record_exceptions: bool,
284    pub(crate) exception_field_limit: usize,
285    pub(crate) batch_schedule_delay: Duration,
286    pub(crate) max_export_batch_size: usize,
287    pub(crate) max_queue_size: usize,
288}
289
290impl Default for TracingConfig {
291    fn default() -> Self {
292        Self {
293            sampling: SamplingStrategy::default(),
294            record_exceptions: true,
295            exception_field_limit: 1024,
296            batch_schedule_delay: Duration::from_secs(5),
297            max_export_batch_size: 512,
298            max_queue_size: 2048,
299        }
300    }
301}
302
303impl TracingConfig {
304    /// Create a builder.
305    #[must_use]
306    pub fn builder() -> TracingConfigBuilder {
307        TracingConfigBuilder::new()
308    }
309}
310
311/// Builder for [`TracingConfig`].
312#[derive(Debug, Clone)]
313pub struct TracingConfigBuilder {
314    config: TracingConfig,
315}
316
317impl Default for TracingConfigBuilder {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323impl TracingConfigBuilder {
324    /// Create a new builder with defaults.
325    #[must_use]
326    pub fn new() -> Self {
327        Self {
328            config: TracingConfig::default(),
329        }
330    }
331
332    /// Set the sampling strategy.
333    #[must_use]
334    pub const fn sampling(mut self, strategy: SamplingStrategy) -> Self {
335        self.config.sampling = strategy;
336        self
337    }
338
339    /// Enable or disable automatic exception recording.
340    #[must_use]
341    pub const fn record_exceptions(mut self, enabled: bool) -> Self {
342        self.config.record_exceptions = enabled;
343        self
344    }
345
346    /// Set the maximum length for exception fields.
347    #[must_use]
348    pub const fn exception_field_limit(mut self, limit: usize) -> Self {
349        self.config.exception_field_limit = limit;
350        self
351    }
352
353    /// Set the batch schedule delay for trace export.
354    #[must_use]
355    pub const fn batch_schedule_delay(mut self, delay: Duration) -> Self {
356        self.config.batch_schedule_delay = delay;
357        self
358    }
359
360    /// Set the maximum export batch size.
361    #[must_use]
362    pub const fn max_export_batch_size(mut self, size: usize) -> Self {
363        self.config.max_export_batch_size = size;
364        self
365    }
366
367    /// Set the maximum queue size.
368    #[must_use]
369    pub const fn max_queue_size(mut self, size: usize) -> Self {
370        self.config.max_queue_size = size;
371        self
372    }
373
374    /// Build the tracing configuration.
375    #[must_use]
376    pub fn build(self) -> TracingConfig {
377        self.config
378    }
379}
380
381/// Metrics-specific configuration.
382#[derive(Debug, Clone)]
383pub struct MetricsConfig {
384    pub(crate) export_interval: Duration,
385}
386
387impl Default for MetricsConfig {
388    fn default() -> Self {
389        Self {
390            export_interval: Duration::from_secs(60),
391        }
392    }
393}
394
395impl MetricsConfig {
396    /// Create a builder.
397    #[must_use]
398    pub fn builder() -> MetricsConfigBuilder {
399        MetricsConfigBuilder::new()
400    }
401}
402
403/// Builder for [`MetricsConfig`].
404#[derive(Debug, Clone)]
405pub struct MetricsConfigBuilder {
406    config: MetricsConfig,
407}
408
409impl Default for MetricsConfigBuilder {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415impl MetricsConfigBuilder {
416    /// Create a new builder with defaults.
417    #[must_use]
418    pub fn new() -> Self {
419        Self {
420            config: MetricsConfig::default(),
421        }
422    }
423
424    /// Set the metrics export interval.
425    #[must_use]
426    pub const fn export_interval(mut self, interval: Duration) -> Self {
427        self.config.export_interval = interval;
428        self
429    }
430
431    /// Build the metrics configuration.
432    #[must_use]
433    pub fn build(self) -> MetricsConfig {
434        self.config
435    }
436}
437
438// ── Main config ────────────────────────────────────────────────────
439
440/// Resolved configuration for the observability stack.
441///
442/// Construct via [`OtelConfigBuilder`].
443#[derive(Debug, Clone)]
444pub struct OtelConfig {
445    // Service identity
446    pub(crate) service_name: String,
447    pub(crate) service_version: String,
448    pub(crate) environment: String,
449    pub(crate) service_namespace: Option<String>,
450    pub(crate) service_instance_id: Option<String>,
451
452    // Exporter
453    pub(crate) exporter: ExporterConfig,
454
455    // Feature toggles & sub-configs
456    /// `Some(config)` = tracing enabled, `None` = disabled.
457    pub(crate) tracing: Option<TracingConfig>,
458    /// Whether OTLP log export is enabled.
459    pub(crate) logging: bool,
460    /// `Some(config)` = metrics enabled, `None` = disabled.
461    pub(crate) metrics: Option<MetricsConfig>,
462
463    // Console
464    pub(crate) enable_console_output: bool,
465    pub(crate) log_level: LogLevel,
466    pub(crate) output_format: OutputFormat,
467
468    // Filtering
469    pub(crate) allowed_crates: Vec<String>,
470    pub(crate) custom_filters: Vec<String>,
471
472    // Custom resource attributes
473    pub(crate) custom_attributes: Vec<(String, String)>,
474}
475
476impl Default for OtelConfig {
477    fn default() -> Self {
478        Self {
479            service_name: "unknown-service".to_string(),
480            service_version: "0.0.0".to_string(),
481            environment: "development".to_string(),
482            service_namespace: None,
483            service_instance_id: None,
484            exporter: ExporterConfig::default(),
485            tracing: Some(TracingConfig::default()),
486            logging: true,
487            metrics: Some(MetricsConfig::default()),
488            enable_console_output: true,
489            log_level: LogLevel::Info,
490            output_format: OutputFormat::Pretty,
491            allowed_crates: Vec::new(),
492            custom_filters: Vec::new(),
493            custom_attributes: Vec::new(),
494        }
495    }
496}
497
498impl OtelConfig {
499    /// Create a new builder.
500    #[must_use]
501    pub fn builder() -> OtelConfigBuilder {
502        OtelConfigBuilder::new()
503    }
504}
505
506/// Builder for [`OtelConfig`].
507///
508/// Resolution order: **builder values > env vars > hardcoded defaults**.
509///
510/// Standard OTel environment variables (`OTEL_SERVICE_NAME`,
511/// `OTEL_EXPORTER_OTLP_ENDPOINT`, etc.) are read automatically
512/// and applied as defaults that builder methods can override.
513///
514/// # Example
515///
516/// ```rust,ignore
517/// let _guard = OtelConfig::builder()
518///     .service_name("my-service")
519///     .service_version("1.0.0")
520///     .allow_crate("my_service")
521///     .init()
522///     .await?;
523/// ```
524#[derive(Debug, Clone)]
525pub struct OtelConfigBuilder {
526    service_name: Option<String>,
527    service_version: Option<String>,
528    environment: Option<String>,
529    service_namespace: Option<String>,
530    service_instance_id: Option<String>,
531    exporter: Option<ExporterConfig>,
532    /// `None` = not set (use defaults), `Some(None)` = disabled,
533    /// `Some(Some(c))` = custom config.
534    tracing: Option<Option<TracingConfig>>,
535    logging: Option<bool>,
536    metrics: Option<Option<MetricsConfig>>,
537    enable_console_output: Option<bool>,
538    log_level: Option<LogLevel>,
539    output_format: Option<OutputFormat>,
540    allowed_crates: Vec<String>,
541    custom_filters: Vec<String>,
542    custom_attributes: Vec<(String, String)>,
543}
544
545impl Default for OtelConfigBuilder {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551impl OtelConfigBuilder {
552    /// Create a new builder. All fields start unset and will fall back
553    /// to environment variables, then hardcoded defaults.
554    #[must_use]
555    pub fn new() -> Self {
556        Self {
557            service_name: None,
558            service_version: None,
559            environment: None,
560            service_namespace: None,
561            service_instance_id: None,
562            exporter: None,
563            tracing: None,
564            logging: None,
565            metrics: None,
566            enable_console_output: None,
567            log_level: None,
568            output_format: None,
569            allowed_crates: Vec::new(),
570            custom_filters: Vec::new(),
571            custom_attributes: Vec::new(),
572        }
573    }
574
575    // ── Service identity ───────────────────────────────────────────
576
577    /// Set the service name.
578    #[must_use]
579    pub fn service_name(mut self, name: impl Into<String>) -> Self {
580        self.service_name = Some(name.into());
581        self
582    }
583
584    /// Set the service version.
585    #[must_use]
586    pub fn service_version(mut self, version: impl Into<String>) -> Self {
587        self.service_version = Some(version.into());
588        self
589    }
590
591    /// Set the deployment environment (e.g., `"production"`, `"staging"`).
592    #[must_use]
593    pub fn environment(mut self, env: impl Into<String>) -> Self {
594        self.environment = Some(env.into());
595        self
596    }
597
598    /// Set the service namespace for grouping related services.
599    #[must_use]
600    pub fn namespace(mut self, ns: impl Into<String>) -> Self {
601        self.service_namespace = Some(ns.into());
602        self
603    }
604
605    /// Set a unique instance identifier for this service instance.
606    #[must_use]
607    pub fn instance_id(mut self, id: impl Into<String>) -> Self {
608        self.service_instance_id = Some(id.into());
609        self
610    }
611
612    // ── Sub-configs ────────────────────────────────────────────────
613
614    /// Set the exporter configuration. Overrides any env var defaults
615    /// for endpoint, protocol, timeout, and credentials.
616    #[must_use]
617    pub fn exporter(mut self, config: ExporterConfig) -> Self {
618        self.exporter = Some(config);
619        self
620    }
621
622    /// Set the tracing configuration. Implicitly enables tracing.
623    #[must_use]
624    pub fn tracing(mut self, config: TracingConfig) -> Self {
625        self.tracing = Some(Some(config));
626        self
627    }
628
629    /// Disable distributed tracing.
630    #[must_use]
631    pub fn disable_tracing(mut self) -> Self {
632        self.tracing = Some(None);
633        self
634    }
635
636    /// Enable or disable OTLP log export.
637    #[must_use]
638    pub fn logging(mut self, enabled: bool) -> Self {
639        self.logging = Some(enabled);
640        self
641    }
642
643    /// Set the metrics configuration. Implicitly enables metrics.
644    #[must_use]
645    pub fn metrics(mut self, config: MetricsConfig) -> Self {
646        self.metrics = Some(Some(config));
647        self
648    }
649
650    /// Disable metrics.
651    #[must_use]
652    pub fn disable_metrics(mut self) -> Self {
653        self.metrics = Some(None);
654        self
655    }
656
657    // ── Console & filtering ────────────────────────────────────────
658
659    /// Enable or disable console/stdout output.
660    #[must_use]
661    pub fn console_output(mut self, enabled: bool) -> Self {
662        self.enable_console_output = Some(enabled);
663        self
664    }
665
666    /// Set the minimum log level.
667    #[must_use]
668    pub fn log_level(mut self, level: LogLevel) -> Self {
669        self.log_level = Some(level);
670        self
671    }
672
673    /// Set the console output format.
674    #[must_use]
675    pub fn output_format(mut self, format: OutputFormat) -> Self {
676        self.output_format = Some(format);
677        self
678    }
679
680    /// Allow a crate to emit logs at the configured level.
681    ///
682    /// By default all crates are silenced. Call this for each crate
683    /// whose logs you want to see.
684    #[must_use]
685    pub fn allow_crate(mut self, name: impl Into<String>) -> Self {
686        self.allowed_crates.push(name.into());
687        self
688    }
689
690    /// Allow multiple crates to emit logs.
691    #[must_use]
692    pub fn allow_crates(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
693        for name in names {
694            self.allowed_crates.push(name.into());
695        }
696        self
697    }
698
699    /// Add a custom filter directive (tracing-subscriber `EnvFilter` syntax).
700    #[must_use]
701    pub fn custom_filter(mut self, directive: impl Into<String>) -> Self {
702        self.custom_filters.push(directive.into());
703        self
704    }
705
706    /// Add a custom resource attribute.
707    #[must_use]
708    pub fn attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
709        self.custom_attributes.push((key.into(), value.into()));
710        self
711    }
712
713    // ── Build ──────────────────────────────────────────────────────
714
715    /// Build the resolved configuration.
716    ///
717    /// Resolution: builder values → env vars → hardcoded defaults.
718    #[must_use]
719    pub fn build(self) -> OtelConfig {
720        let env = crate::env::read_env();
721
722        // Exporter: if explicitly set, use as-is; otherwise merge env.
723        let exporter = self.exporter.unwrap_or_else(|| {
724            let mut exp = ExporterConfig::default();
725            if let Some(endpoint) = env.endpoint {
726                exp.endpoint = endpoint;
727            }
728            if let Some(protocol) = env.protocol {
729                exp.protocol = protocol;
730            }
731            if let Some(timeout) = env.timeout {
732                exp.timeout = timeout;
733            }
734            if let Some(headers) = env.headers {
735                exp.credentials = OtlpCredentials::Headers(headers);
736            }
737            exp
738        });
739
740        // Tracing: not-set → defaults + env sampler, disabled → None.
741        let tracing = match self.tracing {
742            Some(t) => t,
743            None => {
744                let mut tc = TracingConfig::default();
745                if let Some(sampler) = env.sampler {
746                    tc.sampling = sampler;
747                }
748                Some(tc)
749            }
750        };
751
752        // Metrics.
753        let metrics = match self.metrics {
754            Some(m) => m,
755            None => Some(MetricsConfig::default()),
756        };
757
758        OtelConfig {
759            service_name: self
760                .service_name
761                .or(env.service_name)
762                .unwrap_or_else(|| "unknown-service".to_string()),
763            service_version: self.service_version.unwrap_or_else(|| "0.0.0".to_string()),
764            environment: self
765                .environment
766                .unwrap_or_else(|| "development".to_string()),
767            service_namespace: self.service_namespace,
768            service_instance_id: self.service_instance_id,
769            exporter,
770            tracing,
771            logging: self.logging.unwrap_or(true),
772            metrics,
773            enable_console_output: self.enable_console_output.unwrap_or(true),
774            log_level: self.log_level.unwrap_or_default(),
775            output_format: self.output_format.unwrap_or_default(),
776            allowed_crates: self.allowed_crates,
777            custom_filters: self.custom_filters,
778            custom_attributes: self.custom_attributes,
779        }
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    #[test]
788    fn builder_defaults() {
789        let config = OtelConfig::builder().build();
790        assert_eq!(config.service_name, "unknown-service");
791        assert!(config.tracing.is_some());
792        assert!(config.logging);
793        assert!(config.metrics.is_some());
794        assert!(config.enable_console_output);
795    }
796
797    #[test]
798    fn builder_with_service_info() {
799        let config = OtelConfig::builder()
800            .service_name("test-svc")
801            .service_version("1.0.0")
802            .environment("test")
803            .build();
804
805        assert_eq!(config.service_name, "test-svc");
806        assert_eq!(config.service_version, "1.0.0");
807        assert_eq!(config.environment, "test");
808    }
809
810    #[test]
811    fn builder_with_exporter() {
812        let config = OtelConfig::builder()
813            .exporter(
814                ExporterConfig::builder()
815                    .endpoint("https://otel.example.com:4317")
816                    .bearer_token("my-token")
817                    .build(),
818            )
819            .build();
820
821        assert_eq!(config.exporter.endpoint, "https://otel.example.com:4317");
822        assert!(matches!(
823            config.exporter.credentials,
824            OtlpCredentials::Bearer(_)
825        ));
826    }
827
828    #[test]
829    fn builder_disable_tracing() {
830        let config = OtelConfig::builder().disable_tracing().build();
831        assert!(config.tracing.is_none());
832    }
833
834    #[test]
835    fn builder_disable_metrics() {
836        let config = OtelConfig::builder().disable_metrics().build();
837        assert!(config.metrics.is_none());
838    }
839
840    #[test]
841    fn exporter_header_accumulation() {
842        let exp = ExporterConfig::builder()
843            .header("x-api-key", "abc")
844            .header("x-team", "eng")
845            .build();
846
847        match exp.credentials {
848            OtlpCredentials::Headers(h) => {
849                assert_eq!(h.get("x-api-key").unwrap(), "abc");
850                assert_eq!(h.get("x-team").unwrap(), "eng");
851            }
852            _ => panic!("expected Headers"),
853        }
854    }
855
856    #[test]
857    fn protocol_parsing() {
858        assert_eq!("grpc".parse::<OtlpProtocol>().unwrap(), OtlpProtocol::Grpc);
859        assert_eq!("http".parse::<OtlpProtocol>().unwrap(), OtlpProtocol::Http);
860        assert_eq!(
861            "http/protobuf".parse::<OtlpProtocol>().unwrap(),
862            OtlpProtocol::Http
863        );
864        assert!("invalid".parse::<OtlpProtocol>().is_err());
865    }
866
867    #[test]
868    fn log_level_parsing() {
869        assert_eq!("info".parse::<LogLevel>().unwrap(), LogLevel::Info);
870        assert_eq!("WARNING".parse::<LogLevel>().unwrap(), LogLevel::Warn);
871        assert!("invalid".parse::<LogLevel>().is_err());
872    }
873
874    #[test]
875    fn log_level_ordering() {
876        assert!(LogLevel::Trace < LogLevel::Debug);
877        assert!(LogLevel::Debug < LogLevel::Info);
878        assert!(LogLevel::Info < LogLevel::Warn);
879        assert!(LogLevel::Warn < LogLevel::Error);
880    }
881}