Rust in Everything
基于 tracing-subscriber 0.3.23

EnvFilter

主线位置:事件主线的第二、三、四道门。max_level_hint 决定 LevelFilter::current(),register_callsite 决定调用点缓存的 Interest,enabled 在 sometimes 时每次被问。用了动态指令时它还在 span 主线上:new_span 记下 span 的字段,enter / exit 把 span 对应的级别压进、弹出一个线程局部的栈,事件靠这个栈判断「我在不在某个 span 里」。

flowchart TB
  s["RUST_LOG 字符串"] --> split["按逗号切分"]
  split --> parse["Directive::parse 状态机"]
  parse -->|出错| err["lossy 打印并跳过 / 严格返回 Err"]
  parse --> tables["make_tables"]
  tables --> stat["statics:target 加级别"]
  tables --> dyn["dynamics:带 span 名或字段值"]
  stat --> rc["register_callsite:always 或 never"]
  dyn --> some["其余调用点一律 sometimes"]
  some --> en["enabled:查当前 span 栈"]

EnvFilter 是最常用的过滤器:开了 env-filter feature 时,tracing_subscriber::fmt::try_init()(以及调用它的 fmt::init())就用 EnvFilter::from_default_env() 读 RUST_LOG;fmt() 构造器默认只有一个 LevelFilter,要 with_env_filter(..) 才换成它。它实现了 Layer,也实现了 Filter,所以既能当全局过滤(.with(filter)、fmt().with_env_filter(..)),也能当每层过滤(layer.with_filter(filter)),两者的区别见 过滤:全局与每层。这一页讲它自己:指令怎么解析、存成什么、每道门上怎么回答。代码在 filter/env/ 下,约 2.9k 行,另有和 Targets 共用的 filter/directive.rs。

结构:两张表和三份状态

EnvFilter:

pub struct EnvFilter {
    statics: directive::Statics,
    dynamics: directive::Dynamics,
    has_dynamics: bool,
    by_id: RwLock<HashMap<span::Id, directive::SpanMatcher>>,
    by_cs: RwLock<HashMap<callsite::Identifier, directive::CallsiteMatcher>>,
    scope: ThreadLocal<RefCell<Vec<LevelFilter>>>,
    regex: bool,
}
字段是什么
staticsDirectiveSet<StaticDirective>:只看 target、字段名和级别的指令
dynamicsDirectiveSet<Directive>:带 span 名或字段值的指令
has_dynamicsdynamics 非空
by_cs被动态指令看中的 span 调用点 → 匹配器,register_callsite 时填
by_id这些调用点创建出的 span 实例 → 带字段匹配状态的匹配器,on_new_span 时填
scope每个线程一个栈,存「当前进入的、被看中的 span」允许的级别
regex字段值按正则还是按 Debug 输出原样匹配,默认 true

只用静态指令时,后四个字段都是空的,EnvFilter 就是一张按 target 查级别的表,和 Targets 差不多。

DirectiveSet 是一个按「具体程度」排序的 SmallVec(add 用二分查找插入),另记一个 max_level,是所有指令里最宽的级别。排序先比 target 长度(没有 target 的最不具体),再比字段个数,Directive 还在两者之间比有没有 span 名。查询时取第一条「关心」这个元数据的指令,用它的级别作答,见 DirectiveSet::enabled:

    pub(crate) fn enabled(&self, meta: &Metadata<'_>) -> bool {
        let level = meta.level();
        match self.directives_for(meta).next() {
            Some(d) => d.level >= *level,
            None => false,
        }
    }

一条都不关心就是 false,所以没有裸级别指令(如 info)兜底时,没提到的 target 全部关闭。

「关心」的判断在 StaticDirective::cares_about,target 用的是 starts_with:

        if let Some(ref target) = self.target {
            if !meta.target().starts_with(&target[..]) {
                return false;
            }
        }

字符串前缀,不按 :: 分段。验证一下前缀匹配和「最具体的优先」:

tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", features = ["env-filter"] }
use tracing_subscriber::EnvFilter;

fn main() {
  tracing_subscriber::fmt()
    .with_env_filter(EnvFilter::new("warn,my_app=debug,my_app::db=error"))
    .with_ansi(false)
    .without_time()
    .init();
  tracing::debug!(target: "my_app", "1");
  tracing::debug!(target: "my_app::http", "2");
  tracing::debug!(target: "my_app_extra", "3");
  tracing::warn!(target: "my_app::db", "4");
  tracing::error!(target: "my_app::db", "5");
  tracing::debug!(target: "other", "6");
  tracing::warn!(target: "other", "7");
}

M2 Max、rustc 1.99.0、--release,跑 3 次,输出相同:

DEBUG my_app: 1
DEBUG my_app::http: 2
DEBUG my_app_extra: 3
ERROR my_app::db: 5
 WARN other: 7

my_app=debug 也放行了 my_app_extra;my_app::db 的 WARN 被更具体的 my_app::db=error 挡住,虽然更宽的 my_app=debug 和 warn 都会放行它。指令的先后顺序无关,只看具体程度。

my-app 不是 my_app

事件的 target 默认是 module_path!()(Metadata、Level 与 LevelFilter),模块路径里 crate 名的 - 都变成了 _。一个名为 my-app 的二进制(src/bin/my-app.rs):

use tracing_subscriber::EnvFilter;

fn main() {
  tracing_subscriber::fmt()
    .with_env_filter(EnvFilter::from_default_env())
    .with_ansi(false)
    .without_time()
    .init();
  println!("module_path!() = {}", module_path!());
  tracing::error!("error event");
  tracing::debug!("debug event");
}

跑 3 次,输出相同:

$ RUST_LOG=my-app=debug ./my-app
module_path!() = my_app
$ RUST_LOG=my_app=debug ./my-app
module_path!() = my_app
ERROR my_app: error event
DEBUG my_app: debug event

my-app=debug 是一条合法指令(解析器允许 target 里有 -,下一节的表里能看到),只是 "my_app".starts_with("my-app") 为假,它不关心任何事件。连 ERROR 都没有打印,是因为下面「默认指令」一节的规则:RUST_LOG 里有了一条指令,from_default_env 的默认 ERROR 就不再加上。EnvFilter 的文档也写了这一条:「When a crate has a dash in its name, the default target for events will be the crate's module path as it appears in Rust. This means every dash will be replaced with an underscore.」

指令语法

文档给出的完整形式是:

target[span{field=value}]=level

每一部分都可省略。RUST_LOG 的值先在 Builder::parse 里按 , 切开、去掉空段,每段交给 Directive::parse。后者是一个逐字符的状态机:

        let mut state = Start;
        for (i, c) in from.trim().char_indices() {
            state = match (state, c) {
                (Start, '[') => Span { span_start: i + 1 },
                (Start, c) if !['-', ':', '_'].contains(&c) && !c.is_alphanumeric() => {
                    return Err(ParseError::new())
                }
                (Start, _) => LevelOrTarget { start: i },
                (LevelOrTarget { start }, '=') => {
                    cur.target = Some(from[start..i].to_owned());
                    Level { level_start: i + 1 }
                }
                (LevelOrTarget { start }, '[') => {
                    cur.target = Some(from[start..i].to_owned());
                    Span { span_start: i + 1 }
                }
                // ...
                (Span { span_start }, ']') => {
                    cur.in_span = Some(from[span_start..i].to_owned());
                    Target
                }
                (Span { span_start }, '{') => {
                    // ...
                    Field { field_start: i + 1 }
                }
                // ...
                (Field { field_start }, '}') => {
                    cur.fields.push(match &from[field_start..i] {
                        "" => return Err(ParseError::new()),
                        field => field::Match::parse(field, regex)?,
                    });
                    Fields
                }
                // ...
                _ => return Err(ParseError::new()),
            };
        }

没有 = 的单段,能解析成级别就是裸级别(作用于所有 target),否则是一个 target,级别为 TRACE。级别字符串交给 LevelFilter::from_str,大小写不敏感,也接受 0 到 5。

用 EnvFilter::try_new(严格解析,见下一节)逐个试,成功时打印 Display(它把指令按内部顺序写回来):

use tracing_subscriber::EnvFilter;

fn main() {
  let cases = [
    "info",
    "my_app=debug",
    "my-app=debug",
    "tokio::net",
    "warn,tokio::net=info",
    "[req]=debug",
    "my_app[req]=trace",
    "[{user}]=debug",
    "[req{id=42}]=debug",
    "[req{user=bob}]",
    "my_app[req{a=1,b=2}]=debug",
    "info, my_app=debug",
    "info, my_app",
    "my_app=verbose",
    "my_app=info=debug",
    "=info",
    "my_app[{}]=info",
    "[req{user=(}]=debug",
    "my_app[req",
  ];
  for s in cases {
    match EnvFilter::try_new(s) {
      Ok(f) => println!("{s:<28} Ok   {f}"),
      Err(e) => println!("{s:<28} Err  {e}"),
    }
  }
  // 不经过按逗号切分,直接解析一条 Directive
  let d: tracing_subscriber::filter::Directive = "my_app[req{a=1,b=2}]=debug".parse().unwrap();
  println!("Directive::from_str + add_directive: {}", EnvFilter::default().add_directive(d));
}

跑 3 次,输出相同:

info                         Ok   info
my_app=debug                 Ok   my_app=debug
my-app=debug                 Ok   my-app=debug
tokio::net                   Ok   tokio::net=trace
warn,tokio::net=info         Ok   tokio::net=info,warn
[req]=debug                  Ok   [req]=debug
my_app[req]=trace            Ok   my_app[req]=trace
[{user}]=debug               Ok   [{user}]=debug,[{user}]=debug
[req{id=42}]=debug           Ok   [req{id=42}]=debug
[req{user=bob}]              Ok   [req{user=bob}]=trace
my_app[req{a=1,b=2}]=debug   Err  invalid filter directive
info, my_app=debug           Err  error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
info, my_app                 Ok    my_app=trace,info
my_app=verbose               Err  error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
my_app=info=debug            Err  error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
=info                        Err  invalid filter directive
my_app[{}]=info              Err  invalid filter directive
[req{user=(}]=debug          Err  invalid field filter: error building NFA
Directive::from_str + add_directive: my_app[req{a=1,b=2}]=debug

逐行看:

输入结果原因
tokio::nettokio::net=trace只写 target,级别是 TRACE
warn,tokio::net=info顺序变了Display 按具体程度输出,有 target 的在前
[req]=debug动态指令任何 target 下、名为 req 的 span 里面
[{user}]=debug出现两次只有字段名、没有值的指令同时进了两张表,下一节讲
[req{user=bob}]=trace省略级别等于 trace
my_app[req{a=1,b=2}]=debug解析失败状态机能处理字段间的逗号,但 Builder::parse 先按逗号把它切成了 my_app[req{a=1 和 b=2}]=debug 两段;同一条指令用 Directive::from_str 加 add_directive 就能成功(最后一行)
info, my_app=debug级别解析失败逗号后的空格。parse 对 from.trim() 取字符下标,却拿这些下标去切没 trim 过的 from,整体错开一位:target 成了 my_ap,级别部分成了 =debug
info, my_app成功,但 target 带空格同上,没有 = 时走另一个分支,target 是 my_app(Display 里能看到前面的空格),它永远匹配不上
my_app=info=debug级别解析失败第一个 = 之后的全部 info=debug 被当作级别
=info失败第一个字符必须是字母、数字、-、:、_ 或 [
my_app[{}]=info失败空字段名
[req{user=(}]=debug失败默认按正则解析字段值,( 不是合法正则

RUST_LOG="info, my_app=debug" 这种带空格的写法,在严格解析下整个失败,在 lossy 解析下只丢掉带空格的那一段(下一节)。

📝NOTE
冷知识:解析指令曾经要先编译三个正则

tracing-subscriber 0.3.19 及以前,Directive::parse 用三个 Lazy<Regex> 解析指令,env-filter feature 依赖 regex crate。issue #3174 报告 EnvFilter::builder().with_regex(false).parse("debug") 在 release 构建下也要约 600µs,「It seems to me that lazy-compiling the regex used to parse the directives is where the time is spent」。PR #3243 换成了上面的状态机,提交说明是「This change replaces the regex based parsing of the directives with a state machine implementation that is faster and also easier to reason about.」同一个提交把 regex 从 env-filter 的依赖里去掉,留下一个空的 regex = [] feature,注释「For backwards compatibility only」。随 tracing-subscriber 0.3.20 发布。字段值的正则匹配仍然需要正则引擎,现在由 matchers 0.2 和 regex-automata 0.4 提供。下面「开销」一节测了现在的解析耗时。

构造:坏指令和默认指令

几种构造函数的差别在两件事上:遇到坏指令怎么办;一条指令都没有时用什么。所有路径最后都汇到 Builder::from_directives,它的结尾:

        let (dynamics, statics) = Directive::make_tables(directives);
        let has_dynamics = !dynamics.is_empty();

        let mut filter = EnvFilter {
            // ...
        };

        if !has_dynamics && filter.statics.is_empty() {
            if let Some(ref default) = self.default_directive {
                filter = filter.add_directive(default.clone());
            }
        }

        filter

默认指令只在一条指令都没有时才加。 它不是「其余 target 的兜底级别」:RUST_LOG=my_app=debug 时默认指令不生效,my_app 以外的全部关闭。要兜底,得在 RUST_LOG 里自己写一个裸级别,如 RUST_LOG=info,my_app=debug。

坏指令的处理:lossy 的 parse_lossy 用 eprintln! 打印「ignoring ..: 原因」后跳过这一段,其余照用;严格的 parse 遇到第一个错误就返回 Err。读环境变量时,from_env_lossy 和 from_env 把「变量不存在」当作空字符串,只有 try_from_env 把它当作错误。EnvFilter 上的快捷函数是这些的组合:

函数等价于坏指令没有指令时
from_default_env()builder().with_default_directive(ERROR).from_env_lossy()打印并跳过error
from_env(name)同上,换变量名打印并跳过error
new(s)builder().with_default_directive(ERROR).parse_lossy(s)打印并跳过error
try_new(s)builder().parse(s)Err空,什么都不放行
try_from_default_env()builder().try_from_env()Err变量不存在是 Err;空字符串是空过滤器

builder() 默认没有默认指令,所以 try_* 系列遇到空字符串得到的是一个空的 EnvFilter,max_level_hint 是 OFF。实测各函数在不同 RUST_LOG 下的结果(打印的是 Display,ignoring 那几行来自 stderr):

use std::io::Write;

use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::LevelFilter;

fn set(v: Option<&str>) {
  // edition 2024 里改环境变量是 unsafe;这里是单线程
  unsafe {
    match v {
      Some(v) => std::env::set_var("RUST_LOG", v),
      None => std::env::remove_var("RUST_LOG"),
    }
  }
}

fn line(s: String) {
  // stdout 和 stderr 交错打印,先刷 stdout 让顺序可读
  println!("{s}");
  std::io::stdout().flush().unwrap();
}

fn main() {
  for v in [None, Some(""), Some("my_app=debug"), Some("my_app=debug,oops=loud"), Some("oops=loud")] {
    set(v);
    line(format!("== RUST_LOG={v:?}"));
    line(format!("  from_default_env()        -> {}", EnvFilter::from_default_env()));
    match EnvFilter::try_from_default_env() {
      Ok(f) => line(format!("  try_from_default_env()    -> Ok({f})")),
      Err(e) => line(format!("  try_from_default_env()    -> Err({e})")),
    }
    let f = EnvFilter::builder()
      .with_default_directive(LevelFilter::INFO.into())
      .from_env_lossy();
    line(format!("  builder(default INFO).from_env_lossy() -> {f}"));
    match EnvFilter::builder()
      .with_default_directive(LevelFilter::INFO.into())
      .from_env()
    {
      Ok(f) => line(format!("  builder(default INFO).from_env()       -> Ok({f})")),
      Err(e) => line(format!("  builder(default INFO).from_env()       -> Err({e})")),
    }
  }
}

运行时 2>&1,跑 3 次,输出相同:

== RUST_LOG=None
  from_default_env()        -> error
  try_from_default_env()    -> Err(environment variable not found)
  builder(default INFO).from_env_lossy() -> info
  builder(default INFO).from_env()       -> Ok(info)
== RUST_LOG=Some("")
  from_default_env()        -> error
  try_from_default_env()    -> Ok()
  builder(default INFO).from_env_lossy() -> info
  builder(default INFO).from_env()       -> Ok(info)
== RUST_LOG=Some("my_app=debug")
  from_default_env()        -> my_app=debug
  try_from_default_env()    -> Ok(my_app=debug)
  builder(default INFO).from_env_lossy() -> my_app=debug
  builder(default INFO).from_env()       -> Ok(my_app=debug)
== RUST_LOG=Some("my_app=debug,oops=loud")
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  from_default_env()        -> my_app=debug
  try_from_default_env()    -> Err(error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5)
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  builder(default INFO).from_env_lossy() -> my_app=debug
  builder(default INFO).from_env()       -> Err(error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5)
== RUST_LOG=Some("oops=loud")
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  from_default_env()        -> error
  try_from_default_env()    -> Err(error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5)
ignoring `oops=loud`: error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5
  builder(default INFO).from_env_lossy() -> info
  builder(default INFO).from_env()       -> Err(error parsing level filter: expected one of "off", "error", "warn", "info", "debug", "trace", or a number 0-5)
  • RUST_LOG="" 下 try_from_default_env() 返回 Ok():一个空过滤器,不是错误。
  • my_app=debug,oops=loud:lossy 版本丢掉坏的一段,留下 my_app=debug;默认指令仍然不加,因为还剩一条。
  • oops=loud:唯一的一段被丢掉后一条都不剩,lossy 版本退回默认指令(error 或 info)。
  • 错误信息只说级别解析失败,不说是哪一段;lossy 版本打印的 ignoring 行带着原文。

from_directives 还有一项检查:如果某条指令的级别比编译期的 STATIC_MAX_LEVEL 还宽(比如开了 tracing 的 max_level_info feature,RUST_LOG 却写了 debug),它在 stderr 打印一段「some trace filter directives would enable traces that are disabled statically」警告,并提示去掉哪个 feature(提示按 STATIC_MAX_LEVEL 查一张写死的表,只会说 max_level_*,即使真正的原因是 release_max_level_*,见 release 构建里 debug! 全部消失)。过滤器照常构造,只是那些级别的日志在编译时就没了,见 级别过滤的几道门。

静态指令与动态指令

Directive 有四个部分:in_span、fields、target、level。它是静态还是动态,由 is_static 和 is_dynamic 判断:

    fn is_static(&self) -> bool {
        !self.has_name() && !self.fields.iter().any(field::Match::has_value)
    }

    pub(super) fn is_dynamic(&self) -> bool {
        self.has_name() || self.has_fields()
    }

两个判断不是互补的:只有字段名、没有值的指令([{user}]=debug)既是静态的(没有 span 名、没有值),又是动态的(有字段)。make_tables 先按 is_dynamic 分成两组,再把动态组里能转成静态的也加进 statics,所以它在两张表里各有一份,上面 Display 打印了两次。(add_directive 走的是另一条路:能 to_static 就只进 statics。)

静态指令只看元数据,在 register_callsite 时就能给出确定答案。动态指令看的是「事件是不是在某个 span 里」或「span 的某个字段值是多少」,调用点注册时不知道,只能每次问。register_callsite:

    fn base_interest(&self) -> Interest {
        if self.has_dynamics {
            Interest::sometimes()
        } else {
            Interest::never()
        }
    }

    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
        if self.has_dynamics && metadata.is_span() {
            // If this metadata describes a span, first, check if there is a
            // dynamic filter that should be constructed for it. If so, it
            // should always be enabled, since it influences filtering.
            if let Some(matcher) = self.dynamics.matcher(metadata) {
                let mut by_cs = try_lock!(self.by_cs.write(), else return self.base_interest());
                by_cs.insert(metadata.callsite(), matcher);
                return Interest::always();
            }
        }

        // Otherwise, check if any of our static filters enable this metadata.
        if self.statics.enabled(metadata) {
            Interest::always()
        } else {
            self.base_interest()
        }
    }

三种结果:

调用点Interest
span,且有动态指令关心它(target、span 名、字段名都对得上)always,并在 by_cs 里登记匹配器
静态指令放行always
其余没有动态指令时 never;只要有一条动态指令,就是 sometimes

最后一行是动态指令的主要代价:[req]=debug 这一条,会让所有没被静态指令放行的调用点都变成 sometimes,不管它们离 req 多远。Callsite 与 Interest 缓存 讲过 sometimes 意味着每次经过都要问 enabled。

被看中的 span 一定是 always:它决定了里面的事件放不放行,EnvFilter 必须收到它的 new_span、enter、exit。之后:

  • on_new_span:调用点在 by_cs 里,就用 span 的字段值建一个 SpanMatcher 存进 by_id。
  • on_enter:span 在 by_id 里,就把它此刻允许的级别压进本线程的 scope 栈;on_exit 弹出,on_close 从 by_id 删掉,on_record 用新的字段值更新匹配状态。

enabled 先看动态,再看静态:

    pub fn enabled<S>(&self, metadata: &Metadata<'_>, _: Context<'_, S>) -> bool {
        let level = metadata.level();

        // is it possible for a dynamic filter directive to enable this event?
        // if not, we can avoid the thread local access + iterating over the
        // spans in the current scope.
        if self.has_dynamics && self.dynamics.max_level >= *level {
            if metadata.is_span() {
                // If the metadata is a span, see if we care about its callsite.
                let enabled_by_cs = self
                    .by_cs
                    .read()
                    .ok()
                    .map(|by_cs| by_cs.contains_key(&metadata.callsite()))
                    .unwrap_or(false);
                if enabled_by_cs {
                    return true;
                }
            }

            let enabled_by_scope = {
                let scope = self.scope.get_or_default().borrow();
                for filter in &*scope {
                    if filter >= level {
                        return true;
                    }
                }
                false
            };
            if enabled_by_scope {
                return true;
            }
        }

        // is it possible for a static filter directive to enable this event?
        if self.statics.max_level >= *level {
            // Otherwise, fall back to checking if the callsite is
            // statically enabled.
            return self.statics.enabled(metadata);
        }

        false
    }

「在 span 里」靠的是 scope 栈,它由 on_enter / on_exit 维护,所以只对进入了的 span 有效:创建了 span 但没有 enter,或者用 parent: 显式指定父 span 而当前线程并不在它里面,动态指令都看不到。dynamics.max_level 是一个便宜的预检:事件比所有动态指令都啰嗦时,连线程局部变量都不碰。

实测四种过滤器下,四个调用点各自的 Interest、enabled 次数和送达次数。把 EnvFilter 包在一个转发调用、顺便计数的 layer 里:

use std::collections::BTreeMap;
use std::sync::Mutex;

use tracing::span::{Attributes, Id, Record};
use tracing::subscriber::Interest;
use tracing::{Event, Metadata, Subscriber};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, Registry};

// 每个调用点(按行号)记三样:注册时的 Interest、enabled 次数、真正送达的次数
#[derive(Default)]
struct Row {
  interest: String,
  enabled: usize,
  delivered: usize,
}

static ROWS: Mutex<BTreeMap<u32, Row>> = Mutex::new(BTreeMap::new());

fn row(meta: &Metadata<'_>, f: impl FnOnce(&mut Row)) {
  f(ROWS.lock().unwrap().entry(meta.line().unwrap()).or_default());
}

// 包住 EnvFilter,把调用原样转过去,顺便记数
struct Counted(EnvFilter);

impl<S: Subscriber> Layer<S> for Counted {
  fn register_callsite(&self, meta: &'static Metadata<'static>) -> Interest {
    let i = Layer::<S>::register_callsite(&self.0, meta);
    row(meta, |r| r.interest = format!("{i:?}"));
    i
  }
  fn enabled(&self, meta: &Metadata<'_>, cx: Context<'_, S>) -> bool {
    row(meta, |r| r.enabled += 1);
    Layer::enabled(&self.0, meta, cx)
  }
  fn max_level_hint(&self) -> Option<LevelFilter> {
    Layer::<S>::max_level_hint(&self.0)
  }
  fn on_new_span(&self, a: &Attributes<'_>, id: &Id, cx: Context<'_, S>) {
    Layer::on_new_span(&self.0, a, id, cx)
  }
  fn on_record(&self, id: &Id, v: &Record<'_>, cx: Context<'_, S>) {
    Layer::on_record(&self.0, id, v, cx)
  }
  fn on_enter(&self, id: &Id, cx: Context<'_, S>) {
    Layer::on_enter(&self.0, id, cx)
  }
  fn on_exit(&self, id: &Id, cx: Context<'_, S>) {
    Layer::on_exit(&self.0, id, cx)
  }
  fn on_close(&self, id: Id, cx: Context<'_, S>) {
    Layer::on_close(&self.0, id, cx)
  }
}

// 最内层的记录者:数送达的事件和 span
struct Sink;

impl<S: Subscriber> Layer<S> for Sink {
  fn on_event(&self, e: &Event<'_>, _: Context<'_, S>) {
    row(e.metadata(), |r| r.delivered += 1);
  }
  fn on_new_span(&self, a: &Attributes<'_>, _: &Id, _: Context<'_, S>) {
    row(a.metadata(), |r| r.delivered += 1);
  }
}

fn work() {
  tracing::info!("info outside");
  tracing::debug!("debug outside");
  let span = tracing::info_span!("req", user = "bob");
  let _g = span.enter();
  tracing::debug!("debug inside");
}

fn main() {
  let names = [(72, "info! 外"), (73, "debug! 外"), (74, "info_span!(req)"), (76, "debug! 内")];
  for spec in [
    "info",
    "info,my_app::db=debug",
    "info,[req]=debug",
    "info,[req{user=bob}]=debug",
  ] {
    ROWS.lock().unwrap().clear();
    let sub = Registry::default().with(Sink).with(Counted(EnvFilter::new(spec)));
    tracing::subscriber::with_default(sub, || {
      for _ in 0..1000 {
        work();
      }
      println!("== {spec}   LevelFilter::current() = {}", LevelFilter::current());
    });
    let rows = ROWS.lock().unwrap();
    for (line, name) in names {
      match rows.get(&line) {
        Some(r) => println!(
          "  {name:<16} interest={:<20} enabled={:<5} delivered={}",
          r.interest, r.enabled, r.delivered
        ),
        None => println!("  {name:<16} (没有注册)"),
      }
    }
  }
}

(names 里的数字是 work 里四个宏所在的行号。)跑 3 次,输出相同:

== info   LevelFilter::current() = info
  info! 外          interest=Interest(Always)     enabled=0     delivered=1000
  debug! 外         (没有注册)
  info_span!(req)  interest=Interest(Always)     enabled=0     delivered=1000
  debug! 内         (没有注册)
== info,my_app::db=debug   LevelFilter::current() = debug
  info! 外          interest=Interest(Always)     enabled=0     delivered=1000
  debug! 外         interest=Interest(Never)      enabled=0     delivered=0
  info_span!(req)  interest=Interest(Always)     enabled=0     delivered=1000
  debug! 内         interest=Interest(Never)      enabled=0     delivered=0
== info,[req]=debug   LevelFilter::current() = debug
  info! 外          interest=Interest(Always)     enabled=0     delivered=1000
  debug! 外         interest=Interest(Sometimes)  enabled=1000  delivered=0
  info_span!(req)  interest=Interest(Always)     enabled=0     delivered=1000
  debug! 内         interest=Interest(Sometimes)  enabled=1000  delivered=1000
== info,[req{user=bob}]=debug   LevelFilter::current() = trace
  info! 外          interest=Interest(Always)     enabled=0     delivered=1000
  debug! 外         interest=Interest(Sometimes)  enabled=1000  delivered=0
  info_span!(req)  interest=Interest(Always)     enabled=0     delivered=1000
  debug! 内         interest=Interest(Sometimes)  enabled=1000  delivered=1000
  • info:两个 debug! 连注册都没有,LevelFilter::current() 是 info,第二道门就挡住了。
  • info,my_app::db=debug:上限升到 debug,两个 debug! 走到注册,target 不匹配,缓存成 never,之后每次只读一个字节。
  • info,[req]=debug:两个 debug! 都是 sometimes,每次都问 enabled;req 里面的放行,外面的不放行。区别全在运行时。
  • [req{user=bob}]=debug:结果和上一组相同,但 LevelFilter::current() 变成了 trace,原因在下面的 max_level_hint。

字段值匹配

字段部分 {name=value} 由 field::Match::parse 解析,值按顺序尝试,见 ValueMatch::parse_regex:

    fn parse_regex(s: &str) -> Result<Self, matchers::BuildError> {
        s.parse::<bool>()
            .map(ValueMatch::Bool)
            .or_else(|_| s.parse::<u64>().map(ValueMatch::U64))
            .or_else(|_| s.parse::<i64>().map(ValueMatch::I64))
            .or_else(|_| s.parse::<f64>().map(value_match_f64))
            .or_else(|_| {
                s.parse::<MatchPattern>()
                    .map(|p| ValueMatch::Pat(Box::new(p)))
            })
    }

bool、u64、i64、f64 都不是,才当作正则(MatchPattern,内部是 matchers 0.2 的 Pattern,即 regex-automata 0.4 的稠密 DFA)。Builder::with_regex(false) 时最后一步换成 parse_non_regex 的 ValueMatch::Debug:拿值的 Debug 输出和字符串原样比较。

匹配发生在 span 记录字段时。SpanMatcher 实现了 Visit,按值的类型走不同的 record_*:数字和 bool 只和同类型的 ValueMatch 比(record_i64 也接受 U64 匹配器,f64 用 f64::EPSILON 判等);字符串走 record_str,正则直接匹配字符串本身,Debug 匹配器则比较 Debug 输出(带引号);其余类型走 record_debug,两种匹配器都比较 Debug 输出。

一个带六种字段的 span,逐条试指令,看里面的 debug! 是否放行:

use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

use tracing::{Event, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, Registry};

static HITS: AtomicUsize = AtomicUsize::new(0);

struct Sink;
impl<S: Subscriber> Layer<S> for Sink {
  fn on_event(&self, _: &Event<'_>, _: Context<'_, S>) {
    HITS.fetch_add(1, Relaxed);
  }
}

#[derive(Debug)]
#[allow(dead_code)]
struct User {
  name: &'static str,
}

fn work() {
  let span = tracing::info_span!(
    "req",
    user = "bob",
    id = 42u64,
    delta = -3i64,
    ok = true,
    ratio = 0.5,
    who = ?User { name: "bob" },
  );
  let _g = span.enter();
  tracing::debug!("inside");
}

fn event_only() {
  tracing::debug!(user = "bob", "no span");
}

fn check(spec: &str, regex: bool, f: fn()) {
  HITS.store(0, Relaxed);
  let filter = EnvFilter::builder().with_regex(regex).parse(spec).unwrap();
  tracing::subscriber::with_default(Registry::default().with(Sink).with(filter), f);
  let hit = if HITS.load(Relaxed) > 0 { "命中" } else { "不中" };
  println!("regex={regex:<5} {spec:<36} {hit}");
}

fn main() {
  for (spec, regex) in [
    ("[req{user=bob}]=debug", true),
    ("[req{user=bo}]=debug", true),
    ("[req{user=ob}]=debug", true),
    ("[req{user=.*bo.*}]=debug", true),
    ("[req{user=^b.b$}]=debug", true),
    ("[req{user=\"bob\"}]=debug", true),
    ("[req{user=bob}]=debug", false),
    ("[req{user=\"bob\"}]=debug", false),
    ("[req{user=bobby}]=debug", false),
    ("[req{user=\"bob\"xyz}]=debug", false),
    ("[req{id=42}]=debug", true),
    ("[req{id=4}]=debug", true),
    ("[req{delta=-3}]=debug", true),
    ("[req{ok=true}]=debug", true),
    ("[req{ratio=0.5}]=debug", true),
    ("[req{who=bob}]=debug", true),
    ("[req{who=.*bob.*}]=debug", true),
    ("[req{who=User}]=debug", false),
  ] {
    check(spec, regex, work);
  }
  println!("-- 没有 span,字段在事件上");
  check("[{user=bob}]=debug", true, event_only);
  check("[{user}]=debug", true, event_only);
}

跑 3 次,输出相同:

regex=true  [req{user=bob}]=debug                命中
regex=true  [req{user=bo}]=debug                 不中
regex=true  [req{user=ob}]=debug                 命中
regex=true  [req{user=.*bo.*}]=debug             命中
regex=true  [req{user=^b.b$}]=debug              命中
regex=true  [req{user="bob"}]=debug              不中
regex=false [req{user=bob}]=debug                不中
regex=false [req{user="bob"}]=debug              命中
regex=false [req{user=bobby}]=debug              不中
regex=false [req{user="bob"xyz}]=debug           命中
regex=true  [req{id=42}]=debug                   命中
regex=true  [req{id=4}]=debug                    不中
regex=true  [req{delta=-3}]=debug                命中
regex=true  [req{ok=true}]=debug                 命中
regex=true  [req{ratio=0.5}]=debug               命中
regex=true  [req{who=bob}]=debug                 不中
regex=true  [req{who=.*bob.*}]=debug             命中
regex=false [req{who=User}]=debug                不中
-- 没有 span,字段在事件上
regex=true  [{user=bob}]=debug                   不中
regex=true  [{user}]=debug                       命中

几处容易想错的:

  • 正则不是「包含」。 bo 不中、ob 命中、.*bo.* 命中:从结果看,开头不锚定、结尾锚定,值要以一段匹配结束。matchers 的 Pattern::new 用 Anchored::No 建 DFA,matches 把字节全部喂完后才检查是否处于匹配状态。要「包含」就写 .*x.*,要整串相等就写 ^x$。
  • 字符串的引号。 正则模式下 record_str 匹配的是字符串本身,写 bob;with_regex(false) 时比较 Debug 输出,得写 "bob"。
  • with_regex(false) 只检查前缀。 MatchDebug::debug_matches 实现了一个 fmt::Write,每写一段就和模式的剩余部分比较前缀、不符就报错,但写完之后不检查模式是否用完。所以 Debug 输出比模式长时不中(User),输出是模式的前缀时却命中("bob"xyz)。
  • Debug 输出的结构体。 who 的 Debug 输出是 User { name: "bob" },正则 bob 不在结尾,不中。值里含 } 或 , 的模式写不进 RUST_LOG:前者会提前结束字段列表,后者会被 Builder::parse 切开。
  • 数字精确比较。 id=4 不会命中 42;-3 解析成 I64。

文档对正则有一句提醒:「When filters are constructed from potentially untrusted inputs, disabling regular expression matching is strongly recommended.」正则在构造过滤器时编译成 DFA,模式越复杂,构造越慢、占用越大。

📝NOTE
没有蠢问题

问: 最后两行,[{user=bob}]=debug 对 debug!(user = "bob", ..) 不起作用,[{user}]=debug 却起作用,为什么?

答: 带值的指令只对 span 生效。字段值的匹配状态存在 SpanMatcher 里,它只在 on_new_span 时、为被 by_cs 登记过的 span 调用点创建;事件的 enabled 只查 scope 栈和 statics,从来不看事件自己的字段值。只有字段名的指令能转成 StaticDirective,后者对事件检查「元数据里有没有这个字段名」(cares_about 里的 meta.is_event() && !self.field_names.is_empty()),这一步只看元数据,所以对事件有效。要按事件的字段值过滤,得用每层过滤的 Filter::event_enabled 或 Layer::event_enabled 自己写。

max_level_hint

max_level_hint:

    pub fn max_level_hint(&self) -> Option<LevelFilter> {
        if self.dynamics.has_value_filters() {
            // If we perform any filtering on span field *values*, we will
            // enable *all* spans, because their field values are not known
            // until recording.
            return Some(LevelFilter::TRACE);
        }
        std::cmp::max(
            self.statics.max_level.into(),
            self.dynamics.max_level.into(),
        )
    }

平时是两张表里最宽的级别。只要有一条带字段值的指令,就直接报 TRACE:带值的指令要看 span 的字段值才能判断,任何级别的 span 都可能是它要找的那个,所以第二道门不能替它挡任何东西。过滤:全局与每层 的「谁给了第二道门提示」实测过:EnvFilter::new("info") 是 info,new("info,[req]=debug") 是 debug,new("info,[{id=1}]=debug") 是 trace,new("") 落到默认指令是 error,try_new("") 是空表的 off。

这个值在 Dispatch::new 和 rebuild_interest_cache 时被读走(Callsite 与 Interest 缓存)。运行时换过滤器要经过 reload,它会负责重建缓存。

动态指令的开销

只有 Registry 和 EnvFilter、没有输出层,量过滤本身的开销:

use std::hint::black_box;
use std::time::Instant;

use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, Registry};

const N: u32 = 10_000_000;

fn per_op(f: impl Fn()) -> f64 {
  for _ in 0..N / 10 {
    f(); // 预热,也让调用点先注册好
  }
  let t = Instant::now();
  for _ in 0..N {
    f();
  }
  t.elapsed().as_nanos() as f64 / N as f64
}

fn bench(spec: &str) {
  // 只有 Registry 和 EnvFilter,没有输出层:量的是过滤本身
  let sub = Registry::default().with(EnvFilter::new(spec));
  tracing::subscriber::with_default(sub, || {
    let outside = per_op(|| tracing::debug!(n = black_box(1), "debug"));
    let span = tracing::info_span!("req");
    let enter_exit = per_op(|| {
      let _g = span.enter();
    });
    let _g = span.enter();
    let inside = per_op(|| tracing::debug!(n = black_box(1), "debug"));
    println!(
      "{spec:<20} debug!外 {outside:>6.2} ns   enter+exit {enter_exit:>6.2} ns   debug!内 {inside:>6.2} ns"
    );
  });
}

fn parse_cost(spec: &str) {
  let t = Instant::now();
  let f = EnvFilter::try_new(spec).unwrap();
  let first = t.elapsed();
  black_box(f);
  let n = 1000;
  let t = Instant::now();
  for _ in 0..n {
    black_box(EnvFilter::try_new(spec).unwrap());
  }
  let avg = t.elapsed() / n;
  println!("try_new({spec:?}): 第一次 {first:?},之后平均 {avg:?}");
}

fn main() {
  bench("info");
  bench("info,[req]=debug");
  parse_cost("debug");
  parse_cost("info,[req{user=bob}]=debug");
}

M2 Max、rustc 1.99.0、--release,跑 3 次:

info                 debug!外   0.34 ns   enter+exit  30.98 ns   debug!内   0.33 ns
info,[req]=debug     debug!外   7.24 ns   enter+exit  33.01 ns   debug!内  13.01 ns
try_new("debug"): 第一次 3.708µs,之后平均 270ns
try_new("info,[req{user=bob}]=debug"): 第一次 1.651291ms,之后平均 8.738µs

info                 debug!外   0.29 ns   enter+exit  30.57 ns   debug!内   0.33 ns
info,[req]=debug     debug!外   7.20 ns   enter+exit  32.54 ns   debug!内  13.08 ns
try_new("debug"): 第一次 3.917µs,之后平均 257ns
try_new("info,[req{user=bob}]=debug"): 第一次 58.416µs,之后平均 8.981µs

info                 debug!外   0.32 ns   enter+exit  30.56 ns   debug!内   0.30 ns
info,[req]=debug     debug!外   7.29 ns   enter+exit  33.10 ns   debug!内  13.27 ns
try_new("debug"): 第一次 3.625µs,之后平均 263ns
try_new("info,[req{user=bob}]=debug"): 第一次 64.792µs,之后平均 9.036µs
  • 被关掉的 debug!:只有静态指令时约 0.3 ns,第二道门的一次原子读就挡住了。加一条 [req]=debug 后约 7.2 ns:上限升到 debug,调用点变成 sometimes,每次要走 Dispatch 的 enabled、EnvFilter 的线程局部 scope、再查 statics。约 20 倍,但绝对值仍是纳秒级。要紧的是范围:这 7 ns 落在所有 target 的每一条 debug! 上,包括依赖库里的,不只是 req 附近的。
  • req 里面的 debug! 约 13 ns:过滤放行,事件真的被构造、分发给 Registry(没有输出层,所以到此为止)。
  • enter + exit:两种过滤器下都在 31~33 ns,差约 2 ns。on_enter 无论有没有动态指令都要拿一次 by_id 的读锁查表,动态指令多出来的只是压栈、弹栈。
  • 解析:try_new("debug") 每次约 260 ns。带字段值的指令每次约 9 µs,多出来的是编译正则 DFA;第一次构造在三次运行里分别是 1.65 ms、58 µs、65 µs,不稳定;之后 1000 次的平均值三次相近。事件和 enter / exit 的几项三次之间也相近。
❗IMPORTANT
动脑筋:空的 RUST_LOG

常见的写法:环境变量读不到就退回 info。

use tracing_subscriber::EnvFilter;

fn main() {
  let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
  tracing_subscriber::fmt()
    .with_env_filter(filter)
    .with_ansi(false)
    .without_time()
    .init();
  tracing::info!("info event");
  tracing::error!("error event");
}

M2 Max、rustc 1.99.0、--release。不设 RUST_LOG 时打印两行。用 RUST_LOG= ./env_quiz(设了,但值为空)运行,打印几行?

动脑筋答案

$ ./env_quiz
 INFO env_quiz: info event
ERROR env_quiz: error event
$ RUST_LOG= ./env_quiz

($ 开头的两行是运行脚本打印的命令。)跑 3 次,输出相同:值为空时一行都没有,连 ERROR 也没有。

  • try_from_default_env 是 builder().try_from_env():变量存在,env::var 返回 Ok(""),不走错误分支。
  • Builder::parse("") 对空字符串直接返回 from_directives(empty),得到一个空的 EnvFilter;builder() 没有默认指令,所以什么都不补。unwrap_or_else 里的 info 根本没有机会生效。
  • 空过滤器的 statics.max_level 是 OFF,max_level_hint 也是 OFF,LevelFilter::current() 是 off,所有日志在第二道门就停了。

想让「存在但为空」也退回默认,用 EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy():空字符串没有指令,默认指令补上;上面构造函数的实验里 RUST_LOG="" 那一组就是这个结果。

小结

  • EnvFilter 把指令分进两张按具体程度排序的表:statics 只看 target(字符串前缀匹配,不按 :: 分段)、字段名和级别;dynamics 带 span 名或字段值。每个元数据取最具体的那条指令作答,一条都不关心就是关闭。
  • 指令先按逗号切分再交给状态机,所以字段列表里不能有逗号,逗号后也不能有空格;my-app=debug 能解析但匹配不到 target my_app。
  • 默认指令只在一条指令都没有时才加。from_default_env、new 默认 error、坏指令打印 ignoring 后跳过;try_new、try_from_default_env 遇到坏指令返回 Err,空字符串得到什么都不放行的空过滤器。
  • 静态指令在 register_callsite 时给出 always / never。只要有一条动态指令,其余所有没被静态放行的调用点都是 sometimes;被动态指令看中的 span 是 always,enter / exit 时把级别压进线程局部的 scope 栈,事件的 enabled 查这个栈。
  • 字段值按 bool、u64、i64、f64、正则的顺序解析;正则要匹配到值的结尾,with_regex(false) 比较 Debug 输出、只查前缀。带值的指令只对 span 起作用,对事件自己的字段不起作用。
  • 有带值的指令时 max_level_hint 是 TRACE;否则是两张表的最大级别。
  • 实测一条 [req]=debug 让所有 target 上被关掉的 debug! 从约 0.3 ns 变成约 7 ns;解析一条普通指令约 260 ns,带正则字段值的约 9 µs。

💬 关联讨论

发起讨论
加载中...