use chrono::Local; use serde::{Deserialize, Serialize}; use std::fs::{self, File, OpenOptions}; use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; /// 日志级别 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum LogLevel { Debug, Info, Warn, Error, } /// 单条日志记录 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { pub timestamp: String, pub level: LogLevel, pub module: String, pub message: String, } /// 日志文件信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogInfo { pub log_dir: String, pub log_files: Vec, pub total_size_bytes: u64, pub max_file_size_bytes: u64, pub max_files: u32, } /// 日志管理器 —— 负责文件轮转、写入、查询 pub struct LogManager { log_dir: PathBuf, max_file_size: u64, max_files: u32, base_name: String, } impl LogManager { pub fn new(log_dir: PathBuf) -> Self { fs::create_dir_all(&log_dir).ok(); Self { log_dir, max_file_size: 5 * 1024 * 1024, max_files: 5, base_name: "thing".to_string(), } } /// 写入一条日志 pub fn log(&self, level: LogLevel, module: &str, message: &str) { let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); let level_str = match level { LogLevel::Debug => "DEBUG", LogLevel::Info => "INFO", LogLevel::Warn => "WARN", LogLevel::Error => "ERROR", }; let line = format!( "[{}] [{}] [{}] {}\n", timestamp, level_str, module, message ); let current_log = self.log_dir.join(format!("{}.log", self.base_name)); // 检查是否需要轮转 if let Ok(meta) = fs::metadata(¤t_log) { if meta.len() >= self.max_file_size { self.rotate(); } } // 追加写入 if let Ok(mut file) = OpenOptions::new() .create(true) .append(true) .open(¤t_log) { let _ = file.write_all(line.as_bytes()); let _ = file.flush(); } } /// 日志文件轮转: thing.log → thing.1.log, thing.1.log → thing.2.log, … fn rotate(&self) { // 删除最旧的文件 let oldest = self .log_dir .join(format!("{}.{}.log", self.base_name, self.max_files)); fs::remove_file(&oldest).ok(); // 依次重命名 for i in (1..self.max_files).rev() { let src = self .log_dir .join(format!("{}.{}.log", self.base_name, i)); let dst = self .log_dir .join(format!("{}.{}.log", self.base_name, i + 1)); fs::rename(&src, &dst).ok(); } // thing.log → thing.1.log let current = self.log_dir.join(format!("{}.log", self.base_name)); let first_rotated = self .log_dir .join(format!("{}.1.log", self.base_name)); fs::rename(¤t, &first_rotated).ok(); } /// 读取日志(支持按模块/级别过滤、条数限制) pub fn get_logs( &self, module: Option<&str>, level: Option, limit: Option, ) -> Vec { let mut entries: Vec = Vec::new(); // 收集所有日志文件(包括轮转文件) let mut log_files: Vec = Vec::new(); let current = self.log_dir.join(format!("{}.log", self.base_name)); if current.exists() { log_files.push(current); } for i in 1..=self.max_files { let rotated = self .log_dir .join(format!("{}.{}.log", self.base_name, i)); if rotated.exists() { log_files.push(rotated); } } for path in &log_files { if let Ok(file) = File::open(path) { for line in BufReader::new(file).lines().flatten() { if let Some(entry) = Self::parse_line(&line) { if let Some(ref m) = module { if entry.module != *m { continue; } } if let Some(ref l) = level { if entry.level != *l { continue; } } entries.push(entry); } } } } // 按时间戳降序(最新在前) entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); if let Some(n) = limit { entries.truncate(n); } entries } /// 解析一行日志: `[YYYY-MM-DD HH:MM:SS] [LEVEL] [MODULE] message` fn parse_line(line: &str) -> Option { let line = line.trim(); if line.len() < 22 || !line.starts_with('[') { return None; } let ts_end = line[1..].find(']')? + 1; let timestamp = line[1..ts_end].to_string(); let rest = line[ts_end + 1..].trim(); if !rest.starts_with('[') { return None; } let lv_end = rest[1..].find(']')? + 1; let level_str = &rest[1..lv_end]; let level = match level_str { "DEBUG" => LogLevel::Debug, "INFO" => LogLevel::Info, "WARN" => LogLevel::Warn, "ERROR" => LogLevel::Error, _ => return None, }; let rest = rest[lv_end + 1..].trim(); if !rest.starts_with('[') { return None; } let mod_end = rest[1..].find(']')? + 1; let module = rest[1..mod_end].to_string(); let message = rest[mod_end + 1..].trim().to_string(); Some(LogEntry { timestamp, level, module, message, }) } /// 清空所有日志文件 pub fn clear_logs(&self) -> Result<(), String> { let current = self.log_dir.join(format!("{}.log", self.base_name)); fs::remove_file(¤t).map_err(|e| e.to_string())?; for i in 1..=self.max_files { let rotated = self .log_dir .join(format!("{}.{}.log", self.base_name, i)); fs::remove_file(&rotated).ok(); } Ok(()) } /// 获取日志系统信息 pub fn get_info(&self) -> LogInfo { let mut log_files: Vec = Vec::new(); let mut total_size: u64 = 0; let current = self.log_dir.join(format!("{}.log", self.base_name)); if current.exists() { if let Ok(meta) = fs::metadata(¤t) { total_size += meta.len(); } log_files.push(format!("{}.log", self.base_name)); } for i in 1..=self.max_files { let rotated = self .log_dir .join(format!("{}.{}.log", self.base_name, i)); if rotated.exists() { if let Ok(meta) = fs::metadata(&rotated) { total_size += meta.len(); } log_files.push(format!("{}.{}.log", self.base_name, i)); } } LogInfo { log_dir: self.log_dir.to_string_lossy().to_string(), log_files, total_size_bytes: total_size, max_file_size_bytes: self.max_file_size, max_files: self.max_files, } } } // ===== Tauri 命令 ===== #[tauri::command] pub fn log_message( state: tauri::State<'_, LogManager>, level: String, module: String, message: String, ) -> Result<(), String> { let level = match level.as_str() { "debug" => LogLevel::Debug, "info" => LogLevel::Info, "warn" => LogLevel::Warn, "error" => LogLevel::Error, _ => return Err(format!("无效的日志级别: {}", level)), }; state.log(level, &module, &message); Ok(()) } #[tauri::command] pub fn get_logs( state: tauri::State<'_, LogManager>, module: Option, level: Option, limit: Option, ) -> Vec { let level = level.and_then(|l| match l.as_str() { "debug" => Some(LogLevel::Debug), "info" => Some(LogLevel::Info), "warn" => Some(LogLevel::Warn), "error" => Some(LogLevel::Error), _ => None, }); state.get_logs(module.as_deref(), level, limit) } #[tauri::command] pub fn clear_logs(state: tauri::State<'_, LogManager>) -> Result<(), String> { state.clear_logs() } #[tauri::command] pub fn get_log_info(state: tauri::State<'_, LogManager>) -> LogInfo { state.get_info() }