mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-31 09:18:28 +00:00
refactor(obs): enhance log cleanup and rotation (#2040)
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
// Observability Keys
|
// Observability Keys
|
||||||
|
|
||||||
mod metrics;
|
mod metrics;
|
||||||
|
use const_str::concat;
|
||||||
pub use metrics::*;
|
pub use metrics::*;
|
||||||
|
|
||||||
pub const ENV_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
|
pub const ENV_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
|
||||||
@@ -42,7 +43,6 @@ pub const ENV_OBS_LOG_ROTATION_TIME: &str = "RUSTFS_OBS_LOG_ROTATION_TIME";
|
|||||||
pub const ENV_OBS_LOG_KEEP_FILES: &str = "RUSTFS_OBS_LOG_KEEP_FILES";
|
pub const ENV_OBS_LOG_KEEP_FILES: &str = "RUSTFS_OBS_LOG_KEEP_FILES";
|
||||||
|
|
||||||
/// Log cleanup related configurations
|
/// Log cleanup related configurations
|
||||||
pub const ENV_OBS_LOG_KEEP_COUNT: &str = "RUSTFS_OBS_LOG_KEEP_COUNT";
|
|
||||||
pub const ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES: &str = "RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES";
|
pub const ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES: &str = "RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES";
|
||||||
pub const ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES: &str = "RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES";
|
pub const ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES: &str = "RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES";
|
||||||
pub const ENV_OBS_LOG_COMPRESS_OLD_FILES: &str = "RUSTFS_OBS_LOG_COMPRESS_OLD_FILES";
|
pub const ENV_OBS_LOG_COMPRESS_OLD_FILES: &str = "RUSTFS_OBS_LOG_COMPRESS_OLD_FILES";
|
||||||
@@ -56,15 +56,16 @@ pub const ENV_OBS_LOG_DRY_RUN: &str = "RUSTFS_OBS_LOG_DRY_RUN";
|
|||||||
pub const ENV_OBS_LOG_MATCH_MODE: &str = "RUSTFS_OBS_LOG_MATCH_MODE";
|
pub const ENV_OBS_LOG_MATCH_MODE: &str = "RUSTFS_OBS_LOG_MATCH_MODE";
|
||||||
|
|
||||||
/// Default values for log cleanup
|
/// Default values for log cleanup
|
||||||
pub const DEFAULT_OBS_LOG_KEEP_COUNT: usize = 10;
|
|
||||||
pub const DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES: u64 = 2 * 1024 * 1024 * 1024; // 2 GiB
|
pub const DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES: u64 = 2 * 1024 * 1024 * 1024; // 2 GiB
|
||||||
pub const DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES: u64 = 0; // No single file limit
|
pub const DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES: u64 = 0; // No single file limit
|
||||||
pub const DEFAULT_OBS_LOG_COMPRESS_OLD_FILES: bool = true;
|
pub const DEFAULT_OBS_LOG_COMPRESS_OLD_FILES: bool = true;
|
||||||
pub const DEFAULT_OBS_LOG_GZIP_COMPRESSION_LEVEL: u32 = 6;
|
pub const DEFAULT_OBS_LOG_GZIP_COMPRESSION_LEVEL: u32 = 6;
|
||||||
|
pub const DEFAULT_OBS_LOG_GZIP_COMPRESSION_EXTENSION: &str = "gz";
|
||||||
|
pub const DEFAULT_OBS_LOG_GZIP_COMPRESSION_ALL_EXTENSION: &str = concat!(".", DEFAULT_OBS_LOG_GZIP_COMPRESSION_EXTENSION);
|
||||||
pub const DEFAULT_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS: u64 = 30;
|
pub const DEFAULT_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS: u64 = 30;
|
||||||
pub const DEFAULT_OBS_LOG_DELETE_EMPTY_FILES: bool = true;
|
pub const DEFAULT_OBS_LOG_DELETE_EMPTY_FILES: bool = true;
|
||||||
pub const DEFAULT_OBS_LOG_MIN_FILE_AGE_SECONDS: u64 = 3600; // 1 hour
|
pub const DEFAULT_OBS_LOG_MIN_FILE_AGE_SECONDS: u64 = 3600; // 1 hour
|
||||||
pub const DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS: u64 = 6 * 3600; // 6 hours
|
pub const DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS: u64 = 1800; // 0.5 hours
|
||||||
pub const DEFAULT_OBS_LOG_DRY_RUN: bool = false;
|
pub const DEFAULT_OBS_LOG_DRY_RUN: bool = false;
|
||||||
pub const DEFAULT_OBS_LOG_MATCH_MODE: &str = "suffix";
|
pub const DEFAULT_OBS_LOG_MATCH_MODE: &str = "suffix";
|
||||||
|
|
||||||
@@ -106,7 +107,6 @@ mod tests {
|
|||||||
assert_eq!(ENV_OBS_METRICS_EXPORT_ENABLED, "RUSTFS_OBS_METRICS_EXPORT_ENABLED");
|
assert_eq!(ENV_OBS_METRICS_EXPORT_ENABLED, "RUSTFS_OBS_METRICS_EXPORT_ENABLED");
|
||||||
assert_eq!(ENV_OBS_LOGS_EXPORT_ENABLED, "RUSTFS_OBS_LOGS_EXPORT_ENABLED");
|
assert_eq!(ENV_OBS_LOGS_EXPORT_ENABLED, "RUSTFS_OBS_LOGS_EXPORT_ENABLED");
|
||||||
// Test log cleanup related env keys
|
// Test log cleanup related env keys
|
||||||
assert_eq!(ENV_OBS_LOG_KEEP_COUNT, "RUSTFS_OBS_LOG_KEEP_COUNT");
|
|
||||||
assert_eq!(ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES, "RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES");
|
assert_eq!(ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES, "RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES");
|
||||||
assert_eq!(ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES, "RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES");
|
assert_eq!(ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES, "RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES");
|
||||||
assert_eq!(ENV_OBS_LOG_COMPRESS_OLD_FILES, "RUSTFS_OBS_LOG_COMPRESS_OLD_FILES");
|
assert_eq!(ENV_OBS_LOG_COMPRESS_OLD_FILES, "RUSTFS_OBS_LOG_COMPRESS_OLD_FILES");
|
||||||
|
|||||||
@@ -132,15 +132,15 @@ All configuration is read from environment variables at startup.
|
|||||||
| `RUSTFS_OBS_LOGGER_LEVEL` | `info` | Log level; `RUST_LOG` syntax supported |
|
| `RUSTFS_OBS_LOGGER_LEVEL` | `info` | Log level; `RUST_LOG` syntax supported |
|
||||||
| `RUSTFS_OBS_LOG_STDOUT_ENABLED` | `false` | When file logging is active, also mirror to stdout |
|
| `RUSTFS_OBS_LOG_STDOUT_ENABLED` | `false` | When file logging is active, also mirror to stdout |
|
||||||
| `RUSTFS_OBS_LOG_DIRECTORY` | _(empty)_ | **Directory for rolling log files. When empty, logs go to stdout only** |
|
| `RUSTFS_OBS_LOG_DIRECTORY` | _(empty)_ | **Directory for rolling log files. When empty, logs go to stdout only** |
|
||||||
| `RUSTFS_OBS_LOG_FILENAME` | `rustfs` | Base filename for rolling logs (date suffix added automatically) |
|
| `RUSTFS_OBS_LOG_FILENAME` | `rustfs.log` | Base filename for rolling logs (date suffix added automatically) |
|
||||||
| `RUSTFS_OBS_LOG_ROTATION_TIME` | `hourly` | Rotation granularity: `minutely`, `hourly`, or `daily` |
|
| `RUSTFS_OBS_LOG_ROTATION_TIME` | `hourly` | Rotation granularity: `minutely`, `hourly`, or `daily` |
|
||||||
| `RUSTFS_OBS_LOG_KEEP_FILES` | `30` | Number of rolling files to keep |
|
| `RUSTFS_OBS_LOG_KEEP_FILES` | `30` | Number of rolling files to keep (also used by cleaner) |
|
||||||
|
| `RUSTFS_OBS_LOG_MATCH_MODE` | `suffix` | File matching mode: `prefix` or `suffix` |
|
||||||
|
|
||||||
### Log cleanup
|
### Log cleanup
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `RUSTFS_OBS_LOG_KEEP_COUNT` | `10` | Minimum files the cleaner must always preserve |
|
|
||||||
| `RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES` | `2147483648` | Hard cap on total log directory size (2 GiB) |
|
| `RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES` | `2147483648` | Hard cap on total log directory size (2 GiB) |
|
||||||
| `RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES` | `0` | Per-file size cap; `0` = unlimited |
|
| `RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES` | `0` | Per-file size cap; `0` = unlimited |
|
||||||
| `RUSTFS_OBS_LOG_COMPRESS_OLD_FILES` | `true` | Gzip-compress files before deleting |
|
| `RUSTFS_OBS_LOG_COMPRESS_OLD_FILES` | `true` | Gzip-compress files before deleting |
|
||||||
@@ -149,7 +149,7 @@ All configuration is read from environment variables at startup.
|
|||||||
| `RUSTFS_OBS_LOG_EXCLUDE_PATTERNS` | _(empty)_ | Comma-separated glob patterns to never clean up |
|
| `RUSTFS_OBS_LOG_EXCLUDE_PATTERNS` | _(empty)_ | Comma-separated glob patterns to never clean up |
|
||||||
| `RUSTFS_OBS_LOG_DELETE_EMPTY_FILES` | `true` | Remove zero-byte files |
|
| `RUSTFS_OBS_LOG_DELETE_EMPTY_FILES` | `true` | Remove zero-byte files |
|
||||||
| `RUSTFS_OBS_LOG_MIN_FILE_AGE_SECONDS` | `3600` | Minimum file age (seconds) before cleanup |
|
| `RUSTFS_OBS_LOG_MIN_FILE_AGE_SECONDS` | `3600` | Minimum file age (seconds) before cleanup |
|
||||||
| `RUSTFS_OBS_LOG_CLEANUP_INTERVAL_SECONDS` | `21600` | How often the cleanup task runs (6 hours) |
|
| `RUSTFS_OBS_LOG_CLEANUP_INTERVAL_SECONDS` | `1800` | How often the cleanup task runs (0.5 hours) |
|
||||||
| `RUSTFS_OBS_LOG_DRY_RUN` | `false` | Report deletions without actually removing files |
|
| `RUSTFS_OBS_LOG_DRY_RUN` | `false` | Report deletions without actually removing files |
|
||||||
|
|
||||||
|
|
||||||
@@ -223,12 +223,12 @@ rustfs-obs/src/
|
|||||||
│ ├── otel.rs # Full OTLP/HTTP pipeline
|
│ ├── otel.rs # Full OTLP/HTTP pipeline
|
||||||
│ └── recorder.rs # metrics-crate → OTel bridge (Recorder)
|
│ └── recorder.rs # metrics-crate → OTel bridge (Recorder)
|
||||||
│
|
│
|
||||||
├── log_cleanup/ # Background log-file cleanup subsystem
|
├── cleaner/ # Background log-file cleanup subsystem
|
||||||
│ ├── mod.rs # LogCleaner public API + tests
|
│ ├── mod.rs # LogCleaner public API + tests
|
||||||
│ ├── types.rs # FileInfo shared type
|
│ ├── types.rs # FileInfo shared type
|
||||||
│ ├── scanner.rs # Filesystem discovery
|
│ ├── scanner.rs # Filesystem discovery
|
||||||
│ ├── compress.rs # Gzip compression helper
|
│ ├── compress.rs # Gzip compression helper
|
||||||
│ └── cleaner.rs # Selection, compression, deletion logic
|
│ └── core.rs # Selection, compression, deletion logic
|
||||||
│
|
│
|
||||||
└── system/ # Host metrics (CPU, memory, disk, GPU)
|
└── system/ # Host metrics (CPU, memory, disk, GPU)
|
||||||
├── mod.rs
|
├── mod.rs
|
||||||
@@ -245,10 +245,12 @@ rustfs-obs/src/
|
|||||||
```rust
|
```rust
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use rustfs_obs::LogCleaner;
|
use rustfs_obs::LogCleaner;
|
||||||
|
use rustfs_obs::types::FileMatchMode;
|
||||||
|
|
||||||
let cleaner = LogCleaner::new(
|
let cleaner = LogCleaner::new(
|
||||||
PathBuf::from("/var/log/rustfs"),
|
PathBuf::from("/var/log/rustfs"),
|
||||||
"rustfs.log.".to_string(), // file_prefix
|
"rustfs.log.".to_string(), // file_pattern
|
||||||
|
FileMatchMode::Prefix, // match_mode
|
||||||
10, // keep_count
|
10, // keep_count
|
||||||
2 * 1024 * 1024 * 1024, // max_total_size_bytes (2 GiB)
|
2 * 1024 * 1024 * 1024, // max_total_size_bytes (2 GiB)
|
||||||
0, // max_single_file_size_bytes (unlimited)
|
0, // max_single_file_size_bytes (unlimited)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
use flate2::Compression;
|
use flate2::Compression;
|
||||||
use flate2::write::GzEncoder;
|
use flate2::write::GzEncoder;
|
||||||
|
use rustfs_config::observability::DEFAULT_OBS_LOG_GZIP_COMPRESSION_EXTENSION;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufReader, BufWriter, Write};
|
use std::io::{BufReader, BufWriter, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -39,7 +40,7 @@ use tracing::{debug, info};
|
|||||||
/// Propagates any I/O error encountered while opening, reading, writing, or
|
/// Propagates any I/O error encountered while opening, reading, writing, or
|
||||||
/// flushing files.
|
/// flushing files.
|
||||||
pub(super) fn compress_file(path: &Path, level: u32, dry_run: bool) -> Result<(), std::io::Error> {
|
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() {
|
if gz_path.exists() {
|
||||||
debug!("Compressed file already exists, skipping: {:?}", gz_path);
|
debug!("Compressed file already exists, skipping: {:?}", gz_path);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub struct LogCleaner {
|
|||||||
pub(super) match_mode: FileMatchMode,
|
pub(super) match_mode: FileMatchMode,
|
||||||
/// The cleaner will never delete files if doing so would leave fewer than
|
/// The cleaner will never delete files if doing so would leave fewer than
|
||||||
/// this many files in the directory.
|
/// 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.
|
/// Hard ceiling on the total bytes of all managed files; `0` = no limit.
|
||||||
pub(super) max_total_size_bytes: u64,
|
pub(super) max_total_size_bytes: u64,
|
||||||
/// Hard ceiling on a single file's size; `0` = no per-file limit.
|
/// Hard ceiling on a single file's size; `0` = no per-file limit.
|
||||||
@@ -81,7 +81,7 @@ impl LogCleaner {
|
|||||||
log_dir: PathBuf,
|
log_dir: PathBuf,
|
||||||
file_pattern: String,
|
file_pattern: String,
|
||||||
match_mode: FileMatchMode,
|
match_mode: FileMatchMode,
|
||||||
keep_count: usize,
|
keep_files: usize,
|
||||||
max_total_size_bytes: u64,
|
max_total_size_bytes: u64,
|
||||||
max_single_file_size_bytes: u64,
|
max_single_file_size_bytes: u64,
|
||||||
compress_old_files: bool,
|
compress_old_files: bool,
|
||||||
@@ -101,7 +101,7 @@ impl LogCleaner {
|
|||||||
log_dir,
|
log_dir,
|
||||||
file_pattern,
|
file_pattern,
|
||||||
match_mode,
|
match_mode,
|
||||||
keep_count,
|
keep_files,
|
||||||
max_total_size_bytes,
|
max_total_size_bytes,
|
||||||
max_single_file_size_bytes,
|
max_single_file_size_bytes,
|
||||||
compress_old_files,
|
compress_old_files,
|
||||||
@@ -207,7 +207,7 @@ impl LogCleaner {
|
|||||||
/// Choose which files from `files` (sorted oldest-first) should be deleted or rotated.
|
/// Choose which files from `files` (sorted oldest-first) should be deleted or rotated.
|
||||||
///
|
///
|
||||||
/// The algorithm respects three constraints in order:
|
/// 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`.
|
/// 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`.
|
/// 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.
|
// We will protect this file from size-based deletion.
|
||||||
let active_file_idx = files.len() - 1;
|
let active_file_idx = files.len() - 1;
|
||||||
|
|
||||||
// The number of files we are allowed to delete.
|
// Calculate how many files we *must* delete to satisfy keep_files.
|
||||||
// Any file with index >= max_deletable_count is protected by keep_count.
|
let must_delete_count = files.len().saturating_sub(self.keep_files);
|
||||||
let max_deletable_count = files.len().saturating_sub(self.keep_count);
|
|
||||||
|
|
||||||
let mut current_size = total_size;
|
let mut current_size = total_size;
|
||||||
|
|
||||||
for (idx, file) in files.iter().enumerate() {
|
for (idx, file) in files.iter().enumerate() {
|
||||||
// If we are in the protected range, we stop deleting.
|
// Condition 1: Enforce keep_files.
|
||||||
if idx >= max_deletable_count {
|
// If we are in the range of files that exceed the count limit, delete them.
|
||||||
// However, if the active file is too large, we might rotate it.
|
if idx < must_delete_count {
|
||||||
if idx == active_file_idx {
|
current_size = current_size.saturating_sub(file.size);
|
||||||
let over_single = self.max_single_file_size_bytes > 0 && file.size > self.max_single_file_size_bytes;
|
to_delete.push(file.clone());
|
||||||
if over_single {
|
|
||||||
to_rotate = Some(file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are in the deletable range. Check if we *should* delete.
|
|
||||||
|
|
||||||
// Condition 2: Enforce max_total_size_bytes.
|
// Condition 2: Enforce max_total_size_bytes.
|
||||||
let over_total = self.max_total_size_bytes > 0 && current_size > self.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"),
|
//! PathBuf::from("/var/log/rustfs"),
|
||||||
//! "rustfs.log.".to_string(),
|
//! "rustfs.log.".to_string(),
|
||||||
//! FileMatchMode::Prefix,
|
//! FileMatchMode::Prefix,
|
||||||
//! 10, // keep_count
|
//! 10, // keep_files
|
||||||
//! 2 * 1024 * 1024 * 1024, // max_total_size_bytes (2 GiB)
|
//! 2 * 1024 * 1024 * 1024, // max_total_size_bytes (2 GiB)
|
||||||
//! 0, // max_single_file_size_bytes (unlimited)
|
//! 0, // max_single_file_size_bytes (unlimited)
|
||||||
//! true, // compress_old_files
|
//! true, // compress_old_files
|
||||||
@@ -106,7 +106,7 @@ mod tests {
|
|||||||
create_log_file(&dir, "app.log.2024-01-03", 1024)?;
|
create_log_file(&dir, "app.log.2024-01-03", 1024)?;
|
||||||
create_log_file(&dir, "other.log", 1024)?; // not managed
|
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 cleaner = make_cleaner(dir.clone(), 2, 2048);
|
||||||
let (deleted, freed) = cleaner.cleanup()?;
|
let (deleted, freed) = cleaner.cleanup()?;
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cleanup_respects_keep_count() -> std::io::Result<()> {
|
fn test_cleanup_respects_keep_files() -> std::io::Result<()> {
|
||||||
let tmp = TempDir::new()?;
|
let tmp = TempDir::new()?;
|
||||||
let dir = tmp.path().to_path_buf();
|
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)?;
|
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 cleaner = make_cleaner(dir.clone(), 3, 0);
|
||||||
let (deleted, _) = cleaner.cleanup()?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
//! compress any files — it only reports what it found.
|
//! compress any files — it only reports what it found.
|
||||||
|
|
||||||
use super::types::{FileInfo, FileMatchMode};
|
use super::types::{FileInfo, FileMatchMode};
|
||||||
|
use rustfs_config::observability::DEFAULT_OBS_LOG_GZIP_COMPRESSION_ALL_EXTENSION;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@@ -81,7 +82,7 @@ pub(super) fn collect_log_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compressed files are handled by collect_compressed_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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ pub(super) fn collect_expired_compressed_files(
|
|||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !filename.ends_with(".gz") {
|
if !filename.ends_with(DEFAULT_OBS_LOG_GZIP_COMPRESSION_ALL_EXTENSION) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
//! Shared types used across the log-cleanup sub-modules.
|
//! Shared types used across the log-cleanup sub-modules.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
@@ -21,11 +22,29 @@ use std::time::SystemTime;
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum FileMatchMode {
|
pub enum FileMatchMode {
|
||||||
/// The filename must start with the pattern (e.g. "app.log." matches "app.log.2024-01-01").
|
/// The filename must start with the pattern (e.g. "app.log." matches "app.log.2024-01-01").
|
||||||
|
/// Corresponds to config value "prefix".
|
||||||
Prefix,
|
Prefix,
|
||||||
/// The filename must end with the pattern (e.g. ".log" matches "2024-01-01.log").
|
/// The filename must end with the pattern (e.g. ".log" matches "2024-01-01.log").
|
||||||
|
/// Corresponds to config value "suffix".
|
||||||
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.
|
/// Metadata for a single log file discovered by the scanner.
|
||||||
///
|
///
|
||||||
/// Carries enough information to make cleanup decisions (sort by age, compare
|
/// Carries enough information to make cleanup decisions (sort by age, compare
|
||||||
|
|||||||
+16
-22
@@ -25,16 +25,16 @@
|
|||||||
use rustfs_config::observability::{
|
use rustfs_config::observability::{
|
||||||
DEFAULT_OBS_ENVIRONMENT_PRODUCTION, DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS, DEFAULT_OBS_LOG_COMPRESS_OLD_FILES,
|
DEFAULT_OBS_ENVIRONMENT_PRODUCTION, DEFAULT_OBS_LOG_CLEANUP_INTERVAL_SECONDS, DEFAULT_OBS_LOG_COMPRESS_OLD_FILES,
|
||||||
DEFAULT_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS, DEFAULT_OBS_LOG_DELETE_EMPTY_FILES, DEFAULT_OBS_LOG_DRY_RUN,
|
DEFAULT_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS, DEFAULT_OBS_LOG_DELETE_EMPTY_FILES, DEFAULT_OBS_LOG_DRY_RUN,
|
||||||
DEFAULT_OBS_LOG_GZIP_COMPRESSION_LEVEL, DEFAULT_OBS_LOG_KEEP_COUNT, DEFAULT_OBS_LOG_MATCH_MODE,
|
DEFAULT_OBS_LOG_GZIP_COMPRESSION_LEVEL, DEFAULT_OBS_LOG_MATCH_MODE, DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES,
|
||||||
DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES, DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES, DEFAULT_OBS_LOG_MIN_FILE_AGE_SECONDS,
|
DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES, DEFAULT_OBS_LOG_MIN_FILE_AGE_SECONDS, ENV_OBS_ENDPOINT, ENV_OBS_ENVIRONMENT,
|
||||||
ENV_OBS_ENDPOINT, ENV_OBS_ENVIRONMENT, ENV_OBS_LOG_CLEANUP_INTERVAL_SECONDS, ENV_OBS_LOG_COMPRESS_OLD_FILES,
|
ENV_OBS_LOG_CLEANUP_INTERVAL_SECONDS, ENV_OBS_LOG_COMPRESS_OLD_FILES, ENV_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS,
|
||||||
ENV_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS, ENV_OBS_LOG_DELETE_EMPTY_FILES, ENV_OBS_LOG_DIRECTORY, ENV_OBS_LOG_DRY_RUN,
|
ENV_OBS_LOG_DELETE_EMPTY_FILES, ENV_OBS_LOG_DIRECTORY, ENV_OBS_LOG_DRY_RUN, ENV_OBS_LOG_ENDPOINT,
|
||||||
ENV_OBS_LOG_ENDPOINT, ENV_OBS_LOG_EXCLUDE_PATTERNS, ENV_OBS_LOG_FILENAME, ENV_OBS_LOG_GZIP_COMPRESSION_LEVEL,
|
ENV_OBS_LOG_EXCLUDE_PATTERNS, ENV_OBS_LOG_FILENAME, ENV_OBS_LOG_GZIP_COMPRESSION_LEVEL, ENV_OBS_LOG_KEEP_FILES,
|
||||||
ENV_OBS_LOG_KEEP_COUNT, ENV_OBS_LOG_KEEP_FILES, ENV_OBS_LOG_MATCH_MODE, ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES,
|
ENV_OBS_LOG_MATCH_MODE, ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES, ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES,
|
||||||
ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES, ENV_OBS_LOG_MIN_FILE_AGE_SECONDS, ENV_OBS_LOG_ROTATION_TIME, ENV_OBS_LOG_STDOUT_ENABLED,
|
ENV_OBS_LOG_MIN_FILE_AGE_SECONDS, ENV_OBS_LOG_ROTATION_TIME, ENV_OBS_LOG_STDOUT_ENABLED, ENV_OBS_LOGGER_LEVEL,
|
||||||
ENV_OBS_LOGGER_LEVEL, ENV_OBS_LOGS_EXPORT_ENABLED, ENV_OBS_METER_INTERVAL, ENV_OBS_METRIC_ENDPOINT,
|
ENV_OBS_LOGS_EXPORT_ENABLED, ENV_OBS_METER_INTERVAL, ENV_OBS_METRIC_ENDPOINT, ENV_OBS_METRICS_EXPORT_ENABLED,
|
||||||
ENV_OBS_METRICS_EXPORT_ENABLED, ENV_OBS_SAMPLE_RATIO, ENV_OBS_SERVICE_NAME, ENV_OBS_SERVICE_VERSION, ENV_OBS_TRACE_ENDPOINT,
|
ENV_OBS_SAMPLE_RATIO, ENV_OBS_SERVICE_NAME, ENV_OBS_SERVICE_VERSION, ENV_OBS_TRACE_ENDPOINT, ENV_OBS_TRACES_EXPORT_ENABLED,
|
||||||
ENV_OBS_TRACES_EXPORT_ENABLED, ENV_OBS_USE_STDOUT,
|
ENV_OBS_USE_STDOUT,
|
||||||
};
|
};
|
||||||
use rustfs_config::{
|
use rustfs_config::{
|
||||||
APP_NAME, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, DEFAULT_LOG_ROTATION_TIME, DEFAULT_OBS_LOG_FILENAME,
|
APP_NAME, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, DEFAULT_LOG_ROTATION_TIME, DEFAULT_OBS_LOG_FILENAME,
|
||||||
@@ -65,8 +65,8 @@ use std::env;
|
|||||||
///
|
///
|
||||||
/// - All fields are `Option<T>` to allow partial configuration via environment
|
/// - All fields are `Option<T>` to allow partial configuration via environment
|
||||||
/// variables with sensible defaults provided by constants in `rustfs-config`.
|
/// variables with sensible defaults provided by constants in `rustfs-config`.
|
||||||
/// - `log_keep_count` represents the cleaner's minimum retention; `log_keep_files`
|
/// - `log_keep_files` is used to derive the rolling-appender's upper bound on
|
||||||
/// controls the rolling-appender's file limit (both typically set to the same value).
|
/// retained files (if enabled) and to set the cleaner's minimum retention count.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@@ -128,13 +128,11 @@ pub struct OtelConfig {
|
|||||||
/// Rotation time granularity: `"hourly"` or `"daily"` (default: `"daily"`).
|
/// Rotation time granularity: `"hourly"` or `"daily"` (default: `"daily"`).
|
||||||
pub log_rotation_time: Option<String>,
|
pub log_rotation_time: Option<String>,
|
||||||
/// Number of rolling log files to retain (default: `30`).
|
/// Number of rolling log files to retain (default: `30`).
|
||||||
/// The rolling-appender will delete the oldest file when this limit is exceeded.
|
/// Used by both the rolling-appender (as a loose upper bound) and the
|
||||||
|
/// background cleaner (as the minimum retention count).
|
||||||
pub log_keep_files: Option<usize>,
|
pub log_keep_files: Option<usize>,
|
||||||
|
|
||||||
// ── Log cleanup ───────────────────────────────────────────────────────────
|
// ── Log cleanup ───────────────────────────────────────────────────────────
|
||||||
/// Minimum number of files the cleaner must always preserve.
|
|
||||||
/// Typically set to the same value as `log_keep_files`.
|
|
||||||
pub log_keep_count: Option<usize>,
|
|
||||||
/// Hard ceiling on the total size (bytes) of all log files (default: 2 GiB).
|
/// Hard ceiling on the total size (bytes) of all log files (default: 2 GiB).
|
||||||
pub log_max_total_size_bytes: Option<u64>,
|
pub log_max_total_size_bytes: Option<u64>,
|
||||||
/// Per-file size ceiling (bytes); `0` means unlimited (default: `0`).
|
/// Per-file size ceiling (bytes); `0` means unlimited (default: `0`).
|
||||||
@@ -205,12 +203,9 @@ impl OtelConfig {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// `log_keep_files` (legacy) and `log_keep_count` (new) share the same
|
// `log_keep_files` is the single source of truth for file retention count.
|
||||||
// environment variables but have slightly different semantics.
|
// It defaults to `DEFAULT_LOG_KEEP_FILES` (30).
|
||||||
// `log_keep_files` is the rolling-appender retention count; `log_keep_count`
|
|
||||||
// is the cleaner's minimum-keep threshold. Both default to the same value.
|
|
||||||
let log_keep_files = Some(get_env_usize(ENV_OBS_LOG_KEEP_FILES, DEFAULT_LOG_KEEP_FILES));
|
let log_keep_files = Some(get_env_usize(ENV_OBS_LOG_KEEP_FILES, DEFAULT_LOG_KEEP_FILES));
|
||||||
let log_keep_count = Some(get_env_usize(ENV_OBS_LOG_KEEP_COUNT, DEFAULT_OBS_LOG_KEEP_COUNT));
|
|
||||||
|
|
||||||
// `log_rotation_time` drives the rolling-appender rotation period.
|
// `log_rotation_time` drives the rolling-appender rotation period.
|
||||||
let log_rotation_time = Some(get_env_str(ENV_OBS_LOG_ROTATION_TIME, DEFAULT_LOG_ROTATION_TIME));
|
let log_rotation_time = Some(get_env_str(ENV_OBS_LOG_ROTATION_TIME, DEFAULT_LOG_ROTATION_TIME));
|
||||||
@@ -238,7 +233,6 @@ impl OtelConfig {
|
|||||||
log_rotation_time,
|
log_rotation_time,
|
||||||
log_keep_files,
|
log_keep_files,
|
||||||
// Log cleanup
|
// Log cleanup
|
||||||
log_keep_count,
|
|
||||||
log_max_total_size_bytes: Some(get_env_u64(ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES, DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES)),
|
log_max_total_size_bytes: Some(get_env_u64(ENV_OBS_LOG_MAX_TOTAL_SIZE_BYTES, DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES)),
|
||||||
log_max_single_file_size_bytes: Some(get_env_u64(
|
log_max_single_file_size_bytes: Some(get_env_u64(
|
||||||
ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES,
|
ENV_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
//! 4. Cleanup task — aborted to prevent lingering background work.
|
//! 4. Cleanup task — aborted to prevent lingering background work.
|
||||||
//! 5. Tracing worker guard — flushes buffered log lines written by
|
//! 5. Tracing worker guard — flushes buffered log lines written by
|
||||||
//! `tracing_appender`.
|
//! `tracing_appender`.
|
||||||
|
//! 6. Stdout worker guard — flushes buffered log lines written to stdout.
|
||||||
|
|
||||||
use opentelemetry_sdk::{logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider};
|
use opentelemetry_sdk::{logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider};
|
||||||
|
|
||||||
@@ -39,13 +40,13 @@ pub struct OtelGuard {
|
|||||||
pub(crate) meter_provider: Option<SdkMeterProvider>,
|
pub(crate) meter_provider: Option<SdkMeterProvider>,
|
||||||
/// Optional logger provider for OTLP log export.
|
/// Optional logger provider for OTLP log export.
|
||||||
pub(crate) logger_provider: Option<SdkLoggerProvider>,
|
pub(crate) logger_provider: Option<SdkLoggerProvider>,
|
||||||
|
/// Handle to the background log-cleanup task; aborted on drop.
|
||||||
|
pub(crate) cleanup_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
/// Worker guard that keeps the non-blocking `tracing_appender` thread
|
/// Worker guard that keeps the non-blocking `tracing_appender` thread
|
||||||
/// alive. Dropping it blocks until all buffered records are flushed.
|
/// alive. Dropping it blocks until all buffered records are flushed.
|
||||||
pub(crate) tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
pub(crate) tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||||
/// Optional guard for stdout logging; kept separate to allow independent flushing and shutdown.
|
/// Optional guard for stdout logging; kept separate to allow independent flushing and shutdown.
|
||||||
pub(crate) stdout_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
pub(crate) stdout_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||||
/// Handle to the background log-cleanup task; aborted on drop.
|
|
||||||
pub(crate) cleanup_handle: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for OtelGuard {
|
impl std::fmt::Debug for OtelGuard {
|
||||||
@@ -54,9 +55,9 @@ impl std::fmt::Debug for OtelGuard {
|
|||||||
.field("tracer_provider", &self.tracer_provider.is_some())
|
.field("tracer_provider", &self.tracer_provider.is_some())
|
||||||
.field("meter_provider", &self.meter_provider.is_some())
|
.field("meter_provider", &self.meter_provider.is_some())
|
||||||
.field("logger_provider", &self.logger_provider.is_some())
|
.field("logger_provider", &self.logger_provider.is_some())
|
||||||
|
.field("cleanup_handle", &self.cleanup_handle.is_some())
|
||||||
.field("tracing_guard", &self.tracing_guard.is_some())
|
.field("tracing_guard", &self.tracing_guard.is_some())
|
||||||
.field("stdout_guard", &self.stdout_guard.is_some())
|
.field("stdout_guard", &self.stdout_guard.is_some())
|
||||||
.field("cleanup_handle", &self.cleanup_handle.is_some())
|
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ fn init_file_logging_internal(
|
|||||||
|
|
||||||
let mut builder = RollingFileAppender::builder()
|
let mut builder = RollingFileAppender::builder()
|
||||||
.rotation(rotation)
|
.rotation(rotation)
|
||||||
.max_log_files(keep_files * 2); // Make sure there are some data files to archive to avoid premature deletion
|
.max_log_files(keep_files * 3); // Make sure there are some data files to archive to avoid premature deletion
|
||||||
|
|
||||||
match match_mode {
|
match match_mode {
|
||||||
FileMatchMode::Prefix => builder = builder.filename_prefix(log_filename),
|
FileMatchMode::Prefix => builder = builder.filename_prefix(log_filename),
|
||||||
@@ -355,10 +355,9 @@ fn spawn_cleanup_task(
|
|||||||
_ => FileMatchMode::Suffix,
|
_ => FileMatchMode::Suffix,
|
||||||
};
|
};
|
||||||
|
|
||||||
let keep_count = config.log_keep_count.unwrap_or(keep_files);
|
|
||||||
let max_total_size = config
|
let max_total_size = config
|
||||||
.log_max_total_size_bytes
|
.log_max_total_size_bytes
|
||||||
.unwrap_or(DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES * keep_count as u64);
|
.unwrap_or(DEFAULT_OBS_LOG_MAX_TOTAL_SIZE_BYTES);
|
||||||
let max_single_file_size = config
|
let max_single_file_size = config
|
||||||
.log_max_single_file_size_bytes
|
.log_max_single_file_size_bytes
|
||||||
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
|
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
|
||||||
@@ -387,7 +386,7 @@ fn spawn_cleanup_task(
|
|||||||
log_dir,
|
log_dir,
|
||||||
file_pattern,
|
file_pattern,
|
||||||
match_mode,
|
match_mode,
|
||||||
keep_count,
|
keep_files,
|
||||||
max_total_size,
|
max_total_size,
|
||||||
max_single_file_size,
|
max_single_file_size,
|
||||||
compress,
|
compress,
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ export RUSTFS_CONSOLE_ADDRESS=":9001"
|
|||||||
#export RUSTFS_OBS_METER_INTERVAL=1 # Sampling interval in seconds
|
#export RUSTFS_OBS_METER_INTERVAL=1 # Sampling interval in seconds
|
||||||
#export RUSTFS_OBS_SERVICE_NAME=rustfs # Service name
|
#export RUSTFS_OBS_SERVICE_NAME=rustfs # Service name
|
||||||
#export RUSTFS_OBS_SERVICE_VERSION=0.1.0 # Service version
|
#export RUSTFS_OBS_SERVICE_VERSION=0.1.0 # Service version
|
||||||
export RUSTFS_OBS_ENVIRONMENT=develop # Environment name
|
export RUSTFS_OBS_ENVIRONMENT=develop # Environment name development, staging, production
|
||||||
export RUSTFS_OBS_LOGGER_LEVEL=info # Log level, supports trace, debug, info, warn, error
|
export RUSTFS_OBS_LOGGER_LEVEL=info # Log level, supports trace, debug, info, warn, error
|
||||||
export RUSTFS_OBS_LOG_STDOUT_ENABLED=true # Whether to enable local stdout logging
|
export RUSTFS_OBS_LOG_STDOUT_ENABLED=true # Whether to enable local stdout logging
|
||||||
export RUSTFS_OBS_LOG_DIRECTORY="$current_dir/deploy/logs" # Log directory
|
export RUSTFS_OBS_LOG_DIRECTORY="$current_dir/deploy/logs" # Log directory
|
||||||
|
|||||||
Reference in New Issue
Block a user