mod format;
mod level;
#[cfg(feature = "otel")]
mod otel;
use crate::tracing::format::*;
pub use crate::tracing::level::LogLevel;
#[cfg(feature = "otel")]
pub use crate::tracing::otel::OtelOptions;
use miette::Diagnostic;
use std::error::Error;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::SystemTime;
use std::{env, fmt as std_fmt, fs};
use tracing::subscriber::set_global_default;
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,
};
use tracing_chrome::{ChromeLayerBuilder, FlushGuard};
use tracing_subscriber::fmt::{self, SubscriberBuilder};
use tracing_subscriber::{EnvFilter, prelude::*};
pub type TracingResult<T> = Result<T, TracingError>;
#[derive(Debug, Diagnostic)]
pub struct TracingError {
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl TracingError {
#[cfg(feature = "otel")]
pub(super) fn otlp_exporter(
signal: &'static str,
source: impl Error + Send + Sync + 'static,
) -> Self {
Self {
message: format!("failed to initialize OTLP {signal} exporter"),
source: Some(Box::new(source)),
}
}
}
impl std_fmt::Display for TracingError {
fn fmt(&self, f: &mut std_fmt::Formatter<'_>) -> std_fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for TracingError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn Error + 'static))
}
}
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>,
#[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,
#[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>,
}
#[tracing::instrument(skip_all)]
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 {
options
.filter_modules
.iter()
.map(|prefix| format!("{prefix}={level}"))
.collect::<Vec<_>>()
.join(",")
},
)
};
#[cfg(feature = "log-compat")]
if options.intercept_log {
tracing_log::LogTracer::init().expect("Failed to initialize log interceptor.");
}
let subscriber = SubscriberBuilder::default()
.event_format(EventFormatter {
show_spans: options.show_spans,
})
.fmt_fields(FieldFormatter)
.with_env_filter(EnvFilter::from_env(options.log_env))
.with_writer(io::stderr)
.finish();
let mut guard = TracingGuard {
chrome_guard: None,
log_file: None,
#[cfg(feature = "otel")]
otel_guard: None,
};
let subscriber = subscriber
.with(if let Some(log_file) = options.log_file {
if let Some(dir) = log_file.parent() {
fs::create_dir_all(dir).expect("Failed to create log directory.");
}
let file = Arc::new(File::create(log_file).expect("Failed to create log file."));
guard.log_file = Some(Arc::clone(&file));
Some(fmt::layer().with_ansi(false).with_writer(file))
} 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)
}