Skip to main content

tdm_server_rust/middleware/
error_log.rs

1//! HTTP 错误响应文件日志中间件
2//!
3//! 拦截所有 HTTP 4xx/5xx 响应,将错误信息写入 `logs/error.log`。
4//! 该中间件在所有 profile 下均生效。
5//!
6//! ## 日志内容
7//!
8//! 每条记录包含:时间戳、HTTP 方法、请求路径、状态码、当前组员 ID、响应 body。
9//!
10//! ## 路径还原
11//!
12//! axum 嵌套路由可能截断路径前缀,本中间件通过 `api_request_path()`
13//! 自动补全 `/api` 前缀,确保日志中的路径与实际请求 URI 一致。
14
15use crate::{middleware::AuthMember, utils::error_log};
16use axum::{body::Body, http::Request, middleware::Next, response::Response};
17use bytes::Bytes;
18use http_body_util::BodyExt;
19
20/// 错误日志中间件:记录 4xx/5xx 至 error.log
21#[tracing::instrument(skip_all, level = "info")]
22pub async fn error_log_middleware(req: Request<Body>, next: Next) -> Response {
23    let method = req.method().to_string();
24    let path = api_request_path(req.uri().path());
25    let member_id = req
26        .extensions()
27        .get::<AuthMember>()
28        .and_then(|auth| auth.0.as_ref().map(|m| m.id));
29
30    let response = next.run(req).await;
31    let status = response.status().as_u16();
32
33    if status < 400 {
34        return response;
35    }
36
37    let (parts, body) = response.into_parts();
38    let body_bytes: Bytes = body
39        .collect()
40        .await
41        .map(|b| b.to_bytes())
42        .unwrap_or_default();
43    let body_str = String::from_utf8_lossy(&body_bytes);
44    error_log::log_http_error(&method, &path, status, &body_str, member_id);
45
46    Response::from_parts(parts, Body::from(body_bytes))
47}
48
49/// 还原完整请求路径
50///
51/// axum 嵌套 `/api` 路由时,handler 看到的 `uri.path()` 可能缺少 `/api` 前缀。
52/// 此函数自动补全,确保日志路径准确。
53fn api_request_path(path: &str) -> String {
54    if path.starts_with("/api/") || path == "/api" {
55        path.to_string()
56    } else {
57        format!("/api{path}")
58    }
59}