Skip to content

Commit

Permalink
subscriber: Implement subscribe::Filter for Option<Filter> (#2407)
Browse files Browse the repository at this point in the history
## Motivation

It's currently awkward to have an optional per-subscriber filter.

## Solution

Implement `Filter<S>` for `Option<F> where F: Filter<S>`, following the example
of `Subscribe`. 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 authored Sep 28, 2023
1 parent cf8a8c5 commit 62d57a6
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 3 deletions.
99 changes: 99 additions & 0 deletions tracing-subscriber/src/filter/subscriber_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> subscribe::Filter<S> for Box<dyn subscribe::Filter<S> + Send + Sync + 's
filter_impl_body!();
}

// Implement Filter for Option<Filter> where None => allow
#[cfg(feature = "registry")]
#[cfg_attr(docsrs, doc(cfg(feature = "registry")))]
impl<F, S> subscribe::Filter<S> for Option<F>
where
F: subscribe::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<S, F, C> Filtered<S, F, C> {
Expand Down
24 changes: 21 additions & 3 deletions tracing-subscriber/src/subscribe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,30 @@
//!
//! This crate's [`filter`] module provides a number of types which implement
//! the [`Filter`] trait, such as [`LevelFilter`], [`Targets`], and
//! [`FilterFn`]. These [`Filter`]s provide ready-made implementations of
//! common forms of filtering. For custom filtering policies, the [`FilterFn`]
//! and [`DynFilterFn`] types allow implementing a [`Filter`] with a closure or
//! [`FilterFn`]. These [`Filter`]s provide ready-made implementations of common
//! forms of filtering. For custom filtering policies, the [`FilterFn`] and
//! [`DynFilterFn`] types allow implementing a [`Filter`] with a closure or
//! 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, Subscribe};
//! # use tracing_core::{Metadata, collect::Collect};
//! # struct MySubscriber<C>(std::marker::PhantomData<C>);
//! # impl<C> MySubscriber<C> { fn new() -> Self { Self(std::marker::PhantomData)} }
//! # impl<C: Collect> Subscribe<C> for MySubscriber<C> {}
//! # fn my_filter(_: &str) -> impl Fn(&Metadata) -> bool { |_| true }
//! fn setup_tracing<C: Collect>(filter_config: Option<&str>) {
//! let layer = MySubscriber::<C>::new()
//! .with_filter(filter_config.map(|config| filter_fn(my_filter(config))));
//! //...
//! }
//! ```
//!
//! <div class="example-wrap" style="display:inline-block">
//! <pre class="compile_fail" style="white-space:normal;font:inherit;">
//! <strong>Warning</strong>: Currently, the <a href="../struct.Registry.html">
Expand Down
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, subscriber};
use tracing_subscriber::{filter, prelude::*, Subscribe};

/// 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 (subscribe_none, handle_none) = subscriber::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let subscribe_none = subscribe_none.with_filter(None::<filter::DynFilterFn<_>>);

let times_filtered = Arc::new(AtomicUsize::new(0));
let (subscribe_filter_fn, handle_filter_fn) = subscriber::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let subscribe_filter_fn = subscribe_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(subscribe_none)
.with(subscribe_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();
}
1 change: 1 addition & 0 deletions tracing-subscriber/tests/subscriber_filters/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![cfg(feature = "registry")]
mod filter_scopes;
mod option;
mod per_event;
mod targets;
mod trees;
Expand Down
149 changes: 149 additions & 0 deletions tracing-subscriber/tests/subscriber_filters/option.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use super::*;
use tracing::Collect;
use tracing_subscriber::{
filter::{self, LevelFilter},
prelude::*,
Subscribe,
};

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 (subscribe, handle) = subscriber::mock().only().run_with_handle();
let subscribe = subscribe.with_filter(Some(filter_out_everything()));

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

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

handle.assert_finished();
}

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

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

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

handle.assert_finished();
}

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

let _guard = tracing_subscriber::registry().with(subscribe).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 (subscribe_none, handle_none) = subscriber::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let subscribe_none = subscribe_none.with_filter(None::<filter::DynFilterFn<_>>);
assert!(subscribe_none.max_level_hint().is_none());

let (subscribe_filter_fn, handle_filter_fn) = subscriber::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 (subscribe_some, handle_some) = subscriber::mock()
.event(expect::event())
.event(expect::event())
.only()
.run_with_handle();
let subscribe_some = subscribe_some.with_filter(Some(
filter::dynamic_filter_fn(move |meta, _| return meta.level() <= &Level::DEBUG)
.with_max_level_hint(Level::DEBUG),
));
assert_eq!(subscribe_some.max_level_hint(), Some(LevelFilter::DEBUG));

let (subscribe_filter_fn, handle_filter_fn) = subscriber::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(subscribe_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();
}

0 comments on commit 62d57a6

Please sign in to comment.