lib

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

commit d76101fbad2701c7e6ff65c68dcc25f04f543011
parent 7eeee7dfc9d04d1791018c1577d9887f1588f782
Author: triesap <tyson@radroots.org>
Date:   Mon, 23 Mar 2026 02:03:04 +0000

log: resolve dated file paths explicitly

- add logging option helpers to resolve current dated file paths
- report the actual resolved file path during shared logger initialization
- cover prefixed and dated filename layouts with focused tests
- preserve existing appender layout behavior for current callers

Diffstat:
MCargo.lock | 1+
Mcrates/log/Cargo.toml | 8+++++++-
Mcrates/log/src/init.rs | 38++++++++++++++++++++++++++------------
Mcrates/log/src/options.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 108 insertions(+), 13 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2195,6 +2195,7 @@ dependencies = [ name = "radroots-log" version = "0.1.0-alpha.1" dependencies = [ + "chrono", "thiserror 1.0.69", "tracing", "tracing-appender", diff --git a/crates/log/Cargo.toml b/crates/log/Cargo.toml @@ -15,9 +15,15 @@ readme.workspace = true [features] default = ["std"] -std = ["dep:thiserror", "dep:tracing-subscriber", "dep:tracing-appender"] +std = [ + "dep:chrono", + "dep:thiserror", + "dep:tracing-subscriber", + "dep:tracing-appender", +] [dependencies] +chrono = { workspace = true, optional = true } tracing = { workspace = true, default-features = false } thiserror = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true, features = [ diff --git a/crates/log/src/init.rs b/crates/log/src/init.rs @@ -49,9 +49,8 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { let _ = INIT.set(()); info!( "logging initialized (file: {}, stdout: {})", - opts.dir - .as_ref() - .map(|d| d.join(&opts.file_name).display().to_string()) + opts.resolved_current_log_file_path() + .map(|path| path.display().to_string()) .unwrap_or_else(|| "<disabled>".into()), opts.also_stdout() ); @@ -75,7 +74,10 @@ pub fn init_stdout() -> Result<()> { }) } -fn build_file_appender(dir: &std::path::Path, opts: &LoggingOptions) -> Result<RollingFileAppender> { +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()), @@ -122,7 +124,13 @@ mod tests { let names: Vec<String> = std::fs::read_dir(&dir) .expect("read dir") - .map(|entry| entry.expect("entry").file_name().to_string_lossy().to_string()) + .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.")); @@ -149,7 +157,13 @@ mod tests { let names: Vec<String> = std::fs::read_dir(&dir) .expect("read dir") - .map(|entry| entry.expect("entry").file_name().to_string_lossy().to_string()) + .map(|entry| { + entry + .expect("entry") + .file_name() + .to_string_lossy() + .to_string() + }) .collect(); assert_eq!(names.len(), 1); assert!(names[0].ends_with(".log")); @@ -175,12 +189,12 @@ mod tests { 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, - }, + 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/options.rs b/crates/log/src/options.rs @@ -1,3 +1,4 @@ +use chrono::Local; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -19,6 +20,19 @@ impl LoggingOptions { pub fn also_stdout(&self) -> bool { self.stdout } + + pub fn resolved_log_file_name_for_date(&self, date: &str) -> String { + match self.file_layout { + LogFileLayout::PrefixedDate => format!("{}.{}", self.file_name, date), + LogFileLayout::DatedFileName => format!("{}.{}", date, self.file_name), + } + } + + pub fn resolved_current_log_file_path(&self) -> Option<PathBuf> { + let dir = self.dir.as_ref()?; + let date = Local::now().format("%Y-%m-%d").to_string(); + Some(dir.join(self.resolved_log_file_name_for_date(date.as_str()))) + } } impl Default for LoggingOptions { @@ -32,3 +46,63 @@ impl Default for LoggingOptions { } } } + +#[cfg(test)] +mod tests { + use super::{LogFileLayout, LoggingOptions}; + use std::path::PathBuf; + + #[test] + fn prefixed_date_layout_resolves_expected_file_name() { + let options = LoggingOptions { + dir: Some(PathBuf::from("/tmp/logs")), + file_name: "myc.log".to_owned(), + stdout: false, + default_level: None, + file_layout: LogFileLayout::PrefixedDate, + }; + + assert_eq!( + options.resolved_log_file_name_for_date("2026-03-23"), + "myc.log.2026-03-23" + ); + } + + #[test] + fn dated_file_name_layout_resolves_expected_file_name() { + let options = LoggingOptions { + dir: Some(PathBuf::from("/tmp/logs")), + file_name: "log".to_owned(), + stdout: false, + default_level: None, + file_layout: LogFileLayout::DatedFileName, + }; + + assert_eq!( + options.resolved_log_file_name_for_date("2026-03-23"), + "2026-03-23.log" + ); + } + + #[test] + fn current_log_file_path_joins_dir_and_layout_shape() { + let options = LoggingOptions { + dir: Some(PathBuf::from("/tmp/logs")), + file_name: "log".to_owned(), + stdout: false, + default_level: None, + file_layout: LogFileLayout::DatedFileName, + }; + + let path = options + .resolved_current_log_file_path() + .expect("resolved path"); + + assert_eq!(path.parent(), Some(PathBuf::from("/tmp/logs").as_path())); + assert!( + path.file_name() + .and_then(|value| value.to_str()) + .is_some_and(|value| value.ends_with(".log")) + ); + } +}