Skip to main content

tdm_server_rust/utils/
error_log.rs

1//! 错误日志记录 (Error Log)
2//!
3//! 统一将 HTTP 错误响应和业务异常写入 `logs/error.log` 文件,
4//! 同时在 dev 环境下写入内存环形缓冲供控制台查询。
5//!
6//! ## 两条写入路径
7//!
8//! | 函数 | 触发场景 | 格式 |
9//! |------|----------|------|
10//! | [`log_http_error`] | HTTP 4xx/5xx 响应 | `[ts] HTTP METHOD /path status=N member=N body=...` |
11//! | [`log_app_error`] | 业务 AppError | `[ts] APP kind=xxx code=N msg=... detail=...` |
12//!
13//! ## 初始化
14//!
15//! 在 `AppState::new()` 中调用 `init()` 设置日志文件路径,
16//! 未初始化时回退到 `{CARGO_MANIFEST_DIR}/logs/error.log`。
17
18use crate::dev::error_log as dev_error_log;
19use std::fs::OpenOptions;
20use std::io::Write;
21use std::path::PathBuf;
22use std::sync::{OnceLock, RwLock};
23
24/// 单条日志 body 最大展示长度(超出截断)
25const MAX_BODY_LEN: usize = 512;
26
27/// 运行时 error.log 路径持有者
28struct ErrorLogRuntime {
29    /// error.log 文件的完整路径
30    error_log_path: PathBuf,
31}
32
33static RUNTIME: OnceLock<RwLock<ErrorLogRuntime>> = OnceLock::new();
34
35/// 初始化错误日志路径
36///
37/// 可重复调用,仅首次生效。若路径的父目录不存在会自动创建。
38///
39/// # 参数
40///
41/// - `error_log_path`: error.log 文件的完整路径
42pub fn init(error_log_path: PathBuf) {
43    if RUNTIME.get().is_some() {
44        return;
45    }
46    ensure_log_file(&error_log_path);
47    let _ = RUNTIME.set(RwLock::new(ErrorLogRuntime { error_log_path }));
48}
49
50/// 确保日志目录和 error.log 文件存在(启动时预创建空文件)
51fn ensure_log_file(path: &PathBuf) {
52    if let Some(parent) = path.parent() {
53        let _ = std::fs::create_dir_all(parent);
54    }
55    let _ = OpenOptions::new().create(true).append(true).open(path);
56}
57
58fn runtime() -> Option<&'static RwLock<ErrorLogRuntime>> {
59    RUNTIME.get()
60}
61
62/// 默认 error.log 路径(未调用 [`init`] 时使用)
63fn fallback_error_log_path() -> PathBuf {
64    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
65        .join("logs")
66        .join("error.log")
67}
68
69/// 截断超长文本,追加 `...(len=N)` 后缀
70fn truncate(text: &str, max: usize) -> String {
71    if text.len() <= max {
72        return text.to_string();
73    }
74    format!("{}...(len={})", &text[..max], text.len())
75}
76
77/// 追加一行到指定日志文件
78fn append_line(path: &PathBuf, line: &str) {
79    if let Some(parent) = path.parent() {
80        let _ = std::fs::create_dir_all(parent);
81    }
82    if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
83        let _ = writeln!(file, "{line}");
84    }
85}
86
87/// 记录 HTTP 4xx/5xx 响应
88///
89/// 由 `error_log_middleware` 在每次响应 status >= 400 时调用。
90///
91/// # 参数
92///
93/// - `method`: HTTP 方法
94/// - `path`: 请求路径(经 `api_request_path()` 还原后的完整路径)
95/// - `status`: HTTP 状态码
96/// - `body`: 响应 body(会被截断至 512 字符)
97/// - `member_id`: 当前登录组员 ID(未登录为 None)
98pub fn log_http_error(method: &str, path: &str, status: u16, body: &str, member_id: Option<i32>) {
99    let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
100    let member = member_id
101        .map(|id| id.to_string())
102        .unwrap_or_else(|| "-".to_string());
103    let line = format!(
104        "[{ts}] HTTP {method} {path} status={status} member={member} body={}",
105        truncate(body, MAX_BODY_LEN)
106    );
107
108    if let Some(rt) = runtime() {
109        if let Ok(guard) = rt.read() {
110            append_line(&guard.error_log_path, &line);
111        }
112    } else {
113        append_line(&fallback_error_log_path(), &line);
114    }
115    dev_error_log::try_persist_http_error(method, path, status, body, member_id);
116    crate::telemetry::log_error_event(&format!("HTTP {method} {path} status={status}"));
117    tracing::warn!("{line}");
118}
119
120/// 记录业务层 AppError
121///
122/// 由 `AppError::into_response()` 在各变体转换时调用。
123///
124/// # 参数
125///
126/// - `kind`: 错误类别标识(如 "business"、"login_expired"、"oss")
127/// - `code`: 错误码
128/// - `msg`: 错误消息(会被截断至 512 字符)
129/// - `detail`: 详细错误栈(可选,会被截断至 512 字符)
130pub fn log_app_error(kind: &str, code: i32, msg: &str, detail: Option<&str>) {
131    let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
132    let extra = detail.unwrap_or("");
133    let line = format!(
134        "[{ts}] APP kind={kind} code={code} msg={} detail={}",
135        truncate(msg, MAX_BODY_LEN),
136        truncate(extra, MAX_BODY_LEN)
137    );
138
139    if let Some(rt) = runtime() {
140        if let Ok(guard) = rt.read() {
141            append_line(&guard.error_log_path, &line);
142        }
143    } else {
144        append_line(&fallback_error_log_path(), &line);
145    }
146    dev_error_log::try_persist_app_error(kind, code, msg, detail);
147    crate::telemetry::log_error_event(&format!("APP kind={kind} code={code} msg={msg}"));
148    tracing::warn!("{line}");
149}