如何让Rust日志宏在调用Result扩展辅助函数时显示调用方模块名而非函数定义模块名
我完全懂你的烦恼——当你给Result写了扩展trait来统一处理错误转译和日志时,结果日志里显示的模块总是扩展函数所在的my_library::extensions::logging,而不是调用这个方法的业务代码模块(比如my_application_code::routes_example)。这确实会让日志的溯源变得麻烦,得想办法把模块信息改成调用方的才行。
问题根源
log crate的便捷宏(比如log::warn!、log::info!)是编译时确定日志上下文的:它们会把宏调用位置的模块路径、文件、行号硬编码到最终的日志记录里。你的扩展方法里的log::warn!是在扩展模块里调用的,所以日志自然会显示那个模块,哪怕你用#[track_caller]拿到了调用方的位置信息也没用——因为日志宏根本没用到这个位置。
解决方案:手动构造日志记录
核心思路是:绕过log的便捷宏,手动构建log::Record对象,把#[track_caller]拿到的调用方位置(包括模块路径、文件、行号)手动传入日志记录中。
步骤1:修改扩展函数的日志逻辑
你已经通过#[track_caller]和std::panic::Location::caller()拿到了调用方的位置信息,现在只需要用这个信息手动构造日志,而不是直接用log::warn!。
以下是适配你现有代码的修改版本:
pub mod my_library { pub mod extensions { pub mod logging { use crate::FrameworkError; use log::{Record, Level, Metadata}; use std::panic::Location; pub trait LogChangeErrForRespExt { type R; #[track_caller] fn log_and_explain_400(self) -> Self::R; } impl<T, E: std::error::Error + 'static> LogChangeErrForRespExt for Result<T, E> { type R = Result<T, FrameworkError>; /// Logs the client error and returns a 400 error for the response #[track_caller] fn log_and_explain_400(self) -> Self::R { match self { Ok(ok) => Ok(ok), Err(err) => { // 拿到调用方的位置(多亏了#[track_caller]) let caller_loc = Location::caller(); // 手动构建日志记录,替换模块路径为调用方的模块 let log_record = Record::builder() .args(format_args!( "Error at {:?}:{}: {}", caller_loc.file(), caller_loc.line(), err )) .level(Level::Warn) // 用你的包名作为日志target(也可以自定义) .target(env!("CARGO_PKG_NAME")) // 关键:把模块路径设为调用方的模块 .module_path(caller_loc.module_path()) // 同步调用方的文件和行号 .file(Some(caller_loc.file())) .line(Some(caller_loc.line())) .build(); // 检查当前日志级别是否允许输出,然后记录 let metadata = Metadata::builder() .level(Level::Warn) .target(log_record.target()) .build(); if log::logger().enabled(&metadata) { log::logger().log(&log_record); } Err(FrameworkError::Error400(Box::new(err))) } } } } } } }
步骤2:封装辅助函数简化代码
如果你的扩展trait有多个方法(比如log_hide_res、log_and_explain_400),可以把手动构造日志的逻辑封装成一个辅助函数,避免重复代码:
use log::{Level, Metadata, Record}; use std::panic::Location; /// 用调用方的位置信息记录日志 #[track_caller] fn tracked_log(level: Level, message: impl std::fmt::Display) { let caller_loc = Location::caller(); let record = Record::builder() .args(format_args!("{}", message)) .level(level) .target(env!("CARGO_PKG_NAME")) .module_path(caller_loc.module_path()) .file(Some(caller_loc.file())) .line(Some(caller_loc.line())) .build(); let metadata = Metadata::builder() .level(level) .target(record.target()) .build(); if log::logger().enabled(&metadata) { log::logger().log(&record); } }
然后在扩展方法里直接调用:
// 在log_and_explain_400的Err分支里: tracked_log( Level::Warn, format!("Error at {:?}:{}: {}", caller_loc.file(), caller_loc.line(), err) );
效果验证
修改后,你的日志输出就会变成:
[2025-08-15T10:08:55Z WARN question_location_log::my_application_code::routes_example] Error at "src/main.rs":61: ParseIntError { kind: InvalidDigit }
模块路径显示的是调用log_and_explain_400的业务模块,完美符合你的需求!
为什么不用From转换?
正如你所说,为actix_web::Error实现From确实不够灵活——你没法给不同的错误转译方法传递自定义参数(比如不同的状态码、日志级别、错误提示),而扩展trait的方式能让你针对不同场景定义不同的处理逻辑,配合上面的日志修复方案就能完全满足你的需求了。
内容来源于stack exchange

