77//! ```no_run
88//! use init_tracing_opentelemetry::TracingConfig;
99//!
10- //! // Use preset
10+ //! // Use preset with global subscriber (default)
1111//! let _guard = TracingConfig::development().init_subscriber()?;
1212//!
13- //! // Custom configuration
13+ //! // Custom configuration with global subscriber
1414//! let _guard = TracingConfig::default()
1515//! .with_json_format()
1616//! .with_stderr()
1717//! .with_log_directives("debug")
1818//! .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+ //! }
1939//! # Ok::<(), Box<dyn std::error::Error>>(())
2040//! ```
2141
@@ -32,6 +52,67 @@ use crate::formats::{CompactLayerBuilder, JsonLayerBuilder, LayerBuilder, Pretty
3252use crate :: tracing_subscriber_ext:: regiter_otel_layers;
3353use crate :: { otlp:: OtelGuard , resource:: DetectResource , Error } ;
3454
55+ /// Combined guard that handles both `OtelGuard` and optional `DefaultGuard`
56+ ///
57+ /// This struct holds the various guards needed to maintain the tracing subscriber.
58+ /// - `otel_guard`: OpenTelemetry guard for flushing traces/metrics on drop (None when OTEL disabled)
59+ /// - `default_guard`: Subscriber default guard for non-global subscribers (None when using global)
60+ #[ must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/log/metrics are sent to the server and subscriber is maintained" ]
61+ pub struct Guard {
62+ /// OpenTelemetry guard for proper cleanup (None when OTEL is disabled)
63+ pub otel_guard : Option < OtelGuard > ,
64+ /// Default subscriber guard for non-global mode (None when using global subscriber)
65+ pub default_guard : Option < tracing:: subscriber:: DefaultGuard > ,
66+ // Easy to add in the future:
67+ // pub log_guard: Option<LogGuard>,
68+ // pub metrics_guard: Option<MetricsGuard>,
69+ }
70+
71+ impl Guard {
72+ /// Create a new Guard for global subscriber mode
73+ pub fn global ( otel_guard : Option < OtelGuard > ) -> Self {
74+ Self {
75+ otel_guard,
76+ default_guard : None ,
77+ }
78+ }
79+
80+ /// Create a new Guard for non-global subscriber mode
81+ pub fn non_global (
82+ otel_guard : Option < OtelGuard > ,
83+ default_guard : tracing:: subscriber:: DefaultGuard ,
84+ ) -> Self {
85+ Self {
86+ otel_guard,
87+ default_guard : Some ( default_guard) ,
88+ }
89+ }
90+
91+ /// Get a reference to the underlying `OtelGuard` if present
92+ #[ must_use]
93+ pub fn otel_guard ( & self ) -> Option < & OtelGuard > {
94+ self . otel_guard . as_ref ( )
95+ }
96+
97+ /// Check if OpenTelemetry is enabled for this guard
98+ #[ must_use]
99+ pub fn has_otel ( & self ) -> bool {
100+ self . otel_guard . is_some ( )
101+ }
102+
103+ /// Check if this guard is managing a non-global (thread-local) subscriber
104+ #[ must_use]
105+ pub fn is_non_global ( & self ) -> bool {
106+ self . default_guard . is_some ( )
107+ }
108+
109+ /// Check if this guard is for a global subscriber
110+ #[ must_use]
111+ pub fn is_global ( & self ) -> bool {
112+ self . default_guard . is_none ( )
113+ }
114+ }
115+
35116/// Configuration for log output format
36117#[ derive( Debug , Clone ) ]
37118pub enum LogFormat {
@@ -147,7 +228,7 @@ impl Default for OtelConfig {
147228
148229/// Main configuration builder for tracing setup
149230/// Default create a new tracing configuration with sensible defaults
150- #[ derive( Debug , Default ) ]
231+ #[ derive( Debug ) ]
151232pub struct TracingConfig {
152233 /// Output format configuration
153234 pub format : LogFormat ,
@@ -159,6 +240,21 @@ pub struct TracingConfig {
159240 pub features : FeatureSet ,
160241 /// OpenTelemetry configuration
161242 pub otel_config : OtelConfig ,
243+ /// Whether to set the subscriber as global default
244+ pub global_subscriber : bool ,
245+ }
246+
247+ impl Default for TracingConfig {
248+ fn default ( ) -> Self {
249+ Self {
250+ format : LogFormat :: default ( ) ,
251+ writer : WriterConfig :: default ( ) ,
252+ level_config : LevelConfig :: default ( ) ,
253+ features : FeatureSet :: default ( ) ,
254+ otel_config : OtelConfig :: default ( ) ,
255+ global_subscriber : true ,
256+ }
257+ }
162258}
163259
164260impl TracingConfig {
@@ -321,6 +417,17 @@ impl TracingConfig {
321417 self
322418 }
323419
420+ /// Set whether to initialize the subscriber as global default
421+ ///
422+ /// When `global` is true (default), the subscriber is set as the global default.
423+ /// When false, the subscriber is set as thread-local default and the returned
424+ /// Guard must be kept alive to maintain the subscriber.
425+ #[ must_use]
426+ pub fn with_global_subscriber ( mut self , global : bool ) -> Self {
427+ self . global_subscriber = global;
428+ self
429+ }
430+
324431 // === Build Methods ===
325432
326433 /// Build a tracing layer with the current configuration
@@ -366,35 +473,49 @@ impl TracingConfig {
366473 . add_directive ( directive_to_allow_otel_trace) )
367474 }
368475
369- /// Initialize the global tracing subscriber with this configuration
370- pub fn init_subscriber ( self ) -> Result < OtelGuard , Error > {
476+ /// Initialize the tracing subscriber with this configuration
477+ ///
478+ /// If `global_subscriber` is true, sets the subscriber as the global default.
479+ /// If false, returns a Guard that maintains the subscriber as the thread-local default.
480+ ///
481+ /// When OpenTelemetry is disabled, the Guard will contain `None` for the `OtelGuard`.
482+ pub fn init_subscriber ( self ) -> Result < Guard , Error > {
371483 // Setup a temporary subscriber for initialization logging
372484 let temp_subscriber = tracing_subscriber:: registry ( )
373485 . with ( self . build_filter_layer ( ) ?)
374486 . with ( self . build_layer ( ) ?) ;
375487 let _guard = tracing:: subscriber:: set_default ( temp_subscriber) ;
376488 info ! ( "init logging & tracing" ) ;
377489
378- // Build the final subscriber
379- let subscriber = tracing_subscriber:: registry ( ) ;
380- let ( subscriber, otel_guard) = if self . otel_config . enabled {
381- regiter_otel_layers ( subscriber) ?
490+ // Build the final subscriber based on OTEL configuration
491+ if self . otel_config . enabled {
492+ let subscriber = tracing_subscriber:: registry ( ) ;
493+ let ( subscriber, otel_guard) = regiter_otel_layers ( subscriber) ?;
494+ let subscriber = subscriber
495+ . with ( self . build_filter_layer ( ) ?)
496+ . with ( self . build_layer ( ) ?) ;
497+
498+ if self . global_subscriber {
499+ tracing:: subscriber:: set_global_default ( subscriber) ?;
500+ Ok ( Guard :: global ( Some ( otel_guard) ) )
501+ } else {
502+ let default_guard = tracing:: subscriber:: set_default ( subscriber) ;
503+ Ok ( Guard :: non_global ( Some ( otel_guard) , default_guard) )
504+ }
382505 } else {
383- // Create a dummy OtelGuard for the case when OTEL is disabled
384- // This will require modifying OtelGuard to handle this case
385- return Err ( std:: io:: Error :: new (
386- std:: io:: ErrorKind :: Unsupported ,
387- "OpenTelemetry disabled - OtelGuard creation not yet supported" ,
388- )
389- . into ( ) ) ;
390- } ;
391-
392- let subscriber = subscriber
393- . with ( self . build_filter_layer ( ) ?)
394- . with ( self . build_layer ( ) ?) ;
395-
396- tracing:: subscriber:: set_global_default ( subscriber) ?;
397- Ok ( otel_guard)
506+ info ! ( "OpenTelemetry disabled - proceeding without OTEL layers" ) ;
507+ let subscriber = tracing_subscriber:: registry ( )
508+ . with ( self . build_filter_layer ( ) ?)
509+ . with ( self . build_layer ( ) ?) ;
510+
511+ if self . global_subscriber {
512+ tracing:: subscriber:: set_global_default ( subscriber) ?;
513+ Ok ( Guard :: global ( None ) )
514+ } else {
515+ let default_guard = tracing:: subscriber:: set_default ( subscriber) ;
516+ Ok ( Guard :: non_global ( None , default_guard) )
517+ }
518+ }
398519 }
399520
400521 // === Preset Configurations ===
@@ -470,6 +591,7 @@ impl TracingConfig {
470591 /// - Output to stderr to separate from test output
471592 /// - Basic metadata
472593 /// - OpenTelemetry disabled for speed
594+ /// - non global registration (of subscriber)
473595 #[ must_use]
474596 pub fn testing ( ) -> Self {
475597 Self :: default ( )
@@ -479,5 +601,130 @@ impl TracingConfig {
479601 . with_thread_names ( false )
480602 . without_span_events ( )
481603 . with_otel ( false )
604+ . with_global_subscriber ( false )
605+ }
606+ }
607+
608+ #[ cfg( test) ]
609+ mod tests {
610+ use super :: * ;
611+
612+ #[ test]
613+ fn test_global_subscriber_true_returns_global_guard ( ) {
614+ let config = TracingConfig :: minimal ( )
615+ . with_global_subscriber ( true )
616+ . with_otel ( false ) ; // Disable for simple test
617+
618+ // This would actually initialize the subscriber, so we'll just test that
619+ // the config has the right value
620+ assert ! ( config. global_subscriber) ;
621+ }
622+
623+ #[ test]
624+ fn test_global_subscriber_false_sets_config ( ) {
625+ let config = TracingConfig :: minimal ( )
626+ . with_global_subscriber ( false )
627+ . with_otel ( false ) ; // Disable for simple test
628+
629+ assert ! ( !config. global_subscriber) ;
630+ }
631+
632+ #[ test]
633+ fn test_default_global_subscriber_is_true ( ) {
634+ let config = TracingConfig :: default ( ) ;
635+ assert ! ( config. global_subscriber) ;
636+ }
637+
638+ #[ test]
639+ fn test_init_subscriber_without_otel_succeeds ( ) {
640+ // Test that initialization succeeds when OTEL is disabled
641+ let guard = TracingConfig :: minimal ( )
642+ . with_otel ( false )
643+ . with_global_subscriber ( false ) // Use non-global to avoid affecting other tests
644+ . init_subscriber ( ) ;
645+
646+ assert ! ( guard. is_ok( ) ) ;
647+ let guard = guard. unwrap ( ) ;
648+
649+ // Verify that the guard indicates no OTEL
650+ assert ! ( !guard. has_otel( ) ) ;
651+ assert ! ( guard. otel_guard( ) . is_none( ) ) ;
652+ }
653+
654+ #[ test]
655+ fn test_init_subscriber_with_otel_disabled_global ( ) {
656+ // Test global subscriber mode with OTEL disabled
657+ let guard = TracingConfig :: minimal ( )
658+ . with_otel ( false )
659+ . with_global_subscriber ( true )
660+ . init_subscriber ( ) ;
661+
662+ assert ! ( guard. is_ok( ) ) ;
663+ let guard = guard. unwrap ( ) ;
664+
665+ // Should be global mode with no OTEL
666+ assert ! ( guard. is_global( ) ) ;
667+ assert ! ( !guard. has_otel( ) ) ;
668+ assert ! ( guard. otel_guard( ) . is_none( ) ) ;
669+ }
670+
671+ #[ test]
672+ fn test_init_subscriber_with_otel_disabled_non_global ( ) {
673+ // Test non-global subscriber mode with OTEL disabled
674+ let guard = TracingConfig :: minimal ( )
675+ . with_otel ( false )
676+ . with_global_subscriber ( false )
677+ . init_subscriber ( ) ;
678+
679+ assert ! ( guard. is_ok( ) ) ;
680+ let guard = guard. unwrap ( ) ;
681+
682+ // Should be non-global mode with no OTEL
683+ assert ! ( guard. is_non_global( ) ) ;
684+ assert ! ( !guard. has_otel( ) ) ;
685+ assert ! ( guard. otel_guard( ) . is_none( ) ) ;
686+ }
687+
688+ #[ test]
689+ fn test_guard_helper_methods ( ) {
690+ // Test the Guard helper methods work correctly with None values
691+ let guard_global_none = Guard :: global ( None ) ;
692+ assert ! ( !guard_global_none. has_otel( ) ) ;
693+ assert ! ( guard_global_none. otel_guard( ) . is_none( ) ) ;
694+ assert ! ( guard_global_none. is_global( ) ) ;
695+ assert ! ( !guard_global_none. is_non_global( ) ) ;
696+ assert ! ( guard_global_none. default_guard. is_none( ) ) ;
697+
698+ // We can't easily create a DefaultGuard for testing, but we can test the constructor
699+ // Note: We can't actually create a DefaultGuard without setting up a real subscriber,
700+ // so we'll just test the struct design is sound
701+ }
702+
703+ #[ test]
704+ fn test_guard_struct_direct_field_access ( ) {
705+ // Test that we can directly access fields, which is a benefit of the struct design
706+ let guard = Guard :: global ( None ) ;
707+
708+ // Direct field access is now possible
709+ assert ! ( guard. otel_guard. is_none( ) ) ;
710+ assert ! ( guard. default_guard. is_none( ) ) ;
711+
712+ // Helper methods still work
713+ assert ! ( !guard. has_otel( ) ) ;
714+ assert ! ( guard. is_global( ) ) ;
715+ }
716+
717+ #[ test]
718+ fn test_guard_struct_extensibility ( ) {
719+ // This test demonstrates how the struct design makes it easier to extend
720+ // We can easily add more optional guards in the future without breaking existing code
721+ let guard = Guard {
722+ otel_guard : None ,
723+ default_guard : None ,
724+ // Future: log_guard: None, metrics_guard: None, etc.
725+ } ;
726+
727+ assert ! ( guard. is_global( ) ) ;
728+ assert ! ( !guard. has_otel( ) ) ;
482729 }
483730}
0 commit comments