Add time crate for session log filename generation and refactor logging setup

- Introduced the `time` crate to generate session log filenames in UTC format, enhancing log file naming consistency.
- Refactored the logging setup to create log files based on the generated filenames, improving the clarity of log management.
- Updated the `compress_old_logs` function to streamline log file handling, ensuring proper cleanup of old logs.

These changes aim to improve the logging system's functionality and maintainability within the application.
This commit is contained in:
Joe 2026-03-01 14:22:20 -06:00
parent 491506fafd
commit 1f7682dfac
3 changed files with 113 additions and 72 deletions

1
Cargo.lock generated
View file

@ -251,6 +251,7 @@ dependencies = [
"tempfile",
"termcolor",
"thiserror 2.0.18",
"time 0.3.47",
"tokio",
"toml",
"tracing",

View file

@ -97,6 +97,7 @@ lexical-write-float = "1.0.6"
strum = { version = "0.27", features = ["derive"] }
toml = "0.9"
termcolor = "1.4.1"
time = "0.3"
thiserror = "2"
tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros", "sync"] }
tracing = "0.1"

View file

@ -16,6 +16,22 @@ pub struct LogGuard {
_file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
}
/// Generates a session log filename like `atlas-serve.2026-03-01_14-32-05.log`.
/// Uses UTC time, hyphens and underscores only (safe on all OSes).
fn session_log_filename(command_name: &str) -> String {
let now = time::OffsetDateTime::now_utc();
format!(
"{}.{:04}-{:02}-{:02}_{:02}-{:02}-{:02}.log",
command_name,
now.year(),
now.month() as u8,
now.day(),
now.hour(),
now.minute(),
now.second(),
)
}
pub fn init_logging(
verbosity: u8,
color: ColorChoice,
@ -55,16 +71,18 @@ pub fn init_logging(
match std::fs::create_dir_all(&log_dir) {
Ok(()) => {
compress_old_logs(&log_dir, command_name);
compress_old_logs(&log_dir);
let file_appender = tracing_appender::rolling::Builder::new()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix(command_name)
.filename_suffix("log")
.build(&log_dir)
.expect("Failed to create rolling file appender");
let log_filename = session_log_filename(command_name);
let log_path = log_dir.join(&log_filename);
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
Ok(file) => {
let (non_blocking, guard) = tracing_appender::non_blocking(file);
file_guard = Some(guard);
let file_filter = EnvFilter::new(level.to_string());
@ -80,6 +98,15 @@ pub fn init_logging(
Some(layer)
}
Err(e) => {
eprintln!(
"Warning: could not create log file {}: {e}",
log_path.display()
);
None
}
}
}
Err(e) => {
eprintln!(
"Warning: could not create log directory {}: {e}",
@ -102,15 +129,12 @@ pub fn init_logging(
}
}
fn compress_old_logs(log_dir: &Path, command_name: &str) {
let today = {
let now = std::time::SystemTime::now();
let since_epoch = now
fn compress_old_logs(log_dir: &Path) {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let days = since_epoch.as_secs() / 86400;
days
};
.unwrap_or_default()
.as_secs();
let today = now_secs / 86400;
let entries = match std::fs::read_dir(log_dir) {
Ok(entries) => entries,
@ -143,22 +167,12 @@ fn compress_old_logs(log_dir: &Path, command_name: &str) {
continue;
}
if !file_name.starts_with(command_name) {
// Try to open for exclusive write to check if another process holds it.
// On Windows this fails if another Atlas process has the file open.
if std::fs::OpenOptions::new().write(true).open(&path).is_err() {
continue;
}
if let Ok(meta) = entry.metadata() {
if let Ok(modified) = meta.modified() {
let file_days = modified
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() / 86400)
.unwrap_or(today);
if file_days >= today {
continue;
}
}
}
let gz_path = path.with_extension("log.gz");
if let Ok(input) = std::fs::read(&path) {
let gz_file = match std::fs::File::create(&gz_path) {
@ -458,62 +472,87 @@ mod tests {
}
#[test]
fn compress_old_logs_compresses_old_files() {
fn compress_old_logs_compresses_all_log_files() {
let dir = tempfile::tempdir().unwrap();
let log_dir = dir.path();
let old_file = log_dir.join("atlas-serve.2020-01-01.log");
std::fs::write(&old_file, "old log content").unwrap();
let serve_file = log_dir.join("atlas-serve.2020-01-01_10-00-00.log");
std::fs::write(&serve_file, "serve log content").unwrap();
let mtime =
std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86400 * 18262);
filetime::set_file_mtime(&old_file, filetime::FileTime::from_system_time(mtime))
.unwrap_or_default();
let build_file = log_dir.join("atlas-build.2020-01-01_11-00-00.log");
std::fs::write(&build_file, "build log content").unwrap();
compress_old_logs(log_dir, "atlas-serve");
compress_old_logs(log_dir);
assert!(!old_file.exists(), "original .log file should be deleted");
let gz_file = log_dir.join("atlas-serve.2020-01-01.log.gz");
assert!(gz_file.exists(), ".log.gz file should be created");
assert!(!serve_file.exists(), "serve .log should be deleted");
assert!(!build_file.exists(), "build .log should be deleted");
let gz_data = std::fs::read(&gz_file).unwrap();
let serve_gz = log_dir.join("atlas-serve.2020-01-01_10-00-00.log.gz");
assert!(serve_gz.exists(), "serve .log.gz should be created");
let build_gz = log_dir.join("atlas-build.2020-01-01_11-00-00.log.gz");
assert!(build_gz.exists(), "build .log.gz should be created");
let gz_data = std::fs::read(&serve_gz).unwrap();
let mut decoder = flate2::read::GzDecoder::new(&gz_data[..]);
let mut decompressed = String::new();
decoder.read_to_string(&mut decompressed).unwrap();
assert_eq!(decompressed, "old log content");
assert_eq!(decompressed, "serve log content");
}
#[test]
fn compress_old_logs_skips_other_commands() {
fn compress_old_logs_skips_non_log_files() {
let dir = tempfile::tempdir().unwrap();
let log_dir = dir.path();
let other_file = log_dir.join("atlas-build.2020-01-01.log");
std::fs::write(&other_file, "build log").unwrap();
let txt_file = log_dir.join("notes.txt");
std::fs::write(&txt_file, "not a log").unwrap();
compress_old_logs(log_dir);
assert!(txt_file.exists(), "non-.log files should be left alone");
}
#[test]
fn compress_old_logs_prunes_old_gz() {
let dir = tempfile::tempdir().unwrap();
let log_dir = dir.path();
let old_gz = log_dir.join("atlas-serve.2020-01-01_10-00-00.log.gz");
std::fs::write(&old_gz, "old compressed").unwrap();
let mtime =
std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86400 * 18262);
filetime::set_file_mtime(&other_file, filetime::FileTime::from_system_time(mtime))
filetime::set_file_mtime(&old_gz, filetime::FileTime::from_system_time(mtime))
.unwrap_or_default();
compress_old_logs(log_dir, "atlas-serve");
compress_old_logs(log_dir);
assert!(
other_file.exists(),
"other command's log should NOT be compressed"
);
assert!(!old_gz.exists(), "old .log.gz (>7 days) should be pruned");
}
#[test]
fn compress_old_logs_skips_today() {
let dir = tempfile::tempdir().unwrap();
let log_dir = dir.path();
fn session_log_filename_format() {
let name = session_log_filename("atlas-serve");
assert!(
name.starts_with("atlas-serve."),
"should start with command prefix"
);
assert!(name.ends_with(".log"), "should end with .log");
assert!(!name.contains(':'), "no colons (Windows-safe)");
assert!(!name.contains('/'), "no slashes");
assert!(!name.contains('\\'), "no backslashes");
let today_file = log_dir.join("atlas-serve.today.log");
std::fs::write(&today_file, "today's log").unwrap();
compress_old_logs(log_dir, "atlas-serve");
assert!(today_file.exists(), "today's log should NOT be compressed");
let middle = &name["atlas-serve.".len()..name.len() - ".log".len()];
assert_eq!(
middle.len(),
19,
"timestamp should be YYYY-MM-DD_HH-MM-SS (19 chars)"
);
assert_eq!(&middle[4..5], "-");
assert_eq!(&middle[7..8], "-");
assert_eq!(&middle[10..11], "_");
assert_eq!(&middle[13..14], "-");
assert_eq!(&middle[16..17], "-");
}
}