init_tracing_opentelemetry/
config.rs

1//! Flexible tracing configuration with builder pattern.
2//!
3//! Provides [`TracingConfig`] for configurable tracing setup with format options,
4//! output destinations, level filtering, and OpenTelemetry integration.
5//!
6//! # Example
7//! ```no_run
8//! use init_tracing_opentelemetry::TracingConfig;
9//!
10//! // Use preset with global subscriber (default)
11//! let _guard = TracingConfig::development().init_subscriber()?;
12//!
13//! // Custom configuration with global subscriber
14//! let _guard = TracingConfig::default()
15//!     .with_json_format()
16//!     .with_stderr()
17//!     .with_log_directives("debug")
18//!     .init_subscriber()?;
19//!
20//! // Non-global subscriber (thread-local)
21//! let guard = TracingConfig::development()
22//!     .with_global_subscriber(false)
23//!     .init_subscriber()?;
24//! // Guard must be kept alive for subscriber to remain active
25//! assert!(guard.is_non_global());
26//!
27//! // Without OpenTelemetry (just logging)
28//! let guard = TracingConfig::minimal()
29//!     .with_otel(false)
30//!     .init_subscriber()?;
31//! // Works fine - guard.otel_guard is None
32//! assert!(!guard.has_otel());
33//! assert!(guard.otel_guard.is_none());
34//!
35//! // Direct field access is also possible
36//! if let Some(otel_guard) = &guard.otel_guard {
37//!     // Use otel_guard...
38//! }
39//! # Ok::<(), Box<dyn std::error::Error>>(())
40//! ```
41
42use std::path::{Path, PathBuf};
43
44use tracing::{info, level_filters::LevelFilter, Subscriber};
45use tracing_subscriber::{
46    filter::EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, registry::LookupSpan, Layer,
47    Registry,
48};
49
50#[cfg(feature = "logfmt")]
51use crate::formats::LogfmtLayerBuilder;
52use crate::formats::{
53    CompactLayerBuilder, FullLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder,
54};
55
56use crate::tracing_subscriber_ext::register_otel_layers_with_resource;
57use crate::{otlp::OtelGuard, resource::DetectResource, Error};
58
59/// Combined guard that handles both `OtelGuard` and optional `DefaultGuard`
60///
61/// This struct holds the various guards needed to maintain the tracing subscriber.
62/// - `otel_guard`: OpenTelemetry guard for flushing traces/metrics on drop (None when OTEL disabled)
63/// - `default_guard`: Subscriber default guard for non-global subscribers (None when using global)
64#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/log/metrics are sent to the server and subscriber is maintained"]
65pub struct Guard {
66    /// OpenTelemetry guard for proper cleanup (None when OTEL is disabled)
67    pub otel_guard: Option<OtelGuard>,
68    /// Default subscriber guard for non-global mode (None when using global subscriber)
69    pub default_guard: Option<tracing::subscriber::DefaultGuard>,
70    // Easy to add in the future:
71    // pub log_guard: Option<LogGuard>,
72    // pub metrics_guard: Option<MetricsGuard>,
73}
74
75impl Guard {
76    /// Create a new Guard for global subscriber mode
77    pub fn global(otel_guard: Option<OtelGuard>) -> Self {
78        Self {
79            otel_guard,
80            default_guard: None,
81        }
82    }
83
84    /// Create a new Guard for non-global subscriber mode
85    pub fn non_global(
86        otel_guard: Option<OtelGuard>,
87        default_guard: tracing::subscriber::DefaultGuard,
88    ) -> Self {
89        Self {
90            otel_guard,
91            default_guard: Some(default_guard),
92        }
93    }
94
95    /// Get a reference to the underlying `OtelGuard` if present
96    #[must_use]
97    pub fn otel_guard(&self) -> Option<&OtelGuard> {
98        self.otel_guard.as_ref()
99    }
100
101    /// Check if OpenTelemetry is enabled for this guard
102    #[must_use]
103    pub fn has_otel(&self) -> bool {
104        self.otel_guard.is_some()
105    }
106
107    /// Check if this guard is managing a non-global (thread-local) subscriber
108    #[must_use]
109    pub fn is_non_global(&self) -> bool {
110        self.default_guard.is_some()
111    }
112
113    /// Check if this guard is for a global subscriber
114    #[must_use]
115    pub fn is_global(&self) -> bool {
116        self.default_guard.is_none()
117    }
118}
119
120/// Configuration for log output format
121#[derive(Debug, Clone)]
122pub enum LogFormat {
123    /// Pretty formatted output with colors and indentation (development)
124    Pretty,
125    /// Structured JSON output (production)
126    Json,
127    /// Single-line output
128    Full,
129    /// Single-line compact output
130    Compact,
131    /// Key=value logfmt format
132    #[cfg(feature = "logfmt")]
133    Logfmt,
134}
135
136impl Default for LogFormat {
137    fn default() -> Self {
138        if cfg!(debug_assertions) {
139            LogFormat::Pretty
140        } else {
141            LogFormat::Json
142        }
143    }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum LogTimer {
148    None,
149    Time,
150    Uptime,
151}
152
153impl Default for LogTimer {
154    fn default() -> Self {
155        if cfg!(debug_assertions) {
156            LogTimer::Uptime
157        } else {
158            LogTimer::Time
159        }
160    }
161}
162
163/// Configuration for log output destination
164#[derive(Debug, Clone, Default)]
165pub enum WriterConfig {
166    /// Write to stdout
167    #[default]
168    Stdout,
169    /// Write to stderr
170    Stderr,
171    /// Write to a file
172    File(PathBuf),
173}
174
175/// Configuration for log level filtering
176#[derive(Debug, Clone)]
177pub struct LevelConfig {
178    /// Log directives string (takes precedence over env vars)
179    pub directives: String,
180    /// Environment variable fallbacks (checked in order)
181    pub env_fallbacks: Vec<String>,
182    /// Default level when no directives or env vars are set
183    pub default_level: LevelFilter,
184    /// OpenTelemetry tracing level
185    pub otel_trace_level: LevelFilter,
186}
187
188impl Default for LevelConfig {
189    fn default() -> Self {
190        Self {
191            directives: String::new(),
192            env_fallbacks: vec!["RUST_LOG".to_string(), "OTEL_LOG_LEVEL".to_string()],
193            default_level: LevelFilter::INFO,
194            otel_trace_level: LevelFilter::TRACE,
195        }
196    }
197}
198
199/// Configuration for optional tracing features
200#[derive(Debug, Clone)]
201#[allow(clippy::struct_excessive_bools)]
202pub struct FeatureSet {
203    /// Include file names in output
204    pub file_names: bool,
205    /// Include line numbers in output
206    pub line_numbers: bool,
207    /// Include thread names in output
208    pub thread_names: bool,
209    /// Include thread IDs in output
210    pub thread_ids: bool,
211    /// Configure time logging (wall clock, uptime or none)
212    pub timer: LogTimer,
213    /// Configure span event logging
214    pub span_events: Option<FmtSpan>,
215    /// Display target information
216    pub target_display: bool,
217}
218
219impl Default for FeatureSet {
220    fn default() -> Self {
221        Self {
222            file_names: true,
223            line_numbers: cfg!(debug_assertions),
224            thread_names: cfg!(debug_assertions),
225            thread_ids: false,
226            timer: LogTimer::default(),
227            span_events: if cfg!(debug_assertions) {
228                Some(FmtSpan::NEW | FmtSpan::CLOSE)
229            } else {
230                None
231            },
232            target_display: true,
233        }
234    }
235}
236
237/// Configuration for OpenTelemetry integration
238#[derive(Debug)]
239pub struct OtelConfig {
240    /// Enable OpenTelemetry tracing
241    pub enabled: bool,
242    /// Resource configuration for OTEL
243    pub resource_config: Option<DetectResource>,
244    /// Enable metrics collection
245    pub metrics_enabled: bool,
246}
247
248impl Default for OtelConfig {
249    fn default() -> Self {
250        Self {
251            enabled: true,
252            resource_config: None,
253            metrics_enabled: cfg!(feature = "metrics"),
254        }
255    }
256}
257
258/// Main configuration builder for tracing setup
259/// Default create a new tracing configuration with sensible defaults
260#[derive(Debug)]
261pub struct TracingConfig {
262    /// Output format configuration
263    pub format: LogFormat,
264    /// Output destination configuration
265    pub writer: WriterConfig,
266    /// Level filtering configuration
267    pub level_config: LevelConfig,
268    /// Optional features configuration
269    pub features: FeatureSet,
270    /// OpenTelemetry configuration
271    pub otel_config: OtelConfig,
272    /// Whether to set the subscriber as global default
273    pub global_subscriber: bool,
274}
275
276impl Default for TracingConfig {
277    fn default() -> Self {
278        Self {
279            format: LogFormat::default(),
280            writer: WriterConfig::default(),
281            level_config: LevelConfig::default(),
282            features: FeatureSet::default(),
283            otel_config: OtelConfig::default(),
284            global_subscriber: true,
285        }
286    }
287}
288
289impl TracingConfig {
290    // === Format Configuration ===
291
292    /// Set the log format
293    #[must_use]
294    pub fn with_format(mut self, format: LogFormat) -> Self {
295        self.format = format;
296        self
297    }
298
299    /// Use pretty formatted output (development style)
300    #[must_use]
301    pub fn with_pretty_format(self) -> Self {
302        self.with_format(LogFormat::Pretty)
303    }
304
305    /// Use JSON formatted output (production style)
306    #[must_use]
307    pub fn with_json_format(self) -> Self {
308        self.with_format(LogFormat::Json)
309    }
310
311    /// Use full formatted output
312    #[must_use]
313    pub fn with_full_format(self) -> Self {
314        self.with_format(LogFormat::Full)
315    }
316
317    /// Use compact formatted output
318    #[must_use]
319    pub fn with_compact_format(self) -> Self {
320        self.with_format(LogFormat::Compact)
321    }
322
323    /// Use logfmt formatted output (requires 'logfmt' feature)
324    #[must_use]
325    #[cfg(feature = "logfmt")]
326    pub fn with_logfmt_format(self) -> Self {
327        self.with_format(LogFormat::Logfmt)
328    }
329
330    // === Writer Configuration ===
331
332    /// Set the output writer
333    #[must_use]
334    pub fn with_writer(mut self, writer: WriterConfig) -> Self {
335        self.writer = writer;
336        self
337    }
338
339    /// Write logs to stdout
340    #[must_use]
341    pub fn with_stdout(self) -> Self {
342        self.with_writer(WriterConfig::Stdout)
343    }
344
345    /// Write logs to stderr
346    #[must_use]
347    pub fn with_stderr(self) -> Self {
348        self.with_writer(WriterConfig::Stderr)
349    }
350
351    /// Write logs to a file
352    #[must_use]
353    pub fn with_file<P: AsRef<Path>>(self, path: P) -> Self {
354        self.with_writer(WriterConfig::File(path.as_ref().to_path_buf()))
355    }
356
357    // === Level Configuration ===
358
359    /// Set log directives (takes precedence over environment variables),
360    /// for example if you want to set it from cli arguments (verbosity)
361    #[must_use]
362    pub fn with_log_directives(mut self, directives: impl Into<String>) -> Self {
363        self.level_config.directives = directives.into();
364        self
365    }
366
367    /// Set the default log level
368    #[must_use]
369    pub fn with_default_level(mut self, level: LevelFilter) -> Self {
370        self.level_config.default_level = level;
371        self
372    }
373
374    /// Add an environment variable fallback for log configuration
375    #[must_use]
376    pub fn with_env_fallback(mut self, env_var: impl Into<String>) -> Self {
377        self.level_config.env_fallbacks.push(env_var.into());
378        self
379    }
380
381    /// Set the OpenTelemetry trace level
382    #[must_use]
383    pub fn with_otel_trace_level(mut self, level: LevelFilter) -> Self {
384        self.level_config.otel_trace_level = level;
385        self
386    }
387
388    // === Feature Configuration ===
389
390    /// Enable or disable file names in output
391    #[must_use]
392    pub fn with_file_names(mut self, enabled: bool) -> Self {
393        self.features.file_names = enabled;
394        self
395    }
396
397    /// Enable or disable line numbers in output
398    #[must_use]
399    pub fn with_line_numbers(mut self, enabled: bool) -> Self {
400        self.features.line_numbers = enabled;
401        self
402    }
403
404    /// Enable or disable thread names in output
405    #[must_use]
406    pub fn with_thread_names(mut self, enabled: bool) -> Self {
407        self.features.thread_names = enabled;
408        self
409    }
410
411    /// Enable or disable thread IDs in output
412    #[must_use]
413    pub fn with_thread_ids(mut self, enabled: bool) -> Self {
414        self.features.thread_ids = enabled;
415        self
416    }
417
418    /// Configure span event logging
419    #[must_use]
420    pub fn with_span_events(mut self, events: FmtSpan) -> Self {
421        self.features.span_events = Some(events);
422        self
423    }
424
425    /// Disable span event logging
426    #[must_use]
427    pub fn without_span_events(mut self) -> Self {
428        self.features.span_events = None;
429        self
430    }
431
432    /// Enable or disable uptime timer (vs wall clock)
433    #[must_use]
434    #[deprecated = "Use `TracingConfig::with_timer` instead"]
435    pub fn with_uptime_timer(mut self, enabled: bool) -> Self {
436        self.features.timer = if enabled {
437            LogTimer::Uptime
438        } else {
439            LogTimer::Time
440        };
441        self
442    }
443
444    /// Configure time logging (wall clock, uptime or none)
445    #[must_use]
446    pub fn with_timer(mut self, timer: LogTimer) -> Self {
447        self.features.timer = timer;
448        self
449    }
450
451    /// Enable or disable target display
452    #[must_use]
453    pub fn with_target_display(mut self, enabled: bool) -> Self {
454        self.features.target_display = enabled;
455        self
456    }
457
458    // === OpenTelemetry Configuration ===
459
460    /// Enable or disable OpenTelemetry tracing
461    #[must_use]
462    pub fn with_otel(mut self, enabled: bool) -> Self {
463        self.otel_config.enabled = enabled;
464        self
465    }
466
467    /// Enable or disable metrics collection
468    #[must_use]
469    pub fn with_metrics(mut self, enabled: bool) -> Self {
470        self.otel_config.metrics_enabled = enabled;
471        self
472    }
473
474    /// Set resource configuration for OpenTelemetry
475    #[must_use]
476    pub fn with_resource_config(mut self, config: DetectResource) -> Self {
477        self.otel_config.resource_config = Some(config);
478        self
479    }
480
481    /// Set whether to initialize the subscriber as global default
482    ///
483    /// When `global` is true (default), the subscriber is set as the global default.
484    /// When false, the subscriber is set as thread-local default and the returned
485    /// Guard must be kept alive to maintain the subscriber.
486    #[must_use]
487    pub fn with_global_subscriber(mut self, global: bool) -> Self {
488        self.global_subscriber = global;
489        self
490    }
491
492    // === Build Methods ===
493
494    /// Build a tracing layer with the current configuration
495    pub fn build_layer<S>(&self) -> Result<Box<dyn Layer<S> + Send + Sync + 'static>, Error>
496    where
497        S: Subscriber + for<'a> LookupSpan<'a>,
498    {
499        match &self.format {
500            LogFormat::Pretty => PrettyLayerBuilder.build_layer(self),
501            LogFormat::Json => JsonLayerBuilder.build_layer(self),
502            LogFormat::Full => FullLayerBuilder.build_layer(self),
503            LogFormat::Compact => CompactLayerBuilder.build_layer(self),
504            #[cfg(feature = "logfmt")]
505            LogFormat::Logfmt => LogfmtLayerBuilder.build_layer(self),
506        }
507    }
508
509    /// Build a level filter layer with the current configuration
510    pub fn build_filter_layer(&self) -> Result<EnvFilter, Error> {
511        // Use existing function but with our configuration
512        let dirs = if self.level_config.directives.is_empty() {
513            // Try environment variables in order
514            self.level_config
515                .env_fallbacks
516                .iter()
517                .find_map(|var| std::env::var(var).ok())
518                .unwrap_or_else(|| self.level_config.default_level.to_string().to_lowercase())
519        } else {
520            self.level_config.directives.clone()
521        };
522
523        let directive_to_allow_otel_trace = format!(
524            "otel::tracing={}",
525            self.level_config
526                .otel_trace_level
527                .to_string()
528                .to_lowercase()
529        )
530        .parse()?;
531
532        Ok(EnvFilter::builder()
533            .with_default_directive(self.level_config.default_level.into())
534            .parse_lossy(dirs)
535            .add_directive(directive_to_allow_otel_trace))
536    }
537
538    /// Initialize the tracing subscriber with this configuration
539    ///
540    /// If `global_subscriber` is true, sets the subscriber as the global default.
541    /// If false, returns a Guard that maintains the subscriber as the thread-local default.
542    ///
543    /// When OpenTelemetry is disabled, the Guard will contain `None` for the `OtelGuard`.
544    pub fn init_subscriber(self) -> Result<Guard, Error> {
545        self.init_subscriber_ext(Self::transform_identity)
546    }
547
548    fn transform_identity(s: Registry) -> Registry {
549        s
550    }
551
552    /// `transform` parameter allow to customize the registry/subscriber before
553    /// the setup of opentelemetry, log, logfilter.
554    /// ```text
555    /// let guard = TracingConfig::default()
556    ///    .with_json_format()
557    ///    .with_stderr()
558    ///    .init_subscriber_ext(|subscriber| subscriber.with(my_layer))?;
559    /// ```
560    pub fn init_subscriber_ext<F, SOut>(self, transform: F) -> Result<Guard, Error>
561    where
562        SOut: Subscriber + for<'a> LookupSpan<'a> + Send + Sync,
563        F: FnOnce(Registry) -> SOut,
564    {
565        // Setup a temporary subscriber for initialization logging
566        let temp_subscriber = tracing_subscriber::registry()
567            .with(self.build_layer()?)
568            .with(self.build_filter_layer()?);
569        let _guard = tracing::subscriber::set_default(temp_subscriber);
570        info!("init logging & tracing");
571
572        // Build the final subscriber based on OTEL configuration
573        if self.otel_config.enabled {
574            let subscriber = transform(tracing_subscriber::registry());
575            let layer = self.build_layer()?;
576            let filter_layer = self.build_filter_layer()?;
577            let (subscriber, otel_guard) = register_otel_layers_with_resource(
578                subscriber,
579                self.otel_config.resource_config.unwrap_or_default().build(),
580            )?;
581            let subscriber = subscriber.with(layer).with(filter_layer);
582
583            if self.global_subscriber {
584                tracing::subscriber::set_global_default(subscriber)?;
585                Ok(Guard::global(Some(otel_guard)))
586            } else {
587                let default_guard = tracing::subscriber::set_default(subscriber);
588                Ok(Guard::non_global(Some(otel_guard), default_guard))
589            }
590        } else {
591            info!("OpenTelemetry disabled - proceeding without OTEL layers");
592            let subscriber = transform(tracing_subscriber::registry())
593                .with(self.build_layer()?)
594                .with(self.build_filter_layer()?);
595
596            if self.global_subscriber {
597                tracing::subscriber::set_global_default(subscriber)?;
598                Ok(Guard::global(None))
599            } else {
600                let default_guard = tracing::subscriber::set_default(subscriber);
601                Ok(Guard::non_global(None, default_guard))
602            }
603        }
604    }
605
606    // === Preset Configurations ===
607
608    /// Configuration preset for development environments
609    /// - Pretty formatting with colors
610    /// - Output to stderr
611    /// - Line numbers and thread names enabled
612    /// - Span events for NEW and CLOSE
613    /// - Full OpenTelemetry integration
614    #[must_use]
615    pub fn development() -> Self {
616        Self::default()
617            .with_pretty_format()
618            .with_stderr()
619            .with_line_numbers(true)
620            .with_thread_names(true)
621            .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
622            .with_otel(true)
623    }
624
625    /// Configuration preset for production environments
626    /// - JSON formatting for structured logging
627    /// - Output to stdout
628    /// - Minimal metadata (no line numbers or thread names)
629    /// - No span events to reduce verbosity
630    /// - Full OpenTelemetry integration
631    #[must_use]
632    pub fn production() -> Self {
633        Self::default()
634            .with_json_format()
635            .with_stdout()
636            .with_line_numbers(false)
637            .with_thread_names(false)
638            .without_span_events()
639            .with_otel(true)
640    }
641
642    /// Configuration preset for debugging
643    /// - Pretty formatting with full verbosity
644    /// - Output to stderr
645    /// - All metadata enabled
646    /// - Full span events
647    /// - Debug level logging
648    /// - Full OpenTelemetry integration
649    #[must_use]
650    pub fn debug() -> Self {
651        Self::development()
652            .with_log_directives("debug")
653            .with_span_events(FmtSpan::FULL)
654            .with_target_display(true)
655    }
656
657    /// Configuration preset for minimal logging
658    /// - Compact formatting
659    /// - Output to stdout
660    /// - No metadata or extra features
661    /// - OpenTelemetry disabled for minimal overhead
662    #[must_use]
663    pub fn minimal() -> Self {
664        Self::default()
665            .with_compact_format()
666            .with_stdout()
667            .with_line_numbers(false)
668            .with_thread_names(false)
669            .without_span_events()
670            .with_target_display(false)
671            .with_otel(false)
672    }
673
674    /// Configuration preset for testing environments
675    /// - Compact formatting for less noise
676    /// - Output to stderr to separate from test output
677    /// - Basic metadata
678    /// - OpenTelemetry disabled for speed
679    /// - non global registration (of subscriber)
680    #[must_use]
681    pub fn testing() -> Self {
682        Self::default()
683            .with_compact_format()
684            .with_stderr()
685            .with_line_numbers(false)
686            .with_thread_names(false)
687            .without_span_events()
688            .with_otel(false)
689            .with_global_subscriber(false)
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[test]
698    fn test_global_subscriber_true_returns_global_guard() {
699        let config = TracingConfig::minimal()
700            .with_global_subscriber(true)
701            .with_otel(false); // Disable for simple test
702
703        // This would actually initialize the subscriber, so we'll just test that
704        // the config has the right value
705        assert!(config.global_subscriber);
706    }
707
708    #[test]
709    fn test_global_subscriber_false_sets_config() {
710        let config = TracingConfig::minimal()
711            .with_global_subscriber(false)
712            .with_otel(false); // Disable for simple test
713
714        assert!(!config.global_subscriber);
715    }
716
717    #[test]
718    fn test_default_global_subscriber_is_true() {
719        let config = TracingConfig::default();
720        assert!(config.global_subscriber);
721    }
722
723    #[test]
724    fn test_init_subscriber_without_otel_succeeds() {
725        // Test that initialization succeeds when OTEL is disabled
726        let guard = TracingConfig::minimal()
727            .with_otel(false)
728            .with_global_subscriber(false) // Use non-global to avoid affecting other tests
729            .init_subscriber();
730
731        assert!(guard.is_ok());
732        let guard = guard.unwrap();
733
734        // Verify that the guard indicates no OTEL
735        assert!(!guard.has_otel());
736        assert!(guard.otel_guard().is_none());
737    }
738
739    #[test]
740    fn test_init_subscriber_with_otel_disabled_global() {
741        // Test global subscriber mode with OTEL disabled
742        let guard = TracingConfig::minimal()
743            .with_otel(false)
744            .with_global_subscriber(true)
745            .init_subscriber();
746
747        assert!(guard.is_ok());
748        let guard = guard.unwrap();
749
750        // Should be global mode with no OTEL
751        assert!(guard.is_global());
752        assert!(!guard.has_otel());
753        assert!(guard.otel_guard().is_none());
754    }
755
756    #[test]
757    fn test_init_subscriber_with_otel_disabled_non_global() {
758        // Test non-global subscriber mode with OTEL disabled
759        let guard = TracingConfig::minimal()
760            .with_otel(false)
761            .with_global_subscriber(false)
762            .init_subscriber();
763
764        assert!(guard.is_ok());
765        let guard = guard.unwrap();
766
767        // Should be non-global mode with no OTEL
768        assert!(guard.is_non_global());
769        assert!(!guard.has_otel());
770        assert!(guard.otel_guard().is_none());
771    }
772
773    #[test]
774    fn test_guard_helper_methods() {
775        // Test the Guard helper methods work correctly with None values
776        let guard_global_none = Guard::global(None);
777        assert!(!guard_global_none.has_otel());
778        assert!(guard_global_none.otel_guard().is_none());
779        assert!(guard_global_none.is_global());
780        assert!(!guard_global_none.is_non_global());
781        assert!(guard_global_none.default_guard.is_none());
782
783        // We can't easily create a DefaultGuard for testing, but we can test the constructor
784        // Note: We can't actually create a DefaultGuard without setting up a real subscriber,
785        // so we'll just test the struct design is sound
786    }
787
788    #[test]
789    fn test_guard_struct_direct_field_access() {
790        // Test that we can directly access fields, which is a benefit of the struct design
791        let guard = Guard::global(None);
792
793        // Direct field access is now possible
794        assert!(guard.otel_guard.is_none());
795        assert!(guard.default_guard.is_none());
796
797        // Helper methods still work
798        assert!(!guard.has_otel());
799        assert!(guard.is_global());
800    }
801
802    #[test]
803    fn test_guard_struct_extensibility() {
804        // This test demonstrates how the struct design makes it easier to extend
805        // We can easily add more optional guards in the future without breaking existing code
806        let guard = Guard {
807            otel_guard: None,
808            default_guard: None,
809            // Future: log_guard: None, metrics_guard: None, etc.
810        };
811
812        assert!(guard.is_global());
813        assert!(!guard.has_otel());
814    }
815
816    #[tokio::test]
817    async fn test_init_with_transform() {
818        use std::time::Duration;
819        use tokio_blocked::TokioBlockedLayer;
820        let blocked =
821            TokioBlockedLayer::new().with_warn_busy_single_poll(Some(Duration::from_micros(150)));
822
823        let guard = TracingConfig::default()
824            .with_json_format()
825            .with_stderr()
826            .with_log_directives("debug")
827            .with_global_subscriber(false)
828            .init_subscriber_ext(|subscriber| subscriber.with(blocked))
829            .unwrap();
830
831        assert!(!guard.is_global());
832        assert!(guard.has_otel());
833    }
834}