use std::path::{Path, PathBuf};
use mold_core::LoggingConfig;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
pub struct LogGuard {
_file_guard: Option<WorkerGuard>,
}
pub fn init_tracing(
log_file: bool,
json: bool,
config: &LoggingConfig,
default_level: &str,
log_dir: PathBuf,
) -> LogGuard {
let filter_str = resolve_filter(config, default_level);
let file_enabled = log_file || config.file;
let mut file_guard: Option<WorkerGuard> = None;
if file_enabled {
let _ = std::fs::create_dir_all(&log_dir);
cleanup_old_logs(&log_dir, config.max_days);
let appender = match tracing_appender::rolling::RollingFileAppender::builder()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("mold-server")
.filename_suffix("log")
.build(&log_dir)
{
Ok(a) => a,
Err(e) => {
eprintln!("warning: failed to create log file appender: {e}");
eprintln!("warning: file logging disabled, continuing with stderr only");
let filter = make_filter(&filter_str, default_level);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
return LogGuard { _file_guard: None };
}
};
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
file_guard = Some(guard);
let stderr_filter = make_filter(&filter_str, default_level);
let file_filter = make_filter(&filter_str, default_level);
let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false);
tracing_subscriber::registry()
.with(stderr_layer.with_filter(stderr_filter))
.with(file_layer.with_filter(file_filter))
.init();
} else if json {
let filter = make_filter(&filter_str, default_level);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.json()
.init();
} else {
let filter = make_filter(&filter_str, default_level);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
LogGuard {
_file_guard: file_guard,
}
}
pub fn init_tracing_file_only(
config: &LoggingConfig,
default_level: &str,
log_dir: PathBuf,
) -> LogGuard {
let filter_str = resolve_filter(config, default_level);
let _ = std::fs::create_dir_all(&log_dir);
cleanup_old_logs(&log_dir, config.max_days);
let appender = match tracing_appender::rolling::RollingFileAppender::builder()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("mold-tui")
.filename_suffix("log")
.build(&log_dir)
{
Ok(a) => a,
Err(e) => {
eprintln!("warning: failed to create TUI log file appender: {e}");
let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
return LogGuard { _file_guard: None };
}
};
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
let filter = make_filter(&filter_str, default_level);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(non_blocking)
.with_ansi(false)
.init();
LogGuard {
_file_guard: Some(guard),
}
}
pub(crate) fn resolve_filter(config: &LoggingConfig, default_level: &str) -> String {
std::env::var("MOLD_LOG").unwrap_or_else(|_| {
if config.level.is_empty() {
default_level.to_string()
} else {
config.level.clone()
}
})
}
pub(crate) fn make_filter(filter_str: &str, default_level: &str) -> EnvFilter {
EnvFilter::try_new(filter_str).unwrap_or_else(|_| EnvFilter::new(default_level))
}
pub(crate) fn cleanup_old_logs(log_dir: &Path, max_days: u32) {
let now = std::time::SystemTime::now();
let max_age = std::time::Duration::from_secs(max_days as u64 * 86400);
let entries = match std::fs::read_dir(log_dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
let filename = path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or_default();
let is_mold_log = filename.starts_with("mold-server.") || filename.starts_with("mold-tui.");
if !is_mold_log {
continue;
}
if let Ok(metadata) = path.metadata() {
if let Ok(modified) = metadata.modified() {
if let Ok(age) = now.duration_since(modified) {
if age > max_age {
let _ = std::fs::remove_file(&path);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::{Duration, SystemTime};
#[test]
fn resolve_filter_precedence_chain() {
let prev = std::env::var("MOLD_LOG").ok();
std::env::set_var("MOLD_LOG", "trace");
let cfg = LoggingConfig {
level: "warn".to_string(),
file: false,
dir: None,
max_days: 7,
};
assert_eq!(resolve_filter(&cfg, "info"), "trace");
std::env::remove_var("MOLD_LOG");
let cfg = LoggingConfig {
level: "debug".to_string(),
file: false,
dir: None,
max_days: 7,
};
assert_eq!(resolve_filter(&cfg, "info"), "debug");
let cfg = LoggingConfig {
level: String::new(),
file: false,
dir: None,
max_days: 7,
};
assert_eq!(resolve_filter(&cfg, "info"), "info");
match prev {
Some(v) => std::env::set_var("MOLD_LOG", v),
None => std::env::remove_var("MOLD_LOG"),
}
}
#[test]
fn make_filter_accepts_both_valid_and_invalid_input_without_panicking() {
let _ = make_filter("mold=trace,hyper=warn", "info");
let _ = make_filter("@@@@@@", "warn");
let _ = make_filter("invalid=garbage=syntax", "info");
}
#[test]
fn cleanup_old_logs_deletes_aged_mold_files_and_keeps_fresh_ones() {
let dir = tempfile::tempdir().expect("tempdir");
let dir_path = dir.path();
let aged = dir_path.join("mold-server.2020-01-01");
let aged_with_suffix = dir_path.join("mold-tui.2020-01-01.log");
let fresh = dir_path.join("mold-server.fresh.log");
let unrelated = dir_path.join("not-our-business.log");
for path in [&aged, &aged_with_suffix, &fresh, &unrelated] {
fs::write(path, b"x").expect("write fixture");
}
let ancient = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 30); for path in [&aged, &aged_with_suffix] {
let f = fs::File::open(path).expect("open fixture");
f.set_modified(ancient).expect("backdate fixture");
}
cleanup_old_logs(dir_path, 7);
assert!(!aged.exists(), "30-day-old mold-server log must be removed");
assert!(
!aged_with_suffix.exists(),
"30-day-old mold-tui log must be removed",
);
assert!(fresh.exists(), "fresh mold-server log must be retained");
assert!(
unrelated.exists(),
"unrelated *.log file must be left untouched",
);
}
#[test]
fn cleanup_old_logs_returns_silently_for_missing_dir() {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("does-not-exist");
cleanup_old_logs(&missing, 1);
}
#[test]
fn cleanup_old_logs_skips_subdirectories() {
let dir = tempfile::tempdir().expect("tempdir");
let dir_path = dir.path();
let subdir = dir_path.join("mold-server.subdir");
fs::create_dir(&subdir).expect("create subdir");
let ancient = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 30);
let f = fs::File::open(&subdir).expect("open subdir");
let _ = f.set_modified(ancient);
cleanup_old_logs(dir_path, 7);
assert!(
subdir.exists(),
"subdirectories must be skipped even when they pattern-match",
);
}
}