use crate::Layout; use std::convert::Infallible; use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, fmt}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum LogLevel { Off, Error, #[default] Warn, Info, Debug, Trace, } impl std::fmt::Display for LogLevel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } impl LogLevel { /// 是否需要激活写盘排查模式(Info 及以上更详细的级别) pub fn is_verbose(&self) -> bool { *self >= LogLevel::Info } pub fn as_str(&self) -> &'static str { match self { LogLevel::Off => "off", LogLevel::Error => "error", LogLevel::Warn => "warn", LogLevel::Info => "info", LogLevel::Debug => "debug", LogLevel::Trace => "trace", } } } impl FromStr for LogLevel { type Err = String; fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "off" => Ok(LogLevel::Off), "error" => Ok(LogLevel::Error), "warn" => Ok(LogLevel::Warn), "info" => Ok(LogLevel::Info), "debug" => Ok(LogLevel::Debug), "trace" => Ok(LogLevel::Trace), _ => Err(format!( "无效的日志级别 '{s}',可选值为: off, error, warn, info, debug, trace" )), } } } /// 日志配置模型(对应 mirror.ini 的纯强类型表示) #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct LogConfig { pub level: LogLevel, pub log_dir: Option, } impl LogConfig { /// 零依赖解析 ini / key-value 文本内容 /// 计算日志目录的实际物理路径(相对路径基于 base_dir 拼接) pub fn resolve_log_dir(&self, base_dir: &Path) -> PathBuf { match &self.log_dir { Some(p) if p.is_absolute() => p.clone(), Some(p) => base_dir.join(p), None => base_dir.join("logs"), } } } // 协议 1: 反序列化 (INI 文本 -> LogConfig) impl FromStr for LogConfig { type Err = Infallible; fn from_str(content: &str) -> Result { let mut config = Self::default(); for line in content.lines() { let line = line.trim(); // 跳过空行、注释 (#, ;) 与 section 标头 ([...]) if line.is_empty() || line.starts_with(['#', ';', '[']) { continue; } if let Some((key, val)) = line.split_once('=') { let key = key.trim(); let val = val.trim().trim_matches(['"', '\'']); match key { "level" => { if let Ok(lvl) = LogLevel::from_str(val) { config.level = lvl; } } "log_dir" => { if !val.is_empty() { config.log_dir = Some(PathBuf::from(val)); } } _ => {} } } } Ok(config) } } // 协议 2: 序列化 (LogConfig -> INI 文本) impl std::fmt::Display for LogConfig { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let dir_str = self .log_dir .as_deref() .and_then(|p| p.to_str()) .unwrap_or("logs"); writeln!(f, "level = \"{}\"", self.level.as_str())?; writeln!(f, "log_dir = \"{}\"", dir_str) } } /// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。 /// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。 pub fn init_logging_from(layout: &Layout) -> Option { // 1. 在 layout.base_dir 目录下寻找 log.toml // let config_path = layout.base_dir.join("mirror.toml"); let config_path = layout.base_dir.join("mirror.ini"); // 1. 读取并反序列化配置文件(缺失或异常时安全降级为 Default) let config: LogConfig = fs::read_to_string(&config_path) .ok() .and_then(|s| s.parse().ok()) .unwrap_or_default(); // 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level let effective_level = std::env::var("MIRROR_LOG_LEVEL") .ok() .and_then(|val| LogLevel::from_str(&val).ok()) .unwrap_or(config.level); if effective_level == LogLevel::Off { return None; } let filter = EnvFilter::new(effective_level.as_str()); // 基础终端输出层 let stderr_layer = fmt::layer().with_writer(std::io::stderr).with_target(false); // 3. 动态配置落盘文件层(仅在 verbose 级别启用) let (file_layer, guard) = if effective_level.is_verbose() { let (non_blocking, guard) = create_file_appender(&config.resolve_log_dir(&layout.base_dir)); let layer = fmt::layer() .with_writer(non_blocking) .with_ansi(false) .with_target(false); (Some(layer), Some(guard)) } else { (None, None) }; // 4. 统一组装并初始化 Subscriber(Option 天然实现了 Layer trait) let _ = tracing_subscriber::registry() .with(filter) .with(stderr_layer) .with(file_layer) .try_init(); guard } fn create_file_appender(output_dir: &Path) -> (NonBlocking, WorkerGuard) { if let Err(err) = fs::create_dir_all(output_dir) { eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err); } let file_appender = tracing_appender::rolling::daily(output_dir, "mirror.log"); tracing_appender::non_blocking(file_appender) }