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:
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"))
+ );
+ }
+}