Rust in Everything
基于 tracing-attributes 0.1.31

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):和参数同名的字段替换掉参数
错误类型只有 Debugerr(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。
📝NOTE
没有蠢问题

问: 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 约束。

💬 关联讨论

发起讨论
加载中...