use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use tracing::Level;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
static LOG_DIR: LazyLock<PathBuf> = LazyLock::new(resolve_log_dir);
pub fn log_dir() -> PathBuf {
LOG_DIR.clone()
}
fn resolve_log_dir() -> PathBuf {
if let Ok(dir) = std::env::var("RALPH_LOG_DIR") {
let path = PathBuf::from(dir);
if ensure_writable_dir(&path) {
return path;
}
}
if let Some(home) = dirs::home_dir() {
let path = home.join(".ralph").join("logs");
if ensure_writable_dir(&path) {
return path;
}
}
if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") {
let path = PathBuf::from(xdg_state).join("ralph").join("logs");
if ensure_writable_dir(&path) {
return path;
}
}
if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
let path = PathBuf::from(xdg_cache).join("ralph").join("logs");
if ensure_writable_dir(&path) {
return path;
}
}
std::env::temp_dir().join(".ralph").join("logs")
}
fn ensure_writable_dir(path: &Path) -> bool {
if std::fs::create_dir_all(path).is_err() {
return false;
}
let test_path = path.join(format!(".write-test-{}", std::process::id()));
let result = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&test_path)
.is_ok();
let _ = std::fs::remove_file(&test_path);
result
}
pub fn current_log_file() -> PathBuf {
let now = chrono::Local::now();
log_dir().join(format!("ralph.{}", now.format("%Y-%m-%d")))
}
pub fn init_logging(verbosity: u8, log_to_stderr: bool) -> Result<WorkerGuard> {
let log_dir = log_dir();
std::fs::create_dir_all(&log_dir)?;
let file_appender = tracing_appender::rolling::daily(&log_dir, "ralph");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let level = match verbosity {
0 => Level::INFO,
1 => Level::DEBUG,
_ => Level::TRACE,
};
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level.to_string()));
let file_layer = fmt::layer()
.json()
.with_writer(non_blocking)
.with_ansi(false);
if log_to_stderr {
let stderr_layer = fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(true)
.compact();
tracing_subscriber::registry()
.with(filter)
.with(file_layer)
.with(stderr_layer)
.init();
} else {
tracing_subscriber::registry()
.with(filter)
.with(file_layer)
.init();
}
Ok(guard)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_dir() {
let dir = log_dir();
assert!(dir.to_string_lossy().contains(".ralph"));
assert!(dir.to_string_lossy().contains("logs"));
}
#[test]
fn test_current_log_file() {
let file = current_log_file();
assert!(file.to_string_lossy().contains("ralph."));
}
}