use std::path::PathBuf;
use std::sync::OnceLock;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_appender::rolling::{Builder, Rotation};
use tracing_subscriber::EnvFilter;
use crate::paths;
const LOG_FILENAME_PREFIX: &str = "splashboard";
const LOG_FILENAME_SUFFIX: &str = "log";
const MAX_LOG_FILES: usize = 3;
const DEFAULT_FILTER: &str = "error";
static GUARD: OnceLock<WorkerGuard> = OnceLock::new();
pub fn init() {
if GUARD.get().is_some() {
return;
}
let Some(dir) = paths::logs_dir() else {
return;
};
if std::fs::create_dir_all(&dir).is_err() {
return;
}
let Some(appender) = build_appender(&dir) else {
return;
};
let (writer, guard) = tracing_appender::non_blocking(appender);
let filter = EnvFilter::try_from_env("SPLASHBOARD_LOG")
.unwrap_or_else(|_| EnvFilter::new(DEFAULT_FILTER));
let subscriber = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(writer)
.with_ansi(false)
.with_target(false);
if subscriber.try_init().is_ok() {
let _ = GUARD.set(guard);
}
}
fn build_appender(dir: &PathBuf) -> Option<tracing_appender::rolling::RollingFileAppender> {
Builder::new()
.rotation(Rotation::DAILY)
.filename_prefix(LOG_FILENAME_PREFIX)
.filename_suffix(LOG_FILENAME_SUFFIX)
.max_log_files(MAX_LOG_FILES)
.build(dir)
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::paths::TEST_ENV_LOCK;
use std::path::Path;
use std::time::Duration;
use tracing_appender::non_blocking;
use tracing_subscriber::fmt::MakeWriter;
fn restore_env(key: &str, value: Option<String>) {
unsafe {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
fn wait_for_log_file(dir: &Path) -> bool {
let mut seen_once = false;
for _ in 0..40 {
let any = std::fs::read_dir(dir)
.ok()
.into_iter()
.flat_map(|entries| entries.filter_map(Result::ok))
.any(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with(LOG_FILENAME_PREFIX)
});
if any && seen_once {
return true;
}
seen_once = any;
std::thread::sleep(Duration::from_millis(25));
}
false
}
#[test]
fn build_appender_creates_a_file_on_first_write() {
let dir = tempfile::tempdir().unwrap();
let appender = build_appender(&dir.path().to_path_buf()).expect("appender builds");
let (writer, _guard) = non_blocking(appender);
{
let mut w = writer.make_writer();
use std::io::Write;
writeln!(w, "hello from tests").unwrap();
}
drop(_guard);
assert!(
wait_for_log_file(dir.path()),
"log file must be created under {:?}",
dir.path()
);
}
#[test]
fn build_appender_rejects_file_path() {
let dir = tempfile::tempdir().unwrap();
let occupied = dir.path().join("occupied");
std::fs::write(&occupied, "not a directory").unwrap();
assert!(build_appender(&occupied).is_none());
}
#[test]
fn init_skips_invalid_home_then_initializes_once() {
let _lock = TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile::tempdir().unwrap();
let blocked_home = tmp.path().join("blocked-home");
std::fs::write(&blocked_home, "occupied").unwrap();
let good_home = tmp.path().join("good-home");
let later_home = tmp.path().join("later-home");
let outer_previous_home = std::env::var("SPLASHBOARD_HOME").ok();
let outer_previous_filter = std::env::var("SPLASHBOARD_LOG").ok();
unsafe {
std::env::set_var("SPLASHBOARD_HOME", tmp.path().join("previous-home"));
std::env::set_var("SPLASHBOARD_LOG", "warn");
}
let previous_home = std::env::var("SPLASHBOARD_HOME").ok();
let previous_filter = std::env::var("SPLASHBOARD_LOG").ok();
unsafe {
std::env::set_var("SPLASHBOARD_HOME", &blocked_home);
std::env::remove_var("SPLASHBOARD_LOG");
}
assert!(GUARD.get().is_none());
init();
assert!(GUARD.get().is_none());
unsafe {
std::env::set_var("SPLASHBOARD_HOME", &good_home);
}
init();
assert!(GUARD.get().is_some());
tracing::error!("logging smoke test");
assert!(
wait_for_log_file(&good_home.join("logs")),
"log file must be created under {:?}",
good_home
);
unsafe {
std::env::set_var("SPLASHBOARD_HOME", &later_home);
}
init();
assert!(!later_home.join("logs").exists());
restore_env("SPLASHBOARD_HOME", previous_home);
restore_env("SPLASHBOARD_LOG", previous_filter);
restore_env("SPLASHBOARD_HOME", outer_previous_home);
restore_env("SPLASHBOARD_LOG", outer_previous_filter);
}
}