← 中文资源

简体中文版本

Axum Tower 日志中间件

为 Axum 装配请求日志中间件:耗时与状态统一记录、请求 ID 由 tower-http 层自动生成贯穿整条链路、tracing span 关联全链。

阅读英文原文

示例说明

tower 生态把中间件抽象成 layer 叠加:SetRequestIdLayer 在最外层为每个请求生成 UUID 并写入头,后续所有层都能读到同一个 ID——日志、tracing span、响应头三处共享。自定义中间件用 from_fn 包装:next.run 之前的代码是请求阶段、之后是响应阶段,Instant 计时横跨两段。

技术要点

  • layer 顺序决定洋葱嵌套次序
  • 请求 ID 层放最外全局可用
  • from_fn 免手写 Service trait

代码解读

timing_middleware 先取请求头里的 ID(或占位符),调用下游后用 info! 输出六组结构化字段;5xx 追加 warn 级告警便于告警规则捕获。TraceLayer 的 make_span_with 定制 span 携带请求 ID——tracing 与日志共用同一标识。两个路由分别演示正常与错误路径。

使用建议

layer 从下往上包裹,注册顺序要与依赖关系对齐。慢请求阈值告警可在中间件里追加。生产日志走 JSON 格式对接采集器。

代码示例

// logging middleware tower layer for axum
use axum::{
    body::Body,
    extract::Request,
    http::{HeaderValue, StatusCode},
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router,
};
use std::time::Instant;
use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer, XRequestIdName};
use tower_http::trace::TraceLayer;
use tracing::{info, info_span};

async fn timing_middleware(request: Request, next: Next) -> Response {
    let started = Instant::now();
    let request_id = request
        .headers()
        .get("x-request-id")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("-")
        .to_string();

    let method = request.method().clone();
    let path = request.uri().path().to_string();

    let response = next.run(request).await;

    info!(
        request_id = %request_id,
        method = %method,
        path = %path,
        status = %response.status().as_u16(),
        elapsed_ms = started.elapsed().as_millis() as u64,
        "request completed"
    );

    if response.status().is_server_error() {
        tracing::warn!(path = %path, "server error served");
    }
    response
}

async fn handler() -> StatusCode {
    StatusCode::OK
}

async fn fail() -> StatusCode {
    StatusCode::BAD_GATEWAY
}

#[tokio::main]
async fn main() {
    let app: Router = Router::new()
        .route("/ok", get(handler))
        .route("/boom", get(fail))
        .layer(middleware::from_fn(timing_middleware))
        .layer(TraceLayer::new_for_http().make_span_with(|request: &Request<Body>| {
            let request_id = request
                .headers()
                .get(XRequestIdName::default())
                .cloned()
                .unwrap_or_else(|| HeaderValue::from_static("-"));
            info_span!("http", request_id = ?request_id)
        }))
        .layer(SetRequestIdLayer::new(
            XRequestIdName::default(),
            MakeRequestUuid,
        ));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}