Rust in Everything
基于 tracing-core 0.1.36

Subscriber trait

主线位置:两条主线的终点。事件主线上,register_callsite、max_level_hint、enabled 决定前几道门开不开,event_enabled 和 event 收下最终的事件;span 主线上,new_span 分配 Id,enter / exit / clone_span / try_close 跟着 span 的一生被调用。

flowchart TB
  subgraph judge["判断"]
    rc["register_callsite"]
    en["enabled"]
    mlh["max_level_hint"]
    ee["event_enabled"]
  end
  subgraph life["span 生命周期"]
    ns["new_span 分配 Id"]
    rec["record / record_follows_from"]
    ent["enter / exit"]
    cs["clone_span / try_close"]
    cur["current_span"]
  end
  subgraph evgroup["事件"]
    ev["event"]
  end
  mac["宏:info! / info_span!"] --> judge
  judge -->|span| ns
  judge -->|事件| ev
  ns --> rec --> ent --> cs

Subscriber 是 tracing-core 里唯一需要「别人来实现」的大 trait。宏和 Span 只认它;fmt、Registry、Layered 都是它的实现。本页按三组讲它的方法,最后写一个把每个方法的调用次数数出来的最小实现。Interest 的三个值和 register_callsite 的结果怎么缓存,见 Callsite 与 Interest 缓存;Subscriber 被装进 Dispatch、设成默认的过程,见 Dispatch:全局与线程局部。

定义一览

去掉文档注释和默认方法体后,trait 是这样(第 80 行):

pub trait Subscriber: 'static {
    fn on_register_dispatch(&self, subscriber: &Dispatch) { /* ... */ }
    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest { /* ... */ }
    fn enabled(&self, metadata: &Metadata<'_>) -> bool;
    fn max_level_hint(&self) -> Option<LevelFilter> { /* ... */ }
    fn new_span(&self, span: &span::Attributes<'_>) -> span::Id;

    // === Notification methods ===============================================
    fn record(&self, span: &span::Id, values: &span::Record<'_>);
    fn record_follows_from(&self, span: &span::Id, follows: &span::Id);
    fn event_enabled(&self, event: &Event<'_>) -> bool { /* ... */ }
    fn event(&self, event: &Event<'_>);
    fn enter(&self, span: &span::Id);
    fn exit(&self, span: &span::Id);
    fn clone_span(&self, id: &span::Id) -> span::Id { /* ... */ }
    #[deprecated(since = "0.1.2", note = "use `Subscriber::try_close` instead")]
    fn drop_span(&self, _id: span::Id) {}
    fn try_close(&self, id: span::Id) -> bool { /* ... */ }
    fn current_span(&self) -> span::Current { /* ... */ }

    // === Downcasting methods ================================================
    unsafe fn downcast_raw(&self, id: TypeId) -> Option<*const ()> { /* ... */ }
}

所有方法都是 &self:同一个 subscriber 被所有线程共享,要记状态只能靠内部可变性(原子变量、锁、线程局部)。trait 本身只要求 'static,Send + Sync 是 Dispatch::new 加的约束。

组方法默认实现默认做什么
判断register_callsite有调 enabled,true 返回 Interest::always(),否则 never()
判断enabled无—
判断max_level_hint有None:不提示,按「可能要所有级别」处理
判断event_enabled有true
spannew_span无—
spanrecord无—
spanrecord_follows_from无—
spanenter / exit无—
spanclone_span有返回 id.clone(),不计数
spantry_close有调已废弃的 drop_span(空函数),返回 false
spancurrent_span有span::Current::unknown()
事件event无—
其他on_register_dispatch有什么都不做
其他downcast_raw有只认自己的 TypeId

必须实现的是 7 个:enabled、new_span、record、record_follows_from、event、enter、exit。

判断组:四个问题,问的时机不同

方法谁在什么时候调用拿到什么
max_level_hint重算调用点缓存时(Dispatch::new、rebuild_interest_cache),所有 dispatcher 的答案取最大值存进 LevelFilter::current()什么都不拿
register_callsite每个调用点第一次被经过时一次,之后重算缓存时再问&'static Metadata
enabled调用点的 Interest 是 sometimes 时每次都问;默认的 register_callsite 里问一次&Metadata:名字、target、级别、字段名,没有字段值
event_enabled每条事件、在 event 之前&Event:字段值已经在里面了

register_callsite 与 enabled。 默认的 register_callsite 把 enabled 的答案冻结成 always / never:

    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
        if self.enabled(metadata) {
            Interest::always()
        } else {
            Interest::never()
        }
    }

这意味着只实现 enabled 的 subscriber,enabled 对每个调用点只会被调一次,之后的判断全靠调用点里缓存的 Interest。如果过滤结果会随运行时状态变化(比如按当前 span 过滤),就要覆盖 register_callsite 返回 Interest::sometimes(),让 enabled 每次都被问到。参数类型也说明了分工:register_callsite 拿的是 &'static Metadata<'static>,可以存起来按调用点建表;enabled 只借用一下。

max_level_hint。 它只在重算缓存时被读,文档写明:返回值变了,要由 subscriber 自己调用 callsite::rebuild_interest_cache(),否则新值不生效。返回 None 的 subscriber 会被当成 TRACE,于是第二道门 LevelFilter::current() 什么都挡不住。

event_enabled。 它不由宏调用,而是由 Dispatch::event 在转交事件前调用:

    pub fn event(&self, event: &Event<'_>) {
        let subscriber = self.subscriber();
        if subscriber.event_enabled(event) {
            subscriber.event(event);
        }
    }

前面三个方法只看元数据,这时字段值还没求出来;到 event_enabled 时 ValueSet 已经构造好了,可以按字段值过滤。代价是被它挡掉的事件已经付过求值字段的开销。

📝NOTE
冷知识:event_enabled 是为了 Layer 加的

PR #2008 之前,Subscriber 只能在元数据层面说「要」或「不要」,按字段值过滤没有专门的钩子。PR 描述自己承认,对单独的 subscriber 来说这个方法「is not particulary necessary, as the subscriber can just skip the event call」;真正的用途在 tracing-subscriber:同一个 PR 加了 Layer::event_enabled,「This allows filter Layers to filter out an Event based on its fields」——一个过滤层返回 false,后面所有层的 on_event 都不会被调用,而单靠某一层在自己的 on_event 里提前返回做不到这一点。随 tracing-core 0.1.28 发布,CHANGELOG 记作「Subscriber::event_enabled method to enable filtering based on Event field values」。

span 生命周期组:Id 是 subscriber 发的

new_span 返回一个 span::Id,它就是一个 NonZeroU64。Id::from_u64(0) 会 panic("span IDs must be > 0");零被排除在外,Option<Id> 因此和 Id 一样是 8 字节。文档对 Id 的唯一要求是:同一个 subscriber 内部用它判断 span 是否相等;唯一性、顺序、能不能复用,全由 subscriber 决定。

这决定了 tracing 的结构:tracing::Span 里只有 Id、创建它的那个 Dispatch 和元数据(见 Span 与 Entered)。span 的名字、字段值、父子关系,tracing 本身一样也不存,要不要存、存在哪,是 subscriber 的事。NoSubscriber 给所有 span 发同一个 0xDEAD,因为它什么都不存;tracing-subscriber 的 Registry 在 sharded-slab 里占一个槽,Id 是 sharded-slab 的 key 加一(key 里打包了槽地址、线程号和代数)(见 Registry)。

span 的其余回调和 tracing::Span 的操作一一对应:

Span 上的操作调用的方法
info_span!(..)new_span
span.record("k", v)record
span.follows_from(&other)record_follows_from
span.enter() / 守卫 dropenter / exit
span.clone()clone_span
drop(span)try_close
Span::current()current_span

clone_span 和 try_close 要一起覆盖:前者每克隆一次加一,后者每 drop 一次减一,减到零时返回 true 表示 span 真的关闭了。默认实现不计数、永远返回 false,对不存 span 数据的 subscriber 正合适。drop_span 是 0.1.2 之前的旧名字,已废弃,默认的 try_close 仍然调用它,保证只覆盖了 drop_span 的老实现还能收到通知。

current_span 默认返回 Current::unknown()。「当前在哪个 span 里」同样只有 subscriber 知道:它要在 enter / exit 里自己维护一个栈(Registry 用线程局部的 span 栈)。不覆盖的话,Span::current() 拿到的是一个禁用的 span。

事件组

event 只有一个参数 &Event。Event 借用宏在栈上构造的 ValueSet,event 返回后就失效,字段值要留下只能当场拷贝。事件的父 span 由 Event 自己带着:显式指定、Current 或根,见 span 与 Event 的数据;字段值通过 Visit 取出,见 字段:Field、Value 与 Visit。

on_register_dispatch

on_register_dispatch 在 subscriber 被装进 Dispatch 时调用一次,调用点在 callsite::register_dispatch:

pub(crate) fn register_dispatch(dispatch: &Dispatch) {
    let dispatchers = DISPATCHERS.register_dispatch(dispatch);
    dispatch.subscriber().on_register_dispatch(dispatch);
    CALLSITES.rebuild_interest(dispatchers);
}

它的用途是让 subscriber 拿到「装着自己的 Dispatch」,比如交给后台线程。文档提醒不要把这个 Dispatch 存在自己身上,那会形成引用环;要存就 downgrade() 成 WeakDispatch(见 Dispatch 一页的 WeakDispatch)。tracing-subscriber 的 Layered 从 0.3.21 起会把它依次转给内层和外层;但包在 Option<L> 或 Vec<L> 里的 layer 收不到(见 Layer 与 Layered)。

tracing-core 为 Box<S> 和 Arc<S> 实现了 Subscriber(第 707 行、第 792 行),逐个方法转给内部,唯独没有转发 on_register_dispatch:这两个 impl 是 tracing-core 0.1.28 加的,on_register_dispatch 是 0.1.30 才加的,带默认实现,于是这两个 impl 不改也能编译。实测:

use std::sync::Arc;
use tracing::span::{Attributes, Id, Record};
use tracing::{Dispatch, Event, Metadata, Subscriber};

struct Hello;

impl Subscriber for Hello {
  fn on_register_dispatch(&self, _: &Dispatch) {
    println!("  on_register_dispatch 被调用");
  }
  fn enabled(&self, _: &Metadata<'_>) -> bool {
    true
  }
  fn new_span(&self, _: &Attributes<'_>) -> Id {
    Id::from_u64(1)
  }
  fn record(&self, _: &Id, _: &Record<'_>) {}
  fn record_follows_from(&self, _: &Id, _: &Id) {}
  fn event(&self, _: &Event<'_>) {}
  fn enter(&self, _: &Id) {}
  fn exit(&self, _: &Id) {}
}

fn main() {
  println!("Dispatch::new(Hello)");
  let _a = Dispatch::new(Hello);
  println!("Dispatch::new(Box::new(Hello))");
  let _b = Dispatch::new(Box::new(Hello));
  println!("Dispatch::new(Arc::new(Hello))");
  let _c = Dispatch::new(Arc::new(Hello));
  println!("Dispatch::none()");
  let _d = Dispatch::none();
}

依赖只要 tracing = "=0.1.44"。M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:

Dispatch::new(Hello)
  on_register_dispatch 被调用
Dispatch::new(Box::new(Hello))
Dispatch::new(Arc::new(Hello))
Dispatch::none()

依赖 on_register_dispatch 的 subscriber 不要先装箱再交给 Dispatch::new。Dispatch::none() 不调用它,因为 none() 不经过 register_dispatch。

downcast_raw:从包装里找到 Registry

downcast_raw 的默认实现只认自己:

    unsafe fn downcast_raw(&self, id: TypeId) -> Option<*const ()> {
        if id == TypeId::of::<Self>() {
            Some(self as *const Self as *const ())
        } else {
            None
        }
    }

dyn Subscriber 上的 downcast_ref::<T>() 和 is::<T>() 就建在它上面,Dispatch 也有同名方法转过来。为什么不直接用 std::any::Any?Any::downcast_ref 只能认出最外层的具体类型,而 downcast_raw 可以被覆盖:一个包装类型可以在「不是我」的时候去问它包着的东西,甚至返回自己某个字段的指针。方法标成 unsafe,因为覆盖它的人必须保证返回的指针真的指向一个 T,downcast_ref 会直接把它转成 &T。

tracing-subscriber 正是这样用的。Layered 作为 Subscriber 的 downcast_raw 先认自己,再问外层的 layer,再问内层:

        // If downcasting to `Self`, return a pointer to `self`.
        if id == TypeId::of::<Self>() {
            return Some(self as *const _ as *const ());
        }

        unsafe { self.layer.downcast_raw(id) }.or_else(|| unsafe { self.inner.downcast_raw(id) })

Layered<C, Layered<B, Layered<A, Registry>>> 这样一层层包下去,downcast_ref::<Registry>() 会一路问到最里面的 Registry。用到它的地方举两个:

  • Layered::try_close 先把内层 downcast 成 Registry,拿到一个 start_close 返回的 CloseGuard,保证所有层的 on_close 都跑完才释放 span 的槽。
  • Context::lookup_current_filtered 在用了每层过滤时,downcast 到 Registry 读它的线程局部 span 栈,找出对这一层可见的当前 span。

fmt::Layer 的 downcast_raw 还会把自己的事件格式器、字段格式器和 MakeWriter 交出去,测试里 dispatch.downcast_ref::<format::Format>() 就能拿到。tracing-subscriber 还拿 downcast_raw 传递一些「标记类型」,用来在类型擦除之后判断某一层有没有每层过滤;这些细节见 Layer 与 Layered 和 Registry。

一个数调用次数的完整实现

下面的 Counter 实现了除 current_span、废弃的 drop_span 和 unsafe 的 downcast_raw 之外的全部 13 个方法,每个方法被调用时给对应的计数器加一。max_level_hint 返回 INFO,event_enabled 挡掉带 skip 字段的事件,Id 用一个原子计数器从 1 开始发:

[dependencies]
tracing = "=0.1.44"
tracing-core = "=0.1.36"
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering::Relaxed};
use tracing::span::{Attributes, Id, Record};
use tracing::subscriber::Interest;
use tracing::{Dispatch, Event, Metadata, Subscriber};
use tracing_core::LevelFilter;

const NAMES: [&str; 13] = [
  "on_register_dispatch",
  "register_callsite",
  "enabled",
  "max_level_hint",
  "new_span",
  "record",
  "record_follows_from",
  "event_enabled",
  "event",
  "enter",
  "exit",
  "clone_span",
  "try_close",
];

#[derive(Default)]
struct Counter {
  calls: [AtomicUsize; 13],
  next_id: AtomicU64,
}

impl Counter {
  fn hit(&self, name: &str) {
    let i = NAMES.iter().position(|n| *n == name).unwrap();
    self.calls[i].fetch_add(1, Relaxed);
  }
}

impl Subscriber for Counter {
  fn on_register_dispatch(&self, _: &Dispatch) {
    self.hit("on_register_dispatch");
  }
  fn register_callsite(&self, meta: &'static Metadata<'static>) -> Interest {
    self.hit("register_callsite");
    // 和默认实现一样:问 enabled,把答案缓存成 always / never
    if self.enabled(meta) { Interest::always() } else { Interest::never() }
  }
  fn enabled(&self, _: &Metadata<'_>) -> bool {
    self.hit("enabled");
    true
  }
  fn max_level_hint(&self) -> Option<LevelFilter> {
    self.hit("max_level_hint");
    Some(LevelFilter::INFO)
  }
  fn new_span(&self, _: &Attributes<'_>) -> Id {
    self.hit("new_span");
    // Id 由 subscriber 自己分配;必须非零
    Id::from_u64(self.next_id.fetch_add(1, Relaxed) + 1)
  }
  fn record(&self, _: &Id, _: &Record<'_>) {
    self.hit("record");
  }
  fn record_follows_from(&self, _: &Id, _: &Id) {
    self.hit("record_follows_from");
  }
  fn event_enabled(&self, event: &Event<'_>) -> bool {
    self.hit("event_enabled");
    // 带 skip 字段的事件在这里被拦下
    event.fields().all(|f| f.name() != "skip")
  }
  fn event(&self, _: &Event<'_>) {
    self.hit("event");
  }
  fn enter(&self, _: &Id) {
    self.hit("enter");
  }
  fn exit(&self, _: &Id) {
    self.hit("exit");
  }
  fn clone_span(&self, id: &Id) -> Id {
    self.hit("clone_span");
    id.clone()
  }
  fn try_close(&self, _: Id) -> bool {
    self.hit("try_close");
    false
  }
}

fn main() {
  let dispatch = Dispatch::new(Counter::default());

  tracing::dispatcher::with_default(&dispatch, || {
    let parent = tracing::info_span!("parent", n = tracing::field::Empty);
    let child = tracing::info_span!("child");
    child.follows_from(&parent);
    parent.record("n", 1);
    for i in 0..3 {
      let _e = parent.enter();
      tracing::info!(i, "loop");
      tracing::debug!(i, "被 max_level_hint 挡掉");
    }
    tracing::info!(skip = true, "被 event_enabled 挡掉");
    let copy = parent.clone();
    drop(copy);
  });

  let counter = dispatch.downcast_ref::<Counter>().unwrap();
  for (name, n) in NAMES.iter().zip(&counter.calls) {
    println!("{name:<22}{}", n.load(Relaxed));
  }
}

最后一段用的是 Dispatch::downcast_ref:Counter 已经被 Dispatch 拿走装进了 Arc,从外面读回计数靠的就是上一节的 downcast_raw。没有用 Arc<Counter> 共享,是因为上一节说过,包一层 Arc 会让 on_register_dispatch 收不到调用。

❗IMPORTANT
动脑筋:register_callsite 和 enabled 各被调用几次?

程序里有 5 个宏调用点:两个 info_span!、循环里的 info! 和 debug!、最后带 skip 的 info!。循环跑 3 次。M2 Max、rustc 1.99.0、--release。先预测这两个数,再看 event_enabled 和 event 差几次。

动脑筋答案

连跑 3 次输出相同:

on_register_dispatch  1
register_callsite     4
enabled               4
max_level_hint        1
new_span              2
record                1
record_follows_from   1
event_enabled         4
event                 3
enter                 3
exit                  3
clone_span            1
try_close             3
  • register_callsite 4 次:5 个调用点里只有 debug! 没注册。Dispatch::new 时 max_level_hint 被问了 1 次,返回 INFO,于是 LevelFilter::current() 是 INFO;debug! 在第二道门 level_enabled! 就被挡住,根本没走到 __CALLSITE.interest(),循环 3 次都是这样。另外 4 个调用点各注册一次,即使循环里的 info! 被经过 3 次。
  • enabled 4 次:全部来自 register_callsite 里那一次调用。返回的 Interest 是 always,之后宏连第四道门都跳过,不再问 enabled。
  • max_level_hint 1 次:只在 Dispatch::new 触发的重算里读了一次;注册单个调用点不重算最大级别。
  • event_enabled 4 次、event 3 次:循环里 3 条加 skip 那条都走到了 Dispatch::event,skip 那条被 event_enabled 拦下。
  • new_span 2、record 1、record_follows_from 1、enter / exit 各 3:和代码里的操作一一对应。
  • clone_span 1、try_close 3:parent.clone() 一次克隆;copy、child、parent 三次 drop,各调用一次 try_close。Counter 永远返回 false,对 tracing 没有影响,tracing 不管 span 是否「真的」关了。
  • on_register_dispatch 1:Dispatch::new 时调用。

小结

  • Subscriber 的方法分三组:判断(register_callsite、enabled、max_level_hint、event_enabled)、span 生命周期(new_span、record、record_follows_from、enter、exit、clone_span、try_close、current_span)、事件(event)。必须实现的是 7 个。
  • 默认的 register_callsite 只问一次 enabled 并缓存成 always / never;要动态过滤就返回 sometimes。max_level_hint 只在重算缓存时读;event_enabled 由 Dispatch::event 在 event 前调用,能看到字段值。
  • span 的 Id 由 subscriber 分配,tracing 只拿着 Id 和 Dispatch,不存 span 数据;clone_span / try_close 默认不计数。
  • on_register_dispatch 在 Dispatch::new 里调用;Box<S> / Arc<S> 的 Subscriber 实现不转发它。
  • downcast_raw 可被包装类型覆盖,Layered 借它一路找到最里层的 Registry,用于关闭 span 和每层过滤。

💬 关联讨论

发起讨论
加载中...