feat(cli): 优化 log 子命令并引入 LogLevel 强类型解析,重构 commands.lua 注册表解析与校验逻辑

- 调整 LogLevel 的 FromStr 错误类型为 String,适配 clap 的 value_parser
- log 子命令直接绑定 LogLevel 枚举,消除硬编码校验与类型转换
- 规范化日志级别更新与配置读取逻辑
- handler 类型有 Function 改为 String
This commit is contained in:
2026-09-04 20:20:22 +08:00
parent 0c478ae39d
commit 941558cb92
29 changed files with 2238 additions and 47 deletions

185
mirror-core/src/logger.rs Normal file
View File

@@ -0,0 +1,185 @@
use crate::Layout;
use serde::Deserialize;
use std::fs;
use std::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<Self, Self::Err> {
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"
)),
}
}
}
#[derive(Debug, Deserialize, Default)]
pub struct Logger {
#[serde(default)]
pub level: LogLevel,
pub log_dir: Option<PathBuf>,
}
impl Logger {
/// 零依赖解析 ini / key-value 文本内容
pub fn parse_ini(content: &str) -> Self {
let mut logger = Self::default();
for line in content.lines() {
let line = line.trim();
// 跳过空行、注释行 (# 或 ;) 和 section 头 ([...])
if line.is_empty()
|| line.starts_with('#')
|| line.starts_with(';')
|| line.starts_with('[')
{
continue;
}
if let Some((key, val)) = line.split_once('=') {
let key = key.trim();
let val = val.trim().trim_matches('"').trim_matches('\'');
match key {
"level" => {
if let Ok(lvl) = LogLevel::from_str(val) {
logger.level = lvl;
}
}
"log_dir" => {
if !val.is_empty() {
logger.log_dir = Some(PathBuf::from(val));
}
}
_ => {}
}
}
}
logger
}
}
/// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。
/// 仅在开启详细日志(如 info/debug/trace时激活异步文件记录并返回 `WorkerGuard`。
pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
// 1. 在 layout.base_dir 目录下寻找 log.toml
// let config_path = layout.base_dir.join("mirror.toml");
let config_path = layout.base_dir.join("../../mirror.ini");
// 读取配置文件(如不存在或解析失败,降级回退到默认设置)
let config = if config_path.exists() {
fs::read_to_string(&config_path)
.map(|s| Logger::parse_ini(&s))
// .ok()
// .and_then(|s| toml::from_str::<Logger>(&s).ok())
.unwrap_or_default()
} else {
Logger::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);
// 2. 根据强类型枚举判断是否激活落盘模式
if effective_level.is_verbose() {
let (non_blocking, guard) = create_file_appender(layout, config.log_dir);
let file_layer = fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_target(false);
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.with(file_layer)
.try_init();
Some(guard)
} else {
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.try_init();
None
}
}
fn create_file_appender(
layout: &Layout,
log_dir_override: Option<PathBuf>,
) -> (NonBlocking, WorkerGuard) {
let output_dir = match log_dir_override {
Some(user_path) => {
if user_path.is_relative() {
layout.base_dir.join(user_path)
} else {
user_path
}
}
None => layout.base_dir.join("logs"),
};
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)
}