ds-free-api / src /server /runtime_log.rs
heyeyydss's picture
Upload 154 files
69f686e verified
Raw
History Blame
7.77 kB
//! 运行日志 —— 自定义 log::Log 实现,三路输出 + 文件轮转
//!
//! 三路:stderr(终端可见)+ 内存环形缓冲区(API 可查)+ 文件(持久化)
//! 文件轮转:单文件 10MB,保留 3 个历史文件,总上限 ~40MB
use std::collections::VecDeque;
use std::fs::{self, File, OpenOptions};
use std::io::{IsTerminal, Write};
use std::sync::Arc;
use chrono::Local;
use serde::Serialize;
use tokio::sync::Mutex;
/// 环形缓冲区容量
const BUFFER_CAPACITY: usize = 2000;
/// 单个日志文件最大字节数(10MB)
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
/// 保留的历史日志文件数
const MAX_HISTORY_FILES: usize = 3;
/// 单条运行日志
#[derive(Serialize, Clone, Debug)]
pub struct RuntimeLogEntry {
pub timestamp: String,
pub level: String,
pub target: String,
pub message: String,
}
/// 自定义 Logger
pub struct DualLogger {
/// 内存环形缓冲区
buffer: Mutex<VecDeque<RuntimeLogEntry>>,
/// 当前日志文件(std::sync::Mutex 用于 log 路径的非阻塞写入)
file: std::sync::Mutex<File>,
/// 日志文件路径
log_path: String,
/// 最大日志级别
max_level: log::LevelFilter,
/// 是否启用彩色输出
use_color: bool,
}
impl std::fmt::Debug for DualLogger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DualLogger")
.field("log_path", &self.log_path)
.field("max_level", &self.max_level)
.finish()
}
}
impl DualLogger {
fn new(log_path: &str, max_level: log::LevelFilter) -> Self {
if let Some(parent) = std::path::Path::new(log_path).parent() {
let _ = fs::create_dir_all(parent);
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.expect("无法打开日志文件");
Self {
buffer: Mutex::new(VecDeque::with_capacity(BUFFER_CAPACITY)),
file: std::sync::Mutex::new(file),
log_path: log_path.to_string(),
max_level,
use_color: std::io::stderr().is_terminal(),
}
}
fn rotate_if_needed(&self) {
let size = self
.file
.lock()
.ok()
.and_then(|f| f.metadata().ok().map(|m| m.len()))
.unwrap_or(0);
if size < MAX_FILE_SIZE {
return;
}
for i in (1..=MAX_HISTORY_FILES).rev() {
let old = format!("{}.{}", self.log_path, i);
if i == MAX_HISTORY_FILES {
let _ = fs::remove_file(&old);
} else {
let new = format!("{}.{}", self.log_path, i + 1);
let _ = fs::rename(&old, &new);
}
}
let _ = fs::rename(&self.log_path, format!("{}.1", self.log_path));
if let Ok(new_file) = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_path)
&& let Ok(mut file_guard) = self.file.lock()
{
*file_guard = new_file;
}
}
pub async fn query_logs(&self, offset: usize, limit: usize) -> (usize, Vec<RuntimeLogEntry>) {
self.rotate_if_needed();
let buffer = self.buffer.lock().await;
let total = buffer.len();
let logs: Vec<RuntimeLogEntry> = buffer
.iter()
.rev()
.skip(offset)
.take(limit)
.cloned()
.collect();
(total, logs)
}
}
/// 根据日志级别返回 ANSI 颜色码(仅用于 stderr)
fn color_for_level(level: &str) -> &'static str {
match level {
"ERROR" => "\x1b[31m",
"WARN" => "\x1b[33m",
"INFO" => "\x1b[32m",
"DEBUG" => "\x1b[34m",
"TRACE" => "\x1b[35m",
_ => "\x1b[0m",
}
}
impl log::Log for DualLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= self.max_level
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S%.3f%:z").to_string();
let level = record.level().as_str().to_string();
let target = record.target().to_string();
let message = format!("{}", record.args());
// 1. 写 stderr(终端输出,彩色级别)
if self.use_color {
eprintln!(
"[\x1b[2m{} \x1b[0m{}{}\x1b[0m\x1b[2m {}\x1b[0m] {}",
timestamp,
color_for_level(&level),
level,
target,
message
);
} else {
eprintln!("[{} {:5} {}] {}", timestamp, level, target, message);
}
// 2. 写文件
let file_line = format!("[{} {:5} {}] {}\n", timestamp, level, target, message);
if let Ok(mut file_guard) = self.file.lock() {
let _ = file_guard.write_all(file_line.as_bytes());
let _ = file_guard.flush();
}
// 3. 写环形缓冲区(try_lock 避免阻塞 log 路径)
let entry = RuntimeLogEntry {
timestamp,
level,
target,
message,
};
if let Ok(mut buffer) = self.buffer.try_lock() {
if buffer.len() >= BUFFER_CAPACITY {
buffer.pop_front();
}
buffer.push_back(entry);
}
}
fn flush(&self) {
if let Ok(mut file_guard) = self.file.lock() {
let _ = file_guard.flush();
}
}
}
/// 全局 Logger 引用
static GLOBAL_LOGGER: std::sync::OnceLock<Arc<DualLogger>> = std::sync::OnceLock::new();
/// 初始化自定义 Logger,替换 env_logger
pub fn init(log_path: &str) {
let max_level = match std::env::var("RUST_LOG") {
Ok(ref v) if !v.is_empty() => parse_level(v),
_ => log::LevelFilter::Info,
};
let logger = Arc::new(DualLogger::new(log_path, max_level));
GLOBAL_LOGGER.set(logger.clone()).expect("Logger 已初始化");
// Arc::into_inner 需要 Arc 引用计数为 1,但 GLOBAL_LOGGER 持有一份
// 所以用 Box::new 包装 Arc clone
let boxed: Box<dyn log::Log> = Box::new(LoggerWrapper { inner: logger });
log::set_boxed_logger(boxed).expect("Logger 设置失败");
log::set_max_level(max_level);
}
/// 包装 Arc<DualLogger> 实现 Log(因为 set_boxed_logger 需要 Box<dyn Log>)
struct LoggerWrapper {
inner: Arc<DualLogger>,
}
impl log::Log for LoggerWrapper {
fn enabled(&self, metadata: &log::Metadata) -> bool {
self.inner.enabled(metadata)
}
fn log(&self, record: &log::Record) {
self.inner.log(record);
}
fn flush(&self) {
self.inner.flush();
}
}
fn parse_level(s: &str) -> log::LevelFilter {
let mut max_level = log::LevelFilter::Info;
for segment in s.split(',') {
let level_str = segment.split('=').next_back().unwrap_or(segment).trim();
let level = match level_str {
"trace" => log::LevelFilter::Trace,
"debug" => log::LevelFilter::Debug,
"warn" => log::LevelFilter::Warn,
"error" => log::LevelFilter::Error,
"off" => log::LevelFilter::Off,
_ => continue,
};
if level > max_level {
max_level = level;
}
}
max_level
}
/// 查询运行日志(分页,从最新往旧倒序)
pub async fn query_logs(offset: usize, limit: usize) -> (usize, Vec<RuntimeLogEntry>) {
let logger = GLOBAL_LOGGER.get().expect("Logger 未初始化");
logger.query_logs(offset, limit).await
}