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::{
17    body::Body,
18    http::Request,
19    middleware::Next,
20    response::Response,
21};
22use bytes::Bytes;
23use http_body_util::BodyExt;
24
25/// 错误日志中间件:记录 4xx/5xx 至 error.log
26#[tracing::instrument(skip_all, level = "info")]
27pub async fn error_log_middleware(req: Request<Body>, next: Next) -> Response {
28    let method = req.method().to_string();
29    let path = api_request_path(req.uri().path());
30    let member_id = req
31        .extensions()
32        .get::<AuthMember>()
33        .and_then(|auth| auth.0.as_ref().map(|m| m.id));
34
35    let response = next.run(req).await;
36    let status = response.status().as_u16();
37
38    if status < 400 {
39        return response;
40    }
41
42    let (parts, body) = response.into_parts();
43    let body_bytes: Bytes = body
44        .collect()
45        .await
46        .map(|b| b.to_bytes())
47        .unwrap_or_default();
48    let body_str = String::from_utf8_lossy(&body_bytes);
49    error_log::log_http_error(&method, &path, status, &body_str, member_id);
50
51    Response::from_parts(parts, Body::from(body_bytes))
52}
53
54/// 还原完整请求路径
55///
56/// axum 嵌套 `/api` 路由时,handler 看到的 `uri.path()` 可能缺少 `/api` 前缀。
57/// 此函数自动补全,确保日志路径准确。
58fn api_request_path(path: &str) -> String {
59    if path.starts_with("/api/") || path == "/api" {
60        path.to_string()
61    } else {
62        format!("/api{path}")
63    }
64}