//! 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>( directory: P, file_name_prefix: &str, rotation: AppenderRotation, ) -> Result { 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, } impl FileAppenderBuilder { /// Create a new file appender builder pub fn new>(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>(mut self, prefix: S) -> Self { self.file_prefix = prefix.into(); self } /// Set the file suffix pub fn file_suffix>(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 { // 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>( 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>( 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); } }