fix(cleaner): add LogCleaner
This commit is contained in:
@@ -3,66 +3,128 @@
|
||||
use super::LogOutput;
|
||||
use crate::model::LogRecord;
|
||||
use async_trait::async_trait;
|
||||
use std::path::Path;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs::{File, OpenOptions};
|
||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
pub struct FileOutput {
|
||||
// 使用 Mutex 包裹,因为 write 方法是 &self (不可变引用),但写入文件需要修改内部状态
|
||||
// 使用 BufWriter 提高 IO 性能
|
||||
writer: Mutex<BufWriter<File>>,
|
||||
base_path: PathBuf, // 基础目录,如 "logs/"
|
||||
file_prefix: String, // 文件前缀,如 "order-service"
|
||||
current_writer: Mutex<Option<BufWriter<File>>>, // 当前的写入句柄
|
||||
current_date: Mutex<String>, // 当前文件对应的日期 "2023-10-27"
|
||||
current_path: Mutex<PathBuf>, // 当前正在写入的完整路径
|
||||
}
|
||||
|
||||
impl FileOutput {
|
||||
/// 创建文件输出实例
|
||||
/// path: 日志文件路径,例如 "logs/app.log"
|
||||
pub async fn new(path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
// 确保父目录存在
|
||||
if let Some(parent) = path.as_ref().parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
pub async fn new(dir: impl AsRef<Path>, prefix: &str) -> anyhow::Result<Self> {
|
||||
let dir = dir.as_ref().to_path_buf();
|
||||
tokio::fs::create_dir_all(&dir).await?;
|
||||
|
||||
// 初始化时,先不打开文件,等第一条日志来了再打开 (Lazy Open)
|
||||
// 或者这里可以先做一次 rotate_if_needed,为了简单我们设为空
|
||||
Ok(Self {
|
||||
base_path: dir,
|
||||
file_prefix: prefix.to_string(),
|
||||
current_writer: Mutex::new(None),
|
||||
current_date: Mutex::new(String::new()),
|
||||
current_path: Mutex::new(PathBuf::new()),
|
||||
})
|
||||
}
|
||||
|
||||
// 生成当天的基础文件名: logs/order-service-2023-10-27.log
|
||||
fn get_target_path(&self, date_str: &str) -> PathBuf {
|
||||
self.base_path
|
||||
.join(format!("{}-{}.log", self.file_prefix, date_str))
|
||||
}
|
||||
|
||||
// 核心轮转逻辑
|
||||
async fn rotate_if_needed(&self, timestamp: &DateTime<Utc>) -> anyhow::Result<()> {
|
||||
let date_str = timestamp.format("%Y-%m-%d").to_string();
|
||||
let target_path = self.get_target_path(&date_str);
|
||||
|
||||
let mut date_guard = self.current_date.lock().await;
|
||||
let mut path_guard = self.current_path.lock().await;
|
||||
let mut writer_guard = self.current_writer.lock().await;
|
||||
|
||||
let mut need_open = false;
|
||||
|
||||
// 1. 检查日期是否变化 (跨天)
|
||||
if *date_guard != date_str {
|
||||
*date_guard = date_str.clone();
|
||||
*path_guard = target_path.clone();
|
||||
need_open = true;
|
||||
}
|
||||
|
||||
// 以追加模式打开文件,如果不存在则创建
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.await?;
|
||||
// 2. 检查大小 (是否超过 100MB)
|
||||
if !need_open {
|
||||
if let Some(writer) = writer_guard.as_mut() {
|
||||
// 刷新一下缓冲区,确保获取的文件大小是准确的
|
||||
let _ = writer.flush().await;
|
||||
}
|
||||
// 获取当前文件元数据
|
||||
if let Ok(metadata) = tokio::fs::metadata(&*path_guard).await
|
||||
&& metadata.len() >= MAX_FILE_SIZE
|
||||
{
|
||||
// 需要切割:将当前 active 的日志重命名为 archive
|
||||
// 格式:logs/order-service-2023-10-27.log -> logs/order-service-2023-10-27.TIMESTAMP.log
|
||||
let backup_name = format!(
|
||||
"{}-{}.{}.log",
|
||||
self.file_prefix,
|
||||
date_str,
|
||||
Utc::now().format("%H%M%S") // 加个时间后缀避免重名
|
||||
);
|
||||
let backup_path = self.base_path.join(backup_name);
|
||||
|
||||
Ok(Self {
|
||||
writer: Mutex::new(BufWriter::new(file)),
|
||||
})
|
||||
// 重命名
|
||||
if let Err(e) = tokio::fs::rename(&*path_guard, backup_path).await {
|
||||
eprintln!("Failed to rotate log file: {}", e);
|
||||
}
|
||||
need_open = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果需要,重新打开文件
|
||||
if need_open || writer_guard.is_none() {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&*path_guard)
|
||||
.await?;
|
||||
*writer_guard = Some(BufWriter::new(file));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LogOutput for FileOutput {
|
||||
async fn write(&self, record: &LogRecord) {
|
||||
// 1. 格式化日志字符串 (简单文本格式)
|
||||
let log_line = format!(
|
||||
"{} [{}] ({}) - {}\n",
|
||||
record.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"), // 精确到毫秒
|
||||
record.level,
|
||||
record.module,
|
||||
record.message
|
||||
);
|
||||
|
||||
// 2. 获取锁并写入
|
||||
let mut writer = self.writer.lock().await;
|
||||
|
||||
// 写入缓冲区
|
||||
if let Err(e) = writer.write_all(log_line.as_bytes()).await {
|
||||
eprintln!("Failed to write to log file: {}", e);
|
||||
// 1. 写入前检查轮转
|
||||
if let Err(e) = self.rotate_if_needed(&record.timestamp).await {
|
||||
eprintln!("Log rotation failed: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 刷新缓冲区 (Flush)
|
||||
// 生产环境权衡:
|
||||
// - 每次 flush: 数据最安全,但性能略低
|
||||
// - 只要 writer 没丢,Tokio 会自动管理,但如果程序崩溃可能丢最后几行
|
||||
if let Err(e) = writer.flush().await {
|
||||
eprintln!("Failed to flush log file: {}", e);
|
||||
let log_line = format!(
|
||||
"{} [{}] - {}\n",
|
||||
record.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
|
||||
record.level,
|
||||
record.message
|
||||
);
|
||||
|
||||
// 2. 写入
|
||||
let mut writer_guard = self.current_writer.lock().await;
|
||||
if let Some(writer) = writer_guard.as_mut() {
|
||||
if let Err(e) = writer.write_all(log_line.as_bytes()).await {
|
||||
eprintln!("Failed to write to file: {}", e);
|
||||
}
|
||||
// 生产环境可以不每次 flush,交给 buffer 满自动刷新,提高性能
|
||||
let _ = writer.flush().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user