285 lines
9.1 KiB
Rust
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);
|
|
}
|
|
} |