mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 16:37:07 +00:00
refactor(obs): enhance log cleanup and rotation (#2040)
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use rustfs_config::observability::DEFAULT_OBS_LOG_GZIP_COMPRESSION_EXTENSION;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Write};
|
||||
use std::path::Path;
|
||||
@@ -39,7 +40,7 @@ use tracing::{debug, info};
|
||||
/// Propagates any I/O error encountered while opening, reading, writing, or
|
||||
/// flushing files.
|
||||
pub(super) fn compress_file(path: &Path, level: u32, dry_run: bool) -> Result<(), std::io::Error> {
|
||||
let gz_path = path.with_extension("gz");
|
||||
let gz_path = path.with_extension(DEFAULT_OBS_LOG_GZIP_COMPRESSION_EXTENSION);
|
||||
|
||||
if gz_path.exists() {
|
||||
debug!("Compressed file already exists, skipping: {:?}", gz_path);
|
||||
|
||||
@@ -47,7 +47,7 @@ pub struct LogCleaner {
|
||||
pub(super) match_mode: FileMatchMode,
|
||||
/// The cleaner will never delete files if doing so would leave fewer than
|
||||
/// this many files in the directory.
|
||||
pub(super) keep_count: usize,
|
||||
pub(super) keep_files: usize,
|
||||
/// Hard ceiling on the total bytes of all managed files; `0` = no limit.
|
||||
pub(super) max_total_size_bytes: u64,
|
||||
/// Hard ceiling on a single file's size; `0` = no per-file limit.
|
||||
@@ -81,7 +81,7 @@ impl LogCleaner {
|
||||
log_dir: PathBuf,
|
||||
file_pattern: String,
|
||||
match_mode: FileMatchMode,
|
||||
keep_count: usize,
|
||||
keep_files: usize,
|
||||
max_total_size_bytes: u64,
|
||||
max_single_file_size_bytes: u64,
|
||||
compress_old_files: bool,
|
||||
@@ -101,7 +101,7 @@ impl LogCleaner {
|
||||
log_dir,
|
||||
file_pattern,
|
||||
match_mode,
|
||||
keep_count,
|
||||
keep_files,
|
||||
max_total_size_bytes,
|
||||
max_single_file_size_bytes,
|
||||
compress_old_files,
|
||||
@@ -207,7 +207,7 @@ impl LogCleaner {
|
||||
/// Choose which files from `files` (sorted oldest-first) should be deleted or rotated.
|
||||
///
|
||||
/// The algorithm respects three constraints in order:
|
||||
/// 1. Always keep at least `keep_count` files.
|
||||
/// 1. Always keep at least `keep_files` files.
|
||||
/// 2. Delete old files while the total size exceeds `max_total_size_bytes`.
|
||||
/// 3. Delete any file whose individual size exceeds `max_single_file_size_bytes`.
|
||||
///
|
||||
@@ -226,27 +226,20 @@ impl LogCleaner {
|
||||
// We will protect this file from size-based deletion.
|
||||
let active_file_idx = files.len() - 1;
|
||||
|
||||
// The number of files we are allowed to delete.
|
||||
// Any file with index >= max_deletable_count is protected by keep_count.
|
||||
let max_deletable_count = files.len().saturating_sub(self.keep_count);
|
||||
// Calculate how many files we *must* delete to satisfy keep_files.
|
||||
let must_delete_count = files.len().saturating_sub(self.keep_files);
|
||||
|
||||
let mut current_size = total_size;
|
||||
|
||||
for (idx, file) in files.iter().enumerate() {
|
||||
// If we are in the protected range, we stop deleting.
|
||||
if idx >= max_deletable_count {
|
||||
// However, if the active file is too large, we might rotate it.
|
||||
if idx == active_file_idx {
|
||||
let over_single = self.max_single_file_size_bytes > 0 && file.size > self.max_single_file_size_bytes;
|
||||
if over_single {
|
||||
to_rotate = Some(file.clone());
|
||||
}
|
||||
}
|
||||
// Condition 1: Enforce keep_files.
|
||||
// If we are in the range of files that exceed the count limit, delete them.
|
||||
if idx < must_delete_count {
|
||||
current_size = current_size.saturating_sub(file.size);
|
||||
to_delete.push(file.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// We are in the deletable range. Check if we *should* delete.
|
||||
|
||||
// Condition 2: Enforce max_total_size_bytes.
|
||||
let over_total = self.max_total_size_bytes > 0 && current_size > self.max_total_size_bytes;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
//! PathBuf::from("/var/log/rustfs"),
|
||||
//! "rustfs.log.".to_string(),
|
||||
//! FileMatchMode::Prefix,
|
||||
//! 10, // keep_count
|
||||
//! 10, // keep_files
|
||||
//! 2 * 1024 * 1024 * 1024, // max_total_size_bytes (2 GiB)
|
||||
//! 0, // max_single_file_size_bytes (unlimited)
|
||||
//! true, // compress_old_files
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
create_log_file(&dir, "app.log.2024-01-03", 1024)?;
|
||||
create_log_file(&dir, "other.log", 1024)?; // not managed
|
||||
|
||||
// Total managed = 3 072 bytes; limit = 2 048; keep_count = 2 → must delete 1.
|
||||
// Total managed = 3 072 bytes; limit = 2 048; keep_files = 2 → must delete 1.
|
||||
let cleaner = make_cleaner(dir.clone(), 2, 2048);
|
||||
let (deleted, freed) = cleaner.cleanup()?;
|
||||
|
||||
@@ -116,7 +116,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_respects_keep_count() -> std::io::Result<()> {
|
||||
fn test_cleanup_respects_keep_files() -> std::io::Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let dir = tmp.path().to_path_buf();
|
||||
|
||||
@@ -124,10 +124,11 @@ mod tests {
|
||||
create_log_file(&dir, &format!("app.log.2024-01-0{i}"), 1024)?;
|
||||
}
|
||||
|
||||
// No size limit, keep_count = 3 → nothing to delete (5 > 3 but size == 0 limit).
|
||||
let cleaner = make_cleaner(dir.clone(), 3, 0);
|
||||
let (deleted, _) = cleaner.cleanup()?;
|
||||
assert_eq!(deleted, 0, "keep_count prevents deletion when no size limit");
|
||||
|
||||
// Updated expectation: keep_files acts as a limit (ceiling), so excess files are deleted.
|
||||
assert_eq!(deleted, 2, "keep_files should enforce a maximum file count");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//! compress any files — it only reports what it found.
|
||||
|
||||
use super::types::{FileInfo, FileMatchMode};
|
||||
use rustfs_config::observability::DEFAULT_OBS_LOG_GZIP_COMPRESSION_ALL_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tracing::debug;
|
||||
@@ -81,7 +82,7 @@ pub(super) fn collect_log_files(
|
||||
}
|
||||
|
||||
// Compressed files are handled by collect_compressed_files.
|
||||
if filename.ends_with(".gz") {
|
||||
if filename.ends_with(DEFAULT_OBS_LOG_GZIP_COMPRESSION_ALL_EXTENSION) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ pub(super) fn collect_expired_compressed_files(
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if !filename.ends_with(".gz") {
|
||||
if !filename.ends_with(DEFAULT_OBS_LOG_GZIP_COMPRESSION_ALL_EXTENSION) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
//! Shared types used across the log-cleanup sub-modules.
|
||||
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -21,11 +22,29 @@ use std::time::SystemTime;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileMatchMode {
|
||||
/// The filename must start with the pattern (e.g. "app.log." matches "app.log.2024-01-01").
|
||||
/// Corresponds to config value "prefix".
|
||||
Prefix,
|
||||
/// The filename must end with the pattern (e.g. ".log" matches "2024-01-01.log").
|
||||
/// Corresponds to config value "suffix".
|
||||
Suffix,
|
||||
}
|
||||
|
||||
impl FileMatchMode {
|
||||
/// Returns the string representation of the match mode.
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
FileMatchMode::Prefix => "prefix",
|
||||
FileMatchMode::Suffix => "suffix",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FileMatchMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata for a single log file discovered by the scanner.
|
||||
///
|
||||
/// Carries enough information to make cleanup decisions (sort by age, compare
|
||||
|
||||
Reference in New Issue
Block a user