Rust in Everything
基于 tracing-core 0.1.36

字段: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 不算「包含」。

💡TIP
打个比方:每个调用点是一种印好的表格

FieldSet 是一种印好的空白表格:表格编号(callsite)印在右上角,每一栏的标题(names)印在左边。Field 是「编号 X 的表格的第 3 栏」,只说栏号不说表格编号是没意义的。每次 info! 执行,就是拿一张这种表格填一次:ValueSet 是填好的那张纸,有的栏空着(None)。subscriber 收到一张纸,先看右上角编号,再按栏号读内容。

比方到这里为止:真的表格是一张一张印出来的,FieldSet 却只有一份,放在 static 的 Metadata 里,每次「填表」只是在栈上摆一排指针,借用这份唯一的表头。

用别的调用点的 Field 会怎样

既然 Field 带着调用点,拿 span a 的 Field 去记 span b 的值会怎样?答案是:在三个地方被挡下,什么也不记,也不报错。

  1. Span::record 先把参数转成 Field。对 Field 和 &Field 的 AsField 实现比较调用点,不一样就返回 None,Span::record 随即什么都不做,subscriber 的 record 根本不会被调用。传名字(&str)则用 FieldSet::field 在 b 自己的表里查。

  2. 如果绕过 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 也同样只数调用点相同的那些。

  3. record_all!(span, nope = 5) 写了一个 span 没声明的名字:valueset! 用 $fields.field($field) 查不到时,换成 __macro_support::FAKE_FIELD。它属于 tracing 内部一个假调用点 __fake_tracing_callsite,下标是 usize::MAX(FieldSet::fake_field),于是落进上一条的 continue。

这个设计的意思是:字段的身份是「调用点 + 下标」,名字只是给人看的。本页末尾的「动脑筋」把这三条都跑了一遍。

📝NOTE
冷知识: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)>]),
}

两种形态:

形态构造函数谁用读的时候
AllFieldSet::value_set_allevent!、span! 及所有快捷宏(经 valueset_all!)和 FieldSet 按位置 zip,不查调用点
ExplicitFieldSet::value_setSpan::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 一章讲。

📝NOTE
冷知识: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 i64record_i64impl_values!
u64;usize、u32、u16、u8 先 as u64record_u64同上
i128 / u128record_i128 / record_u128同上
f64;f32 先 as f64record_f64同上
boolrecord_bool同上
以上整数的 NonZero* 版本取 get() 后同上同上
str、Stringrecord_strstr、String
[u8]record_bytesL579
dyn Error + 'static(及 + Send / + Sync 组合)record_errorL590
fmt::Arguments(格式化消息)record_debugL655
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 真的 {:?} 时才发生,宏里只是包了一层引用。

📝NOTE
没有蠢问题

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

❗IMPORTANT
动脑筋: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。
  • 大小:Field 40 字节(下标 8 + FieldSet 32),FieldSet 是一个切片引用(16)加一个 &dyn Callsite(16),ValueSet 32 字节,每个值槽 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 的可选扩展。

💬 关联讨论

发起讨论
加载中...