Skip to content

Commit

Permalink
subscriber: Implement layer::Filter for Option<Filter> (#2407)
Browse files Browse the repository at this point in the history
It's currently awkward to have an optional per-layer filter.

Implement `Filter<L>` for `Option<F> where F: Filter<L>`, following the example
of `Layer`. A `None` filter passes everything through.

Also, it looks like Filter for Arc/Box doesn't pass through all the methods, so
extend the `filter_impl_body` macro to include them.

This also adds a couple of tests and updates the docs.

---------

Co-authored-by: David Barsky <[email protected]>
Co-authored-by: Ryan Johnson <[email protected]>
Co-authored-by: Eliza Weisman <[email protected]>
  • Loading branch information
4 people committed Sep 29, 2023
1 parent 9afa906 commit 5b74376
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 0 deletions.
99 changes: 99 additions & 0 deletions tracing-subscriber/src/filter/layer_filters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,36 @@ macro_rules! filter_impl_body {
fn max_level_hint(&self) -> Option<LevelFilter> {
self.deref().max_level_hint()
}

#[inline]
fn event_enabled(&self, event: &Event<'_>, cx: &Context<'_, S>) -> bool {
self.deref().event_enabled(event, cx)
}

#[inline]
fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
self.deref().on_new_span(attrs, id, ctx)
}

#[inline]
fn on_record(&self, id: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
self.deref().on_record(id, values, ctx)
}

#[inline]
fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
self.deref().on_enter(id, ctx)
}

#[inline]
fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
self.deref().on_exit(id, ctx)
}

#[inline]
fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
self.deref().on_close(id, ctx)
}
};
}

Expand All @@ -493,6 +523,75 @@ impl<S> layer::Filter<S> for Box<dyn layer::Filter<S> + Send + Sync + 'static> {
filter_impl_body!();
}

// Implement Filter for Option<Filter> where None => allow
#[cfg(feature = "registry")]
#[cfg_attr(docsrs, doc(cfg(feature = "registry")))]
impl<F, S> layer::Filter<S> for Option<F>
where
F: layer::Filter<S>,
{
#[inline]
fn enabled(&self, meta: &Metadata<'_>, ctx: &Context<'_, S>) -> bool {
self.as_ref()
.map(|inner| inner.enabled(meta, ctx))
.unwrap_or(true)
}

#[inline]
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
self.as_ref()
.map(|inner| inner.callsite_enabled(meta))
.unwrap_or_else(Interest::always)
}

#[inline]
fn max_level_hint(&self) -> Option<LevelFilter> {
self.as_ref().and_then(|inner| inner.max_level_hint())
}

#[inline]
fn event_enabled(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> bool {
self.as_ref()
.map(|inner| inner.event_enabled(event, ctx))
.unwrap_or(true)
}

#[inline]
fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
if let Some(inner) = self {
inner.on_new_span(attrs, id, ctx)
}
}

#[inline]
fn on_record(&self, id: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
if let Some(inner) = self {
inner.on_record(id, values, ctx)
}
}

#[inline]
fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
if let Some(inner) = self {
inner.on_enter(id, ctx)
}
}

#[inline]
fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
if let Some(inner) = self {
inner.on_exit(id, ctx)
}
}

#[inline]
fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
if let Some(inner) = self {
inner.on_close(id, ctx)
}
}
}

// === impl Filtered ===

impl<L, F, S> Filtered<L, F, S> {
Expand Down
18 changes: 18 additions & 0 deletions tracing-subscriber/src/layer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,24 @@
//! function pointer. In addition, when more control is required, the [`Filter`]
//! trait may also be implemented for user-defined types.
//!
//! //! [`Option<Filter>`] also implements [`Filter`], which allows for an optional
//! filter. [`None`](Option::None) filters out _nothing_ (that is, allows
//! everything through). For example:
//!
//! ```rust
//! # use tracing_subscriber::{filter::filter_fn, Layer};
//! # use tracing_core::{Metadata, subscriber::Subscriber};
//! # struct MyLayer<S>(std::marker::PhantomData<S>);
//! # impl<S> MyLayer<S> { fn new() -> Self { Self(std::marker::PhantomData)} }
//! # impl<S: Subscriber> Layer<S> for MyLayer<S> {}
//! # fn my_filter(_: &str) -> impl Fn(&Metadata) -> bool { |_| true }
//! fn setup_tracing<S: Subscriber>(filter_config: Option<&str>) {
//! let layer = MyLayer::<S>::new()
//! .with_filter(filter_config.map(|config| filter_fn(my_filter(config))));
//! //...
//! }
//! ```
//!
//! <pre class="compile_fail" style="white-space:normal;font:inherit;">
//! <strong>Warning</strong>: Currently, the <a href="../struct.Registry.html">
//! <code>Registry</code></a> type defined in this crate is the only root
Expand Down
1 change: 1 addition & 0 deletions tracing-subscriber/tests/layer_filters/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mod boxed;
mod downcast_raw;
mod filter_scopes;
mod option;
mod per_event;
mod targets;
mod trees;
Expand Down
149 changes: 149 additions & 0 deletions tracing-subscriber/tests/layer_filters/option.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use super::*;
use tracing::Subscriber;
use tracing_subscriber::{
filter::{self, LevelFilter},
prelude::*,
Layer,
};

fn filter_out_everything<S>() -> filter::DynFilterFn<S> {
// Use dynamic filter fn to disable interest caching and max-level hints,
// allowing us to put all of these tests in the same file.
filter::dynamic_filter_fn(|_, _| false)
}

#[test]
fn option_some() {
let (layer, handle) = layer::mock().only().run_with_handle();
let layer = layer.with_filter(Some(filter_out_everything()));

let _guard = tracing_subscriber::registry().with(layer).set_default();

for i in 0..2 {
tracing::info!(i);
}

handle.assert_finished();
}

#[test]
fn option_none() {
let (layer, handle) = layer::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let layer = layer.with_filter(None::<filter::DynFilterFn<_>>);

let _guard = tracing_subscriber::registry().with(layer).set_default();

for i in 0..2 {
tracing::info!(i);
}

handle.assert_finished();
}

#[test]
fn option_mixed() {
let (layer, handle) = layer::mock()
.event(expect::event())
.only()
.run_with_handle();
let layer = layer
.with_filter(filter::dynamic_filter_fn(|meta, _ctx| {
meta.target() == "interesting"
}))
.with_filter(None::<filter::DynFilterFn<_>>);

let _guard = tracing_subscriber::registry().with(layer).set_default();

tracing::info!(target: "interesting", x="foo");
tracing::info!(target: "boring", x="bar");

handle.assert_finished();
}

/// The lack of a max level hint from a `None` filter should result in no max
/// level hint when combined with other filters/subscribers.
#[test]
fn none_max_level_hint() {
let (layer_some, handle_none) = layer::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let subscribe_none = layer_some.with_filter(None::<filter::DynFilterFn<_>>);
assert!(subscribe_none.max_level_hint().is_none());

let (subscribe_filter_fn, handle_filter_fn) = layer::mock()
.event(expect::event())
.only()
.run_with_handle();
let max_level = Level::INFO;
let subscribe_filter_fn = subscribe_filter_fn.with_filter(
filter::dynamic_filter_fn(move |meta, _| return meta.level() <= &max_level)
.with_max_level_hint(max_level),
);
assert_eq!(
subscribe_filter_fn.max_level_hint(),
Some(LevelFilter::INFO)
);

let subscriber = tracing_subscriber::registry()
.with(subscribe_none)
.with(subscribe_filter_fn);
// The absence of a hint from the `None` filter upgrades the `INFO` hint
// from the filter fn subscriber.
assert!(subscriber.max_level_hint().is_none());

let _guard = subscriber.set_default();
tracing::info!(target: "interesting", x="foo");
tracing::debug!(target: "sometimes_interesting", x="bar");

handle_none.assert_finished();
handle_filter_fn.assert_finished();
}

/// The max level hint from inside a `Some(filter)` filter should be propagated
/// and combined with other filters/subscribers.
#[test]
fn some_max_level_hint() {
let (layer_some, handle_some) = layer::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let layer_some = layer_some.with_filter(Some(
filter::dynamic_filter_fn(move |meta, _| return meta.level() <= &Level::DEBUG)
.with_max_level_hint(Level::DEBUG),
));
assert_eq!(layer_some.max_level_hint(), Some(LevelFilter::DEBUG));

let (subscribe_filter_fn, handle_filter_fn) = layer::mock()
.event(expect::event())
.only()
.run_with_handle();
let subscribe_filter_fn = subscribe_filter_fn.with_filter(
filter::dynamic_filter_fn(move |meta, _| return meta.level() <= &Level::INFO)
.with_max_level_hint(Level::INFO),
);
assert_eq!(
subscribe_filter_fn.max_level_hint(),
Some(LevelFilter::INFO)
);

let subscriber = tracing_subscriber::registry()
.with(layer_some)
.with(subscribe_filter_fn);
// The `DEBUG` hint from the `Some` filter upgrades the `INFO` hint from the
// filter fn subscriber.
assert_eq!(subscriber.max_level_hint(), Some(LevelFilter::DEBUG));

let _guard = subscriber.set_default();
tracing::info!(target: "interesting", x="foo");
tracing::debug!(target: "sometimes_interesting", x="bar");

handle_some.assert_finished();
handle_filter_fn.assert_finished();
}
53 changes: 53 additions & 0 deletions tracing-subscriber/tests/option_filter_interest_caching.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// A separate test crate for `Option<Filter>` for isolation from other tests
// that may influence the interest cache.

use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use tracing_mock::{expect, layer};
use tracing_subscriber::{filter, prelude::*, Layer};

/// A `None` filter should always be interested in events, and it should not
/// needlessly degrade the caching of other filters.
#[test]
fn none_interest_cache() {
let (layer_none, handle_none) = layer::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let layer_none = layer_none.with_filter(None::<filter::DynFilterFn<_>>);

let times_filtered = Arc::new(AtomicUsize::new(0));
let (layer_filter_fn, handle_filter_fn) = layer::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let layer_filter_fn = layer_filter_fn.with_filter(filter::filter_fn({
let times_filtered = Arc::clone(&times_filtered);
move |_| {
times_filtered.fetch_add(1, Ordering::Relaxed);
true
}
}));

let subscriber = tracing_subscriber::registry()
.with(layer_none)
.with(layer_filter_fn);

let _guard = subscriber.set_default();
for _ in 0..2 {
tracing::debug!(target: "always_interesting", x="bar");
}

// The `None` filter is unchanging and performs no filtering, so it should
// be cacheable and always be interested in events. The filter fn is a
// non-dynamic filter fn, which means the result can be cached per callsite.
// The filter fn should only need to be called once, and the `Option` filter
// should not interfere in the caching of that result.
assert_eq!(times_filtered.load(Ordering::Relaxed), 1);
handle_none.assert_finished();
handle_filter_fn.assert_finished();
}

0 comments on commit 5b74376

Please sign in to comment.