Files
mirror/mirror-core/src/logger.rs
CNWei 941558cb92 feat(cli): 优化 log 子命令并引入 LogLevel 强类型解析,重构 commands.lua 注册表解析与校验逻辑
- 调整 LogLevel 的 FromStr 错误类型为 String,适配 clap 的 value_parser
- log 子命令直接绑定 LogLevel 枚举,消除硬编码校验与类型转换
- 规范化日志级别更新与配置读取逻辑
- handler 类型有 Function 改为 String
2026-09-04 20:20:22 +08:00

186 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}