use std::path::PathBuf;
use std::sync::OnceLock;
use tracing_subscriber::{
fmt::{self, MakeWriter},
layer::SubscriberExt,
util::SubscriberInitExt,
EnvFilter, Layer,
};
static INITIALIZED: OnceLock<()> = OnceLock::new();
pub struct LogGuard {
_file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
}
pub fn init_logging(app_name: &str, default_level: &str) -> LogGuard {
init_logging_with_writer(app_name, default_level, std::io::stderr)
}
pub fn init_logging_with_writer<W>(app_name: &str, default_level: &str, writer: W) -> LogGuard
where
W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
if INITIALIZED.set(()).is_err() {
return LogGuard { _file_guard: None };
}
let env_filter = build_env_filter(default_level);
let log_format = read_env("ARCANUM_LOG_FORMAT", "text");
let file_enabled = matches!(
read_env("ARCANUM_LOG_FILE", "").to_lowercase().as_str(),
"1" | "true" | "yes"
);
let json_mode = log_format == "json";
let (file_layer_json, file_layer_text, file_guard) = if file_enabled {
let dir = log_dir();
std::fs::create_dir_all(&dir).ok();
let file_appender = tracing_appender::rolling::daily(&dir, format!("{app_name}.log"));
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
if json_mode {
(
Some(fmt::layer().json().with_writer(non_blocking).boxed()),
None,
Some(guard),
)
} else {
(
None,
Some(fmt::layer().with_writer(non_blocking).boxed()),
Some(guard),
)
}
} else {
(None, None, None)
};
let (console_json, console_text) = if json_mode {
(Some(fmt::layer().json().with_writer(writer).boxed()), None)
} else {
(None, Some(fmt::layer().with_writer(writer).boxed()))
};
#[cfg(feature = "otel")]
let otel_layer = build_otel_layer(app_name).map(|l| l.boxed());
let registry = tracing_subscriber::registry()
.with(env_filter)
.with(console_json)
.with(console_text)
.with(file_layer_json)
.with(file_layer_text);
#[cfg(feature = "otel")]
let registry = registry.with(otel_layer);
let _ = registry.try_init();
#[cfg(not(feature = "otel"))]
let _ = app_name;
LogGuard {
_file_guard: file_guard,
}
}
pub fn log_dir() -> PathBuf {
if let Ok(dir) = std::env::var("ARCANUM_LOG_DIR") {
return PathBuf::from(dir);
}
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".arcanum")
.join("logs")
}
fn build_env_filter(default_level: &str) -> EnvFilter {
let level = read_env_chain(&["ARCANUM_LOG_LEVEL", "RUST_LOG"], default_level);
EnvFilter::try_new(&level).unwrap_or_else(|_| EnvFilter::new(default_level))
}
fn read_env(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
fn read_env_chain(keys: &[&str], default: &str) -> String {
for key in keys {
if let Ok(val) = std::env::var(key) {
if !val.is_empty() {
return val;
}
}
}
default.to_string()
}
#[cfg(feature = "otel")]
fn build_otel_layer<S>(
service_name: &str,
) -> Option<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig as _;
let endpoint = std::env::var("ARCANUM_OTEL_ENDPOINT").ok()?;
if endpoint.is_empty() {
return None;
}
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(&endpoint)
.build()
.ok()?;
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(
opentelemetry_sdk::Resource::builder()
.with_service_name(service_name.to_string())
.build(),
)
.build();
let tracer = tracer_provider.tracer(service_name.to_string());
std::mem::forget(tracer_provider);
Some(tracing_opentelemetry::layer().with_tracer(tracer))
}
#[cfg(test)]
mod tests {
use super::build_env_filter;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_clean_env<F: FnOnce() -> R, R>(f: F) -> R {
let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let prev_arcanum = std::env::var("ARCANUM_LOG_LEVEL").ok();
let prev_rust = std::env::var("RUST_LOG").ok();
std::env::remove_var("ARCANUM_LOG_LEVEL");
std::env::remove_var("RUST_LOG");
let result = f();
match prev_arcanum {
Some(v) => std::env::set_var("ARCANUM_LOG_LEVEL", v),
None => std::env::remove_var("ARCANUM_LOG_LEVEL"),
}
match prev_rust {
Some(v) => std::env::set_var("RUST_LOG", v),
None => std::env::remove_var("RUST_LOG"),
}
result
}
#[test]
fn test_default_level_used_when_no_env_set() {
with_clean_env(|| {
let filter = build_env_filter("error");
assert_eq!(format!("{}", filter), "error");
});
}
#[test]
fn test_arcanum_log_level_overrides_default() {
with_clean_env(|| {
std::env::set_var("ARCANUM_LOG_LEVEL", "debug");
let filter = build_env_filter("error");
assert_eq!(format!("{}", filter), "debug");
});
}
#[test]
fn test_rust_log_overrides_default() {
with_clean_env(|| {
std::env::set_var("RUST_LOG", "info");
let filter = build_env_filter("error");
assert_eq!(format!("{}", filter), "info");
});
}
#[test]
fn test_arcanum_log_level_takes_precedence_over_rust_log() {
with_clean_env(|| {
std::env::set_var("ARCANUM_LOG_LEVEL", "warn");
std::env::set_var("RUST_LOG", "trace");
let filter = build_env_filter("error");
assert_eq!(format!("{}", filter), "warn");
});
}
}