字段:Field、Value 与 Visit
主线位置:两条主线都经过这里。事件主线上,四道门都放行以后,宏才把字段值摆成一个
ValueSet交给Event::dispatch;subscriber 在event里拿一个Visit去逐个读值,fmt就是这样把字段写成key=value。span 主线上,创建时的字段随Attributes交给new_span,之后Span::record补的字段随Record交给Subscriber::record。
flowchart TB macro["info!(a = 1, b = %x)"] --> fs["FieldSet:名字表"] fs --> meta["静态 Metadata"] macro --> arr["栈上数组 Option dyn Value"] arr --> vs["ValueSet:借用两者"] meta --> vs vs --> ev["Event / Attributes / Record"] ev --> sub["Subscriber 回调"] sub --> visit["record(visitor)"] visit --> val["Value::record"] val --> rec["visitor.record_i64 等"]
本页讲 tracing-core/src/field.rs 里的五个类型:描述「有哪些字段」的 FieldSet 和 Field,装「这次的值」的 ValueSet,值本身的 Value,以及读值用的 Visit。字段名表挂在 Metadata 上,Metadata 怎么由宏在编译期生成见 Metadata、Level 与 LevelFilter;调用点和它的标识见 Callsite 与 Interest 缓存。
FieldSet:名字表加调用点
FieldSet 只有两个字段:
pub struct FieldSet {
/// The names of each field on the described span.
names: &'static [&'static str],
/// The callsite where the described span originates.
callsite: callsite::Identifier,
}
names 是一个 'static 的字符串数组,由宏的 fieldset! 在编译期写出来;callsite 是 callsite::Identifier,里面是一个 &'static dyn Callsite,指向宏生成的那个 static __CALLSITE。Identifier 的 PartialEq 只比较指针地址,所以「是不是同一个调用点」是一次指针比较。
用 cargo expand(cargo-expand 1.0.104)展开一条 info!(answer = 42, user = %name, p = ?p, "hi {}", 7),Metadata 里的 FieldSet 是这样(FieldName 那段常量是为了去掉 r# 前缀,这里不展开):
::tracing_core::field::FieldSet::new(
&[
"message",
{
const NAME: ::tracing::__macro_support::FieldName<
{ ::tracing::__macro_support::FieldName::len("answer") },
> = ::tracing::__macro_support::FieldName::new("answer");
NAME.as_str()
},
// ... "user"、"p" 同样
],
::tracing_core::callsite::Identifier(&__CALLSITE),
),
两点:格式化消息被当成一个名叫 message 的字段,而且总排在第一位(fieldset! 遇到解析不了的剩余 token,就把 "message" 放到最前面);整张表连同调用点标识都是常量,运行期不用构造。
FieldSet::eq 也利用了「一个调用点只有一张名字表」:release 构建只比较 callsite,开了 debug_assertions 才再逐个比较名字。
Field:下标加调用点
Field 是「某个调用点的第 i 个字段」:
pub struct Field {
i: usize,
fields: FieldSet,
}
它不是一个裸下标,而是下标加一份 FieldSet 的拷贝(两个指针),在 64 位上 40 字节(下面实验里实测)。带着 FieldSet,Field::name() 才能直接 self.fields.names[self.i] 取名字,Field::callsite() 才能回答它属于哪个调用点。subscriber 通常在 new_span 时按名字查一次 FieldSet::field(线性查找 names),把得到的 Field 存起来,之后都按下标取,这就是文档里说的「O(1) access」。
Field 的相等和哈希都看两样东西,PartialEq:
impl PartialEq for Field {
fn eq(&self, other: &Self) -> bool {
self.callsite() == other.callsite() && self.i == other.i
}
}
所以两个调用点各有一个叫 x 的字段,它们的 Field 不相等。FieldSet::contains 的文档专门说明了这一点:名字相同、来自别的调用点的 Field 不算「包含」。
FieldSet 是一种印好的空白表格:表格编号(callsite)印在右上角,每一栏的标题(names)印在左边。Field 是「编号 X 的表格的第 3 栏」,只说栏号不说表格编号是没意义的。每次 info! 执行,就是拿一张这种表格填一次:ValueSet 是填好的那张纸,有的栏空着(None)。subscriber 收到一张纸,先看右上角编号,再按栏号读内容。
比方到这里为止:真的表格是一张一张印出来的,FieldSet 却只有一份,放在 static 的 Metadata 里,每次「填表」只是在栈上摆一排指针,借用这份唯一的表头。
用别的调用点的 Field 会怎样
既然 Field 带着调用点,拿 span a 的 Field 去记 span b 的值会怎样?答案是:在三个地方被挡下,什么也不记,也不报错。
Span::record先把参数转成Field。对Field和&Field的AsField实现比较调用点,不一样就返回None,Span::record随即什么都不做,subscriber 的record根本不会被调用。传名字(&str)则用FieldSet::field在b自己的表里查。如果绕过
Span::record,直接拼一个混进外来Field的ValueSet,ValueSet::record会逐个检查:Values::Explicit(values) => { let my_callsite = self.callsite(); for (field, value) in values { if field.callsite() != my_callsite { continue; } // ... } }ValueSet::len、is_empty、contains也同样只数调用点相同的那些。record_all!(span, nope = 5)写了一个 span 没声明的名字:valueset!用$fields.field($field)查不到时,换成__macro_support::FAKE_FIELD。它属于 tracing 内部一个假调用点__fake_tracing_callsite,下标是usize::MAX(FieldSet::fake_field),于是落进上一条的continue。
这个设计的意思是:字段的身份是「调用点 + 下标」,名字只是给人看的。本页末尾的「动脑筋」把这三条都跑了一遍。
FAKE_FIELD 是 0.1.36 修一个 panic 时加的tracing 0.1.42 新增了 record_all! 宏。同一批改动里(见下一节的冷知识),宏改成假定「值数组和 FieldSet 一一对应」。#3431 报告 record_all!(span, foo=3) 只给了部分字段就 panic,栈顶是 FieldSet::value_set_all 里的 debug_assert_eq!(「left: 1 right: 2」)。修复 #3432 的说明是:「We rename valueset! to valueset_all!, then we re-introduce the original valueset! with some notable changes.」新的 valueset!「uses FieldSet::field(...) to find the correct field. If the field isn't found, we set a fallback field that will be ignored (callsites are not equal).」这个 fallback 就是上面的 FAKE_FIELD。第一个包含它的版本是 tracing-core 0.1.36 和 tracing 0.1.44,正是本卷的版本。
ValueSet:借来的值,不分配
ValueSet 本身只有两个借用:
pub struct ValueSet<'a> {
values: Values<'a>,
fields: &'a FieldSet,
}
enum Values<'a> {
/// A set of field-value pairs. Fields may be for the wrong field set, some
/// fields may be missing, and fields may be in any order.
Explicit(&'a [(&'a Field, Option<&'a (dyn Value + 'a)>)]),
/// A list of values corresponding exactly to the fields in a `FieldSet`.
All(&'a [Option<&'a (dyn Value + 'a)>]),
}
两种形态:
| 形态 | 构造函数 | 谁用 | 读的时候 |
|---|---|---|---|
All | FieldSet::value_set_all | event!、span! 及所有快捷宏(经 valueset_all!) | 和 FieldSet 按位置 zip,不查调用点 |
Explicit | FieldSet::value_set | Span::record、record_all! | 每一对先比较调用点,不同就跳过 |
两个构造函数都是 #[doc(hidden)],不是给用户直接调的。value_set 的参数要满足私有的 ValidLen,它只给定长数组 [(...); N] 实现,所以只能传数组字面量。
还是上面那条 info!,cargo expand 出来的值数组:
__CALLSITE
.metadata()
.fields()
.value_set_all(
&[
(::tracing::__macro_support::Option::Some(
&format_args!("hi {0}", 7) as &dyn ::tracing::field::Value,
)),
(::tracing::__macro_support::Option::Some(
&42 as &dyn ::tracing::field::Value,
)),
(::tracing::__macro_support::Option::Some(
&::tracing::field::display(&name)
as &dyn ::tracing::field::Value,
)),
(::tracing::__macro_support::Option::Some(
&::tracing::field::debug(&p) as &dyn ::tracing::field::Value,
)),
],
)
每一项都是「对一个临时值取引用,再转成胖指针」:format_args! 生成的 fmt::Arguments 在栈上;42 是字面量;display(&name) 和 debug(&p) 只是把 &name、&p 包进一个单字段结构体。数组本身也是栈上的临时值,ValueSet 只借用它和静态的 FieldSet。从类型上看,整条路径上没有 Box、String、Vec。
光看类型还不够,subscriber 那一侧也可能分配。下面用一个数分配次数的全局分配器实测:subscriber 在 event 里用一个写进 256 字节栈缓冲区的 Visit 把所有字段格式化一遍,对照组是写到 io::sink 的 fmt subscriber。
tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", default-features = false, features = ["fmt"] }
// 节选:分配器、StackVisitor、NoAlloc 的定义略
fn run(label: &str) {
let name = "alice";
let p = Point { x: 1, y: 2 };
// 先跑一次,让调用点注册等一次性工作发生在计数之外
for round in 0..2 {
let before = ALLOCS.load(Ordering::Relaxed);
for i in 0..N {
tracing::info!(i, name, user = %name, p = ?p, ok = true, "req {} done", i);
}
let after = ALLOCS.load(Ordering::Relaxed);
println!("{label:<10} round {round}: {} allocs for {N} events", after - before);
}
}
fn main() {
tracing::subscriber::with_default(NoAlloc, || run("NoAlloc"));
let fmt_sub = tracing_subscriber::fmt()
.with_ansi(false)
.with_writer(std::io::sink)
.finish();
tracing::subscriber::with_default(fmt_sub, || run("fmt+sink"));
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
NoAlloc round 0: 0 allocs for 10000 events
NoAlloc round 1: 0 allocs for 10000 events
fmt+sink round 0: 5 allocs for 10000 events
fmt+sink round 1: 0 allocs for 10000 events
一万条带 6 个字段(含格式化消息、%、?)的事件,tracing 这一侧 0 次分配。fmt 第一轮的 5 次来自它自己的缓冲区:fmt::Layer::on_event 把一行先写进线程局部的 BUF: RefCell<String>,写完 clear 而不释放,所以第二轮也是 0。这部分在 fmt 一章讲。
Values::All 是 2025 年为了缩小代码体积加的0.1.42 之前只有一种 ValueSet:宏对每个字段都写出 (&Field, Option<&dyn Value>),Field 从 FieldSet::iter() 现取。#3398 的说明指出,宏生成的 ValueSet 每个字段都有值槽、顺序也对,Field 引用是多余的:「the code generated by the macros is terrible--it must put a Field on the stack for each field--that's 32 bytes per field!」而且这些数据「can't be moved into a read-only data section since it's intermixed with dynamic data」。修法是加上 Values::All,同时保留旧形态,因为「it's still needed by Span::record, by old versions of crates」。PR 给出的数字(x86_64 Linux):一个字段的事件从 258 字节降到 189 字节,五个字段从 638 降到 276,一个大项目的 .text 小了约 5%。核心部分首次发布在 tracing-core 0.1.35;宏这边是 tracing 0.1.42,但 0.1.42 因为另一处破坏性改动被撤回(yank),可用的第一个版本是 0.1.43。上一个冷知识里的 panic 就是这次改动和 record_all! 碰在一起产生的。
Value:一个封口的 trait
Value 只有一个方法:
pub trait Value: crate::sealed::Sealed {
/// Visits this value with the given `Visitor`.
fn record(&self, key: &Field, visitor: &mut dyn Visit);
}
它的工作是「告诉 visitor 我是什么类型」:每个实现选一个 record_* 方法调用。超 trait Sealed 是私有的,用户类型不能实现 Value。在自己的 crate 里写 impl Value for Point,rustc 1.99.0 报:
error[E0277]: the trait bound `Point: tracing_core::sealed::Sealed` is not satisfied
--> src/bin/sealed.rs:5:16
|
5 | impl Value for Point {
| ^^^^^ unsatisfied trait bound
...
= note: `Value` is a "sealed trait", because to implement it you also need to implement `tracing_core::sealed::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
自己的类型要么用 ? / % 走 Debug / Display,要么走下面的 valuable。tracing-core 提供的实现:
| 类型 | 调用 | 位置 |
|---|---|---|
i64;isize、i32、i16、i8 先 as i64 | record_i64 | impl_values! |
u64;usize、u32、u16、u8 先 as u64 | record_u64 | 同上 |
i128 / u128 | record_i128 / record_u128 | 同上 |
f64;f32 先 as f64 | record_f64 | 同上 |
bool | record_bool | 同上 |
以上整数的 NonZero* 版本 | 取 get() 后同上 | 同上 |
str、String | record_str | str、String |
[u8] | record_bytes | L579 |
dyn Error + 'static(及 + Send / + Sync 组合) | record_error | L590 |
fmt::Arguments(格式化消息) | record_debug | L655 |
DebugValue<T>(?) | record_debug(key, &self.0) | L744 |
DisplayValue<T>(%) | record_debug(key, self) | L719 |
Empty | 什么都不调 | L784 |
Option<T> | Some 时转给 T,None 什么都不调 | L791 |
&T、&mut T、Box<T>、Wrapping<T> | 转给 T | 同文件 |
Option<T> 的 None 和 Empty 一样「不出现」:visitor 收不到这个字段。
? 和 % 在宏里分别变成 field::debug 和 field::display,返回 DebugValue / DisplayValue。Visit 里没有 record_display,% 的值怎么交出去?DisplayValue 给自己实现了一个「用 Display 输出」的 Debug,再把自己当 &dyn Debug 交给 record_debug:
impl<T> Value for DisplayValue<T>
where
T: fmt::Display,
{
fn record(&self, key: &Field, visitor: &mut dyn Visit) {
visitor.record_debug(key, self)
}
}
impl<T: fmt::Display> fmt::Debug for DisplayValue<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
visitor 拿到的仍是 &dyn Debug,但 {:?} 打出来是 Display 的结果。下面实验里 disp = %"lit" 打印 lit,dbg = ?"lit" 打印带引号的 "lit",区别就在这里。格式化只在 visitor 真的 {:?} 时才发生,宏里只是包了一层引用。
问: owned = String::from("owned") 和 s = "lit" 都落到 record_str,那 ?owned 呢?? 是不是「更通用」,干脆都写 ??
答: ?owned 会变成 DebugValue(&owned),走 record_debug,类型信息就丢了:visitor 只拿到一个 &dyn Debug,不知道它是字符串。像 JSON 这类输出靠 record_str、record_i64 区分字符串和数字(fmt 的 DefaultVisitor 也在 record_str 里对 message 字段特殊处理)。所以基本类型直接写,只有自定义类型才用 ? / %。
Visit:按类型分派,默认落到 record_debug
Visit 只有 record_debug 必须实现,其余都有默认实现,全部转给它:
pub trait Visit {
// ...
fn record_f64(&mut self, field: &Field, value: f64) {
self.record_debug(field, &value)
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.record_debug(field, &value)
}
// ... record_u64、record_i128、record_u128、record_bool、record_str 同样
fn record_bytes(&mut self, field: &Field, value: &[u8]) {
self.record_debug(field, &HexBytes(value))
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
self.record_debug(field, &DisplayValue(value))
}
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug);
}
有两个默认实现不是简单的 &value:record_bytes 包一层 HexBytes,打印成 [de ad be ef];record_error 包一层 DisplayValue,所以错误默认按 Display 输出。
分派的流程是双重的:subscriber 调 event.record(&mut visitor),ValueSet 对每个有值的字段调 value.record(field, visitor)(第一次动态分派,选中 Value 的实现),那个实现再调 visitor.record_xxx(...)(第二次动态分派,选中 visitor 的方法)。Value 和 Visit 都是 trait object,所以 tracing-core 不需要知道任何具体的值类型或 subscriber 类型。
下面的程序用两个 visitor 读同一条事件:WhichMethod 覆盖所有 record_*,打印被调用的方法名;DebugOnly 只实现 record_debug,看默认实现把值变成了什么。
tracing = "=0.1.44"
use std::{error::Error, fmt, io};
use tracing::{
field::{Field, Visit},
span, Event, Metadata, Subscriber,
};
// 覆盖全部 record_* 方法,只打印「哪个方法被调用了」
struct WhichMethod;
impl Visit for WhichMethod {
fn record_i64(&mut self, f: &Field, v: i64) {
println!(" {:<6} record_i64({v})", f.name());
}
// ... record_u64、record_f64、record_bool 同样
fn record_str(&mut self, f: &Field, v: &str) {
println!(" {:<6} record_str({v:?})", f.name());
}
fn record_error(&mut self, f: &Field, v: &(dyn Error + 'static)) {
println!(" {:<6} record_error({v})", f.name());
}
fn record_debug(&mut self, f: &Field, v: &dyn fmt::Debug) {
println!(" {:<6} record_debug({v:?})", f.name());
}
}
// 只实现必需的 record_debug,其余全靠默认实现
struct DebugOnly;
impl Visit for DebugOnly {
fn record_debug(&mut self, f: &Field, v: &dyn fmt::Debug) {
println!(" {:<6} {v:?}", f.name());
}
}
struct Printer;
impl Subscriber for Printer {
fn enabled(&self, _: &Metadata<'_>) -> bool {
true
}
fn new_span(&self, _: &span::Attributes<'_>) -> span::Id {
span::Id::from_u64(1)
}
fn record(&self, _: &span::Id, _: &span::Record<'_>) {}
fn record_follows_from(&self, _: &span::Id, _: &span::Id) {}
fn event(&self, event: &Event<'_>) {
println!("WhichMethod:");
event.record(&mut WhichMethod);
println!("DebugOnly:");
event.record(&mut DebugOnly);
}
fn enter(&self, _: &span::Id) {}
fn exit(&self, _: &span::Id) {}
}
#[derive(Debug)]
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let owned = String::from("owned");
let err = io::Error::new(io::ErrorKind::NotFound, "no such file");
tracing::subscriber::with_default(Printer, || {
tracing::info!(
i = -1i64,
u = 1u64,
small = 7u8,
f = 1.5f64,
b = true,
s = "lit",
owned,
p = ?Point { x: 1, y: 2 },
disp = %"lit",
dbg = ?"lit",
e = &err as &dyn Error,
"hello {}",
42
);
});
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
WhichMethod:
message record_debug(hello 42)
i record_i64(-1)
u record_u64(1)
small record_u64(7)
f record_f64(1.5)
b record_bool(true)
s record_str("lit")
owned record_str("owned")
p record_debug(Point { x: 1, y: 2 })
disp record_debug(lit)
dbg record_debug("lit")
e record_error(no such file)
DebugOnly:
message hello 42
i -1
u 1
small 7
f 1.5
b true
s "lit"
owned "owned"
p Point { x: 1, y: 2 }
disp lit
dbg "lit"
e no such file
逐项对照:
message排第一,值是fmt::Arguments,走record_debug,没有引号。7u8被as u64,visitor 看到的是record_u64;它不知道原来是u8。"lit"和String都是record_str。DebugOnly里它们经默认实现变成&value的Debug,所以带引号。?和%都是record_debug,差别只在打印出来带不带引号。- 错误要先转成
&dyn Error才是Value(宏里写e = &err as &dyn Error),走record_error;默认实现按Display打印no such file。fmt的DefaultVisitor覆盖了record_str、record_error、record_debug三个方法,record_error会顺着source()把错误链打成e.sources=[...],这就是单独有record_error的用处。
tracing-core 自己也给三个类型实现了 Visit(L419 起):fmt::DebugStruct、fmt::DebugMap 和任何 FnMut(&Field, &dyn fmt::Debug) 闭包。ValueSet 的 Debug 就是拿 DebugStruct 当 visitor 实现的;写小工具时直接传一个闭包当 visitor 最省事。
field::Empty:先占位,后填值
span 的字段集合在调用点就定死了:Span::record("name", v) 用名字在 FieldSet 里查,查不到就什么也不做。所以创建时不知道值的字段,要先用 field::Empty 占一个名字:
let span = tracing::info_span!("req", user = tracing::field::Empty);
// ...
span.record("user", "alice");
Empty 让 fieldset! 把 user 写进名字表,它的 Value 实现是空的(fn record(&self, _: &Field, _: &mut dyn Visit) {}),所以 new_span 时 visitor 看不到这个字段;之后 record 才通过 Record 交给 Subscriber::record。Span::record 和 record_all! 这两个用户接口在 Span 与 Entered 讲,subscriber 怎么把后补的值合并进去看 Registry。
valuable:可选的结构化值
Value 只认基本类型,自定义类型只能变成 Debug / Display 文本;tracing-core 的 valuable feature 加上编译参数 --cfg tracing_unstable(两者缺一不可,见代码里的 #[cfg(all(tracing_unstable, feature = "valuable"))])会给 Visit 多一个默认转给 record_debug 的 record_value,让实现了 valuable::Valuable 的类型以嵌套结构交给 subscriber。
record 会被调用几次,各收到什么?tracing = "=0.1.44"
use std::fmt;
use tracing::{
field::{self, Field, Value, Visit},
span, Event, Metadata, Subscriber,
};
struct Print;
impl Visit for Print {
fn record_debug(&mut self, f: &Field, v: &dyn fmt::Debug) {
print!(" {}#{}={v:?}", f.name(), f.index());
}
}
struct Printer;
impl Subscriber for Printer {
fn enabled(&self, _: &Metadata<'_>) -> bool {
true
}
fn new_span(&self, attrs: &span::Attributes<'_>) -> span::Id {
print!("new_span {}:", attrs.metadata().name());
attrs.record(&mut Print);
println!();
span::Id::from_u64(1)
}
fn record(&self, _: &span::Id, values: &span::Record<'_>) {
print!("record (len={}):", values.len());
values.record(&mut Print);
println!();
}
fn record_follows_from(&self, _: &span::Id, _: &span::Id) {}
fn event(&self, _: &Event<'_>) {}
fn enter(&self, _: &span::Id) {}
fn exit(&self, _: &span::Id) {}
}
fn main() {
tracing::subscriber::with_default(Printer, || {
let a = tracing::info_span!("a", x = 1, y = field::Empty);
let b = tracing::info_span!("b", x = 1, y = field::Empty);
let a_fields = a.metadata().unwrap().fields();
let b_fields = b.metadata().unwrap().fields();
let ax = a_fields.field("x").unwrap();
let bx = b_fields.field("x").unwrap();
println!("ax == bx: {}", ax == bx);
println!("b_fields.contains(&ax): {}", b_fields.contains(&ax));
// 用 a 的 Field 去记 b:AsField 比较调用点,直接返回 None
b.record(&ax, 2);
// 用 b 自己的 Field、或者用名字:正常
b.record(&bx, 3);
b.record("y", "later");
// 绕过 AsField,直接拼一个混了外来 Field 的 ValueSet
let ay = a_fields.field("y").unwrap();
let by = b_fields.field("y").unwrap();
let pairs = [
(&ax, Some(&10 as &dyn Value)),
(&by, Some(&20 as &dyn Value)),
(&ay, Some(&30 as &dyn Value)),
];
b.record_all(&b_fields.value_set(&pairs));
// record_all! 只给一部分字段,名字不存在的字段
tracing::record_all!(b, y = 4);
tracing::record_all!(b, nope = 5, x = 6);
});
println!(
"size_of: Field={} FieldSet={} ValueSet={} Option<&dyn Value>={}",
std::mem::size_of::<Field>(),
std::mem::size_of::<field::FieldSet>(),
std::mem::size_of::<field::ValueSet<'static>>(),
std::mem::size_of::<Option<&dyn Value>>(),
);
}
b.record(...) 和 record_all 一共 6 次调用,Printer::record 打印几行?带 10、30、5 的那几个值会不会出现?
动脑筋答案
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
new_span a: x#0=1
new_span b: x#0=1
ax == bx: false
b_fields.contains(&ax): false
record (len=1): x#0=3
record (len=1): y#1="later"
record (len=1): y#1=20
record (len=1): y#1=4
record (len=1): x#0=6
size_of: Field=40 FieldSet=32 ValueSet=32 Option<&dyn Value>=16
6 次调用,record 只打印了 5 行;10、30、5 都没有出现。
new_span两行里只有x:y = field::Empty不会交给 visitor。ax == bx为false:名字和下标都一样,调用点不同。b.record(&ax, 2)一行都没有:AsField发现调用点不同,返回None,Span::record直接返回,subscriber 没被调用。b.record(&bx, 3)和b.record("y", "later")正常。- 混合的
ValueSet交给了 subscriber,但len=1,visitor 只看到y#1=20:ax和ay被ValueSet::record和ValueSet::len按调用点跳过。 record_all!(b, y = 4)只给一部分字段,在 0.1.44 里正常(0.1.43 / tracing-core 0.1.35 的 debug 构建会在这里 panic,见上文冷知识)。record_all!(b, nope = 5, x = 6):nope查不到,换成FAKE_FIELD后被跳过,只剩x#0=6。- 大小:
Field40 字节(下标 8 +FieldSet32),FieldSet是一个切片引用(16)加一个&dyn Callsite(16),ValueSet32 字节,每个值槽Option<&dyn Value>16 字节(None用空指针表示,不多占)。
小结
FieldSet是编译期写好的名字表加调用点标识;格式化消息是排在第一位的message字段。Field是下标加一份FieldSet(40 字节),相等看「调用点 + 下标」。别的调用点的Field在AsField、ValueSet::record、FAKE_FIELD三处被静默忽略。- 宏用
value_set_all构造Values::All:栈上一排Option<&dyn Value>,按位置对应FieldSet;Span::record/record_all!用带Field的Explicit。实测一万条事件 0 次分配。 Value是封口的 trait:基本类型、字符串、字节、错误、fmt::Arguments有各自的record_*;?/%包成DebugValue/DisplayValue,都走record_debug。Visit只有record_debug必须实现,其余默认都转给它;需要区分类型的 subscriber(如fmt的record_error、JSON 的数字)才覆盖。field::Empty在名字表里占位、不产生值;valuable是要--cfg tracing_unstable的可选扩展。