init_tracing_opentelemetry/
config.rs1use 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#[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 pub otel_guard: Option<OtelGuard>,
68 pub default_guard: Option<tracing::subscriber::DefaultGuard>,
70 }
74
75impl Guard {
76 pub fn global(otel_guard: Option<OtelGuard>) -> Self {
78 Self {
79 otel_guard,
80 default_guard: None,
81 }
82 }
83
84 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 #[must_use]
97 pub fn otel_guard(&self) -> Option<&OtelGuard> {
98 self.otel_guard.as_ref()
99 }
100
101 #[must_use]
103 pub fn has_otel(&self) -> bool {
104 self.otel_guard.is_some()
105 }
106
107 #[must_use]
109 pub fn is_non_global(&self) -> bool {
110 self.default_guard.is_some()
111 }
112
113 #[must_use]
115 pub fn is_global(&self) -> bool {
116 self.default_guard.is_none()
117 }
118}
119
120#[derive(Debug, Clone)]
122pub enum LogFormat {
123 Pretty,
125 Json,
127 Full,
129 Compact,
131 #[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#[derive(Debug, Clone, Default)]
165pub enum WriterConfig {
166 #[default]
168 Stdout,
169 Stderr,
171 File(PathBuf),
173}
174
175#[derive(Debug, Clone)]
177pub struct LevelConfig {
178 pub directives: String,
180 pub env_fallbacks: Vec<String>,
182 pub default_level: LevelFilter,
184 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#[derive(Debug, Clone)]
201#[allow(clippy::struct_excessive_bools)]
202pub struct FeatureSet {
203 pub file_names: bool,
205 pub line_numbers: bool,
207 pub thread_names: bool,
209 pub thread_ids: bool,
211 pub timer: LogTimer,
213 pub span_events: Option<FmtSpan>,
215 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#[derive(Debug)]
239pub struct OtelConfig {
240 pub enabled: bool,
242 pub resource_config: Option<DetectResource>,
244 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#[derive(Debug)]
261pub struct TracingConfig {
262 pub format: LogFormat,
264 pub writer: WriterConfig,
266 pub level_config: LevelConfig,
268 pub features: FeatureSet,
270 pub otel_config: OtelConfig,
272 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 #[must_use]
294 pub fn with_format(mut self, format: LogFormat) -> Self {
295 self.format = format;
296 self
297 }
298
299 #[must_use]
301 pub fn with_pretty_format(self) -> Self {
302 self.with_format(LogFormat::Pretty)
303 }
304
305 #[must_use]
307 pub fn with_json_format(self) -> Self {
308 self.with_format(LogFormat::Json)
309 }
310
311 #[must_use]
313 pub fn with_full_format(self) -> Self {
314 self.with_format(LogFormat::Full)
315 }
316
317 #[must_use]
319 pub fn with_compact_format(self) -> Self {
320 self.with_format(LogFormat::Compact)
321 }
322
323 #[must_use]
325 #[cfg(feature = "logfmt")]
326 pub fn with_logfmt_format(self) -> Self {
327 self.with_format(LogFormat::Logfmt)
328 }
329
330 #[must_use]
334 pub fn with_writer(mut self, writer: WriterConfig) -> Self {
335 self.writer = writer;
336 self
337 }
338
339 #[must_use]
341 pub fn with_stdout(self) -> Self {
342 self.with_writer(WriterConfig::Stdout)
343 }
344
345 #[must_use]
347 pub fn with_stderr(self) -> Self {
348 self.with_writer(WriterConfig::Stderr)
349 }
350
351 #[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 #[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 #[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 #[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 #[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 #[must_use]
392 pub fn with_file_names(mut self, enabled: bool) -> Self {
393 self.features.file_names = enabled;
394 self
395 }
396
397 #[must_use]
399 pub fn with_line_numbers(mut self, enabled: bool) -> Self {
400 self.features.line_numbers = enabled;
401 self
402 }
403
404 #[must_use]
406 pub fn with_thread_names(mut self, enabled: bool) -> Self {
407 self.features.thread_names = enabled;
408 self
409 }
410
411 #[must_use]
413 pub fn with_thread_ids(mut self, enabled: bool) -> Self {
414 self.features.thread_ids = enabled;
415 self
416 }
417
418 #[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 #[must_use]
427 pub fn without_span_events(mut self) -> Self {
428 self.features.span_events = None;
429 self
430 }
431
432 #[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 #[must_use]
446 pub fn with_timer(mut self, timer: LogTimer) -> Self {
447 self.features.timer = timer;
448 self
449 }
450
451 #[must_use]
453 pub fn with_target_display(mut self, enabled: bool) -> Self {
454 self.features.target_display = enabled;
455 self
456 }
457
458 #[must_use]
462 pub fn with_otel(mut self, enabled: bool) -> Self {
463 self.otel_config.enabled = enabled;
464 self
465 }
466
467 #[must_use]
469 pub fn with_metrics(mut self, enabled: bool) -> Self {
470 self.otel_config.metrics_enabled = enabled;
471 self
472 }
473
474 #[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 #[must_use]
487 pub fn with_global_subscriber(mut self, global: bool) -> Self {
488 self.global_subscriber = global;
489 self
490 }
491
492 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 pub fn build_filter_layer(&self) -> Result<EnvFilter, Error> {
511 let dirs = if self.level_config.directives.is_empty() {
513 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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); 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); 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 let guard = TracingConfig::minimal()
727 .with_otel(false)
728 .with_global_subscriber(false) .init_subscriber();
730
731 assert!(guard.is_ok());
732 let guard = guard.unwrap();
733
734 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 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 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 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 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 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 }
787
788 #[test]
789 fn test_guard_struct_direct_field_access() {
790 let guard = Guard::global(None);
792
793 assert!(guard.otel_guard.is_none());
795 assert!(guard.default_guard.is_none());
796
797 assert!(!guard.has_otel());
799 assert!(guard.is_global());
800 }
801
802 #[test]
803 fn test_guard_struct_extensibility() {
804 let guard = Guard {
807 otel_guard: None,
808 default_guard: None,
809 };
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}