use std::path::Path;
use std::time::Duration;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Filter(#[from] tracing_subscriber::filter::ParseError),
#[error(transparent)]
Subscriber(#[from] tracing::subscriber::SetGlobalDefaultError),
}
#[derive(Debug)]
pub enum LogFile<'a> {
RollingLog { cache_dir: &'a Path },
LogFile { path: &'a Path },
}
fn prune_old_rolling_logs(cache_dir: &Path) {
let Ok(entries) = std::fs::read_dir(cache_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if !name.to_string_lossy().starts_with("micromux.log") {
continue;
}
let expired = entry
.metadata()
.and_then(|meta| meta.modified())
.map(|modified| {
modified
.elapsed()
.is_ok_and(|age| age > Duration::from_hours(168))
})
.unwrap_or(false);
if expired && let Err(err) = std::fs::remove_file(entry.path()) {
tracing::debug!(?err, "failed to remove old rolling log file");
}
}
}
pub fn setup(
log_level: Option<tracing::metadata::Level>,
log_file: &LogFile<'_>,
) -> Result<tracing_appender::non_blocking::WorkerGuard, Error> {
let (log_writer, guard) = match log_file {
LogFile::RollingLog { cache_dir } => {
prune_old_rolling_logs(cache_dir);
let file_appender =
RollingFileAppender::new(Rotation::DAILY, cache_dir, "micromux.log");
tracing_appender::non_blocking(file_appender)
}
LogFile::LogFile { path } => {
let _ = std::fs::remove_file(path);
let log_file = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.or_else(|_| {
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
})?;
tracing_appender::non_blocking(log_file)
}
};
let default_log_level = log_level.unwrap_or(tracing::metadata::Level::WARN);
let default_log_directive = ["none".to_string()]
.into_iter()
.chain(
["micromux_cli", "micromux_tui", "micromux"]
.into_iter()
.map(|pkg| {
format!(
"{pkg}={}",
default_log_level.to_string().to_ascii_lowercase()
)
}),
)
.collect::<Vec<String>>()
.join(",");
let default_env_filter = tracing_subscriber::filter::EnvFilter::builder()
.with_regex(true)
.with_default_directive(default_log_level.into())
.parse(default_log_directive)?;
let use_rust_log_override = std::env::var(EnvFilter::DEFAULT_ENV).is_ok_and(|s| !s.is_empty());
let env_filter = if use_rust_log_override {
match tracing_subscriber::filter::EnvFilter::builder()
.with_env_var(EnvFilter::DEFAULT_ENV)
.try_from_env()
{
Ok(env_filter) => env_filter,
Err(err) => {
eprintln!("invalid log filter: {err}");
eprintln!("falling back to default logging");
default_env_filter
}
}
} else {
default_env_filter
};
let fmt_layer_pretty_compact = tracing_subscriber::fmt::Layer::new()
.compact()
.with_ansi(false)
.with_writer(log_writer);
let subscriber = tracing_subscriber::registry()
.with(fmt_layer_pretty_compact)
.with(env_filter);
tracing::subscriber::set_global_default(subscriber)?;
Ok(guard)
}