T doesn't implement Debug
主线位置:span 主线的起点。
#[instrument]生成的span!要把每个参数变成一个字段,字段值必须能转成&dyn Value;err/ret在返回处生成的event!也一样。哪个值转不过去,函数就编译不过,两条主线都还没开始。
flowchart TB
param["参数 x: T"] --> skipq{"skip 了吗"}
skipq -->|是| gone["不进 span!"]
skipq -->|否| ty{"类型名在基本类型表里"}
ty -->|是| val["x 直接当 Value"]
ty -->|否| dbg["包成 field::debug"]
dbg --> need["要求 T: Debug"]
errarg["err"] --> disp["error = %e"]
disp --> needd["要求 E: Display"]
现象
最常见的三种写法:泛型参数、没派生 Debug 的 self、没派生 Debug 的普通参数。泛型参数:
tracing = "=0.1.44"
use tracing::instrument;
#[instrument]
fn store<T>(key: &str, item: T) {}
fn main() {}
rustc 1.99.0 报两个错误,第一个(registry 路径缩短为 ~/.cargo/registry/...):
error[E0277]: `T` doesn't implement `Debug`
--> src/bin/err_generic.rs:3:1
|
3 | #[instrument]
| ^^^^^^^^^^^^^ the trait `Debug` is not implemented for `T`
|
= note: required for `&T` to implement `Debug`
note: required by a bound in `debug`
--> ~/.cargo/registry/.../tracing-core-0.1.36/src/field.rs:379:8
|
377 | pub fn debug<T>(t: T) -> DebugValue<T>
| ----- required by a bound in this function
378 | where
379 | T: fmt::Debug,
| ^^^^^^^^^^ required by this bound in `debug`
= note: this error originates in the attribute macro `instrument` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider restricting type parameter `T` with trait `Debug`
|
4 | fn store<T: std::fmt::Debug>(key: &str, item: T) {}
| +++++++++++++++++
第二个错误的标题相同,note 换成了 required for DebugValue<&T> to implement Value 和 required for the cast from &DebugValue<&T> to &dyn Value。
方法上的 self:
struct Db {
url: String,
}
impl Db {
#[instrument]
fn query(&self, sql: &str) {}
}
error[E0277]: `Db` doesn't implement `Debug`
--> src/bin/err_self.rs:8:3
|
8 | #[instrument]
| ^^^^^^^^^^^^^ the trait `Debug` is not implemented for `Db`
|
= note: required for `&Db` to implement `Debug`
= note: 1 redundant requirement hidden
= note: required for `&&Db` to implement `Debug`
// ... 以下同上,另有第二个同名错误
普通参数 fn query(conn: &Conn, sql: &str) 的报错和 self 一样,只是类型换成 Conn,完整输出见 参数。
三种的共同点:箭头指在 #[instrument] 那一行,不指参数。生成的代码借用了属性的 span,rustc 只能指到那里。认出是哪个参数,看报错里的类型:&T 是按值传的 T,&&Db 是 &self(参数是 &Db,宏又取了一次引用)。
err 的变体报的是 Display。错误类型只派生了 Debug:
#[derive(Debug)]
struct MyError; // 只有 Debug,没有 Display
#[instrument(err)]
fn work() -> Result<(), MyError> {
Err(MyError)
}
报三个错误,第一个的开头:
error[E0277]: `MyError` doesn't implement `std::fmt::Display`
--> src/bin/err_no_display.rs:6:1
|
6 | #[instrument(err)]
| ^^^^^^^^^^^^^^^^^^
| |
| unsatisfied trait bound
| required by a bound introduced by this call
|
help: the trait `std::fmt::Display` is not implemented for `MyError`
--> src/bin/err_no_display.rs:4:1
|
4 | struct MyError; // 只有 Debug,没有 Display
| ^^^^^^^^^^^^^^
= note: required for `&MyError` to implement `std::fmt::Display`
note: required by a bound in `display`
// ...
#[instrument(ret)] 而返回类型没有 Debug 时,报三个 error[E0277]: `Conn` doesn't implement `Debug`,箭头同样指在属性上。
从哪来
gen_block 给每个参数定一个记法(RecordType)。parse_from_ty 只看类型路径的最后一段,在 TYPES_FOR_VALUE(bool、str、整数、浮点、String、NonZero*、Wrapping)里的按 Value 记(引用类型先剥掉 & 再查),其余一律是 Debug;self 不看类型,直接是 Debug(第 164 行)。泛型参数 T 不在表里,所以也是 Debug。
没被跳过的参数在 第 231 行 变成字段:
.map(|(user_name, (real_name, record_type))| match record_type {
RecordType::Value => quote!(#user_name = #real_name),
RecordType::Debug => quote!(#user_name = ::tracing::field::debug(&#real_name)),
})
field::debug 要求 T: fmt::Debug,这就是报错里 required by a bound in debug 的那条约束。err 的事件在 第 268 行 生成,默认模式写的是 error = %e,% 走 field::display,要求 Display;ret 默认 return = ?x,要求 Debug。各个参数的完整含义见 参数。
被 skip 的参数在 第 203 行起 的过滤里就被去掉了,生成的代码里根本不出现,所以不需要任何 trait。
修法
| 情况 | 写法 |
|---|---|
| 某个参数不需要记 | skip(item) |
self 不需要记,或只记一部分 | skip(self), fields(db = %self.url) |
| 大部分参数都不需要 | skip_all, fields(...) 只列想要的 |
| 想记参数的某个属性而不是整个参数 | fields(conn = conn.id):和参数同名的字段替换掉参数 |
错误类型只有 Debug | err(Debug) |
返回类型没有 Debug | 去掉 ret,或给它实现 Debug |
把这几种放进一个程序:
tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", default-features = false, features = ["fmt", "std"] }
use tracing::instrument;
use tracing_subscriber::fmt::format::FmtSpan;
struct Db {
url: String,
}
impl Db {
// 1. 跳过 self,需要的部分用 fields 记
#[instrument(skip(self), fields(db = %self.url))]
fn query(&self, sql: &str) {}
}
// 2. 跳过泛型参数,而不是给 T 加 Debug 约束
#[instrument(skip(item))]
fn store<T>(key: &str, item: T) {
drop(item);
}
struct Conn {
id: u32,
}
// 3. 全部跳过,只记想要的
#[instrument(skip_all, fields(conn_id = conn.id, len = payload.len()))]
fn send(conn: &Conn, payload: Vec<u8>) {}
// 4. 和参数同名的字段替换掉这个参数
#[instrument(fields(conn = conn.id))]
fn close(conn: &Conn) {}
#[derive(Debug)]
struct MyError;
// 5. 错误类型只有 Debug
#[instrument(err(Debug))]
fn work() -> Result<(), MyError> {
Err(MyError)
}
fn main() {
tracing_subscriber::fmt()
.with_ansi(false)
.without_time()
.with_target(false)
.with_span_events(FmtSpan::NEW)
.init();
let db = Db { url: "pg://db1".into() };
db.query("select 1");
store("k", vec![std::rc::Rc::new(())]);
let conn = Conn { id: 7 };
send(&conn, vec![0; 16]);
close(&conn);
let _ = work();
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同(FmtSpan::NEW 让每个 span 创建时打一行 new,用来看字段):
INFO query{sql="select 1" db=pg://db1}: new
INFO store{key="k"}: new
INFO send{conn_id=7 len=16}: new
INFO close{conn=7}: new
INFO work: new
ERROR work: error=MyError
query:self不见了,db用Display记了 URL。fields里的表达式可以引用被 skip 的参数,包括self。store:传进去的是Vec<Rc<()>>,和Debug无关,skip以后只剩key。send:skip_all后两个参数都不自动记,fields里记了id和长度。close:字段conn和参数同名,参数不再按Debug记,只剩conn=7。这条规则的源码见 参数。work:err(Debug)把%e换成了?e,输出的是MyError的Debug。
问: rustc 的 help 建议把签名改成 fn store<T: std::fmt::Debug>(...),照做不就好了?
答: 能编译,但为了打日志把约束加到了函数签名上:每个调用者传的类型都必须实现 Debug,泛型函数的约束还会顺着调用链往上传。而且 Debug 输出的可能是整个大结构、一整个 Vec,或者密码、token 这类不该进日志的东西。#[instrument] 默认把所有参数都记下来,是为了上手方便;生产代码里多数时候应该反过来,skip_all 再用 fields 列出真正有用的那几个。
怎么确认自己没踩
这是编译错误,不会漏到运行时。要留意的是反方向:编译通过不等于记得合适。所有参数都实现了 Debug 时,#[instrument] 会把它们全部记下来。给一个函数加 #[instrument] 后,用 FmtSpan::NEW(像上面那样)或在函数里打一条事件,看一眼 span 上到底有哪些字段,再决定 skip 什么。
小结
#[instrument]把没跳过的参数按类型名分两种:基本类型和String直接当Value,其余(包括泛型参数和self)包成field::debug(&x),要求Debug。err默认error = %e,要求Display;ret默认return = ?x,要求Debug。- 报错的箭头都指在
#[instrument]上,看报错里的类型(&T、&&Db)认出是哪个参数。 - 修法:
skip(x)、skip_all加fields(...)、和参数同名的字段替换参数、err(Debug)。不要为了日志给泛型加Debug约束。