opentelemetry_configuration/
builder.rs

1//! Builder for OpenTelemetry SDK configuration.
2//!
3//! The builder supports layered configuration from multiple sources:
4//! 1. Compiled defaults (protocol-specific endpoints)
5//! 2. Configuration files (TOML, JSON, YAML)
6//! 3. Environment variables
7//! 4. Programmatic overrides
8//!
9//! Sources are merged in order, with later sources taking precedence.
10
11use crate::SdkError;
12use crate::config::{ComputeEnvironment, OtelSdkConfig, Protocol, ResourceConfig};
13use crate::fallback::ExportFallback;
14use crate::guard::OtelGuard;
15use figment::Figment;
16use figment::providers::{Env, Format, Serialized, Toml};
17use opentelemetry_sdk::Resource;
18use std::path::Path;
19
20/// Builder for configuring and initialising the OpenTelemetry SDK.
21///
22/// # Example
23///
24/// ```no_run
25/// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
26///
27/// fn main() -> Result<(), SdkError> {
28///     // Simple case - uses defaults (localhost:4318 for HTTP)
29///     let _guard = OtelSdkBuilder::new().build()?;
30///
31///     // With environment variables
32///     let _guard = OtelSdkBuilder::new()
33///         .with_env("OTEL_")
34///         .build()?;
35///
36///     // Full configuration
37///     let _guard = OtelSdkBuilder::new()
38///         .with_file("/var/task/otel-config.toml")
39///         .with_env("OTEL_")
40///         .endpoint("http://collector:4318")
41///         .service_name("my-lambda")
42///         .build()?;
43///
44///     Ok(())
45/// }
46/// ```
47#[must_use = "builders do nothing unless .build() is called"]
48pub struct OtelSdkBuilder {
49    figment: Figment,
50    fallback: ExportFallback,
51    custom_resource: Option<Resource>,
52    resource_attributes: std::collections::HashMap<String, String>,
53}
54
55impl OtelSdkBuilder {
56    /// Creates a new builder with default configuration.
57    ///
58    /// Defaults include:
59    /// - Protocol: HTTP with protobuf encoding
60    /// - Endpoint: `http://localhost:4318` (or 4317 for gRPC)
61    /// - All signals enabled (traces, metrics, logs)
62    /// - Tracing subscriber initialisation enabled
63    /// - Lambda resource detection enabled
64    pub fn new() -> Self {
65        Self {
66            figment: Figment::from(Serialized::defaults(OtelSdkConfig::default())),
67            fallback: ExportFallback::default(),
68            custom_resource: None,
69            resource_attributes: std::collections::HashMap::new(),
70        }
71    }
72
73    /// Creates a builder from an existing figment.
74    ///
75    /// This allows power users to construct complex configuration chains
76    /// before passing them to the SDK builder.
77    ///
78    /// # Example
79    ///
80    /// ```no_run
81    /// use figment::{Figment, providers::{Env, Format, Toml}};
82    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
83    ///
84    /// let figment = Figment::new()
85    ///     .merge(Toml::file("/etc/otel-defaults.toml"))
86    ///     .merge(Toml::file("/var/task/otel-config.toml"))
87    ///     .merge(Env::prefixed("OTEL_").split("_"));
88    ///
89    /// let _guard = OtelSdkBuilder::from_figment(figment)
90    ///     .service_name("my-lambda")
91    ///     .build()?;
92    /// # Ok::<(), SdkError>(())
93    /// ```
94    pub fn from_figment(figment: Figment) -> Self {
95        Self {
96            figment,
97            fallback: ExportFallback::default(),
98            custom_resource: None,
99            resource_attributes: std::collections::HashMap::new(),
100        }
101    }
102
103    /// Merges configuration from a TOML file.
104    ///
105    /// If the file doesn't exist, it's silently skipped.
106    /// This allows optional configuration files that may or may not be present.
107    ///
108    /// # Example
109    ///
110    /// ```no_run
111    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
112    ///
113    /// let _guard = OtelSdkBuilder::new()
114    ///     .with_file("/var/task/otel-config.toml")  // Optional
115    ///     .with_file("./otel-local.toml")           // For development
116    ///     .build()?;
117    /// # Ok::<(), SdkError>(())
118    /// ```
119    pub fn with_file<P: AsRef<Path>>(mut self, path: P) -> Self {
120        let path = path.as_ref();
121        if path.exists() {
122            self.figment = self.figment.merge(Toml::file(path));
123        }
124        self
125    }
126
127    /// Merges configuration from environment variables with the given prefix.
128    ///
129    /// Environment variables are split on underscores to match nested config.
130    /// For example, with prefix `OTEL_`:
131    /// - `OTEL_ENDPOINT_URL` → `endpoint.url`
132    /// - `OTEL_ENDPOINT_PROTOCOL` → `endpoint.protocol`
133    /// - `OTEL_TRACES_ENABLED` → `traces.enabled`
134    /// - `OTEL_RESOURCE_SERVICE_NAME` → `resource.service_name`
135    ///
136    /// # Example
137    ///
138    /// ```bash
139    /// export OTEL_ENDPOINT_URL=http://collector:4318
140    /// export OTEL_ENDPOINT_PROTOCOL=grpc
141    /// export OTEL_TRACES_ENABLED=true
142    /// export OTEL_RESOURCE_SERVICE_NAME=my-lambda
143    /// ```
144    ///
145    /// ```no_run
146    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
147    ///
148    /// let _guard = OtelSdkBuilder::new()
149    ///     .with_env("OTEL_")
150    ///     .build()?;
151    /// # Ok::<(), SdkError>(())
152    /// ```
153    pub fn with_env(mut self, prefix: &str) -> Self {
154        self.figment = self.figment.merge(Env::prefixed(prefix).split("_"));
155        self
156    }
157
158    /// Merges configuration from standard OpenTelemetry environment variables.
159    ///
160    /// This reads the standard `OTEL_*` environment variables as defined by
161    /// the OpenTelemetry specification:
162    /// - `OTEL_EXPORTER_OTLP_ENDPOINT` → endpoint URL
163    /// - `OTEL_EXPORTER_OTLP_PROTOCOL` → protocol (grpc, http/protobuf, http/json)
164    /// - `OTEL_SERVICE_NAME` → service name
165    /// - `OTEL_TRACES_EXPORTER` → traces exporter (otlp, none)
166    /// - `OTEL_METRICS_EXPORTER` → metrics exporter (otlp, none)
167    /// - `OTEL_LOGS_EXPORTER` → logs exporter (otlp, none)
168    pub fn with_standard_env(mut self) -> Self {
169        // Map standard OTEL env vars to our config structure
170        if let Ok(endpoint) = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
171            self.figment = self
172                .figment
173                .merge(Serialized::default("endpoint.url", endpoint));
174        }
175
176        if let Ok(protocol) = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL") {
177            let protocol = match protocol.as_str() {
178                "grpc" => "grpc",
179                "http/protobuf" => "httpbinary",
180                "http/json" => "httpjson",
181                _ => "httpbinary",
182            };
183            self.figment = self
184                .figment
185                .merge(Serialized::default("endpoint.protocol", protocol));
186        }
187
188        if let Ok(service_name) = std::env::var("OTEL_SERVICE_NAME") {
189            self.figment = self
190                .figment
191                .merge(Serialized::default("resource.service_name", service_name));
192        }
193
194        if let Ok(exporter) = std::env::var("OTEL_TRACES_EXPORTER") {
195            let enabled = exporter != "none";
196            self.figment = self
197                .figment
198                .merge(Serialized::default("traces.enabled", enabled));
199        }
200
201        if let Ok(exporter) = std::env::var("OTEL_METRICS_EXPORTER") {
202            let enabled = exporter != "none";
203            self.figment = self
204                .figment
205                .merge(Serialized::default("metrics.enabled", enabled));
206        }
207
208        if let Ok(exporter) = std::env::var("OTEL_LOGS_EXPORTER") {
209            let enabled = exporter != "none";
210            self.figment = self
211                .figment
212                .merge(Serialized::default("logs.enabled", enabled));
213        }
214
215        self
216    }
217
218    /// Sets the OTLP endpoint URL explicitly.
219    ///
220    /// This overrides any configuration from files or environment variables.
221    ///
222    /// For HTTP protocols, signal-specific paths (`/v1/traces`, `/v1/metrics`,
223    /// `/v1/logs`) are appended automatically.
224    ///
225    /// # Example
226    ///
227    /// ```no_run
228    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
229    ///
230    /// let _guard = OtelSdkBuilder::new()
231    ///     .endpoint("http://collector.internal:4318")
232    ///     .build()?;
233    /// # Ok::<(), SdkError>(())
234    /// ```
235    pub fn endpoint(mut self, url: impl Into<String>) -> Self {
236        self.figment = self
237            .figment
238            .merge(Serialized::default("endpoint.url", url.into()));
239        self
240    }
241
242    /// Sets the export protocol.
243    ///
244    /// This overrides any configuration from files or environment variables.
245    ///
246    /// The default endpoint changes based on protocol:
247    /// - `Protocol::Grpc` → `http://localhost:4317`
248    /// - `Protocol::HttpBinary` → `http://localhost:4318`
249    /// - `Protocol::HttpJson` → `http://localhost:4318`
250    pub fn protocol(mut self, protocol: Protocol) -> Self {
251        let protocol_str = match protocol {
252            Protocol::Grpc => "grpc",
253            Protocol::HttpBinary => "httpbinary",
254            Protocol::HttpJson => "httpjson",
255        };
256        self.figment = self
257            .figment
258            .merge(Serialized::default("endpoint.protocol", protocol_str));
259        self
260    }
261
262    /// Sets the service name resource attribute.
263    ///
264    /// This is the most commonly configured resource attribute and identifies
265    /// your service in the telemetry backend.
266    pub fn service_name(mut self, name: impl Into<String>) -> Self {
267        self.figment = self
268            .figment
269            .merge(Serialized::default("resource.service_name", name.into()));
270        self
271    }
272
273    /// Sets the service version resource attribute.
274    pub fn service_version(mut self, version: impl Into<String>) -> Self {
275        self.figment = self.figment.merge(Serialized::default(
276            "resource.service_version",
277            version.into(),
278        ));
279        self
280    }
281
282    /// Sets the deployment environment resource attribute.
283    pub fn deployment_environment(mut self, env: impl Into<String>) -> Self {
284        self.figment = self.figment.merge(Serialized::default(
285            "resource.deployment_environment",
286            env.into(),
287        ));
288        self
289    }
290
291    /// Adds a resource attribute.
292    pub fn resource_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
293        self.resource_attributes.insert(key.into(), value.into());
294        self
295    }
296
297    /// Provides a pre-built OpenTelemetry Resource.
298    ///
299    /// This takes precedence over individual resource configuration.
300    /// Use this when you need fine-grained control over resource construction.
301    pub fn with_resource(mut self, resource: Resource) -> Self {
302        self.custom_resource = Some(resource);
303        self
304    }
305
306    /// Configures the resource using a builder function.
307    ///
308    /// # Example
309    ///
310    /// ```no_run
311    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
312    ///
313    /// let _guard = OtelSdkBuilder::new()
314    ///     .resource(|r| r
315    ///         .service_name("my-lambda")
316    ///         .service_version(env!("CARGO_PKG_VERSION"))
317    ///         .deployment_environment("production"))
318    ///     .build()?;
319    /// # Ok::<(), SdkError>(())
320    /// ```
321    pub fn resource<F>(mut self, f: F) -> Self
322    where
323        F: FnOnce(ResourceConfigBuilder) -> ResourceConfigBuilder,
324    {
325        let builder = f(ResourceConfigBuilder::new());
326        let config = builder.build();
327
328        if let Some(name) = &config.service_name {
329            self.figment = self
330                .figment
331                .merge(Serialized::default("resource.service_name", name.clone()));
332        }
333        if let Some(version) = &config.service_version {
334            self.figment = self.figment.merge(Serialized::default(
335                "resource.service_version",
336                version.clone(),
337            ));
338        }
339        if let Some(env) = &config.deployment_environment {
340            self.figment = self.figment.merge(Serialized::default(
341                "resource.deployment_environment",
342                env.clone(),
343            ));
344        }
345        if config.compute_environment != ComputeEnvironment::default() {
346            let env_str = match config.compute_environment {
347                ComputeEnvironment::Auto => "auto",
348                ComputeEnvironment::Lambda => "lambda",
349                ComputeEnvironment::Kubernetes => "kubernetes",
350                ComputeEnvironment::None => "none",
351            };
352            self.figment = self
353                .figment
354                .merge(Serialized::default("resource.compute_environment", env_str));
355        }
356        for (key, value) in config.attributes {
357            self.resource_attributes.insert(key, value);
358        }
359
360        self
361    }
362
363    /// Enables or disables trace collection.
364    ///
365    /// Default: enabled
366    pub fn traces(mut self, enabled: bool) -> Self {
367        self.figment = self
368            .figment
369            .merge(Serialized::default("traces.enabled", enabled));
370        self
371    }
372
373    /// Enables or disables metrics collection.
374    ///
375    /// Default: enabled
376    pub fn metrics(mut self, enabled: bool) -> Self {
377        self.figment = self
378            .figment
379            .merge(Serialized::default("metrics.enabled", enabled));
380        self
381    }
382
383    /// Enables or disables log collection.
384    ///
385    /// Default: enabled
386    pub fn logs(mut self, enabled: bool) -> Self {
387        self.figment = self
388            .figment
389            .merge(Serialized::default("logs.enabled", enabled));
390        self
391    }
392
393    /// Disables automatic tracing subscriber initialisation.
394    ///
395    /// By default, the SDK sets up a `tracing-subscriber` with
396    /// `tracing-opentelemetry` and `opentelemetry-appender-tracing` integration.
397    /// Disable this if you want to configure the subscriber yourself.
398    pub fn without_tracing_subscriber(mut self) -> Self {
399        self.figment = self
400            .figment
401            .merge(Serialized::default("init_tracing_subscriber", false));
402        self
403    }
404
405    /// Sets the fallback strategy for failed exports (planned feature).
406    ///
407    /// **Note:** The fallback API is defined but not yet wired into the export
408    /// pipeline. The handler will be stored but not invoked on export failures.
409    /// Full implementation is planned for a future release.
410    ///
411    /// When implemented, the fallback handler will be called when an export
412    /// fails after all retry attempts have been exhausted. It will receive the
413    /// original OTLP request payload, which can be preserved via alternative
414    /// transport.
415    ///
416    /// # Example
417    ///
418    /// ```no_run
419    /// use opentelemetry_configuration::{OtelSdkBuilder, ExportFallback, SdkError};
420    ///
421    /// let _guard = OtelSdkBuilder::new()
422    ///     .fallback(ExportFallback::Stdout)
423    ///     .build()?;
424    /// # Ok::<(), SdkError>(())
425    /// ```
426    pub fn fallback(mut self, fallback: ExportFallback) -> Self {
427        self.fallback = fallback;
428        self
429    }
430
431    /// Sets a custom fallback handler using a closure (planned feature).
432    ///
433    /// **Note:** The fallback API is defined but not yet wired into the export
434    /// pipeline. The handler will be stored but not invoked on export failures.
435    /// Full implementation is planned for a future release.
436    ///
437    /// When implemented, the closure will receive the full
438    /// [`ExportFailure`](super::ExportFailure) including the original OTLP
439    /// request payload.
440    ///
441    /// # Example
442    ///
443    /// ```no_run
444    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
445    ///
446    /// let _guard = OtelSdkBuilder::new()
447    ///     .with_fallback(|failure| {
448    ///         // Write the protobuf payload to S3, a queue, etc.
449    ///         let bytes = failure.request.to_protobuf();
450    ///         eprintln!(
451    ///             "Failed to export {} ({} bytes): {}",
452    ///             failure.request.signal_type(),
453    ///             bytes.len(),
454    ///             failure.error
455    ///         );
456    ///         Ok(())
457    ///     })
458    ///     .build()?;
459    /// # Ok::<(), SdkError>(())
460    /// ```
461    pub fn with_fallback<F>(mut self, f: F) -> Self
462    where
463        F: Fn(super::ExportFailure) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
464            + Send
465            + Sync
466            + 'static,
467    {
468        self.fallback = ExportFallback::custom(f);
469        self
470    }
471
472    /// Adds an HTTP header to all export requests.
473    ///
474    /// Useful for authentication or custom routing.
475    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
476        let header_key = format!("endpoint.headers.{}", key.into());
477        self.figment = self
478            .figment
479            .merge(Serialized::default(&header_key, value.into()));
480        self
481    }
482
483    /// Sets the instrumentation scope name (otel.library.name).
484    ///
485    /// If not set, defaults to the service name, then "opentelemetry-configuration".
486    pub fn instrumentation_scope_name(mut self, name: impl Into<String>) -> Self {
487        self.figment = self.figment.merge(Serialized::default(
488            "instrumentation_scope_name",
489            name.into(),
490        ));
491        self
492    }
493
494    /// Sets the compute environment for resource detection.
495    ///
496    /// Controls which resource detectors are run automatically:
497    /// - `Auto` (default): Runs generic detectors and probes for Lambda/K8s
498    /// - `Lambda`: Runs generic detectors + Lambda-specific attributes
499    /// - `Kubernetes`: Runs generic detectors + K8s detector
500    /// - `None`: No automatic detection, only explicit configuration
501    pub fn compute_environment(mut self, env: ComputeEnvironment) -> Self {
502        let env_str = match env {
503            ComputeEnvironment::Auto => "auto",
504            ComputeEnvironment::Lambda => "lambda",
505            ComputeEnvironment::Kubernetes => "kubernetes",
506            ComputeEnvironment::None => "none",
507        };
508        self.figment = self
509            .figment
510            .merge(Serialized::default("resource.compute_environment", env_str));
511        self
512    }
513
514    /// Adds Rust build-time information as resource attributes.
515    ///
516    /// Use with the [`capture_rust_build_info!`](crate::capture_rust_build_info) macro
517    /// to add rustc version and channel information to telemetry.
518    ///
519    /// Requires [`emit_rustc_env`](crate::emit_rustc_env) to be called in your build.rs.
520    ///
521    /// # Example
522    ///
523    /// ```ignore
524    /// // In build.rs:
525    /// fn main() {
526    ///     opentelemetry_configuration::emit_rustc_env();
527    /// }
528    ///
529    /// // In main.rs:
530    /// use opentelemetry_configuration::{OtelSdkBuilder, capture_rust_build_info};
531    ///
532    /// let _guard = OtelSdkBuilder::new()
533    ///     .service_name("my-service")
534    ///     .with_rust_build_info(capture_rust_build_info!())
535    ///     .build()?;
536    /// ```
537    ///
538    /// # Attributes Added
539    ///
540    /// - `process.runtime.version` - rustc version (e.g., "1.84.0")
541    /// - `process.runtime.description` - full version string
542    /// - `rust.channel` - release channel ("stable", "beta", or "nightly")
543    pub fn with_rust_build_info(mut self, info: crate::RustBuildInfo) -> Self {
544        for kv in info.to_key_values() {
545            let key = kv.key.as_str().to_string();
546            let value = kv.value.as_str().to_string();
547            self.resource_attributes.insert(key, value);
548        }
549        self
550    }
551
552    /// Extracts the configuration for inspection or debugging.
553    ///
554    /// # Errors
555    ///
556    /// Returns an error if configuration extraction fails or if the endpoint
557    /// URL is invalid.
558    pub fn extract_config(&self) -> Result<OtelSdkConfig, SdkError> {
559        let mut config: OtelSdkConfig = self
560            .figment
561            .extract()
562            .map_err(|e| SdkError::Config(Box::new(e)))?;
563
564        // Merge resource attributes that couldn't go through figment
565        config
566            .resource
567            .attributes
568            .extend(self.resource_attributes.clone());
569
570        if let Some(ref url) = config.endpoint.url
571            && !url.starts_with("http://")
572            && !url.starts_with("https://")
573        {
574            return Err(SdkError::InvalidEndpoint { url: url.clone() });
575        }
576
577        Ok(config)
578    }
579
580    /// Builds and initialises the OpenTelemetry SDK.
581    ///
582    /// Returns an [`OtelGuard`] that manages provider lifecycle. When the
583    /// guard is dropped, all providers are flushed and shut down.
584    ///
585    /// # Errors
586    ///
587    /// Returns an error if:
588    /// - Configuration extraction fails
589    /// - Provider initialisation fails
590    /// - Tracing subscriber initialisation fails
591    ///
592    /// # Example
593    ///
594    /// ```no_run
595    /// use opentelemetry_configuration::{OtelSdkBuilder, SdkError};
596    ///
597    /// fn main() -> Result<(), SdkError> {
598    ///     let _guard = OtelSdkBuilder::new()
599    ///         .with_env("OTEL_")
600    ///         .service_name("my-lambda")
601    ///         .build()?;
602    ///
603    ///     tracing::info!("Application started");
604    ///
605    ///     // Guard automatically shuts down providers on drop
606    ///     Ok(())
607    /// }
608    /// ```
609    pub fn build(self) -> Result<OtelGuard, SdkError> {
610        let mut config: OtelSdkConfig = self
611            .figment
612            .extract()
613            .map_err(|e| SdkError::Config(Box::new(e)))?;
614
615        // Merge resource attributes that couldn't go through figment
616        config.resource.attributes.extend(self.resource_attributes);
617
618        if let Some(ref url) = config.endpoint.url
619            && !url.starts_with("http://")
620            && !url.starts_with("https://")
621        {
622            return Err(SdkError::InvalidEndpoint { url: url.clone() });
623        }
624
625        OtelGuard::from_config(config, self.fallback, self.custom_resource)
626    }
627}
628
629impl Default for OtelSdkBuilder {
630    fn default() -> Self {
631        Self::new()
632    }
633}
634
635/// Builder for resource configuration.
636///
637/// Used with [`OtelSdkBuilder::resource`] for fluent configuration.
638#[derive(Default)]
639#[must_use = "builders do nothing unless .build() is called"]
640pub struct ResourceConfigBuilder {
641    config: ResourceConfig,
642}
643
644impl ResourceConfigBuilder {
645    /// Creates a new resource config builder.
646    pub fn new() -> Self {
647        Self::default()
648    }
649
650    /// Sets the service name.
651    pub fn service_name(mut self, name: impl Into<String>) -> Self {
652        self.config.service_name = Some(name.into());
653        self
654    }
655
656    /// Sets the service version.
657    pub fn service_version(mut self, version: impl Into<String>) -> Self {
658        self.config.service_version = Some(version.into());
659        self
660    }
661
662    /// Sets the deployment environment.
663    pub fn deployment_environment(mut self, env: impl Into<String>) -> Self {
664        self.config.deployment_environment = Some(env.into());
665        self
666    }
667
668    /// Adds a resource attribute.
669    pub fn attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
670        self.config.attributes.insert(key.into(), value.into());
671        self
672    }
673
674    /// Sets the compute environment for resource detection.
675    pub fn compute_environment(mut self, env: ComputeEnvironment) -> Self {
676        self.config.compute_environment = env;
677        self
678    }
679
680    /// Builds the resource configuration.
681    pub fn build(self) -> ResourceConfig {
682        self.config
683    }
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    #[test]
691    fn test_builder_default() {
692        let builder = OtelSdkBuilder::new();
693        let config = builder.extract_config().unwrap();
694
695        assert!(config.traces.enabled);
696        assert!(config.metrics.enabled);
697        assert!(config.logs.enabled);
698        assert!(config.init_tracing_subscriber);
699        assert_eq!(config.endpoint.protocol, Protocol::HttpBinary);
700    }
701
702    #[test]
703    fn test_builder_endpoint() {
704        let builder = OtelSdkBuilder::new().endpoint("http://collector:4318");
705        let config = builder.extract_config().unwrap();
706
707        assert_eq!(
708            config.endpoint.url,
709            Some("http://collector:4318".to_string())
710        );
711    }
712
713    #[test]
714    fn test_builder_protocol() {
715        let builder = OtelSdkBuilder::new().protocol(Protocol::Grpc);
716        let config = builder.extract_config().unwrap();
717
718        assert_eq!(config.endpoint.protocol, Protocol::Grpc);
719    }
720
721    #[test]
722    fn test_builder_service_name() {
723        let builder = OtelSdkBuilder::new().service_name("my-service");
724        let config = builder.extract_config().unwrap();
725
726        assert_eq!(config.resource.service_name, Some("my-service".to_string()));
727    }
728
729    #[test]
730    fn test_builder_disable_signals() {
731        let builder = OtelSdkBuilder::new()
732            .traces(false)
733            .metrics(false)
734            .logs(false);
735        let config = builder.extract_config().unwrap();
736
737        assert!(!config.traces.enabled);
738        assert!(!config.metrics.enabled);
739        assert!(!config.logs.enabled);
740    }
741
742    #[test]
743    fn test_builder_resource_fluent() {
744        let builder = OtelSdkBuilder::new().resource(|r| {
745            r.service_name("my-service")
746                .service_version("1.0.0")
747                .deployment_environment("production")
748                .attribute("custom.key", "custom.value")
749        });
750        let config = builder.extract_config().unwrap();
751
752        assert_eq!(config.resource.service_name, Some("my-service".to_string()));
753        assert_eq!(config.resource.service_version, Some("1.0.0".to_string()));
754        assert_eq!(
755            config.resource.deployment_environment,
756            Some("production".to_string())
757        );
758        assert_eq!(
759            config.resource.attributes.get("custom.key"),
760            Some(&"custom.value".to_string())
761        );
762    }
763
764    #[test]
765    fn test_builder_without_tracing_subscriber() {
766        let builder = OtelSdkBuilder::new().without_tracing_subscriber();
767        let config = builder.extract_config().unwrap();
768
769        assert!(!config.init_tracing_subscriber);
770    }
771
772    #[test]
773    fn test_builder_header() {
774        let builder = OtelSdkBuilder::new().header("Authorization", "Bearer token123");
775        let config = builder.extract_config().unwrap();
776
777        assert_eq!(
778            config.endpoint.headers.get("Authorization"),
779            Some(&"Bearer token123".to_string())
780        );
781    }
782
783    #[test]
784    fn test_builder_fallback() {
785        let builder = OtelSdkBuilder::new().fallback(ExportFallback::Stdout);
786        assert!(matches!(builder.fallback, ExportFallback::Stdout));
787    }
788
789    #[test]
790    fn test_builder_custom_fallback() {
791        let builder = OtelSdkBuilder::new().with_fallback(|_failure| Ok(()));
792        assert!(matches!(builder.fallback, ExportFallback::Custom(_)));
793    }
794
795    #[test]
796    fn test_with_standard_env_endpoint() {
797        temp_env::with_var(
798            "OTEL_EXPORTER_OTLP_ENDPOINT",
799            Some("http://custom:4318"),
800            || {
801                let builder = OtelSdkBuilder::new().with_standard_env();
802                let config = builder.extract_config().unwrap();
803                assert_eq!(config.endpoint.url, Some("http://custom:4318".to_string()));
804            },
805        );
806    }
807
808    #[test]
809    fn test_with_standard_env_service_name() {
810        temp_env::with_var("OTEL_SERVICE_NAME", Some("test-service"), || {
811            let builder = OtelSdkBuilder::new().with_standard_env();
812            let config = builder.extract_config().unwrap();
813            assert_eq!(
814                config.resource.service_name,
815                Some("test-service".to_string())
816            );
817        });
818    }
819
820    #[test]
821    fn test_with_standard_env_protocol_grpc() {
822        temp_env::with_var("OTEL_EXPORTER_OTLP_PROTOCOL", Some("grpc"), || {
823            let builder = OtelSdkBuilder::new().with_standard_env();
824            let config = builder.extract_config().unwrap();
825            assert_eq!(config.endpoint.protocol, Protocol::Grpc);
826        });
827    }
828
829    #[test]
830    fn test_with_standard_env_protocol_http_protobuf() {
831        temp_env::with_var("OTEL_EXPORTER_OTLP_PROTOCOL", Some("http/protobuf"), || {
832            let builder = OtelSdkBuilder::new().with_standard_env();
833            let config = builder.extract_config().unwrap();
834            assert_eq!(config.endpoint.protocol, Protocol::HttpBinary);
835        });
836    }
837
838    #[test]
839    fn test_with_standard_env_traces_disabled() {
840        temp_env::with_var("OTEL_TRACES_EXPORTER", Some("none"), || {
841            let builder = OtelSdkBuilder::new().with_standard_env();
842            let config = builder.extract_config().unwrap();
843            assert!(!config.traces.enabled);
844        });
845    }
846
847    #[test]
848    fn test_with_standard_env_metrics_disabled() {
849        temp_env::with_var("OTEL_METRICS_EXPORTER", Some("none"), || {
850            let builder = OtelSdkBuilder::new().with_standard_env();
851            let config = builder.extract_config().unwrap();
852            assert!(!config.metrics.enabled);
853        });
854    }
855
856    #[test]
857    fn test_with_standard_env_logs_disabled() {
858        temp_env::with_var("OTEL_LOGS_EXPORTER", Some("none"), || {
859            let builder = OtelSdkBuilder::new().with_standard_env();
860            let config = builder.extract_config().unwrap();
861            assert!(!config.logs.enabled);
862        });
863    }
864
865    #[test]
866    fn test_with_standard_env_multiple_vars() {
867        temp_env::with_vars(
868            [
869                ("OTEL_EXPORTER_OTLP_ENDPOINT", Some("http://collector:4317")),
870                ("OTEL_EXPORTER_OTLP_PROTOCOL", Some("grpc")),
871                ("OTEL_SERVICE_NAME", Some("multi-test")),
872                ("OTEL_TRACES_EXPORTER", Some("otlp")),
873            ],
874            || {
875                let builder = OtelSdkBuilder::new().with_standard_env();
876                let config = builder.extract_config().unwrap();
877
878                assert_eq!(
879                    config.endpoint.url,
880                    Some("http://collector:4317".to_string())
881                );
882                assert_eq!(config.endpoint.protocol, Protocol::Grpc);
883                assert_eq!(config.resource.service_name, Some("multi-test".to_string()));
884                assert!(config.traces.enabled);
885            },
886        );
887    }
888
889    #[test]
890    fn test_programmatic_overrides_env() {
891        temp_env::with_vars(
892            [
893                ("OTEL_EXPORTER_OTLP_ENDPOINT", Some("http://env:4318")),
894                ("OTEL_SERVICE_NAME", Some("env-service")),
895            ],
896            || {
897                let builder = OtelSdkBuilder::new()
898                    .with_standard_env()
899                    .endpoint("http://programmatic:4318")
900                    .service_name("programmatic-service");
901                let config = builder.extract_config().unwrap();
902
903                assert_eq!(
904                    config.endpoint.url,
905                    Some("http://programmatic:4318".to_string())
906                );
907                assert_eq!(
908                    config.resource.service_name,
909                    Some("programmatic-service".to_string())
910                );
911            },
912        );
913    }
914
915    #[test]
916    fn test_invalid_endpoint_url_rejected() {
917        let builder = OtelSdkBuilder::new().endpoint("not-a-valid-url");
918        let result = builder.extract_config();
919
920        assert!(result.is_err());
921        let err = result.unwrap_err();
922        assert!(
923            matches!(err, SdkError::InvalidEndpoint { ref url } if url == "not-a-valid-url"),
924            "Expected InvalidEndpoint error, got: {:?}",
925            err
926        );
927    }
928
929    #[test]
930    fn test_valid_http_endpoint_accepted() {
931        let builder = OtelSdkBuilder::new().endpoint("http://localhost:4318");
932        let config = builder.extract_config().unwrap();
933        assert_eq!(
934            config.endpoint.url,
935            Some("http://localhost:4318".to_string())
936        );
937    }
938
939    #[test]
940    fn test_valid_https_endpoint_accepted() {
941        let builder = OtelSdkBuilder::new().endpoint("https://collector.example.com:4318");
942        let config = builder.extract_config().unwrap();
943        assert_eq!(
944            config.endpoint.url,
945            Some("https://collector.example.com:4318".to_string())
946        );
947    }
948}