306 lines
8.8 KiB
Rust
306 lines
8.8 KiB
Rust
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<String>,
|
|
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<LogLevel>,
|
|
limit: Option<usize>,
|
|
) -> Vec<LogEntry> {
|
|
let mut entries: Vec<LogEntry> = Vec::new();
|
|
|
|
// 收集所有日志文件(包括轮转文件)
|
|
let mut log_files: Vec<PathBuf> = 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<LogEntry> {
|
|
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<String> = 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<String>,
|
|
level: Option<String>,
|
|
limit: Option<usize>,
|
|
) -> Vec<LogEntry> {
|
|
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()
|
|
}
|