Skip to content

Commit 2a5da8d

Browse files
authored
core: change Interest-combining to play nicer with multiple subscribers (#927)
## Motivation Currently, when multiple subscribers are in use, the way that `Interest` values are combined results in surprising behavior. If _any_ subscriber returns `Interest::always` for a callsite, that interest currently takes priority over any other `Interest`s. This means that if two threads have separate subscribers, one of which enables a callsite `always`, and the other `never`, _both_ subscribers will always record that callsite, without the option to disable it. This is not quite right. Instead, we should check whether the current subscriber will enable that callsite in this case. This issue is described in #902. ## Solution This branch changes the rules for combining `Interest`s so that `always` is only returned if _both_ `Interest`s are `always`. If only _one_ is `always`, the combined interest is now `sometimes`, instead. This means that when multiple subscribers exist, `enabled` will always be called on the _current_ subscriber, except when _all_ subscribers have indicated that they are `always` or `never` interested in a callsite. I've added tests that reproduce the issues with leaky filters. Fixing this revealed an additional issue where `tracing-subscriber`'s `EnvFilter` assumes that `enabled` is only called for events, and never for spans, because it always returns either `always` or `never` from `register_callsite` for spans. Therefore, the dynamic span matching directives that might enable a span were never checked in `enabled`. However, under the new interest-combining rules, `enabled` *may* still be called even if a subscriber returns an `always` or `never` interest, if *another* subscriber exists that returned an incompatible interest. This PR fixes that, as well. Depends on #919 This was previously approved by @yaahc as PR #920, but I accidentally merged it into #919 _after_ that branch merged, rather than into master (my bad!). Signed-off-by: Eliza Weisman <[email protected]>
1 parent c2e5772 commit 2a5da8d

File tree

4 files changed

+116
-27
lines changed

4 files changed

+116
-27
lines changed

tracing-core/src/callsite.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,21 @@ impl Registry {
2828
fn rebuild_callsite_interest(&self, callsite: &'static dyn Callsite) {
2929
let meta = callsite.metadata();
3030

31-
let mut interest = Interest::never();
32-
33-
for registrar in &self.dispatchers {
34-
if let Some(sub_interest) = registrar.try_register(meta) {
35-
interest = interest.and(sub_interest);
36-
}
37-
}
31+
// Iterate over the subscribers in the registry, and — if they are
32+
// active — register the callsite with them.
33+
let mut interests = self
34+
.dispatchers
35+
.iter()
36+
.filter_map(|registrar| registrar.try_register(meta));
37+
38+
// Use the first subscriber's `Interest` as the base value.
39+
let interest = if let Some(interest) = interests.next() {
40+
// Combine all remaining `Interest`s.
41+
interests.fold(interest, Interest::and)
42+
} else {
43+
// If nobody was interested in this thing, just return `never`.
44+
Interest::never()
45+
};
3846

3947
callsite.set_interest(interest)
4048
}

tracing-core/src/subscriber.rs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -556,24 +556,15 @@ impl Interest {
556556

557557
/// Returns the common interest between these two Interests.
558558
///
559-
/// The common interest is defined as the least restrictive, so if one
560-
/// interest is `never` and the other is `always` the common interest is
561-
/// `always`.
559+
/// If both interests are the same, this propagates that interest.
560+
/// Otherwise, if they differ, the result must always be
561+
/// `Interest::sometimes` --- if the two subscribers differ in opinion, we
562+
/// will have to ask the current subscriber what it thinks, no matter what.
562563
pub(crate) fn and(self, rhs: Interest) -> Self {
563-
match rhs.0 {
564-
// If the added interest is `never()`, don't change anything —
565-
// either a different subscriber added a higher interest, which we
566-
// want to preserve, or the interest is 0 anyway (as it's
567-
// initialized to 0).
568-
InterestKind::Never => self,
569-
// If the interest is `sometimes()`, that overwrites a `never()`
570-
// interest, but doesn't downgrade an `always()` interest.
571-
InterestKind::Sometimes if self.0 == InterestKind::Never => rhs,
572-
// If the interest is `always()`, we overwrite the current interest,
573-
// as always() is the highest interest level and should take
574-
// precedent.
575-
InterestKind::Always => rhs,
576-
_ => self,
564+
if self.0 == rhs.0 {
565+
self
566+
} else {
567+
Interest::sometimes()
577568
}
578569
}
579570
}

tracing-subscriber/src/filter/env/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,19 @@ impl<S: Subscriber> Layer<S> for EnvFilter {
313313
// if not, we can avoid the thread local access + iterating over the
314314
// spans in the current scope.
315315
if self.has_dynamics && self.dynamics.max_level >= *level {
316+
if metadata.is_span() {
317+
// If the metadata is a span, see if we care about its callsite.
318+
let enabled_by_cs = self
319+
.by_cs
320+
.read()
321+
.ok()
322+
.map(|by_cs| by_cs.contains_key(&metadata.callsite()))
323+
.unwrap_or(false);
324+
if enabled_by_cs {
325+
return true;
326+
}
327+
}
328+
316329
let enabled_by_scope = SCOPE.with(|scope| {
317330
for filter in scope.borrow().iter() {
318331
if filter >= level {
@@ -330,9 +343,6 @@ impl<S: Subscriber> Layer<S> for EnvFilter {
330343
if self.statics.max_level >= *level {
331344
// Otherwise, fall back to checking if the callsite is
332345
// statically enabled.
333-
// TODO(eliza): we *might* want to check this only if the `log`
334-
// feature is enabled, since if this is a `tracing` event with a
335-
// real callsite, it would already have been statically enabled...
336346
return self.statics.enabled(metadata);
337347
}
338348

tracing/tests/filters_dont_leak.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#![cfg(feature = "std")]
2+
3+
mod support;
4+
use self::support::*;
5+
6+
#[test]
7+
fn spans_dont_leak() {
8+
fn do_span() {
9+
let span = tracing::debug_span!("alice");
10+
let _e = span.enter();
11+
}
12+
13+
let (subscriber, handle) = subscriber::mock()
14+
.named("spans/subscriber1")
15+
.with_filter(|_| false)
16+
.done()
17+
.run_with_handle();
18+
19+
let _guard = tracing::subscriber::set_default(subscriber);
20+
21+
do_span();
22+
23+
let alice = span::mock().named("alice");
24+
let (subscriber2, handle2) = subscriber::mock()
25+
.named("spans/subscriber2")
26+
.with_filter(|_| true)
27+
.new_span(alice.clone())
28+
.enter(alice.clone())
29+
.exit(alice.clone())
30+
.drop_span(alice)
31+
.done()
32+
.run_with_handle();
33+
34+
tracing::subscriber::with_default(subscriber2, || {
35+
println!("--- subscriber 2 is default ---");
36+
do_span()
37+
});
38+
39+
println!("--- subscriber 1 is default ---");
40+
do_span();
41+
42+
handle.assert_finished();
43+
handle2.assert_finished();
44+
}
45+
46+
#[test]
47+
fn events_dont_leak() {
48+
fn do_event() {
49+
tracing::debug!("alice");
50+
}
51+
52+
let (subscriber, handle) = subscriber::mock()
53+
.named("events/subscriber1")
54+
.with_filter(|_| false)
55+
.done()
56+
.run_with_handle();
57+
58+
let _guard = tracing::subscriber::set_default(subscriber);
59+
60+
do_event();
61+
62+
let (subscriber2, handle2) = subscriber::mock()
63+
.named("events/subscriber2")
64+
.with_filter(|_| true)
65+
.event(event::mock())
66+
.done()
67+
.run_with_handle();
68+
69+
tracing::subscriber::with_default(subscriber2, || {
70+
println!("--- subscriber 2 is default ---");
71+
do_event()
72+
});
73+
74+
println!("--- subscriber 1 is default ---");
75+
76+
do_event();
77+
78+
handle.assert_finished();
79+
handle2.assert_finished();
80+
}

0 commit comments

Comments
 (0)