You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust Axum如何实现错误日志自动捕获,无需手动重复记录?

解决Axum自动记录错误日志的方案

核心思路:统一错误处理+自动日志

无需在每个路由分支手动调用日志接口,通过统一错误转换层自动捕获错误并记录,同时满足Axum返回Response的要求。

步骤1:定义自定义错误类型

实现std::error::Error和IntoResponse trait,在IntoResponse的逻辑中自动写入错误日志:

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};
use std::fmt;

#[derive(Debug)]
enum AppError {
    DbError(sqlx::Error),
    ValidationError(String),
    // 根据业务需求扩展其他错误类型
}

// 实现Display,输出友好的错误描述
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::DbError(e) => write!(f, "数据库错误: {}", e),
            AppError::ValidationError(msg) => write!(f, "参数验证失败: {}", msg),
        }
    }
}

// 实现Error trait,满足标准错误类型要求
impl std::error::Error for AppError {}

// 实现IntoResponse,自动记录日志并转换为HTTP响应
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // 自动记录错误,使用Display输出可读信息
        error!("请求处理失败: {}", self);
        
        // 根据错误类型映射对应的HTTP状态码和响应内容
        let (status, body) = match self {
            AppError::DbError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "服务器内部错误".to_string()),
            AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
        };
        
        (status, body).into_response()
    }
}

步骤2:路由返回Result<Response, AppError>

路由处理器直接返回Result类型,Axum会自动调用IntoResponse完成错误转换与日志记录,无需手动处理:

use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct UserQuery {
    id: u32,
}

// 路由无需手动写日志,错误会被统一处理
async fn get_user(Query(query): Query<UserQuery>) -> Result<Response, AppError> {
    let user = sqlx::query!("SELECT * FROM users WHERE id = $1", query.id)
        .fetch_one(&db_pool)
        .await
        .map_err(AppError::DbError)?;
    
    if user.is_deleted {
        return Err(AppError::ValidationError("用户已被删除".to_string()));
    }
    
    Ok(serde_json::to_string(&user).unwrap().into_response())
}

替代方案:用全局错误处理层适配已有路由

如果已有大量路由不想修改返回类型,可通过HandleErrorLayer配合tower::ServiceBuilder实现全局错误捕获:

use axum::{
    error_handling::HandleErrorLayer,
    http::StatusCode,
    routing::get,
    Router,
};
use tower::ServiceBuilder;

// 统一错误处理函数,自动记录日志并返回HTTP响应
async fn handle_error(err: AppError) -> (StatusCode, String) {
    error!("请求处理失败: {}", err);
    
    match err {
        AppError::DbError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "服务器内部错误".to_string()),
        AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
    }
}

// 构建路由时注入错误处理层
let app = Router::new()
    .route("/user", get(get_user))
    .layer(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(handle_error))
            // 可添加其他中间件(如请求日志、超时控制等)
            .into_inner(),
    );

关于#[instrument(err)]与之前中间件的问题

  • #[instrument(err)]需要为每个路由单独添加宏,且仅能捕获Result类型的错误,无法感知已转换为Response的错误场景,不符合批量处理需求。
  • 你之前的中间件无法获取有效错误信息,是因为Axum的Response body为流式结构(UnsyncBoxBody),读取后会消耗body导致无法返回给客户端。正确逻辑是在错误被包装为Response前完成捕获与记录,而非从最终响应中提取。

内容的提问来源于stack exchange,提问作者Thermatix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 17:56:15