Skip to content

Commit c126cb6

Browse files
committed
feat!: Guard struct allow future evolution, init_subscriber can be used for non global (like test,...)
1 parent 68fd718 commit c126cb6

File tree

3 files changed

+311
-32
lines changed

3 files changed

+311
-32
lines changed

init-tracing-opentelemetry/src/config.rs

Lines changed: 271 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,35 @@
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
3252
use crate::tracing_subscriber_ext::regiter_otel_layers;
3353
use 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)]
37118
pub 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)]
151232
pub 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

164260
impl 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
}

init-tracing-opentelemetry/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ fn propagator_from_string(
120120

121121
// Re-export the new configuration API for easier access
122122
#[cfg(feature = "tracing_subscriber_ext")]
123-
pub use config::{FeatureSet, LevelConfig, LogFormat, OtelConfig, TracingConfig, WriterConfig};
123+
pub use config::{
124+
FeatureSet, Guard, LevelConfig, LogFormat, OtelConfig, TracingConfig, WriterConfig,
125+
};
124126

125127
#[cfg(feature = "tracing_subscriber_ext")]
126128
pub use formats::{CompactLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder};

0 commit comments

Comments
 (0)