From 106e46ed5125be33d0427cab9c5c066802f68791 Mon Sep 17 00:00:00 2001 From: kris-ava <72474513+kris-ava@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:46:10 +0000 Subject: [PATCH] feat(log): Add KeepSome rotation strategy (#677) Co-authored-by: Krzysztof Krolak Co-authored-by: FabianLars --- .changes/log-keep-some.md | 6 +++ plugins/log/Cargo.toml | 8 +++- plugins/log/src/lib.rs | 84 +++++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 .changes/log-keep-some.md diff --git a/.changes/log-keep-some.md b/.changes/log-keep-some.md new file mode 100644 index 00000000..5add4845 --- /dev/null +++ b/.changes/log-keep-some.md @@ -0,0 +1,6 @@ +--- +log: minor +log-js: minor +--- + +Added the `KeepSome` rotation strategy. Like `KeepAll` it will rename files when the max file size is exceeded but will keep only the specified amount of files around. diff --git a/plugins/log/Cargo.toml b/plugins/log/Cargo.toml index 660cae32..d873ab69 100644 --- a/plugins/log/Cargo.toml +++ b/plugins/log/Cargo.toml @@ -31,11 +31,15 @@ thiserror = { workspace = true } serde_repr = "0.1" byte-unit = "5" log = { workspace = true, features = ["kv_unstable"] } -time = { version = "0.3", features = ["formatting", "local-offset", "macros"] } +time = { version = "0.3", features = [ + "formatting", + "local-offset", + "macros", + "parsing", +] } fern = "0.7" tracing = { workspace = true, optional = true } - [target."cfg(target_os = \"android\")".dependencies] android_logger = "0.15" diff --git a/plugins/log/src/lib.rs b/plugins/log/src/lib.rs index e16ddc57..de5c5d54 100644 --- a/plugins/log/src/lib.rs +++ b/plugins/log/src/lib.rs @@ -47,6 +47,7 @@ const DEFAULT_LOG_TARGETS: [Target; 2] = [ Target::new(TargetKind::Stdout), Target::new(TargetKind::LogDir { file_name: None }), ]; +const LOG_DATE_FORMAT: &str = "[year]-[month]-[day]_[hour]-[minute]-[second]"; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -115,8 +116,12 @@ impl From for LogLevel { } pub enum RotationStrategy { + /// Will keep all the logs, renaming them to include the date. KeepAll, + /// Will only keep the most recent log up to its maximal size. KeepOne, + /// Will keep some of the most recent logs, renaming them to include the date. + KeepSome(usize), } #[derive(Debug, Clone)] @@ -577,6 +582,34 @@ pub fn attach_logger( Ok(()) } +fn rename_file_to_dated( + path: &impl AsRef, + dir: &impl AsRef, + file_name: &str, + timezone_strategy: &TimezoneStrategy, +) -> Result<(), Error> { + let to = dir.as_ref().join(format!( + "{}_{}.log", + file_name, + timezone_strategy + .get_now() + .format(&time::format_description::parse(LOG_DATE_FORMAT).unwrap()) + .unwrap(), + )); + if to.is_file() { + // designated rotated log file name already exists + // highly unlikely but defensively handle anyway by adding .bak to filename + let mut to_bak = to.clone(); + to_bak.set_file_name(format!( + "{}.bak", + to_bak.file_name().unwrap().to_string_lossy() + )); + fs::rename(&to, to_bak)?; + } + fs::rename(path, to)?; + Ok(()) +} + fn get_log_file_path( dir: &impl AsRef, file_name: &str, @@ -591,27 +624,37 @@ fn get_log_file_path( if log_size > max_file_size { match rotation_strategy { RotationStrategy::KeepAll => { - let to = dir.as_ref().join(format!( - "{}_{}.log", - file_name, - timezone_strategy.get_now().format(&format_description!( - "[year]-[month]-[day]_[hour]-[minute]-[second]" - ))?, - )); - if to.is_file() { - // designated rotated log file name already exists - // highly unlikely but defensively handle anyway by adding .bak to filename - let mut to_bak = to.clone(); - to_bak.set_file_name(format!( - "{}.bak", - to_bak - .file_name() - .map(|f| f.to_string_lossy()) - .unwrap_or_default() - )); - fs::rename(&to, to_bak)?; + rename_file_to_dated(&path, dir, file_name, timezone_strategy)?; + } + RotationStrategy::KeepSome(how_many) => { + let mut files = fs::read_dir(dir)? + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + let old_file_name = path.file_name()?.to_string_lossy().into_owned(); + if old_file_name.starts_with(file_name) { + let date = old_file_name + .strip_prefix(file_name)? + .strip_prefix("_")? + .strip_suffix(".log")?; + Some((path, date.to_string())) + } else { + None + } + }) + .collect::>(); + // Regular sorting, so the oldest files are first. Lexicographical + // sorting is fine due to the date format. + files.sort_by(|a, b| a.1.cmp(&b.1)); + // We want to make space for the file we will be soon renaming, AND + // the file we will be creating. Thus we need to keep how_many - 2 files. + if files.len() > (*how_many - 2) { + files.truncate(files.len() + 2 - *how_many); + for (old_log_path, _) in files { + fs::remove_file(old_log_path)?; + } } - fs::rename(&path, to)?; + rename_file_to_dated(&path, dir, file_name, timezone_strategy)?; } RotationStrategy::KeepOne => { fs::remove_file(&path)?; @@ -619,6 +662,5 @@ fn get_log_file_path( } } } - Ok(path) }