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 |
| span | new_span | 无 | — |
| span | record | 无 | — |
| span | record_follows_from | 无 | — |
| span | enter / exit | 无 | — |
| span | clone_span | 有 | 返回 id.clone(),不计数 |
| span | try_close | 有 | 调已废弃的 drop_span(空函数),返回 false |
| span | current_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 已经构造好了,可以按字段值过滤。代价是被它挡掉的事件已经付过求值字段的开销。
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() / 守卫 drop | enter / 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 收不到调用。
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_callsite4 次:5 个调用点里只有debug!没注册。Dispatch::new时max_level_hint被问了 1 次,返回INFO,于是LevelFilter::current()是INFO;debug!在第二道门level_enabled!就被挡住,根本没走到__CALLSITE.interest(),循环 3 次都是这样。另外 4 个调用点各注册一次,即使循环里的info!被经过 3 次。enabled4 次:全部来自register_callsite里那一次调用。返回的Interest是always,之后宏连第四道门都跳过,不再问enabled。max_level_hint1 次:只在Dispatch::new触发的重算里读了一次;注册单个调用点不重算最大级别。event_enabled4 次、event3 次:循环里 3 条加skip那条都走到了Dispatch::event,skip那条被event_enabled拦下。new_span2、record1、record_follows_from1、enter/exit各 3:和代码里的操作一一对应。clone_span1、try_close3:parent.clone()一次克隆;copy、child、parent三次 drop,各调用一次try_close。Counter永远返回false,对 tracing 没有影响,tracing 不管 span 是否「真的」关了。on_register_dispatch1: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 和每层过滤。