lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 13c9fd88b93ea1831a030003bce44d9b3ddae659
parent 789cda9eb3f77d98830b6844e3520ac883d293b7
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 19:54:03 +0000

log: add canonical daily file layout

- add an explicit log file layout option while keeping the existing prefixed-date default stable
- support date-named daily log files such as 2026-03-22.log for service directories
- cover both filename layouts and appender initialization paths in the crate test suite
- preserve stdout and env-filter behavior for existing callers

Diffstat:
Mcrates/log/src/error.rs | 4++++
Mcrates/log/src/init.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/log/src/lib.rs | 2+-
Mcrates/log/src/options.rs | 8++++++++
4 files changed, 129 insertions(+), 3 deletions(-)

diff --git a/crates/log/src/error.rs b/crates/log/src/error.rs @@ -16,6 +16,10 @@ pub enum Error { #[cfg(feature = "std")] #[error(transparent)] Io(#[from] std::io::Error), + + #[cfg(feature = "std")] + #[error(transparent)] + RollingInit(#[from] tracing_appender::rolling::InitError), } pub type Result<T> = core::result::Result<T, Error>; diff --git a/crates/log/src/init.rs b/crates/log/src/init.rs @@ -2,11 +2,12 @@ use std::fs; use std::sync::OnceLock; use tracing::info; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::prelude::*; use tracing_subscriber::{EnvFilter, fmt}; use crate::Result; -use crate::options::LoggingOptions; +use crate::options::{LogFileLayout, LoggingOptions}; static GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new(); static INIT: OnceLock<()> = OnceLock::new(); @@ -18,7 +19,7 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { let writer = if let Some(dir) = &opts.dir { fs::create_dir_all(dir)?; - let file_appender = tracing_appender::rolling::daily(dir, &opts.file_name); + let file_appender = build_file_appender(dir, &opts)?; let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); let _ = GUARD.set(guard); Some(non_blocking) @@ -64,5 +65,118 @@ pub fn init_stdout() -> Result<()> { file_name: "radroots.log".into(), stdout: true, default_level: None, + file_layout: LogFileLayout::PrefixedDate, }) } + +fn build_file_appender(dir: &std::path::Path, opts: &LoggingOptions) -> Result<RollingFileAppender> { + let builder = RollingFileAppender::builder().rotation(Rotation::DAILY); + let builder = match opts.file_layout { + LogFileLayout::PrefixedDate => builder.filename_prefix(opts.file_name.as_str()), + LogFileLayout::DatedFileName => builder.filename_suffix(opts.file_name.as_str()), + }; + Ok(builder.build(dir)?) +} + +#[cfg(test)] +mod tests { + use super::{build_file_appender, init_logging}; + use crate::{LogFileLayout, LoggingOptions}; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + use tracing_subscriber::fmt::MakeWriter; + + fn temp_log_dir(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + let dir = std::env::temp_dir().join(format!("radroots-log-{name}-{nanos}")); + let _ = std::fs::remove_dir_all(&dir); + dir + } + + #[test] + fn prefixed_date_layout_keeps_existing_filename_shape() { + let dir = temp_log_dir("prefixed-date"); + let appender = build_file_appender( + dir.as_path(), + &LoggingOptions { + dir: Some(dir.clone()), + file_name: "myc.log".to_owned(), + stdout: false, + default_level: Some("info".to_owned()), + file_layout: LogFileLayout::PrefixedDate, + }, + ) + .expect("appender"); + + let writer = appender.make_writer(); + drop(writer); + + let names: Vec<String> = std::fs::read_dir(&dir) + .expect("read dir") + .map(|entry| entry.expect("entry").file_name().to_string_lossy().to_string()) + .collect(); + assert_eq!(names.len(), 1); + assert!(names[0].starts_with("myc.log.")); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn dated_file_name_layout_writes_date_named_log_files() { + let dir = temp_log_dir("dated-file-name"); + let appender = build_file_appender( + dir.as_path(), + &LoggingOptions { + dir: Some(dir.clone()), + file_name: "log".to_owned(), + stdout: false, + default_level: Some("info".to_owned()), + file_layout: LogFileLayout::DatedFileName, + }, + ) + .expect("appender"); + + let writer = appender.make_writer(); + drop(writer); + + let names: Vec<String> = std::fs::read_dir(&dir) + .expect("read dir") + .map(|entry| entry.expect("entry").file_name().to_string_lossy().to_string()) + .collect(); + assert_eq!(names.len(), 1); + assert!(names[0].ends_with(".log")); + assert_eq!(names[0].matches('.').count(), 1); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn init_paths_cover_layout_options() { + let dir = temp_log_dir("init-paths"); + std::fs::create_dir_all(&dir).expect("create dir"); + let invalid = dir.join("not-a-dir"); + std::fs::write(&invalid, "file").expect("write invalid path"); + let err_path = init_logging(LoggingOptions { + dir: Some(invalid), + file_name: "x.log".to_string(), + stdout: false, + default_level: None, + file_layout: LogFileLayout::PrefixedDate, + }); + assert!(err_path.is_err()); + + let first = build_file_appender( + &dir, + &LoggingOptions { + dir: Some(dir.clone()), + file_name: "service".to_string(), + stdout: false, + default_level: Some("info".to_string()), + file_layout: LogFileLayout::DatedFileName, + }, + ); + assert!(first.is_ok()); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/crates/log/src/lib.rs b/crates/log/src/lib.rs @@ -14,7 +14,7 @@ pub use error::{Error, Result}; #[cfg(feature = "std")] pub use init::{init_logging, init_stdout}; #[cfg(feature = "std")] -pub use options::LoggingOptions; +pub use options::{LogFileLayout, LoggingOptions}; use tracing::{debug, error, info}; diff --git a/crates/log/src/options.rs b/crates/log/src/options.rs @@ -1,11 +1,18 @@ use std::path::PathBuf; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogFileLayout { + PrefixedDate, + DatedFileName, +} + #[derive(Debug, Clone)] pub struct LoggingOptions { pub dir: Option<PathBuf>, pub file_name: String, pub stdout: bool, pub default_level: Option<String>, + pub file_layout: LogFileLayout, } impl LoggingOptions { @@ -21,6 +28,7 @@ impl Default for LoggingOptions { file_name: "radroots.log".into(), stdout: true, default_level: None, + file_layout: LogFileLayout::PrefixedDate, } } }