baobab/core/logger/src/file_appender.rs
Maxime Van Hees 9c4fa1a78b logger
2025-08-06 14:34:56 +02:00

285 lines
9.1 KiB
Rust

//! Custom File Appender Implementation
//!
//! This module provides custom file appender functionality with enhanced
//! rotation and directory management capabilities.
use crate::{LoggerError, Result};
use std::path::{Path, PathBuf};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
/// Create a custom rolling file appender with enhanced configuration
pub fn create_rolling_appender<P: AsRef<Path>>(
directory: P,
file_name_prefix: &str,
rotation: AppenderRotation,
) -> Result<RollingFileAppender> {
let directory = directory.as_ref();
// Ensure directory exists
std::fs::create_dir_all(directory)
.map_err(|e| LoggerError::DirectoryCreation(
format!("Failed to create directory {}: {}", directory.display(), e)
))?;
let rotation = match rotation {
AppenderRotation::Hourly => Rotation::HOURLY,
AppenderRotation::Daily => Rotation::DAILY,
AppenderRotation::Never => Rotation::NEVER,
};
let appender = tracing_appender::rolling::Builder::new()
.rotation(rotation)
.filename_prefix(file_name_prefix)
.filename_suffix("log")
.build(directory)
.map_err(|e| LoggerError::Config(format!("Failed to create rolling appender: {}", e)))?;
Ok(appender)
}
/// Enhanced rotation configuration
#[derive(Debug, Clone, Copy)]
pub enum AppenderRotation {
/// Rotate files every hour
Hourly,
/// Rotate files every day
Daily,
/// Never rotate (single file)
Never,
}
/// File appender builder for more complex configurations
pub struct FileAppenderBuilder {
directory: PathBuf,
file_prefix: String,
file_suffix: String,
rotation: AppenderRotation,
max_files: Option<usize>,
}
impl FileAppenderBuilder {
/// Create a new file appender builder
pub fn new<P: AsRef<Path>>(directory: P) -> Self {
Self {
directory: directory.as_ref().to_path_buf(),
file_prefix: "log".to_string(),
file_suffix: "log".to_string(),
rotation: AppenderRotation::Hourly,
max_files: None,
}
}
/// Set the file prefix
pub fn file_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
self.file_prefix = prefix.into();
self
}
/// Set the file suffix
pub fn file_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
self.file_suffix = suffix.into();
self
}
/// Set the rotation policy
pub fn rotation(mut self, rotation: AppenderRotation) -> Self {
self.rotation = rotation;
self
}
/// Set maximum number of files to keep (for cleanup)
pub fn max_files(mut self, max: usize) -> Self {
self.max_files = Some(max);
self
}
/// Build the file appender
pub fn build(self) -> Result<RollingFileAppender> {
// Ensure directory exists
std::fs::create_dir_all(&self.directory)
.map_err(|e| LoggerError::DirectoryCreation(
format!("Failed to create directory {}: {}", self.directory.display(), e)
))?;
let rotation = match self.rotation {
AppenderRotation::Hourly => Rotation::HOURLY,
AppenderRotation::Daily => Rotation::DAILY,
AppenderRotation::Never => Rotation::NEVER,
};
let appender = tracing_appender::rolling::Builder::new()
.rotation(rotation)
.filename_prefix(&self.file_prefix)
.filename_suffix(&self.file_suffix)
.build(&self.directory)
.map_err(|e| LoggerError::Config(format!("Failed to create rolling appender: {}", e)))?;
// Perform cleanup if max_files is set
if let Some(max_files) = self.max_files {
if let Err(e) = cleanup_old_files(&self.directory, &self.file_prefix, max_files) {
tracing::warn!("Failed to cleanup old log files: {}", e);
}
}
Ok(appender)
}
}
/// Clean up old log files, keeping only the most recent ones
fn cleanup_old_files<P: AsRef<Path>>(
directory: P,
file_prefix: &str,
max_files: usize,
) -> Result<()> {
let directory = directory.as_ref();
let mut log_files = Vec::new();
// Read directory and collect log files
let entries = std::fs::read_dir(directory)
.map_err(|e| LoggerError::Io(e))?;
for entry in entries {
let entry = entry.map_err(|e| LoggerError::Io(e))?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with(file_prefix) && file_name.ends_with(".log") {
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
log_files.push((path, modified));
}
}
}
}
}
// Sort by modification time (newest first)
log_files.sort_by(|a, b| b.1.cmp(&a.1));
// Remove old files if we exceed max_files
if log_files.len() > max_files {
for (old_file, _) in log_files.iter().skip(max_files) {
if let Err(e) = std::fs::remove_file(old_file) {
tracing::warn!("Failed to remove old log file {}: {}", old_file.display(), e);
} else {
tracing::debug!("Removed old log file: {}", old_file.display());
}
}
}
Ok(())
}
/// Utility function to get the current log file path for a given configuration
pub fn get_current_log_file<P: AsRef<Path>>(
directory: P,
file_prefix: &str,
rotation: AppenderRotation,
) -> PathBuf {
let directory = directory.as_ref();
match rotation {
AppenderRotation::Hourly => {
let now = chrono::Utc::now();
let timestamp = now.format("%Y-%m-%d-%H");
directory.join(format!("{}.{}.log", file_prefix, timestamp))
}
AppenderRotation::Daily => {
let now = chrono::Utc::now();
let timestamp = now.format("%Y-%m-%d");
directory.join(format!("{}.{}.log", file_prefix, timestamp))
}
AppenderRotation::Never => {
directory.join(format!("{}.log", file_prefix))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::time::Duration;
#[test]
fn test_create_rolling_appender() {
let temp_dir = TempDir::new().unwrap();
let directory = temp_dir.path().join("logs");
let appender = create_rolling_appender(&directory, "test", AppenderRotation::Hourly).unwrap();
// Verify directory was created
assert!(directory.exists());
}
#[test]
fn test_file_appender_builder() {
let temp_dir = TempDir::new().unwrap();
let directory = temp_dir.path().join("logs");
let appender = FileAppenderBuilder::new(&directory)
.file_prefix("custom")
.file_suffix("txt")
.rotation(AppenderRotation::Daily)
.max_files(5)
.build()
.unwrap();
assert!(directory.exists());
}
#[test]
fn test_get_current_log_file() {
let temp_dir = TempDir::new().unwrap();
let directory = temp_dir.path();
// Test hourly rotation
let hourly_file = get_current_log_file(directory, "test", AppenderRotation::Hourly);
assert!(hourly_file.to_string_lossy().contains("test."));
assert!(hourly_file.extension().unwrap() == "log");
// Test daily rotation
let daily_file = get_current_log_file(directory, "test", AppenderRotation::Daily);
assert!(daily_file.to_string_lossy().contains("test."));
assert!(daily_file.extension().unwrap() == "log");
// Test never rotation
let never_file = get_current_log_file(directory, "test", AppenderRotation::Never);
assert_eq!(never_file, directory.join("test.log"));
}
#[test]
fn test_cleanup_old_files() {
let temp_dir = TempDir::new().unwrap();
let directory = temp_dir.path();
// Create some test log files
for i in 0..10 {
let file_path = directory.join(format!("test.{}.log", i));
std::fs::write(&file_path, "test content").unwrap();
// Sleep briefly to ensure different modification times
std::thread::sleep(Duration::from_millis(10));
}
// Cleanup, keeping only 5 files
cleanup_old_files(directory, "test", 5).unwrap();
// Count remaining files
let remaining_files: Vec<_> = std::fs::read_dir(directory)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("test.") && name.ends_with(".log") {
Some(name)
} else {
None
}
})
.collect();
assert_eq!(remaining_files.len(), 5);
}
}