参数
主线位置:两条主线都经过这里。
skip、fields、level、name、target、parent、follows_from决定 span 主线起点那个span!的元数据、字段和父 span;err、ret在函数返回处插入event!,走事件主线。
flowchart TB args["#[instrument(...)]"] --> ia["InstrumentArgs"] ia -->|"name level target"| meta["span! 的元数据"] ia -->|"skip skip_all"| params["哪些参数成字段"] ia -->|"fields"| extra["追加的字段"] ia -->|"parent"| par["span! 的 parent"] ia -->|"follows_from"| ff["span.follows_from"] ia -->|"err ret"| evs["返回处的 event!"] params --> spannode["span!"] extra --> spannode meta --> spannode par --> spannode
上一章 展开成什么 讲了不带参数时生成的代码。本章讲 attr.rs 怎么把括号里的东西解析成 InstrumentArgs,以及 gen_block 把每一项放到生成代码的哪里。实验 crate 的依赖:
tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", default-features = false, features = ["fmt", "registry", "std"] }
本页的程序在 M2 Max、rustc 1.99.0 上以 --release 运行,编译错误引用 rustc 1.99.0 的原始输出,只把 crates.io 源码的路径缩短成 ~/.cargo/registry/.../。
InstrumentArgs:一个手写的解析循环
InstrumentArgs 每个参数一个字段:
pub(crate) struct InstrumentArgs {
level: Option<Level>,
pub(crate) name: Option<LitStrOrIdent>,
target: Option<LitStrOrIdent>,
pub(crate) parent: Option<Expr>,
pub(crate) follows_from: Option<Expr>,
pub(crate) skips: HashSet<Ident>,
pub(crate) skip_all: bool,
pub(crate) fields: Option<Fields>,
pub(crate) err_args: Option<EventArgs>,
pub(crate) ret_args: Option<EventArgs>,
/// Errors describing any unrecognized parse inputs that we skipped.
parse_warnings: Vec<syn::Error>,
}
Parse 是一个 while !input.is_empty() 循环,每轮用 lookahead1 看下一个 token 是哪个关键字(name、target 这些用 syn::custom_keyword! 定义在文件末尾的 kw 模块里),解析对应的一项;逗号直接跳过,所以参数顺序随意。两处设计值得注意:
- 认不出的 token 不报错,而是记进
parse_warnings,再吞掉一个 token 树继续。注释说这是为了向后兼容:「we need to emit compiler warnings rather than errors for unrecognized inputs」。稳定版的过程宏发不了警告,warnings()就在函数体开头放一个#[deprecated(since = "not actually deprecated", note = ...)]的常量并使用它,借deprecatedlint 打出「found unrecognized input, ...」。上一章展开里第一行的{},就是没有警告时它生成的空块。 - 每一项都检查重复,写两次
name报「expected only a singlenameargument」;skip和skip_all同时出现报「expected eitherskiporskip_allargument」。
parent 和 follows_from 的重复检查写错了字段(第 103 行起):
} else if lookahead.peek(kw::parent) {
if args.target.is_some() {
return Err(input.error("expected only a single `parent` argument"));
}
它查的是 args.target。于是 target 写在 parent 前面就编译不过:
use tracing::instrument;
#[instrument(target = "app", parent = None)]
fn detached() {}
fn main() {}
error: expected only a single `parent` argument
--> src/bin/err_target_parent.rs:3:30
|
3 | #[instrument(target = "app", parent = None)]
| ^^^^^^
把两项换个顺序写成 #[instrument(parent = None, target = "app")] 就能编译。反过来,parent 写两次不会报错,后一个生效。这个问题在 0.1.31 发布之后由 PR #3630(「attributes: fix duplicate check for parent and follows_from」)修好,2026-10-09 合进 main,写作时还没有包含它的 tracing-attributes 版本。
skip 与 skip_all
Skips 把 skip(a, b) 解析成 HashSet<Ident>,同一个名字写两次报「tried to skip the same field twice」。gen_block 先核对每个名字确实是参数(第 190 行),拼错了在名字上报错:
#[instrument(skip(pasword))]
fn login(user: &str, password: &str) {}
error: attempting to skip non-existent parameter
--> src/bin/err_skip_missing.rs:3:19
|
3 | #[instrument(skip(pasword))]
| ^^^^^^^
然后在生成字段列表时过滤掉(第 203 行起):skip_all 为真或名字在 skips 里的参数不进 span!。被跳过的参数在生成的代码里根本不出现,所以不需要实现 Debug。
不跳过的参数按上一章的规则记:不在 TYPES_FOR_VALUE 里的类型被包成 ::tracing::field::debug(&x)。类型没实现 Debug 时,rustc 1.99.0 的报错:
use tracing::instrument;
struct Conn; // 没有 Debug
#[instrument]
fn query(conn: &Conn, sql: &str) {}
fn main() {}
error[E0277]: `Conn` doesn't implement `Debug`
--> src/bin/err_no_debug.rs:5:1
|
5 | #[instrument]
| ^^^^^^^^^^^^^ the trait `Debug` is not implemented for `Conn`
|
= note: required for `&Conn` to implement `Debug`
= note: 1 redundant requirement hidden
= note: required for `&&Conn` 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 annotating `Conn` with `#[derive(Debug)]`
|
3 + #[derive(Debug)]
4 | struct Conn; // 没有 Debug
|
error[E0277]: `Conn` doesn't implement `Debug`
--> src/bin/err_no_debug.rs:5:1
|
5 | #[instrument]
| ^^^^^^^^^^^^^ the trait `Debug` is not implemented for `Conn`
|
help: the trait `Value` is conditionally implemented for `DebugValue<T>`
--> ~/.cargo/registry/.../tracing-core-0.1.36/src/field.rs:744:1
|
744 | / impl<T> Value for DebugValue<T>
745 | | where
746 | | T: fmt::Debug,
| |________----------^
| |
| unsatisfied requirement introduced here: `&&Conn: Debug`
= note: required for `&Conn` to implement `Debug`
= note: 1 redundant requirement hidden
= note: required for `&&Conn` to implement `Debug`
= note: required for `DebugValue<&&Conn>` to implement `Value`
= note: required for the cast from `&DebugValue<&&Conn>` to `&dyn Value`
= note: this error originates in the macro `$crate::valueset_all` which comes from the expansion of the attribute macro `instrument` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Conn` with `#[derive(Debug)]`
|
3 + #[derive(Debug)]
4 | struct Conn; // 没有 Debug
|
两个错误指向的都是 #[instrument] 这一行,而不是参数 conn,因为生成的代码借用了属性的 span。看 &&Conn 能认出是哪个参数:参数是 &Conn,宏又取了一次引用。修法是 skip(conn),或者给 Conn 加上 Debug。
fields:追加字段
Field 有名字、可选的值和记法(Value、? 的 Debug、% 的 Display)。Field::parse 接受的名字有两种:点分隔的标识符(http.method),或花括号里的表达式({ KEY },0.1.31 新加,PR #3158)。值是任意表达式。解析完以后 Fields 原样放到生成的 span!(...) 参数末尾(第 255 行),所以 fields(...) 里能写的就是 span! 的字段语法,见 宏展开。
几条和参数相关的规则:
- 值表达式在函数体之前、
span!的门都通过之后求值,可以引用参数。gen_block会用IdentAndTypesRenamer把表达式里的参数名换成内部名,这只对 async-trait 0.1.43 及更早版本(self被改名成_self)有区别。 - 和参数同名的字段替换掉那个参数(第 212 行起):参数不再自动记录,只留你写的那一个。名字是
{ 表达式 }时不做这个去重,注释写着「#3158: Expressions cannot be evaluated at compile time and will incur a runtime cost to de-duplicate」。 - 只写名字不写值,比如
fields(status),生成的是status = ::tracing::field::Empty,不是info!(status)那种「取同名局部变量」的简写。ToTokens for Field里的注释承认这和宏不一致,但已经发布了,改掉是破坏性变更。
field::Empty 的用处是先占一个字段名,函数里再用 Span::current().record(...) 填。Span::record 只能填创建时声明过的字段,原因见 字段:Field、Value 与 Visit。
level、name、target
三项都只影响 span! 的元数据:
level:Level::parse接受字符串(不分大小写,"debug"、"DEBUG")、数字 1–5(1 是TRACE,5 是ERROR),或者任意路径(Level::DEBUG、自己的常量)。路径原样放进生成代码,由 rustc 检查类型。name:字符串字面量或标识符。标识符要写成name = SOME_CONST;字符串字面量还可以不带name =直接写,#[instrument("my_span")],注释说去掉这个写法「is apparently breaking」。target:同样是字符串字面量或标识符,默认module_path!()。name和target接受常量是 tracing-attributes 0.1.28 起(PR #2941)。
target 只改 span 和 err / ret 事件的 target,函数体里你自己写的 info! 仍然用模块路径。下面把常量名字、常量 target、表达式字段名和一个空字段放在一起:
use tracing::{info, instrument};
const KEY: &str = "http.method";
const SPAN: &str = "handle_request";
const TARGET: &str = "app::http";
#[instrument(name = SPAN, target = TARGET, fields({ KEY } = method, empty))]
fn handle(method: &str) {
info!("inside");
}
fn main() {
tracing_subscriber::fmt().with_ansi(false).without_time().with_target(true).init();
handle("GET");
}
连跑 3 次输出相同:
INFO handle_request{method="GET" http.method="GET"}: const_name: inside
span 名是 handle_request;method 参数没有被去重,和 http.method 并存;empty 是 Empty,没填值,fmt 不打印它;事件的 target 是模块路径 const_name,不是 app::http。
parent 与 follows_from
两项的值都是任意表达式(ExprArg):
parent = expr原样变成span!(target: ..., parent: expr, ...),表达式要能转成Option<Id>:None表示根 span,&span或span.id()指定父 span。三种父 span 在 tracing-core 里怎么表示,见 span 与 Event 的数据。follows_from = expr变成for cause in expr { __tracing_attr_span.follows_from(cause); }(第 146 行起),所以它要的是一个可迭代的东西,每个元素能转成Option<Id>;只有一个原因时写成[cause]。同步路径里这个循环在level_enabled!的分支内、enter之前;异步路径里在!is_disabled()的分支内。
err 与 ret:在返回处插入事件
err 和 ret 后面可以跟一对括号,由 EventArgs::parse 解析,里面最多两项、用逗号分开:一个 level = ...,一个格式 Debug 或 Display。格式写成别的名字:
#[instrument(err(Pretty))]
fn work() -> Result<(), std::io::Error> {
Ok(())
}
error: unknown event formatting mode, expected either `Debug` or `Display`
--> src/bin/err_mode.rs:3:18
|
3 | #[instrument(err(Pretty))]
| ^^^^^^
gen_block 用解析结果拼两个事件(第 266 行起):
let target = args.target();
let err_event = match args.err_args {
Some(event_args) => {
let level_tokens = event_args.level(Level::Error);
match event_args.mode {
FormatMode::Default | FormatMode::Display => Some(quote!(
::tracing::event!(target: #target, #level_tokens, error = %e)
)),
FormatMode::Debug => Some(quote!(
::tracing::event!(target: #target, #level_tokens, error = ?e)
)),
}
}
_ => None,
};
let ret_event = match args.ret_args {
Some(event_args) => {
let level_tokens = event_args.level(args_level);
match event_args.mode {
FormatMode::Display => Some(quote!(
::tracing::event!(target: #target, #level_tokens, return = %x)
)),
FormatMode::Default | FormatMode::Debug => Some(quote!(
::tracing::event!(target: #target, #level_tokens, return = ?x)
)),
}
}
_ => None,
};
| 字段名 | 默认格式 | 默认级别 | target | |
|---|---|---|---|---|
err | error | Display(%e) | ERROR,不随 span | 同 span |
ret | return | Debug(?x) | 和 span 相同(args_level) | 同 span |
插在哪里取决于同步还是异步,以及两个事件各有没有:
- 同步函数(第 383 行起):函数体被包成闭包
(move || #block)()立即调用,这样函数体里的return和?只是从闭包返回,返回值还能被接住。有err时match结果,Ok(x)分支发ret事件(如果有),Err(e)分支发err事件,再原样返回;只有ret时不match,let x = (move || #block)(); #ret_event; x。这些都在 span 的守卫之后,所以事件在 span 里。 - 异步(第 306 行起):形状一样,只是闭包换成
async move #block.await,整段放进交给Instrumented的那个 future,事件同样在 span 里。
#[instrument(skip(s), fields(len = s.len()), err, ret)] 的同步展开,两个 event! 内部从略:
#[allow(clippy::redundant_closure_call)]
match (move || {
// ... 假返回
{ s.parse() }
})() {
#[allow(clippy::unit_arg)]
Ok(x) => {
{
// ... event!:名字 "event src/bin/expand_err.rs:4",INFO,字段 return
};
Ok(x)
}
Err(e) => {
{
// ... event!:同名,ERROR,值 ::tracing::field::display(&e)
};
Err(e)
}
}
事件名里的行号是 #[instrument] 那一行,因为生成的代码借用了属性的 span。
err 的 %e 要求错误类型实现 Display。只派生了 Debug 的错误类型:
use tracing::instrument;
#[derive(Debug)]
struct MyError; // 只有 Debug,没有 Display
#[instrument(err)]
fn work() -> Result<(), MyError> {
Err(MyError)
}
fn main() {}
rustc 1.99.0 报三个错误,第一个是:
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`
--> ~/.cargo/registry/.../tracing-core-0.1.36/src/field.rs:370:8
|
368 | pub fn display<T>(t: T) -> DisplayValue<T>
| ------- required by a bound in this function
369 | where
370 | T: fmt::Display,
| ^^^^^^^^^^^^ required by this bound in `display`
= note: this error originates in the macro `$crate::valueset_all` which comes from the expansion of the attribute macro `instrument` (in Nightly builds, run with -Z macro-backtrace for more info)
另外两个分别指向 DisplayValue 的约束(field.rs:360)和 impl<T> Value for DisplayValue<T>(field.rs:719),结尾是「could not compile ... due to 3 previous errors」。改成 err(Debug) 即可。同理,ret 的 ?x 要求返回类型实现 Debug:返回一个没有 Debug 的 Conn 时,报三个 error[E0277]: `Conn` doesn't implement `Debug` ,箭头同样指在 #[instrument(ret)] 上。
用前面的打印思路看实际发出的事件。Print 这次打印 span 和事件的级别、target、父 span,以及 on_record、on_follows_from:
use std::fmt::Write;
use std::num::ParseIntError;
use tracing::field::{Field, Visit};
use tracing::span::{Attributes, Id, Record};
use tracing::{Event, Span, Subscriber, info, info_span, instrument};
use tracing_subscriber::layer::{Context, Layer, SubscriberExt};
use tracing_subscriber::registry::{LookupSpan, Registry};
struct Fields(String);
impl Visit for Fields {
fn record_debug(&mut self, f: &Field, v: &dyn std::fmt::Debug) {
let _ = write!(self.0, " {}={:?}", f.name(), v);
}
}
struct Print;
impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for Print {
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, cx: Context<'_, S>) {
let mut f = Fields(String::new());
attrs.record(&mut f);
let m = attrs.metadata();
let parent = cx.span(id).and_then(|s| s.parent()).map(|p| p.name()).unwrap_or("-");
println!("span {} {} {} parent={}{}", m.level(), m.target(), m.name(), parent, f.0);
}
fn on_record(&self, id: &Id, rec: &Record<'_>, cx: Context<'_, S>) {
let mut f = Fields(String::new());
rec.record(&mut f);
println!("record {}{}", cx.span(id).unwrap().name(), f.0);
}
fn on_follows_from(&self, id: &Id, cause: &Id, cx: Context<'_, S>) {
println!("follows_from {} <- {}", cx.span(id).unwrap().name(), cx.span(cause).unwrap().name());
}
fn on_event(&self, ev: &Event<'_>, cx: Context<'_, S>) {
let mut f = Fields(String::new());
ev.record(&mut f);
let m = ev.metadata();
let cur = cx.lookup_current().map(|s| s.name()).unwrap_or("-");
println!("event {} {} in={}{}", m.level(), m.target(), cur, f.0);
}
}
struct Secret; // 没有 Debug
#[instrument(skip(password), fields(user_len = user.len()))]
fn login(user: &str, password: &str) {
let _ = password;
}
#[instrument(skip_all, fields(n = items.len()))]
fn batch(items: Vec<Secret>) {}
#[instrument(fields(status = tracing::field::Empty))]
fn handle(path: &str) {
Span::current().record("status", 200);
}
#[instrument(level = "debug", name = "load_cfg", target = "app::cfg")]
fn load() {}
#[instrument]
fn attached() {}
#[instrument(parent = None)]
fn detached() {}
#[instrument(fields(path = %path.to_uppercase()))]
fn shout(path: &str) {}
#[instrument(skip(cause), follows_from = [cause])]
fn later(cause: &Span) {}
#[instrument(err, ret)]
fn parse(s: &str) -> Result<u32, ParseIntError> {
s.parse()
}
#[instrument(level = "debug", ret(Display), err(level = "warn", Debug))]
fn parse_custom(s: &str) -> Result<u32, ParseIntError> {
s.parse()
}
fn main() {
tracing::subscriber::set_global_default(Registry::default().with(Print)).unwrap();
login("ann", "hunter2");
batch(vec![Secret, Secret]);
handle("/a");
load();
info_span!("outer").in_scope(|| {
attached();
detached();
});
shout("/a");
let cause = info_span!("cause");
later(&cause);
println!("--");
let _ = parse("42");
let _ = parse("x");
let _ = parse_custom("7");
let _ = parse_custom("y");
info!("done");
}
连跑 3 次输出相同:
span INFO args_demo login parent=- user="ann" user_len=3
span INFO args_demo batch parent=- n=2
span INFO args_demo handle parent=- path="/a"
record handle status=200
span DEBUG app::cfg load_cfg parent=-
span INFO args_demo outer parent=-
span INFO args_demo attached parent=outer
span INFO args_demo detached parent=-
span INFO args_demo shout parent=- path=/A
span INFO args_demo cause parent=-
span INFO args_demo later parent=-
follows_from later <- cause
--
span INFO args_demo parse parent=- s="42"
event INFO args_demo in=parse return=42
span INFO args_demo parse parent=- s="x"
event ERROR args_demo in=parse error=invalid digit found in string
span DEBUG args_demo parse_custom parent=- s="7"
event DEBUG args_demo in=parse_custom return=7
span DEBUG args_demo parse_custom parent=- s="y"
event WARN args_demo in=parse_custom error=ParseIntError { kind: InvalidDigit }
event INFO args_demo in=- message=done
逐行对照:
login没有password,多了表达式字段user_len=3;batch的参数Vec<Secret>没有Debug,skip_all后能编译,只剩n=2。handle创建时status是Empty,所以span行里没有它;record那一行是Span::current().record补的。load_cfg的级别、名字、target 都被改了。- 同在
outer里调用,attached的父 span 是outer,detached因为parent = None没有父 span。 shout的fields(path = ...)和参数同名,参数path被替换,只剩path=/A。later创建后立刻调了一次follows_from。parse("42")发INFO的return=42(跟 span 级别);parse("x")发ERROR的error=,用的是ParseIntError的Display。两条事件都在parse里。parse_custom的 span 是DEBUG,ret(Display)跟着它是DEBUG;err(level = "warn", Debug)改成了WARN和Debug格式。
err / ret 的事件在 0.1.22 之前不跟 span 的 targetPR #2184(「attributes: ensure res and err events inherit target」)之前,#[instrument(target = "some_target", ret)] 生成的 span 的 target 是 some_target,但「the return value event generated by ret will have the current module path as its target, and there is no way to change the return value event's target」。按 target 过滤日志的人会发现 span 留下了、返回值事件却被滤掉,或者反过来。PR 让 err_event 和 ret_event 都用 args.target(),就是上面 event!(target: #target, ...) 里的 #target。CHANGELOG 记在 tracing-attributes 0.1.22:「Error and return value events generated by #[instrument(err)] or #[instrument(ret)] not inheriting an overridden target」。两个事件的级别能单独改(err(level = ...)、ret(level = ...))是之后的 PR #2335,0.1.24 起有。
上面的实验里 err 和 ret 是一起写的。只写其中一个时,文档的说法和代码的行为不完全一样。
ret、函数返回 Err,会发出什么?还是上一章的 Print(打印 new_span、enter、exit、close 和事件),M2 Max、rustc 1.99.0、--release:
use std::num::ParseIntError;
#[instrument(ret)]
fn parse(s: &str) -> Result<u32, ParseIntError> {
s.parse()
}
fn main() {
tracing::subscriber::set_global_default(Registry::default().with(Print)).unwrap();
let _ = parse("x");
}
tracing-attributes 的文档说:「if the function returns a Result<T, E>, ret will record returned values if and only if the function returns [Result::Ok]」。问:这次调用会不会有事件?如果有,return 字段的值是什么?
动脑筋答案
连跑 3 次输出相同:
new_span parse Id(1) s="x"
enter Id(1)
event in parse: return=Err(ParseIntError { kind: InvalidDigit })
exit Id(1)
close Id(1)
有事件,值是整个 Err(...)。文档那句话只在同时写了 err 时成立:(None, Some(ret_event)) 这个分支不 match,生成的是 let x = (move || #block)(); #ret_event; x,x 就是整个 Result,用 ?x 记下来。只有 (Some(err_event), Some(ret_event)) 才把 ret 事件放进 Ok(x) 分支。想只在成功时记返回值,就写 #[instrument(err, ret)],失败时 err 事件会接手。
小结
attr.rs的解析循环顺序无关,认不出的 token 只产生一个借deprecated发出的警告;0.1.31 里parent/follows_from的重复检查查错了字段,target写在它们前面会编译失败(#3630 已修,尚未发布)。skip/skip_all让参数不进span!,被跳过的参数不需要Debug;拼错参数名在名字上报错。没跳过、又没实现Debug的参数报E0277: `T` doesn't implement `Debug`,箭头指在#[instrument]上。fields(...)原样追加到span!:值表达式可以引用参数,同名字段替换参数({ 表达式 }名字除外),只写名字得到field::Empty,之后用Span::current().record填。level、name、target只改 span 的元数据;target也带到err/ret事件上,函数体里的事件不受影响。parent原样给span!,follows_from是一个对表达式迭代的for循环。err:字段error,默认Display、级别ERROR;ret:字段return,默认Debug、级别同 span。两者都可以(level = ..., Debug|Display)。同步函数把函数体包进立即调用的闭包再match,异步把它包进async move块再.await,事件都在 span 里。只写ret时Err也会被当成返回值记下。