Rust in Everything
基于 tracing-subscriber 0.3.23

过滤:全局与每层

主线位置:事件主线的第二、三、四道门,以及最后的 on_event。全局过滤在 register_callsite 和 enabled 上说 never / false,整个栈到此为止;每层过滤在这两处只把自己的意见记进线程局部状态、对外放行,等事件真正分发时,各层在 on_event 前查自己那一位。span 主线上,new_span 时把这份意见存进 span 的数据,之后 enter、exit、close 都按它跳过。

flowchart TB
  ev["info! 经过门"] --> lay["Layered::enabled 从外往里问"]
  lay --> g{"全局过滤层"}
  g -->|false| stop["整个栈都看不到"]
  g -->|true| f{"Filtered:过滤器"}
  f -->|"不要:置位,返回 true"| reg["Registry::enabled"]
  f -->|"要:清位,问里层"| reg
  reg -->|true| disp["构造事件,分发"]
  disp --> oe{"Filtered::on_event 查位"}
  oe -->|"位为 0"| inner["内层 layer 收到"]
  oe -->|"位为 1"| skip["跳过并清位"]

一个 Layer 的 enabled 返回 false,整个栈都看不到这条事件(Layer 与 Layered 讲过这种短路)。这正是过滤器想要的效果,但也只能有这一种效果:想让输出到终端的层只要 INFO、写文件的层要 DEBUG,单靠 enabled 做不到。tracing-subscriber 因此有两种过滤位置:全局,过滤器本身当作一层用 .with(filter) 叠上去;每层,用 layer.with_filter(filter) 包住某一层,返回一个 Filtered。这一页先用一个实验看两者的差别,再拆开每层过滤的实现,最后看现成的几种过滤器。EnvFilter 两种位置都能用,它自己的语法和内部结构在 EnvFilter。

两个位置,一个实验

两个 fmt::Layer,一个叫 stdout,一个叫 file(都打印到标准输出,每行前面加上名字以便区分)。目标是 stdout 只要 INFO,file 要 DEBUG。

tracing = "=0.1.44"
tracing-subscriber = "=0.3.23"
use std::io::{self, Write};

use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{Registry, fmt};

// 给每行加一个前缀,区分是哪一层写的
struct Tagged(&'static str);

impl Write for Tagged {
  fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
    print!("[{}] {}", self.0, String::from_utf8_lossy(buf));
    Ok(buf.len())
  }
  fn flush(&mut self) -> io::Result<()> {
    Ok(())
  }
}

fn layer<S>(tag: &'static str) -> impl tracing_subscriber::Layer<S>
where
  S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
  fmt::layer()
    .with_ansi(false)
    .without_time()
    .with_target(false)
    .with_writer(move || Tagged(tag))
}

fn emit() {
  tracing::info!("info event");
  tracing::debug!("debug event");
}

fn main() {
  println!("== 1. 全局 INFO:.with(LevelFilter::INFO)");
  let sub = Registry::default()
    .with(LevelFilter::INFO)
    .with(layer("stdout"))
    .with(layer("file"));
  tracing::subscriber::with_default(sub, emit);

  println!("== 2. 全局 DEBUG:.with(LevelFilter::DEBUG)");
  let sub = Registry::default()
    .with(LevelFilter::DEBUG)
    .with(layer("stdout"))
    .with(layer("file"));
  tracing::subscriber::with_default(sub, emit);

  println!("== 3. 全局 INFO 放在两层中间");
  let sub = Registry::default()
    .with(layer("stdout"))
    .with(LevelFilter::INFO)
    .with(layer("file"));
  tracing::subscriber::with_default(sub, emit);

  println!("== 4. 每层过滤:stdout INFO,file DEBUG");
  let sub = Registry::default()
    .with(layer("stdout").with_filter(LevelFilter::INFO))
    .with(layer("file").with_filter(LevelFilter::DEBUG));
  tracing::subscriber::with_default(sub, emit);
}

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

== 1. 全局 INFO:.with(LevelFilter::INFO)
[stdout]  INFO info event
[file]  INFO info event
== 2. 全局 DEBUG:.with(LevelFilter::DEBUG)
[stdout]  INFO info event
[file]  INFO info event
[stdout] DEBUG debug event
[file] DEBUG debug event
== 3. 全局 INFO 放在两层中间
[stdout]  INFO info event
[file]  INFO info event
== 4. 每层过滤:stdout INFO,file DEBUG
[stdout]  INFO info event
[file]  INFO info event
[file] DEBUG debug event
  • 1 和 2:全局过滤只有一个级别,两层看到的永远一样。设成 INFO,file 少了 DEBUG;设成 DEBUG,stdout 多了 DEBUG。
  • 3:把 LevelFilter::INFO 挪到两层中间,它并不只挡住外面的 file。enabled 从最外层往里问,问到它时返回 false,整个栈短路,里面的 stdout 也看不到。.with 的顺序不改变全局过滤的作用范围。
  • 4:每层过滤,两层各拿到自己要的。

tracing_subscriber::fmt().with_env_filter(..) 和 fmt().with_max_level(..) 都属于第一种:SubscriberBuilder::finish 先把 fmt::Layer 叠在 Registry 上,再用 self.filter.with_subscriber(..) 把过滤器叠在最外面。

全局过滤:过滤器自己就是一层

LevelFilter 能直接传给 .with,是因为它实现了 Layer,见 filter/level.rs:

impl<S: Subscriber> crate::Layer<S> for LevelFilter {
    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
        if self >= metadata.level() {
            Interest::always()
        } else {
            Interest::never()
        }
    }

    fn enabled(&self, metadata: &Metadata<'_>, _: crate::layer::Context<'_, S>) -> bool {
        self >= metadata.level()
    }

    fn max_level_hint(&self) -> Option<LevelFilter> {
        Some(*self)
    }
}

三个判断方法全部实现,通知方法一个都没有:它不记录任何东西,只回答「要不要」。Targets、FilterFn、EnvFilter 也都实现了 Layer,用法一样。起作用的是 Layered 的 enabled(这是 Layered 作为 Subscriber 时的版本):

    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
        if self.layer.enabled(metadata, self.ctx()) {
            // if the outer layer enables the callsite metadata, ask the subscriber.
            self.inner.enabled(metadata)
        } else {
            // otherwise, the callsite is disabled by the layer

            // If per-layer filters are in use, and we are short-circuiting
            // (rather than calling into the inner type), clear the current
            // per-layer filter `enabled` state.
            #[cfg(feature = "registry")]
            filter::FilterState::clear_enabled();

            false
        }
    }

本层说 false 就返回 false,Dispatch 拿到 false,宏不构造事件,所有层都看不到。register_callsite 也一样:本层说 never,pick_interest 直接返回 never,这个调用点被缓存成 never(Callsite 与 Interest 缓存)。中间那句 clear_enabled 是给每层过滤收拾残局的:外面的 Filtered 可能已经在线程局部里写下了自己的意见,这里一短路,后面就没有人会清掉它,只能当场清。

📝NOTE
冷知识:clear_enabled 是一个 panic 换来的

per-layer filtering 在 tracing-subscriber 0.2.21 首次发布(PR #1523)。几天后的 issue #1563 报告:registry().with(Targets).with(fmt_layer.with_filter(filter_fn(..))) 这种「全局过滤加每层过滤」的组合,在 debug 构建里 panic,报的是下面会看到的那组调试计数器的断言:「if we are in or starting a filter pass, we must not be in an interest pass.」原因是全局过滤短路后,每层过滤留在线程局部里的状态没人清。先修的是 register_callsite 那一半(PR #1569,0.2.23,pick_interest 短路时调用 take_interest),再修 enabled 这一半(PR #1575),提交说明是「When a global filter returns false from enabled, ensure that the per-layer filter FilterMap and debug counters are cleared, so that they are empty on the next enabled call.」后者随 tracing-subscriber 0.2.24 发布,0.3 线从 0.3.0 起就带着它。

每层过滤:Filtered

Layer::with_filter 只是 Filtered::new(self, filter)。Filtered:

pub struct Filtered<L, F, S> {
    filter: F,
    layer: L,
    id: MagicPlfDowncastMarker,
    _s: PhantomData<fn(S)>,
}

filter 实现的是 Filter<S> 而不是 Layer<S>。两个 trait 的判断方法一一对应,语义不同:

Filter 的方法对应 Layer 的区别
enabled(&self, &Metadata, &Context)enabled返回 false 只让被包的这一层看不到,其他层照常
callsite_enabled(&self, &'static Metadata)register_callsite默认返回 sometimes,不像 Layer 的默认那样把 enabled 冻结成 always / never
event_enabled(&self, &Event, &Context)event_enabled默认 true;enabled 已经说 false 时不会被调用
max_level_hint(&self)max_level_hint默认 None

Filter 还有 on_new_span、on_enter、on_exit、on_close、on_record 五个通知方法(默认空),给需要跟踪 span 状态的过滤器用,EnvFilter 的动态指令就靠它们。

id 字段是 FilterId 外面包一层 #[repr(transparent)] 的标记类型,用来让 downcast_raw 认出「这一层带每层过滤」,见 Layer 与 Layered。下面只关心里面的 FilterId。

💡TIP
打个比方:一张传阅单

一条事件经过时,同一线程上的所有 Filtered 共用一张传阅单(线程局部的 FilterMap),单子上每个过滤器有自己的一栏(FilterId 那一位)。传阅时每个过滤器只在自己那栏打钩表示「我这层不要」(置位),然后照样往下传(返回 true)。传到最里面的登记员(Registry::enabled)手上,他看看是不是每栏都打了钩。没全打,就把事件正式发出来;各层收件前看一眼自己那栏,打了钩的不收,顺手把钩擦掉(did_enable 清位),单子就干净了,下一条事件接着用。

比方到这里为止:传阅单不是每条事件一张,而是每个线程只有一张、反复擦写;而且登记员的判断比比方里粗,下面会看到他只在 64 栏全打钩时才认为「没人要」。

FilterId:一个位,最多 64 个

FilterId 是 FilterId(u64),存的是掩码而不是编号。它在 Filtered 被叠上去的那一刻分配,on_layer 向内层要:

    fn on_layer(&mut self, subscriber: &mut S) {
        self.id = MagicPlfDowncastMarker(subscriber.register_filter());
        self.layer.on_layer(subscriber);
    }

register_filter 一路经 Layered 转到 Registry::register_filter,用的是 Registry 里一个 u8 计数器 next_filter_id,再交给 FilterId::new:

    fn register_filter(&mut self) -> FilterId {
        let id = FilterId::new(self.next_filter_id);
        self.next_filter_id += 1;
        id
    }
    pub(crate) fn new(id: u8) -> Self {
        assert!(id < 64, "filter IDs may not be greater than 64");
        Self(1 << id as usize)
    }

第 n 个过滤器拿到 1 << n。位图是一个 u64,所以上限是 64 个。在构造 Filtered 时 id 先填 FilterId::disabled(),即 u64::MAX;没经过 on_layer 就被调用时,Filtered::id() 里的 debug_assert! 会报「was it registered with the subscriber?」。

只有 Registry 实现了 register_filter(LookupSpan 的默认实现直接 panic:「does not currently support filters」),这就是文档说「Registry 是唯一能承载每层过滤的根 subscriber」的原因。Registry::has_per_layer_filters 也只是看 next_filter_id > 0。

用 Vec<Box<dyn Layer>> 一次放 64 个和 65 个 Filtered:

use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::{Identity, Layer};
use tracing_subscriber::prelude::*;
use tracing_subscriber::Registry;

fn build(n: usize) {
  let layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = (0..n)
    .map(|_| Identity::new().with_filter(LevelFilter::INFO).boxed())
    .collect();
  let _sub = Registry::default().with(layers);
  println!("{n} 个 Filtered:ok");
}

fn main() {
  build(64);
  build(65);
}
64 个 Filtered:ok

thread 'main' (1420970) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/filter/layer_filters/mod.rs:913:9:
filter IDs may not be greater than 64
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

跑 3 次,除了线程号都相同。panic 发生在 .with(layers) 里,也就是组装 subscriber 时,不会拖到第一条日志。消息说「不能大于 64」,实际第 65 个(编号 64)就触发了。

掩码而不是编号,是为了嵌套。FilterId::and 把两个掩码按位或起来:Filtered { filter1, Layered { layer1, Filtered { filter2, layer2 } } } 里,layer2 拿到的 Context 带着 filter1 | filter2,查 span 时只要任一位置位就算「被禁用」。源码注释举了这个例子,说用编号的话,layer2 遍历父 span 时会看到被 filter1 挡掉的 span。

enabled:记下意见,对外放行

每个线程有一份 FilterState,放在线程局部变量 FILTERING 里:

pub(crate) struct FilterState {
    enabled: Cell<FilterMap>,
    // TODO(eliza): `Interest`s should _probably_ be `Copy`. The only reason
    // they're not is our Obsessive Commitment to Forwards-Compatibility. If
    // this changes in tracing-core`, we can make this a `Cell` rather than
    // `RefCell`...
    interest: RefCell<Option<Interest>>,

    #[cfg(debug_assertions)]
    counters: DebugCounters,
}

enabled 是这一轮 enabled 调用的结果,FilterMap 就是一个 bits: u64,某位为 1 表示对应的过滤器不要(它的 Debug 输出字段名就叫 disabled_by)。interest 是 register_callsite 那一轮的汇总,下面讲。counters 只在 debug 构建里存在,专门用来断言这些状态在每轮开始时是空的,上面冷知识里的 panic 就出自它。

Filtered::enabled:

    fn enabled(&self, metadata: &Metadata<'_>, cx: Context<'_, S>) -> bool {
        let cx = cx.with_filter(self.id());
        let enabled = self.filter.enabled(metadata, &cx);
        FILTERING.with(|filtering| filtering.set(self.id(), enabled));

        if enabled {
            // If the filter enabled this metadata, ask the wrapped layer if
            // _it_ wants it --- it might have a global filter.
            self.layer.enabled(metadata, cx)
        } else {
            // Otherwise, return `true`. The _per-layer_ filter disabled this
            // metadata, but returning `false` in `Layer::enabled` will
            // short-circuit and globally disable the span or event. This is
            // *not* what we want for per-layer filters, as other layers may
            // still want this event. Returning `true` here means we'll continue
            // asking the next layer in the stack.
            // ...
            true
        }
    }

过滤器说「不要」时,FilterMap::set 把自己那位置 1;说「要」时清 0,再去问被包的层,被包的层自己的 enabled 仍然是全局语义。返回值几乎总是 true,这样 Layered 不会短路,后面的层都有机会表态。

问到最里面,Registry::enabled 做最后的汇总:

    fn enabled(&self, _: &Metadata<'_>) -> bool {
        if self.has_per_layer_filters() {
            return FilterState::event_enabled();
        }
        true
    }

FilterState::event_enabled 读的是 FilterMap::any_enabled:

    #[inline]
    pub(crate) fn any_enabled(self) -> bool {
        self.bits != u64::MAX
    }

FilterState 的文档说这一步的用意是「If all the bits are set, then every per-layer filter has decided it doesn't want to enable that span or event. In that case, the Registry's enabled method will return false」。但比较的对象是 u64::MAX:只有 64 位全为 1 才算「没人要」,而 n 个过滤器最多只会置 n 位。下面用 dynamic_filter_fn(|_, _| false) 让每个过滤器的 Interest 都是 sometimes、每次都说不要,再用一个有副作用的字段表达式看事件有没有被构造:

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

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

static EVALUATED: AtomicUsize = AtomicUsize::new(0);
static DELIVERED: AtomicUsize = AtomicUsize::new(0);

fn bump() -> u64 {
  EVALUATED.fetch_add(1, Relaxed);
  1
}

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

fn run(n: usize) {
  EVALUATED.store(0, Relaxed);
  DELIVERED.store(0, Relaxed);
  // n 个每层过滤,全都说「不要」;dynamic_filter_fn 让 Interest 是 sometimes
  let layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = (0..n)
    .map(|_| Count.with_filter(dynamic_filter_fn(|_, _| false)).boxed())
    .collect();
  let sub = Registry::default().with(layers);
  tracing::subscriber::with_default(sub, || {
    for _ in 0..1000 {
      tracing::info!(x = bump());
    }
  });
  println!(
    "{n:>2} 个过滤全说 false:字段求值 {} 次,on_event {} 次",
    EVALUATED.load(Relaxed),
    DELIVERED.load(Relaxed)
  );
}

fn main() {
  run(1);
  run(2);
  run(63);
  run(64);
}

跑 3 次,输出相同:

 1 个过滤全说 false:字段求值 1000 次,on_event 0 次
 2 个过滤全说 false:字段求值 1000 次,on_event 0 次
63 个过滤全说 false:字段求值 1000 次,on_event 0 次
64 个过滤全说 false:字段求值 0 次,on_event 0 次

结果仍然正确,没有任何一层收到事件;但不满 64 个过滤器时,Registry::enabled 返回了 true,事件被构造、字段被求值、分发到每一层再被各自跳过。这只影响 Interest 是 sometimes 的调用点:所有过滤器都在 register_callsite 时说 never 的调用点,在缓存那一步就停了(下面的「意见不一时的 Interest」)。

on_event:查自己那一位,顺手清掉

事件分发后,Filtered::on_event 把调用包进 did_enable:

    fn did_enable(&self, filter: FilterId, f: impl FnOnce()) {
        let map = self.enabled.get();
        if map.is_enabled(filter) {
            // If the filter didn't disable the current span/event, run the
            // callback.
            f();
        } else {
            // Otherwise, if this filter _did_ disable the span or event
            // currently being processed, clear its bit from this thread's
            // `FilterState`. The bit has already been "consumed" by skipping
            // this callback, and we need to ensure that the `FilterMap` for
            // this thread is reset when the *next* `enabled` call occurs.
            self.enabled.set(map.set(filter, true));
        }
        // ...
    }

is_enabled 是 bits & mask == 0。位为 1 的层跳过 on_event 并把位清掉;位为 0 的层本来就是 0。所有 Filtered 都过一遍之后,FilterMap 回到全 0,下一条事件从干净的状态开始。这套做法能成立,依赖两件事:同一条事件的 enabled 和 on_event 在同一个线程上连续发生(宏里先 enabled 再 Event::dispatch,见 Dispatch);中途短路时有人负责清空(上面的 clear_enabled)。

event_enabled 是第二轮:Filtered::event_enabled 用 FilterState::and,只有本位还是 0 时才调用过滤器的 event_enabled,结果再写回本位。这时字段值已经在 Event 里了,所以每层过滤也能按字段值过滤。

span:位图存进 span 的数据

span 也走同样的 enabled,但 span 不是一次性的,之后还有 enter、exit、record、close。Registry::new_span 在分配槽位时把当前的 FilterMap 拷进 span 的 DataInner:

                data.filter_map = crate::filter::FILTERING.with(|filtering| filtering.filter_map());

之后 Filtered::on_enter / on_exit / on_close / on_record 都用 cx.if_enabled_for(id, self.id()) 查这份存下来的位图(Registry 的 span 数据上的 is_enabled_for),不再问过滤器。on_follows_from 要求两个 span 都对本层可见。

被过滤掉的 span 在这一层的 Context 里也看不到:Context 带着本层的 FilterId,Context::span 用 try_with_filter 过滤,lookup_current 和 scope() 遍历父 span 时跳过本层不可见的。两个 fmt::Layer,一个只要 INFO,一个要 DEBUG,事件在一个 DEBUG 级的 span 里:

use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{Registry, fmt};

fn main() {
  let sub = Registry::default()
    .with(fmt::layer().with_ansi(false).without_time().with_filter(LevelFilter::INFO))
    .with(fmt::layer().with_ansi(false).without_time().with_filter(LevelFilter::DEBUG));
  tracing::subscriber::with_default(sub, || {
    let outer = tracing::info_span!("outer");
    let _a = outer.enter();
    let inner = tracing::debug_span!("inner");
    let _b = inner.enter();
    tracing::info!("hello");
  });
}

跑 3 次,输出相同:

 INFO outer: span_scope: hello
 INFO outer:inner: span_scope: hello

第一行来自 INFO 那层(它在里面,on_event 先跑),前缀里没有 inner;第二行来自 DEBUG 那层。同一条事件,两层看到的 span 树不一样。span 槽位本身的分配和回收在 Registry。

register_callsite:意见不一时的 Interest

enabled 每次都要走一遍,Interest 缓存才是省开销的地方。Filtered::register_callsite:

    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
        let interest = self.filter.callsite_enabled(metadata);

        // If the filter didn't disable the callsite, allow the inner layer to
        // register it — since `register_callsite` is also used for purposes
        // such as reserving/caching per-callsite data, we want the inner layer
        // to be able to perform any other registration steps. However, we'll
        // ignore its `Interest`.
        if !interest.is_never() {
            self.layer.register_callsite(metadata);
        }

        // Add our `Interest` to the current sum of per-layer filter `Interest`s
        // for this callsite.
        FILTERING.with(|filtering| filtering.add_interest(interest));

        // don't short circuit! ...
        Interest::always()
    }

每个过滤器的答案经 add_interest 合并进线程局部的 interest:

        if let Some(curr_interest) = curr_interest.as_mut() {
            if (curr_interest.is_always() && !interest.is_always())
                || (curr_interest.is_never() && !interest.is_never())
            {
                *curr_interest = Interest::sometimes();
            }
            // If the two interests are the same, do nothing. If the current
            // interest is `sometimes`, stay sometimes.
        } else {
            *curr_interest = Some(interest);
        }

规则和 tracing-core 合并多个 dispatcher 时一样:全相同就保留,有分歧就是 sometimes。最后 Registry::register_callsite 用 take_interest 取出汇总值作为自己的答案。Filtered 对外一律返回 always,以及外面再套普通 layer 时 never 会被改成 sometimes,这两条是 pick_interest 里的规则,在 Layer 与 Layered 讲过。

两个过滤器 A(INFO)和 B(DEBUG)意见不一时,实测每个调用点的 enabled 被问了几次。为了数次数,自己写一个和 LevelFilter 行为相同的 Filter:

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

use tracing::subscriber::Interest;
use tracing::{Event, Metadata, Subscriber};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::{Context, Filter, Layer};
use tracing_subscriber::prelude::*;
use tracing_subscriber::Registry;

// 和 LevelFilter 一样的过滤,只是数一下 enabled 被问了几次
struct CountingLevel(LevelFilter, &'static AtomicUsize);

impl<S> Filter<S> for CountingLevel {
  fn enabled(&self, meta: &Metadata<'_>, _: &Context<'_, S>) -> bool {
    self.1.fetch_add(1, Relaxed);
    meta.level() <= &self.0
  }
  fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
    if meta.level() <= &self.0 { Interest::always() } else { Interest::never() }
  }
  fn max_level_hint(&self) -> Option<LevelFilter> {
    Some(self.0)
  }
}

// 被过滤的层:数 on_event
struct Count(&'static AtomicUsize);

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

// 不带过滤的普通层,什么都不做
struct Plain;
impl<S: Subscriber> Layer<S> for Plain {}

static A_ENABLED: AtomicUsize = AtomicUsize::new(0);
static B_ENABLED: AtomicUsize = AtomicUsize::new(0);
static A_EVENTS: AtomicUsize = AtomicUsize::new(0);
static B_EVENTS: AtomicUsize = AtomicUsize::new(0);

fn run(label: &str, f: impl Fn()) {
  for c in [&A_ENABLED, &B_ENABLED, &A_EVENTS, &B_EVENTS] {
    c.store(0, Relaxed);
  }
  for _ in 0..1000 {
    f();
  }
  println!(
    "{label:<7} A.enabled={:<5} B.enabled={:<5} A.on_event={:<5} B.on_event={}",
    A_ENABLED.load(Relaxed),
    B_ENABLED.load(Relaxed),
    A_EVENTS.load(Relaxed),
    B_EVENTS.load(Relaxed),
  );
}

fn body() {
  println!("LevelFilter::current() = {}", LevelFilter::current());
  run("info!", || tracing::info!("x"));
  run("debug!", || tracing::debug!("x"));
  run("trace!", || tracing::trace!("x"));
}

fn main() {
  println!("== A 只要 INFO,B 要 DEBUG");
  let sub = Registry::default()
    .with(Count(&A_EVENTS).with_filter(CountingLevel(LevelFilter::INFO, &A_ENABLED)))
    .with(Count(&B_EVENTS).with_filter(CountingLevel(LevelFilter::DEBUG, &B_ENABLED)));
  tracing::subscriber::with_default(sub, body);

  println!("== 外面再套一个不带过滤的层");
  let sub = Registry::default()
    .with(Count(&A_EVENTS).with_filter(CountingLevel(LevelFilter::INFO, &A_ENABLED)))
    .with(Count(&B_EVENTS).with_filter(CountingLevel(LevelFilter::DEBUG, &B_ENABLED)))
    .with(Plain);
  tracing::subscriber::with_default(sub, body);
}

跑 3 次,输出相同:

== A 只要 INFO,B 要 DEBUG
LevelFilter::current() = debug
info!   A.enabled=0     B.enabled=0     A.on_event=1000  B.on_event=1000
debug!  A.enabled=1000  B.enabled=1000  A.on_event=0     B.on_event=1000
trace!  A.enabled=0     B.enabled=0     A.on_event=0     B.on_event=0
== 外面再套一个不带过滤的层
LevelFilter::current() = trace
info!   A.enabled=0     B.enabled=0     A.on_event=1000  B.on_event=1000
debug!  A.enabled=1000  B.enabled=1000  A.on_event=0     B.on_event=1000
trace!  A.enabled=1000  B.enabled=1000  A.on_event=0     B.on_event=0

第一组:

  • info!:A、B 都是 always,合并后 always,enabled 一次都不问,两层都收到。
  • debug!:A 是 never、B 是 always,合并成 sometimes。于是每次都问两个过滤器的 enabled:A 置位、B 清位,on_event 时 A 跳过、B 收到。意见不一的代价是这个调用点失去了缓存,每条 debug! 都要走一遍两个过滤器。
  • trace!:没走到 Interest。LevelFilter::current() 是两个提示的最大值 debug(Filtered::max_level_hint 直接转发过滤器的提示),第二道门就挡住了,见 级别过滤的几道门。

第二组只是在最外面加了一个什么都不做的 Plain:

  • LevelFilter::current() 变成 trace。Plain 没有 max_level_hint,按 pick_level_hint 的规则,内层含每层过滤而外层提示为 None 时整个栈就是 None,也就是不设上限。
  • trace! 因此走到了 Interest:A、B 都是 never,但外面的 Plain 想要(Layer 默认的 register_callsite 问自己的 enabled,默认是 true,于是得到 always),pick_interest 把 never 改成 sometimes。每条 trace! 都要问两个过滤器、构造事件、交给 Plain。

所以给一个栈里加一个不带过滤、也不给提示的层,会让每层过滤的过滤器在所有级别上都被问到。

📝NOTE
没有蠢问题

问: 每层过滤这么绕,为什么不在自己的 on_event 里判断一下级别,不要就直接 return?

答: 结果一样,代价不一样。on_event 被调用时,事件已经构造好、字段已经求值,前面几道门一道都没起作用。Filter 参与了 register_callsite(所有层都不要的调用点缓存成 never)和 max_level_hint(所有层的上限决定第二道门),这两样是 on_event 里的判断给不了的。此外 Filtered 还会替被包的层隐藏被过滤掉的 span,on_event 里的判断管不到 on_enter、scope() 这些地方。

现成的过滤器

LevelFilter

除了上面的 Layer 实现,impl Filter for LevelFilter 内容相同:callsite_enabled 按级别给 always / never,max_level_hint 返回自己。只按级别过滤时,它是开销最小的选择:所有判断都在缓存阶段完成。LevelFilter 本身的定义在 Metadata、Level 与 LevelFilter。

Targets

Targets 是 Targets(DirectiveSet<StaticDirective>),即「target 前缀 → 级别」的表,可以从 "my_app=debug,hyper=warn,info" 这样的字符串解析(FromStr),也可以用 with_target、with_default 构造。它和 EnvFilter 共用 filter/directive.rs 里的 StaticDirective 和 DirectiveSet,匹配规则(前缀匹配、最具体的指令优先)在 EnvFilter 讲。

区别在于只有静态指令:没有 span 名、没有字段值。所以 interested 对每个调用点总能给出 always 或 never,max_level_hint 是表里的最大级别,内部没有锁、没有线程局部。EnvFilter 的文档自己也说,不需要按 span 和字段过滤时,Targets 是「a lighter-weight alternative」。另外 Targets 不读环境变量,RUST_LOG 要自己读出来再 parse。

filter_fn 与 dynamic_filter_fn

filter_fn 把一个 Fn(&Metadata) -> bool 变成 FilterFn。闭包只看元数据,所以 is_callsite_enabled 直接拿它的结果冻结成 always / never,注释是「we can reasonably assume its results are cacheable」。这也意味着闭包里读一个运行时会变的开关是不生效的:调用点第一次经过时的答案会一直被缓存,除非有人调用 rebuild_interest_cache。

dynamic_filter_fn 的闭包多一个 &Context,可以看当前 span。它的 default_callsite_enabled 除了超出 max_level_hint 的级别给 never,其余一律 sometimes,每次都问;也可以用 with_callsite_filter 自己提供 register_callsite 的逻辑。

两者的 max_level_hint 默认都是 None,要用 with_max_level_hint 手动给。下一节的实验里能看到不给提示的后果。

组合子:and、or、not

FilterExt 给所有 Filter 加了 and、or、not、boxed。注意它们组合的是 Filter,结果只能用在 with_filter 里,不是 Layer。各自的规则:

组合子enabledcallsite_enabledmax_level_hint
Anda && b,短路a 为 never 就是 never;否则 b 不是 always 就取 b;否则取 acmp::min(a, b),任一为 None 结果就是 None
Ora || b,短路任一 always 为 always;任一 sometimes 为 sometimes;否则 never两者都有才取较大者,否则 None
Not!aalways 与 never 互换,sometimes 不变总是 None,注释是「TODO(eliza): figure this out???」

Not 还有一处不对称:它反转 enabled 和 callsite_enabled,但 event_enabled 一律返回 true,不反转。FilterExt::not 的文档解释说,正确的反转应该是 !(enabled() && event_enabled()),而要做到这一点就得额外的线程局部状态记住 enabled 的结果,「to support a very niche use case」,所以没做。用 not 包一个按字段值过滤的过滤器时,字段那部分不会被反转。

谁给了第二道门提示

各种过滤器装上后,LevelFilter::current() 是多少:

use tracing_subscriber::filter::{FilterExt, LevelFilter, Targets, filter_fn};
use tracing_subscriber::layer::Identity;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, Registry};

fn show<L>(label: &str, layer: L)
where
  L: tracing_subscriber::Layer<Registry> + Send + Sync + 'static,
{
  let sub = Registry::default().with(layer);
  tracing::subscriber::with_default(sub, || {
    println!("{label:<52} LevelFilter::current() = {}", LevelFilter::current());
  });
}

fn main() {
  let is_app = |m: &tracing::Metadata<'_>| m.target().starts_with("app");
  // 当全局过滤用:filter 自己就是一层
  show("全局 LevelFilter::INFO", LevelFilter::INFO);
  show("全局 Targets(app=debug,warn)", "app=debug,warn".parse::<Targets>().unwrap());
  show("全局 filter_fn(..)", filter_fn(is_app));
  show(
    "全局 filter_fn(..).with_max_level_hint(INFO)",
    filter_fn(is_app).with_max_level_hint(LevelFilter::INFO),
  );
  show("全局 EnvFilter::new(\"info\")", EnvFilter::new("info"));
  show("全局 EnvFilter::new(\"\")", EnvFilter::new(""));
  show("全局 EnvFilter::try_new(\"\")", EnvFilter::try_new("").unwrap());
  show("全局 EnvFilter::new(\"info,[req]=debug\")", EnvFilter::new("info,[req]=debug"));
  show("全局 EnvFilter::new(\"info,[{id=1}]=debug\")", EnvFilter::new("info,[{id=1}]=debug"));
  // 当每层过滤用
  show("每层 INFO", Identity::new().with_filter(LevelFilter::INFO));
  show(
    "每层 INFO + 每层 DEBUG",
    Identity::new()
      .with_filter(LevelFilter::INFO)
      .and_then(Identity::new().with_filter(LevelFilter::DEBUG)),
  );
  show(
    "每层 INFO.and(filter_fn)",
    Identity::new().with_filter(LevelFilter::INFO.and(filter_fn(is_app))),
  );
  show(
    "每层 INFO.or(filter_fn)",
    Identity::new().with_filter(LevelFilter::INFO.or(filter_fn(is_app))),
  );
  show(
    "每层 WARN.not()",
    Identity::new().with_filter(LevelFilter::WARN.not()),
  );
}

跑 3 次,输出相同:

全局 LevelFilter::INFO                                 LevelFilter::current() = info
全局 Targets(app=debug,warn)                           LevelFilter::current() = debug
全局 filter_fn(..)                                     LevelFilter::current() = trace
全局 filter_fn(..).with_max_level_hint(INFO)           LevelFilter::current() = info
全局 EnvFilter::new("info")                            LevelFilter::current() = info
全局 EnvFilter::new("")                                LevelFilter::current() = error
全局 EnvFilter::try_new("")                            LevelFilter::current() = off
全局 EnvFilter::new("info,[req]=debug")                LevelFilter::current() = debug
全局 EnvFilter::new("info,[{id=1}]=debug")             LevelFilter::current() = trace
每层 INFO                                              LevelFilter::current() = info
每层 INFO + 每层 DEBUG                                   LevelFilter::current() = debug
每层 INFO.and(filter_fn)                               LevelFilter::current() = trace
每层 INFO.or(filter_fn)                                LevelFilter::current() = trace
每层 WARN.not()                                        LevelFilter::current() = trace

(中文字符宽度不同,列没有对齐,按原样贴出。)filter_fn 不给提示,就算和 LevelFilter::INFO 用 and 组合,结果也是 trace:And 的 cmp::min(Some(INFO), None) 是 None。逻辑上 INFO && 任意条件 不可能放行 DEBUG,但实现没有利用这一点。not 同样让上限消失。上限是 trace 时,调用点照样能被 Interest 缓存挡住(filter_fn 给 never),损失的只是第二道门那一次原子读;但对 sometimes 的过滤器,每条被关掉的低级别日志都要走一遍 enabled。EnvFilter 的几行在 EnvFilter 解释。

❗IMPORTANT
动脑筋:全局 INFO 加上一层自己的 DEBUG

还是开头那个程序的 layer 和 emit,main 换成:

fn main() {
  println!("== 全局 INFO + file 层自己的 DEBUG");
  let sub = Registry::default()
    .with(LevelFilter::INFO)
    .with(layer("stdout"))
    .with(layer("file").with_filter(LevelFilter::DEBUG));
  tracing::subscriber::with_default(sub, emit);
}

M2 Max、rustc 1.99.0、--release。[file] 开头的行有几行?

动脑筋答案

== 全局 INFO + file 层自己的 DEBUG
[stdout]  INFO info event
[file]  INFO info event

跑 3 次,都是 1 行,file 没有收到 debug event。

  • max_level_hint:外层 file 的过滤器提示 DEBUG,内层(stdout 加全局 INFO)提示 INFO,取最大值 DEBUG。在同一个 subscriber 里打印 LevelFilter::current(),得到的是 debug,第二道门放行 debug!。
  • register_callsite:最外层 Filtered 先把 file 的意见 always 记进线程局部的汇总,对外返回 always;pick_interest 因为本层是 Filtered,直接用内层的答案。内层里的全局 LevelFilter::INFO 对 debug! 回答 never,它所在的 Layered 短路返回 never,同时调用 take_interest 把汇总清掉,file 的意见就此作废。调用点缓存成 never,enabled 都不会再被问。

全局过滤和每层过滤叠在一起时,是「与」的关系:每层过滤只能在全局放行的范围内再收窄,不能放宽。想让某一层看到比别的层更多的东西,全局那层的级别就得放到最宽,其余的限制都改成每层过滤。

小结

  • 全局过滤是一个实现了 Layer 的过滤器,用 .with(filter) 叠上去;它在 register_callsite 说 never、在 enabled 说 false,整个栈短路,所有层都看不到,.with 的位置不影响这一点。fmt().with_env_filter(..) 属于这一种。
  • 每层过滤用 layer.with_filter(filter),得到 Filtered。每个 Filtered 在 on_layer 时从 Registry 领一个 FilterId,是 1 << n 的掩码,第 65 个会 panic「filter IDs may not be greater than 64」。只有 Registry 能分配 FilterId。
  • Filtered::enabled 把过滤器的意见写进线程局部 FilterMap(位为 1 表示不要),对外返回 true;on_event 前查自己那一位,跳过时把位清掉。span 的 FilterMap 在 new_span 时存进 DataInner,之后的回调和 span 遍历都按它跳过。
  • Registry::enabled 只有在 64 位全为 1 时才返回 false;过滤器不满 64 个时,sometimes 调用点上所有层都不要的事件仍会被构造、分发、再被逐层跳过。
  • 各过滤器的 callsite_enabled 合并规则是「相同保留,不同为 sometimes」;意见不一的调用点每次都要问所有过滤器。不带过滤、不给提示的普通层会让第二道门失效,并把 never 改成 sometimes。
  • LevelFilter、Targets、filter_fn 都能在缓存阶段给出 always / never;dynamic_filter_fn 默认 sometimes。filter_fn 和 not、以及含它们的 and / or 没有 max_level_hint。
  • 全局与每层叠加是「与」:每层过滤只能在全局放行的范围内收窄。

💬 关联讨论

发起讨论
加载中...