#![allow(deprecated)]
use std::path::{Path, PathBuf};
use tracing::{Subscriber, info, level_filters::LevelFilter};
use tracing_subscriber::{
Layer, Registry, filter::EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt,
registry::LookupSpan,
};
#[cfg(feature = "logfmt")]
use crate::formats::LogfmtLayerBuilder;
use crate::formats::{
CompactLayerBuilder, FullLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder,
};
#[cfg(feature = "logs")]
use crate::tracing_subscriber_ext::build_logger_layer_with_resource;
#[cfg(feature = "metrics")]
use crate::tracing_subscriber_ext::build_metrics_layer_with_resource;
use crate::tracing_subscriber_ext::build_tracer_layer_with_resource_and_name;
use crate::{Error, otlp::OtelGuard, resource::DetectResource};
#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/log/metrics are sent to the server and subscriber is maintained"]
pub struct Guard {
pub otel_guard: Option<OtelGuard>,
pub default_guard: Option<tracing::subscriber::DefaultGuard>,
}
impl Guard {
pub fn global(otel_guard: Option<OtelGuard>) -> Self {
Self {
otel_guard,
default_guard: None,
}
}
pub fn non_global(
otel_guard: Option<OtelGuard>,
default_guard: tracing::subscriber::DefaultGuard,
) -> Self {
Self {
otel_guard,
default_guard: Some(default_guard),
}
}
#[must_use]
pub fn otel_guard(&self) -> Option<&OtelGuard> {
self.otel_guard.as_ref()
}
#[must_use]
pub fn has_otel(&self) -> bool {
self.otel_guard.is_some()
}
#[must_use]
pub fn is_non_global(&self) -> bool {
self.default_guard.is_some()
}
#[must_use]
pub fn is_global(&self) -> bool {
self.default_guard.is_none()
}
}
#[derive(Debug, Clone)]
pub enum LogFormat {
Pretty,
Json,
Full,
Compact,
#[cfg(feature = "logfmt")]
Logfmt,
}
impl Default for LogFormat {
fn default() -> Self {
if cfg!(debug_assertions) {
LogFormat::Pretty
} else {
LogFormat::Json
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogTimer {
None,
Time,
Uptime,
}
impl Default for LogTimer {
fn default() -> Self {
if cfg!(debug_assertions) {
LogTimer::Uptime
} else {
LogTimer::Time
}
}
}
#[derive(Debug, Clone, Default)]
pub enum WriterConfig {
#[default]
Stdout,
Stderr,
File(PathBuf),
}
#[derive(Debug, Clone)]
pub struct LevelConfig {
pub directives: String,
pub env_fallbacks: Vec<String>,
pub default_level: LevelFilter,
pub otel_trace_level: LevelFilter,
}
impl Default for LevelConfig {
fn default() -> Self {
Self {
directives: String::new(),
env_fallbacks: vec!["RUST_LOG".to_string(), "OTEL_LOG_LEVEL".to_string()],
default_level: LevelFilter::INFO,
otel_trace_level: LevelFilter::TRACE,
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct FeatureSet {
pub file_names: bool,
pub line_numbers: bool,
pub thread_names: bool,
pub thread_ids: bool,
pub timer: LogTimer,
pub span_events: Option<FmtSpan>,
pub target_display: bool,
}
impl Default for FeatureSet {
fn default() -> Self {
Self {
file_names: true,
line_numbers: cfg!(debug_assertions),
thread_names: cfg!(debug_assertions),
thread_ids: false,
timer: LogTimer::default(),
span_events: if cfg!(debug_assertions) {
Some(FmtSpan::NEW | FmtSpan::CLOSE)
} else {
None
},
target_display: true,
}
}
}
#[derive(Debug)]
pub struct OtelConfig {
pub enabled: bool,
pub resource_config: Option<DetectResource>,
pub logs_enabled: bool,
pub metrics_enabled: bool,
}
impl Default for OtelConfig {
fn default() -> Self {
Self {
enabled: true,
resource_config: None,
logs_enabled: cfg!(feature = "logs"),
metrics_enabled: cfg!(feature = "metrics"),
}
}
}
#[derive(Debug)]
pub struct TracingConfig {
pub format: LogFormat,
pub writer: WriterConfig,
pub level_config: LevelConfig,
pub features: FeatureSet,
pub otel_config: OtelConfig,
pub global_subscriber: bool,
pub tracer_name: String,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
format: LogFormat::default(),
writer: WriterConfig::default(),
level_config: LevelConfig::default(),
features: FeatureSet::default(),
otel_config: OtelConfig::default(),
global_subscriber: true,
tracer_name: String::new(),
}
}
}
impl TracingConfig {
#[must_use]
pub fn with_format(mut self, format: LogFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn with_pretty_format(self) -> Self {
self.with_format(LogFormat::Pretty)
}
#[must_use]
pub fn with_json_format(self) -> Self {
self.with_format(LogFormat::Json)
}
#[must_use]
pub fn with_full_format(self) -> Self {
self.with_format(LogFormat::Full)
}
#[must_use]
pub fn with_compact_format(self) -> Self {
self.with_format(LogFormat::Compact)
}
#[must_use]
#[cfg(feature = "logfmt")]
pub fn with_logfmt_format(self) -> Self {
self.with_format(LogFormat::Logfmt)
}
#[must_use]
pub fn with_writer(mut self, writer: WriterConfig) -> Self {
self.writer = writer;
self
}
#[must_use]
pub fn with_stdout(self) -> Self {
self.with_writer(WriterConfig::Stdout)
}
#[must_use]
pub fn with_stderr(self) -> Self {
self.with_writer(WriterConfig::Stderr)
}
#[must_use]
pub fn with_file<P: AsRef<Path>>(self, path: P) -> Self {
self.with_writer(WriterConfig::File(path.as_ref().to_path_buf()))
}
#[must_use]
pub fn with_log_directives(mut self, directives: impl Into<String>) -> Self {
self.level_config.directives = directives.into();
self
}
#[must_use]
pub fn with_default_level(mut self, level: LevelFilter) -> Self {
self.level_config.default_level = level;
self
}
#[must_use]
pub fn with_env_fallback(mut self, env_var: impl Into<String>) -> Self {
self.level_config.env_fallbacks.push(env_var.into());
self
}
#[must_use]
pub fn with_otel_trace_level(mut self, level: LevelFilter) -> Self {
self.level_config.otel_trace_level = level;
self
}
#[must_use]
pub fn with_otel_tracer_name(mut self, name: impl Into<String>) -> Self {
self.tracer_name = name.into();
self
}
#[must_use]
pub fn with_file_names(mut self, enabled: bool) -> Self {
self.features.file_names = enabled;
self
}
#[must_use]
pub fn with_line_numbers(mut self, enabled: bool) -> Self {
self.features.line_numbers = enabled;
self
}
#[must_use]
pub fn with_thread_names(mut self, enabled: bool) -> Self {
self.features.thread_names = enabled;
self
}
#[must_use]
pub fn with_thread_ids(mut self, enabled: bool) -> Self {
self.features.thread_ids = enabled;
self
}
#[must_use]
pub fn with_span_events(mut self, events: FmtSpan) -> Self {
self.features.span_events = Some(events);
self
}
#[must_use]
pub fn without_span_events(mut self) -> Self {
self.features.span_events = None;
self
}
#[must_use]
#[deprecated = "Use `TracingConfig::with_timer` instead"]
pub fn with_uptime_timer(mut self, enabled: bool) -> Self {
self.features.timer = if enabled {
LogTimer::Uptime
} else {
LogTimer::Time
};
self
}
#[must_use]
pub fn with_timer(mut self, timer: LogTimer) -> Self {
self.features.timer = timer;
self
}
#[must_use]
pub fn with_target_display(mut self, enabled: bool) -> Self {
self.features.target_display = enabled;
self
}
#[must_use]
pub fn with_otel(mut self, enabled: bool) -> Self {
self.otel_config.enabled = enabled;
self
}
#[must_use]
pub fn with_logs(mut self, enabled: bool) -> Self {
self.otel_config.logs_enabled = enabled;
self
}
#[must_use]
pub fn with_metrics(mut self, enabled: bool) -> Self {
self.otel_config.metrics_enabled = enabled;
self
}
#[must_use]
pub fn with_resource_config(mut self, config: DetectResource) -> Self {
self.otel_config.resource_config = Some(config);
self
}
#[must_use]
pub fn with_global_subscriber(mut self, global: bool) -> Self {
self.global_subscriber = global;
self
}
pub fn build_layer<S>(&self) -> Result<Box<dyn Layer<S> + Send + Sync + 'static>, Error>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
match &self.format {
LogFormat::Pretty => PrettyLayerBuilder.build_layer(self),
LogFormat::Json => JsonLayerBuilder.build_layer(self),
LogFormat::Full => FullLayerBuilder.build_layer(self),
LogFormat::Compact => CompactLayerBuilder.build_layer(self),
#[cfg(feature = "logfmt")]
LogFormat::Logfmt => LogfmtLayerBuilder.build_layer(self),
}
}
pub fn build_filter_layer(&self) -> Result<EnvFilter, Error> {
let dirs = if self.level_config.directives.is_empty() {
self.level_config
.env_fallbacks
.iter()
.find_map(|var| std::env::var(var).ok())
.unwrap_or_else(|| self.level_config.default_level.to_string().to_lowercase())
} else {
self.level_config.directives.clone()
};
let directive_to_allow_otel_trace = format!(
"otel::tracing={}",
self.level_config
.otel_trace_level
.to_string()
.to_lowercase()
)
.parse()?;
Ok(EnvFilter::builder()
.with_default_directive(self.level_config.default_level.into())
.parse_lossy(dirs)
.add_directive(directive_to_allow_otel_trace))
}
pub fn init_subscriber(self) -> Result<Guard, Error> {
self.init_subscriber_ext(Self::transform_identity)
}
fn transform_identity(s: Registry) -> Registry {
s
}
pub fn init_subscriber_ext<F, SOut>(self, transform: F) -> Result<Guard, Error>
where
SOut: Subscriber + for<'a> LookupSpan<'a> + Send + Sync,
F: FnOnce(Registry) -> SOut,
{
let temp_subscriber = tracing_subscriber::registry()
.with(self.build_layer()?)
.with(self.build_filter_layer()?);
let _guard = tracing::subscriber::set_default(temp_subscriber);
info!("init logging & tracing");
if self.otel_config.enabled {
let subscriber = transform(tracing_subscriber::registry());
let layer = self.build_layer()?;
let filter_layer = self.build_filter_layer()?;
let (subscriber, otel_guard) = self.register_otel_layers_with_resource(subscriber)?;
let subscriber = subscriber.with(layer).with(filter_layer);
if self.global_subscriber {
tracing::subscriber::set_global_default(subscriber)?;
Ok(Guard::global(Some(otel_guard)))
} else {
let default_guard = tracing::subscriber::set_default(subscriber);
Ok(Guard::non_global(Some(otel_guard), default_guard))
}
} else {
info!("OpenTelemetry disabled - proceeding without OTEL layers");
let subscriber = transform(tracing_subscriber::registry())
.with(self.build_layer()?)
.with(self.build_filter_layer()?);
if self.global_subscriber {
tracing::subscriber::set_global_default(subscriber)?;
Ok(Guard::global(None))
} else {
let default_guard = tracing::subscriber::set_default(subscriber);
Ok(Guard::non_global(None, default_guard))
}
}
}
fn register_otel_layers_with_resource<S>(
&self,
subscriber: S,
) -> Result<(impl Subscriber + for<'span> LookupSpan<'span>, OtelGuard), Error>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let otel_rsrc = self
.otel_config
.resource_config
.clone()
.unwrap_or_default()
.build();
#[cfg(feature = "logs")]
let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?;
#[cfg(feature = "metrics")]
let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?;
let (trace_layer, tracer_provider) =
build_tracer_layer_with_resource_and_name(otel_rsrc, self.tracer_name.clone())?;
let subscriber = subscriber.with(trace_layer);
#[cfg(feature = "logs")]
let subscriber = subscriber.with(self.otel_config.logs_enabled.then_some(logs_layer));
#[cfg(feature = "metrics")]
let subscriber = subscriber.with(self.otel_config.metrics_enabled.then_some(metrics_layer));
Ok((
subscriber,
OtelGuard {
#[cfg(feature = "logs")]
logger_provider,
#[cfg(feature = "metrics")]
meter_provider,
tracer_provider,
},
))
}
#[must_use]
pub fn development() -> Self {
Self::default()
.with_pretty_format()
.with_stderr()
.with_line_numbers(true)
.with_thread_names(true)
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_otel(true)
}
#[must_use]
pub fn production() -> Self {
Self::default()
.with_json_format()
.with_stdout()
.with_line_numbers(false)
.with_thread_names(false)
.without_span_events()
.with_otel(true)
}
#[must_use]
pub fn debug() -> Self {
Self::development()
.with_log_directives("debug")
.with_span_events(FmtSpan::FULL)
.with_target_display(true)
}
#[must_use]
pub fn minimal() -> Self {
Self::default()
.with_compact_format()
.with_stdout()
.with_line_numbers(false)
.with_thread_names(false)
.without_span_events()
.with_target_display(false)
.with_otel(false)
}
#[must_use]
pub fn testing() -> Self {
Self::default()
.with_compact_format()
.with_stderr()
.with_line_numbers(false)
.with_thread_names(false)
.without_span_events()
.with_otel(false)
.with_global_subscriber(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_subscriber_true_returns_global_guard() {
let config = TracingConfig::minimal()
.with_global_subscriber(true)
.with_otel(false);
assert!(config.global_subscriber);
}
#[test]
fn test_global_subscriber_false_sets_config() {
let config = TracingConfig::minimal()
.with_global_subscriber(false)
.with_otel(false);
assert!(!config.global_subscriber);
}
#[test]
fn test_default_global_subscriber_is_true() {
let config = TracingConfig::default();
assert!(config.global_subscriber);
}
#[test]
fn test_init_subscriber_without_otel_succeeds() {
let guard = TracingConfig::minimal()
.with_otel(false)
.with_global_subscriber(false) .init_subscriber();
assert!(guard.is_ok());
let guard = guard.unwrap();
assert!(!guard.has_otel());
assert!(guard.otel_guard().is_none());
}
#[test]
fn test_init_subscriber_with_otel_disabled_global() {
let guard = TracingConfig::minimal()
.with_otel(false)
.with_global_subscriber(true)
.init_subscriber();
assert!(guard.is_ok());
let guard = guard.unwrap();
assert!(guard.is_global());
assert!(!guard.has_otel());
assert!(guard.otel_guard().is_none());
}
#[test]
fn test_init_subscriber_with_otel_disabled_non_global() {
let guard = TracingConfig::minimal()
.with_otel(false)
.with_global_subscriber(false)
.init_subscriber();
assert!(guard.is_ok());
let guard = guard.unwrap();
assert!(guard.is_non_global());
assert!(!guard.has_otel());
assert!(guard.otel_guard().is_none());
}
#[test]
fn test_guard_helper_methods() {
let guard_global_none = Guard::global(None);
assert!(!guard_global_none.has_otel());
assert!(guard_global_none.otel_guard().is_none());
assert!(guard_global_none.is_global());
assert!(!guard_global_none.is_non_global());
assert!(guard_global_none.default_guard.is_none());
}
#[test]
fn test_guard_struct_direct_field_access() {
let guard = Guard::global(None);
assert!(guard.otel_guard.is_none());
assert!(guard.default_guard.is_none());
assert!(!guard.has_otel());
assert!(guard.is_global());
}
#[test]
fn test_guard_struct_extensibility() {
let guard = Guard {
otel_guard: None,
default_guard: None,
};
assert!(guard.is_global());
assert!(!guard.has_otel());
}
#[tokio::test]
async fn test_init_with_transform() {
use std::time::Duration;
use tokio_blocked::TokioBlockedLayer;
let blocked =
TokioBlockedLayer::new().with_warn_busy_single_poll(Some(Duration::from_micros(150)));
let guard = TracingConfig::default()
.with_json_format()
.with_stderr()
.with_log_directives("debug")
.with_global_subscriber(false)
.init_subscriber_ext(|subscriber| subscriber.with(blocked))
.unwrap();
assert!(!guard.is_global());
assert!(guard.has_otel());
}
}