mod format;
mod level;
#[cfg(feature = "otel")]
mod otel;
use crate::tracing::format::*;
use std::env;
use std::fs::{self, File};
use std::io;
use std::path::PathBuf;
use std::sync::{Arc, atomic::Ordering};
use std::time::SystemTime;
use thiserror::Error;
use tracing::subscriber::set_global_default;
use tracing_chrome::{ChromeLayerBuilder, FlushGuard};
use tracing_subscriber::fmt;
use tracing_subscriber::{EnvFilter, prelude::*};
pub use crate::tracing::level::LogLevel;
#[cfg(feature = "otel")]
pub use crate::tracing::otel::OtelOptions;
pub use tracing::{
debug, debug_span, enabled, error, error_span, event, event_enabled, info, info_span,
instrument, span, span_enabled, trace, trace_span, warn, warn_span,
};
pub type TracingResult<T> = Result<T, TracingError>;
#[derive(Debug, Error)]
#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
pub enum TracingError {
#[cfg_attr(
feature = "miette",
diagnostic(code(app::tracing::create_log_dir_failed))
)]
#[error("Failed to create log directory.")]
CreateLogDirFailed {
#[source]
error: std::io::Error,
},
#[cfg_attr(
feature = "miette",
diagnostic(code(app::tracing::create_log_file_failed))
)]
#[error("Failed to create log file.")]
CreateLogFileFailed {
#[source]
error: std::io::Error,
},
#[cfg_attr(feature = "miette", diagnostic(code(app::tracing::log_level_invalid)))]
#[error("Invalid log level: {level}")]
LogLevelInvalid { level: String },
#[cfg(feature = "log-compat")]
#[cfg_attr(
feature = "miette",
diagnostic(code(app::tracing::intercept_log_failed))
)]
#[error("Failed to initialize log interceptor.")]
InterceptLogFailed {
#[source]
error: tracing_log::log_tracer::SetLoggerError,
},
#[cfg(feature = "otel")]
#[cfg_attr(
feature = "miette",
diagnostic(code(app::tracing::otlp_exporter_failed))
)]
#[error("Failed to initialize OTLP {signal} exporter.")]
OtlpExporterFailed {
signal: String,
#[source]
error: opentelemetry_otlp::ExporterBuildError,
},
}
#[derive(Debug)]
pub struct TracingOptions {
pub default_level: LogLevel,
pub dump_trace: bool,
pub filter_modules: Vec<String>,
#[cfg(feature = "log-compat")]
pub intercept_log: bool,
pub log_env: String,
pub log_file: Option<PathBuf>,
pub ndjson: bool,
#[cfg(feature = "otel")]
pub otel: OtelOptions,
pub show_spans: bool,
pub test_env: String,
}
impl Default for TracingOptions {
fn default() -> Self {
TracingOptions {
default_level: LogLevel::Info,
dump_trace: false,
filter_modules: vec![],
#[cfg(feature = "log-compat")]
intercept_log: true,
log_env: "STARBASE_LOG".into(),
log_file: None,
ndjson: false,
#[cfg(feature = "otel")]
otel: OtelOptions::default(),
show_spans: false,
test_env: "STARBASE_TEST".into(),
}
}
}
pub struct TracingGuard {
chrome_guard: Option<FlushGuard>,
log_file: Option<Arc<File>>,
#[cfg(feature = "otel")]
otel_guard: Option<otel::OtelGuard>,
}
#[instrument]
pub fn setup_tracing(options: TracingOptions) -> TracingResult<TracingGuard> {
TEST_ENV.store(env::var(options.test_env).is_ok(), Ordering::Release);
let level = env::var(&options.log_env).unwrap_or_else(|_| options.default_level.to_string());
unsafe {
env::set_var(
&options.log_env,
if options.filter_modules.is_empty()
|| level == "off"
|| level.contains(',')
|| level.contains('=')
{
level
} else if level == "verbose" {
"trace".into()
} else {
options
.filter_modules
.iter()
.map(|prefix| format!("{prefix}={level}"))
.collect::<Vec<_>>()
.join(",")
},
)
};
#[cfg(feature = "log-compat")]
if options.intercept_log {
tracing_log::LogTracer::init()
.map_err(|error| TracingError::InterceptLogFailed { error })?;
}
let fmt_layer = if options.ndjson {
fmt::layer()
.json()
.with_span_list(false)
.with_current_span(options.show_spans)
.with_ansi(false)
.with_target(true)
.with_writer(io::stderr)
.flatten_event(true)
.boxed()
} else {
fmt::layer()
.with_ansi(true)
.with_target(true)
.with_writer(io::stderr)
.event_format(EventFormatter {
show_spans: options.show_spans,
})
.fmt_fields(FieldFormatter)
.boxed()
};
let mut guard = TracingGuard {
chrome_guard: None,
log_file: None,
#[cfg(feature = "otel")]
otel_guard: None,
};
let subscriber = tracing_subscriber::registry()
.with(EnvFilter::from_env(options.log_env))
.with(fmt_layer)
.with(if let Some(log_file) = options.log_file {
if let Some(dir) = log_file.parent() {
fs::create_dir_all(dir)
.map_err(|error| TracingError::CreateLogDirFailed { error })?;
}
let file = Arc::new(
File::create(log_file)
.map_err(|error| TracingError::CreateLogFileFailed { error })?,
);
guard.log_file = Some(Arc::clone(&file));
Some(
fmt::layer()
.with_ansi(false)
.with_target(true)
.with_writer(file)
.event_format(EventFormatter {
show_spans: options.show_spans,
})
.fmt_fields(FieldFormatter),
)
} else {
None
})
.with(if options.dump_trace {
let (chrome_layer, chrome_guard) = ChromeLayerBuilder::new()
.include_args(true)
.include_locations(true)
.file(format!(
"./dump-{}.json",
SystemTime::UNIX_EPOCH.elapsed().unwrap().as_micros()
))
.build();
guard.chrome_guard = Some(chrome_guard);
Some(chrome_layer)
} else {
None
});
#[cfg(feature = "otel")]
let subscriber = {
let (subscriber, otel_guard) = otel::extend_subscriber(subscriber, &options.otel)?;
guard.otel_guard = Some(otel_guard);
subscriber
};
let _ = set_global_default(subscriber);
Ok(guard)
}