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:
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,
}
}
}