Rust in Everything
基于 tracing 0.1.44

怎么读这一卷

主线位置:两条主线的总览。本页把事件主线和 span 主线各画一遍,标出每一步落在哪个函数上;下一部分的「一条日志穿过 tracing」用一个程序把两条线同时跑一遍。

flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1[01 全景]
  use1 --> use2[03 tracing 门面]
  use2 --> use3["04 #[instrument]"]
  use3 --> use4[05 fmt 与 EnvFilter]
  use4 --> use5[07 常见坑]
  q -->|读懂实现| all[00 到 09 按顺序]

锁定版本

crate版本tagcommit日期
tracing0.1.44tracing-0.1.442d55f6fa2025-12-18
tracing-core0.1.36tracing-core-0.1.3610a9e8382025-12-18
tracing-attributes0.1.31tracing-attributes-0.1.31550862312025-11-26
tracing-subscriber0.3.23tracing-subscriber-0.3.2354ede4d52026-03-13
tracing-log(只在一两节出现)0.2.0tracing-log-0.2.04161d8132023-10-24
tracing-appender(只在常见坑出现)0.2.5tracing-appender-0.2.553e149012026-04-17

源码链接形如 https://github.com/tokio-rs/tracing/blob/tracing-core-0.1.36/tracing-core/src/...#L123:链到哪个 crate 的文件,就用哪个 crate 的 tag。sharded-slab、thread_local、matchers 这些不在仓库里的依赖只写 crate 名和版本。

本站自己的 Cargo.lock 锁的正好是 tracing 0.1.44、tracing-core 0.1.36、tracing-attributes 0.1.31、tracing-subscriber 0.3.23、tracing-log 0.2.0,和本卷相同,「在本站中」一部分不用讲版本差异。

为什么是四个 tag

tokio、hyper、axum 那几卷都是一个 tag 管全卷。tracing 不行:仓库里每个 crate 各自发版,发版时只给发的那个 crate 打 tag,同一个 tag 下的其他 crate 是「当时 main 上的样子」,不一定是发出去的那个版本。

  • tracing-0.1.44 这个 tag 里,tracing-subscriber/Cargo.toml 写的是 version = "0.3.22",tracing-appender/Cargo.toml 是 0.2.4。tracing-subscriber 0.3.23 晚了将近三个月才发(2026-03-13),在 tracing 的 tag 里读不到它。
  • 反过来,tracing-subscriber-0.3.23 这个 tag 里的 tracing-core/ 已经不是 0.1.36 了:git diff tracing-core-0.1.36 tracing-subscriber-0.3.23 -- tracing-core 改了 5 个文件,src/lazy.rs 被删掉,Cargo.toml 里去掉了 once_cell 依赖,callsite.rs、sync.rs 跟着改。这些改动没有随任何 tracing-core 版本发布,你的项目里编译的仍然是 0.1.36。

你的项目里实际编译哪个版本,看依赖要求:tracing 0.1.44 依赖 tracing-core = { ..., version = "0.1.36" },tracing-subscriber 0.3.23 依赖 tracing-core = { ..., version = "0.1.35" },都是 ^ 要求,Cargo 解析到同一个 tracing-core 0.1.36。所以每个 crate 都对着自己的 tag 读,才是你编译的那份代码。

仓库的 main 已经走过了 tag

写作时 main 比 tracing-subscriber-0.3.23 多 19 个提交,比 tracing-0.1.44 多 24 个,比 tracing-core-0.1.36 多 25 个,比 tracing-attributes-0.1.31 多 36 个。其中有直接改本卷要讲的代码的,比如「simplify check for interest before creating events/spans/logging/etc in macros」(#3626)改了宏展开里的判断,「Bump MSRV to 1.71」(#3629)。这些都不在本卷的版本里,读 main 会看到对不上的代码和行号。本卷四个 crate 的 MSRV 都是 1.65.0。

0.1 和没发布的 0.2

tag 下的根 README.md 有一节「Branch set-up」:

- main - Default branch, crates.io releases are done from this branch. This was previously the
  `v0.1.x` branch.
- v0.2.x - Branch containing the as-yet unreleased 0.2 version of `tracing-core`, `tracing`, and
  all the other tracing crates that depend on these versions. This was previously the `master`
  branch.

也就是说,crates.io 上的 tracing 一直是 0.1(tracing-core 0.1.0 在 2019-06-27 发布,到 0.1.36 共 37 个版本),0.2 在 v0.2.x 分支上开发,至今没有发布。v0.2.x 上很多名字都改了:tracing-core 的 Subscriber trait 在那里叫 Collect(tracing-core/src/collect.rs),tracing-subscriber 的 Layer 叫 Subscribe(tracing-subscriber/src/subscribe/)。README 顶部「Documentation (v0.2.x)」那个徽章指向 tracing.rs,那里的文档对应 0.2;对照本卷看文档,要看 docs.rs 上的 0.1。本卷只讲 0.1。

分支的角色在 2025 年 6 月换过一次。#3295(2025-06-05 合并)的说明是:

The new `main` branch is forked from the `v0.1.x` branch. It will be
made the default branch and going forward we will merge PRs to this
branch first (and then forward port to a new `v0.2.x` branch forked from
`master`).

在这之前方向相反:PR 先合进 master(0.2),再移植到 v0.1.x。比如 #2820「tracing: add index API for Field」2024-11-18 合进 master,同名提交 2024-11-22 才出现在 0.1 线上,标题里带的还是原来的 PR 号。本卷的冷知识引用 PR 时,会说明这个改动在 0.1 线上第一次随哪个版本发布。

两条主线

tracing 的工作分成两件:记一条事件(info!(...))和维护 span(info_span!(...) 从创建、进入、退出到关闭)。全卷沿这两条线展开,每页开头都会标出本页在哪条主线上。

事件主线(每次 info!):

flowchart TB
  m["info!:static 调用点"] --> g1["门一:STATIC_MAX_LEVEL"]
  g1 --> g2["门二:LevelFilter::current"]
  g2 --> g3["门三:调用点的 Interest"]
  g3 -->|never| skip[什么也不做]
  g3 -->|always| vs[构造 ValueSet]
  g3 -->|sometimes| g4["门四:Subscriber::enabled"]
  g4 --> vs
  vs --> ed["Event::dispatch"]
  ed --> ee["event_enabled"]
  ee --> lay["Layered:每层 on_event"]
  lay --> fmtl["fmt::Layer 写一行"]
  1. 每个 info! 都经过 event!,在调用点展开成一个 static __CALLSITE(类型是 DefaultCallsite)和它的 'static 元数据:名字、target、级别、文件行号、字段名都在编译期定下来。见宏展开和 Metadata、Level 与 LevelFilter。
  2. 门一、门二都在 level_enabled! 里:先比 STATIC_MAX_LEVEL,它是由 max_level_* / release_max_level_* feature 决定的常量,比不过的分支在编译期就被优化掉;再比 LevelFilter::current(),它读的是全局原子变量 MAX_LEVEL,值是所有 subscriber 的 max_level_hint 里最大的那个。见级别过滤的几道门。
  3. 门三是 DefaultCallsite::interest:读调用点自己缓存的 Interest。第一次经过时还没有缓存,走 register,问每个 dispatcher 的 register_callsite,把结果合并成 never / sometimes / always 存起来。never 到此为止。见 Callsite 与 Interest 缓存。
  4. 门四是 __is_enabled:always 直接放行,sometimes 才通过 dispatcher::get_default 找到当前 subscriber,问它 enabled(meta)。见 Dispatch:全局与线程局部和 Subscriber trait。
  5. 四道门都过了,才对字段表达式求值,构造一个 ValueSet,调用 Event::dispatch。Dispatch::event 先问一次 event_enabled(这时能看到字段值了),再调用 Subscriber::event。见字段和 span 与 Event 的数据。
  6. 在 tracing-subscriber 里,subscriber 通常是一串 Layered。Layered::event 先交给内层,再调用本层的 on_event;fmt::Layer::on_event 把事件格式化进一个线程局部的 String,写进 MakeWriter 给的 writer。见 Layer 与 Layered 和 fmt。

第 3 步的注册只在第一次经过调用点时发生;之后只有新建一个 Dispatch、或者用 reload 换掉一层时,才会遍历所有调用点重算一次 Interest。前三道门每次都要过,但只是一次常量比较、一次原子读、再一次原子读。被级别挡掉、或者 Interest 是 never 的日志开销很小,因为它们停在这里,字段表达式不会求值,也不会去找当前的 subscriber。

span 主线(一个 span 的一生):

flowchart TB
  sm["info_span!:同样的四道门"] -->|没通过| none["空 Span:之后都空操作"]
  sm -->|通过| ns["new_span 分配 Id"]
  ns --> en["enter:压进 span 栈"]
  en --> ev["期间的事件找到当前 span"]
  ev --> ex["守卫 drop:exit 出栈"]
  ex --> dr["clone 与 drop:增减计数"]
  dr --> tc["计数归零:try_close"]
  tc --> oc["所有 layer 的 on_close"]
  oc --> fr["释放 span 的槽"]
  1. span! 也展开成一个 static 调用点,过和事件一样的四道门。没过的话得到一个 inner 为 None 的 Span,之后对它的 enter、record、drop 都不会调用 subscriber(开了 log feature 时仍可能发 log 记录)。见 Span 与 Entered。
  2. 过了门,Span::new 把元数据、字段和父 span 包成 span::Attributes,交给 Subscriber::new_span,拿回一个 span::Id。tracing 自己不存任何 span 数据,Id 由 subscriber 分配:Registry::new_span 从 sharded-slab 的池里取一个槽,idx_to_id 把 sharded-slab 的 key 加一当作 Id(key 里打包了槽地址、线程号和代数)。见 Registry。
  3. Span::enter 调用 subscriber 的 enter,返回守卫 Entered。Registry::enter 把 Id 压进当前线程的 span 栈;这期间的事件没有显式指定父 span,就以栈顶为父。守卫 drop 时调用 exit,Registry::exit 出栈。
  4. Span 的 clone 和 drop 对应 subscriber 的 clone_span 和 try_close。Span 的 Drop 调用 try_close,Registry::try_close 把引用计数减一,只有减到零才返回 true。
  5. Layered::try_close 在调用内层之前先向 Registry 要一个 CloseGuard,内层确认关闭后再调用本层的 on_close。守卫层层嵌套,最外层的守卫 drop 时,所有 layer 的 on_close 都已经跑完,这时才真正释放槽。
  6. 异步代码里 span 不能靠 enter 跨 .await 持有。Instrumented 在每次 poll 时进入 span、poll 返回时退出,被 drop 时也先进入一次。见 Instrument 与异步。

两条线的交点是「当前 span」:事件主线第 6 步里 fmt::Layer 打印的 span 上下文,就是 span 主线第 3 步压进栈的那些 Id。

阅读路线

  • 只想会用:读 01 全景、03 tracing 门面、04 #[instrument]、05 里的 fmt 和 EnvFilter,以及常见坑。读完能写出一个带 EnvFilter 的 fmt subscriber,知道 RUST_LOG 的指令怎么写,知道异步代码里为什么用 .instrument(span) 而不是 span.enter()。
  • 想读懂实现:按顺序读。02 是重点,两条主线上的每一步最后都落到 tracing-core 的类型上:Metadata、调用点、Interest、Dispatch、Subscriber、字段。05 讲 tracing-subscriber 怎么把一个 Subscriber 拆成「存 span 的 Registry」加「一层层的 Layer」;06 把各章的取舍汇总。

本卷的约定

  • 每页开头的「主线位置」说明本页在事件主线、span 主线上的哪一步,还是两条都不在(比如源码地图)。
  • 摘自 tracing 各 crate 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照,删掉的行用 // ... 标出;本站自己写的示例用 2 空格缩进。
  • 关于行为的说法(顺序、次数、哪个回调被调用、打印出什么)尽量配一个小程序和它的真实输出。每个示例上方写出它需要的依赖,版本用 = 锁死,例如 tracing = "=0.1.44"、tracing-subscriber = { version = "=0.3.23", features = ["env-filter"] },只开用到的 feature。实验的平台是 M2 Max、rustc 1.99.0。
  • 想看 subscriber 收到了什么,示例通常自己实现一个打印每个回调的 Subscriber 或 Layer,比读 fmt 的输出更直接;引用 fmt 输出的示例都关掉了 ANSI 颜色。

前置知识

这一卷只要求会写 Rust,知道 trait 对象、static、原子变量和线程局部变量是什么。tracing 不依赖任何运行时,只有异步那一章和几个常见坑会碰到前几卷的内容:

概念讲它的页本卷用到它的地方
Future::poll 与任务poll 与 wakeInstrumented 在每次 poll 前后进出 span
工作线程与任务窃取多线程调度器、窃取与空闲一个任务的几次 poll 可能在不同线程上;span 栈和 set_default 都是线程局部的
tower 的 LayerLayer 与 Stack只是同名:tracing-subscriber 的 Layer 叠在 Subscriber 上,不是包 Service
tower-http 的 TraceLayer其余常用中间件每个请求一个 span,事件和 span 都走本卷讲的路径

tracing 仓库的 examples/ 有 40 个示例程序,tracing、tracing-core、tracing-subscriber 的 crate 文档也都很详细。读源码之前先照着写一个带 span 和字段的小程序,会轻松很多。

目录

部分内容
00 导读本页;源码地图
01 全景tracing 解决什么问题;一条日志穿过 tracing
02 tracing-core本卷重点:Metadata 与 Level、调用点与 Interest、Dispatch、Subscriber trait、字段、span 与 Event
03 tracing 门面宏展开、级别过滤的几道门、Span 与 Entered、Instrument、和 log 互通
04 #[instrument]属性宏展开成什么;它的参数
05 tracing-subscriberLayer 与 Layered、Registry、全局与每层过滤、EnvFilter、fmt、reload
06 设计取舍把各章的取舍汇总到一起
07 常见坑用报错原文或现象做标题,讲它在源码里怎么产生
08 在本站中本站(开源)代码里怎么装 subscriber、怎么用 #[instrument]
09 动手不用 tracing,从零写一个带调用点缓存的最小日志门面

小结

  • 四个 crate 各对着自己的 tag 读:tracing-0.1.44(2d55f6fa)、tracing-core-0.1.36(10a9e838)、tracing-attributes-0.1.31(55086231)、tracing-subscriber-0.3.23(54ede4d5)。别的 tag 里的同名目录不是你编译的那份代码。
  • crates.io 上只有 0.1;0.2 在 v0.2.x 分支,没发布,Subscriber 在那里叫 Collect,Layer 叫 Subscribe。2025 年 6 月起 PR 先合进 main,之前是先合进 master 再移植到 0.1。
  • 事件主线:static 调用点 → STATIC_MAX_LEVEL → LevelFilter::current() → 调用点的 Interest → sometimes 时问 enabled → Event::dispatch → event_enabled → 每层 on_event → fmt::Layer 写一行。
  • span 主线:同样的门 → new_span 分配 Id → enter 压栈、守卫 drop 出栈 → clone / drop 增减计数 → 归零 try_close → 所有 on_close 跑完才释放槽。
  • 只想会用读 01、03、04、05 的 fmt 和 EnvFilter、常见坑;想读懂实现按顺序读,02 是重点。

💬 关联讨论

发起讨论
加载中...