Skip to content

Commit facc4e9

Browse files
committed
subscriber: implement per-subscriber filtering (#1523)
## Motivation Currently, filtering with `Subscribe`s is always _global_. If a `Subscribe` performs filtering, it will disable a span or event for _all_ subscribers that compose the current subscriber. In some cases, however, it is often desirable for individual subscribers to see different spans or events than the rest of the `Subscribe` stack. Issues in other projects, such as tokio-rs/console#64 and tokio-rs/console#76, linkerd/linkerd2-proxy#601, influxdata/influxdb_iox#1012 and influxdata/influxdb_iox#1681, jackwaudby/spaghetti#86, etc; as well as `tracing` feature requests like #302, #597, and #1348, all indicate that there is significant demand for the ability to add filters to individual `Subscribe`s. Unfortunately, doing this nicely is somewhat complicated. Although a naive implementation that simply skips events/spans in `Subscribe::on_event` and `Subscribe::new_span` based on some filter is relatively simple, this wouldn't really be an ideal solution, for a number of reasons. A proper per-subscriber filtering implementation would satisfy the following _desiderata_: * If a per-subscriber filter disables a span, it shouldn't be present _for the subscriber that filter is attached to_ when iterating over span contexts (such as `Context::event_scope`, `SpanRef::scope`, etc), or when looking up a span's parents. * When _all_ per-subscriber filters disable a span or event, it should be completely disabled, rather than simply skipped by those particular subscribers. This means that per-subscriber filters should participate in `enabled`, as well as being able to skip spans and events in `new_span` and `on_event`. * Per-subscriber filters shouldn't interfere with non-filtered `Subscribe`s. If a subscriber contains subscribers without any filters, as well as subscribers with per-subscriber filters, the non-filtered `Subscribe`s should behave exactly as they would without any per-subscriber filters present. * Similarly, per-subscriber filters shouldn't interfere with global filtering. If a `Subscribe` in a stack is performing global filtering (e.g. the current filtering behavior), per-subscriber filters should also be effected by the global filter. * Per-subscriber filters _should_ be able to participate in `Interest` caching, _but only when doing so doesn't interfere with non-per-subscriber-filtered subscribers_. * Per-subscriber filters should be _tree-shaped_. If a `Subscriber` consists of multiple subscribers that have been `Subscribeed` together to form new `Subscribe`s, and some of the `Subscribeed` subscribers have per-subscriber filters, those per-subscriber filters should effect all subscribers in that subtree. Similarly, if `Subscribe`s in a per-subscriber filtered subtree have their _own_ per-subscriber filters, those subscribers should be effected by the union of their own filters and any per-subscriber filters that wrap them at higher levels in the tree. Meeting all these requirements means that implementing per-subscriber filtering correctly is somewhat more complex than simply skipping events and spans in a `Subscribe`'s `on_event` and `new_span` callbacks. ## Solution This branch has a basic working implementation of per-subscriber filtering for `tracing-subscriber` v0.2. It should be possible to add this in a point release without a breaking change --- in particular, the new per-subscriber filtering feature _doesn't_ interfere with the global filtering behavior that subscribers currently have when not using the new per-subscriber filtering APIs, so existing configurations should all behave exactly as they do now. ### Implementation The new per-subscriber filtering API consists of a `Filter` trait that defines a filtering strategy and a `Filtered` type that combines a `Filter` with a `Subscribe`. When `enabled` is called on a `Filtered` subscriber, it checks the metadata against the `Filter` and stores whether that filter enabled the span/event in a thread-local cell. If every per-subscriber filter disables a span/event, and there are no non-per-subscriber-filtered subscribers in use, the `Registry`'s `enabled` will return `false`. Otherwise, when the span/event is recorded, each `Filtered` subscriber's `on_event` and `new_span` method checks with the `Registry` to see whether _it_ enabled or disabled that span/event, and skips passing it to the wrapped subscriber if it was disabled. When a span is recorded, the `Registry` which filters disabled it, so that they can skip it when looking up spans in other callbacks. A new `on_subscriber` method was added to the `Subscribe` trait, which allows running a callback when adding a `Subscribe` to a `Subscriber`. This allows mutating both the `Subscribe` and the `Subscriber`, since they are both passed by value when subscribering a `Subscribe` onto a `Subscriber`, and a new `register_filter` method was added to `LookupSpan`, which returns a `FilterId` type. When a `Filtered` subscriber is added to a subscriber that implements `LookupSpan`, it calls `register_filter` in its `on_subscriber` hook to generate a unique ID for its filter. `FilterId` can be used to look up whether a span or event was enabled by the `Filtered` subscriber. Internally, the filter results are stored in a simple bitset to reduce the performance overhead of adding per-subscriber filtering as much as possible. The `FilterId` type is actually a bitmask that's used to set the bit corresponding to a `Filtered` subscriber in that bitmap when it disables a span or event, and check whether the bit is set when querying if a span or event was disabled. Setting a bit in the bitset and testing whether its set is extremely efficient --- it's just a couple of bitwise ops. Additionally, the "filter map" that tracks which filters disabled a span or event is just a single `u64` --- we don't need to allocate a costly `HashSet` or similar every time we want to filter an event. This *does* mean that we are limited to 64 unique filters per `Registry`...however, I would be _very_ surprised if anyone _ever_ builds a subscriber with 64 subscribers, let alone 64 separate per-subscriber filters. Additionally, the `Context` and `SpanRef` types now also potentially carry `FilterId`s, so that they can filter span lookups and span traversals for `Filtered` subscribers. When `Filtered` subscribers are nested using the `Subscribeed`, the `Context` _combines_ the filter ID of the inner and outer subscribers so that spans can be skipped if they were disabled by either. Finally, an implementation of the `Filter` trait was added for `LevelFilter`, and new `FilterFn` and `DynFilterFn` structs were added to produce a `Filter` implementation from a function pointer or closure. ### Notes Some other miscellaneous factors to consider when reviewing this change include: * I did a bit of unrelated refactoring in the `subscriber` module. Since we were adding more code to the `Subscribe` module (the `Filter` trait), the `subscriber.rs` file got significantly longer and was becoming somewhat hard to navigate. Therefore, I split out the `Context` and `Subscribeed` types into their own files. Most of the code in those files is the same as it was before, although some of it was changed in order to support per-subscriber filtering. Apologies in advance to reviewers for this... * In order to test this change, I added some new test-support code in `tracing-subscriber`. The `tracing` test support code provides a `Subscriber` implementation that can be configured to expect a particular set of spans and events, and assert that they occurred as expected. In order to test per-subscriber filtering, I added a new `Subscribe` implementation that does the same thing, but implements `Subscribe` rather than `Subscriber`. This allows testing per-subscriber filter behavior in the same way we test global filter behavior. Reviewers should feel free to just skim the test the test support changes, or skip reviewing them entirely. ## Future Work There is a bunch of additional stuff we can do once this change lands. In order to limit the size of this PR, I didn't make these changes yet, but once this merges, we will want to consider some important follow up changes: * [ ] Currently, _only_ `tracing-subscriber`'s `Registry` is capable of serving as a root subscriber for per-subscriber filters. This is in order to avoid stabilizing a lot of the per-subscriber filtering design as public API without taking the time to ensure there are no serious issues. In the future, we will want to add new APIs to allow users to implement their own root `Subscriber`s that can host subscribers with per-subscriber filtering. * [ ] The `EnvFilter` type doesn't implement the `Filter` trait and thus cannot currently be used as a per-subscriber filter. We should add an implementation of `Filter` for `EnvFilter`. * [ ] The new `FilterFn` and `DynFilterFn` types could conceivably _also_ be used as global filters. We could consider adding `Subscribe` implementations to allow them to be used for global filtering. * [ ] We could consider adding new `Filter` implementations for performing common filtering predicates. For example, "only enable spans/events with a particular target or a set of targets", "only enable spans", "only enable events", etc. Then, we could add a combinator API to combine these predicates to build new filters. * [ ] It would be nice if the `Interest` system could be used to cache the result of multiple individual per-subscriber filter evaluations, so that we don't need to always use `enabled` when there are several per-subscriber filters that disagree on whether or not they want a given span/event. There are a lot of unused bits in `Interest` that could potentially be used for this purpose. However, this would require new API changes in `tracing-core`, and is therefore out of scope for this PR. Closes #302 Closes #597 Closes #1348 Signed-off-by: Eliza Weisman <[email protected]>
1 parent 9329eb3 commit facc4e9

30 files changed

+6152
-1674
lines changed

tracing-mock/src/collector.rs

Lines changed: 56 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use super::{
33
event::MockEvent,
44
field as mock_field,
55
span::{MockSpan, NewSpan},
6-
Parent,
76
};
87
use std::{
98
collections::{HashMap, VecDeque},
@@ -22,7 +21,7 @@ use tracing::{
2221
};
2322

2423
#[derive(Debug, Eq, PartialEq)]
25-
enum Expect {
24+
pub enum Expect {
2625
Event(MockEvent),
2726
Enter(MockSpan),
2827
Exit(MockSpan),
@@ -221,7 +220,8 @@ where
221220
if let Some(name) = expected_span.name() {
222221
assert_eq!(name, span.name);
223222
}
224-
let mut checker = expected_values.checker(format!("span {}: ", span.name));
223+
let context = format!("span {}: ", span.name);
224+
let mut checker = expected_values.checker(&context, &self.name);
225225
values.record(&mut checker);
226226
checker.finish();
227227
}
@@ -234,69 +234,18 @@ where
234234
match self.expected.lock().unwrap().pop_front() {
235235
None => {}
236236
Some(Expect::Event(mut expected)) => {
237-
let spans = self.spans.lock().unwrap();
238-
expected.check(event);
239-
match expected.parent {
240-
Some(Parent::ExplicitRoot) => {
241-
assert!(
242-
event.is_root(),
243-
"[{}] expected {:?} to be an explicit root event",
244-
self.name,
245-
name
246-
);
247-
}
248-
Some(Parent::Explicit(expected_parent)) => {
249-
let actual_parent =
250-
event.parent().and_then(|id| spans.get(id)).map(|s| s.name);
251-
assert_eq!(
252-
Some(expected_parent.as_ref()),
253-
actual_parent,
254-
"[{}] expected {:?} to have explicit parent {:?}",
255-
self.name,
256-
name,
257-
expected_parent,
258-
);
259-
}
260-
Some(Parent::ContextualRoot) => {
261-
assert!(
262-
event.is_contextual(),
263-
"[{}] expected {:?} to have a contextual parent",
264-
self.name,
265-
name
266-
);
267-
assert!(
268-
self.current.lock().unwrap().last().is_none(),
269-
"[{}] expected {:?} to be a root, but we were inside a span",
270-
self.name,
271-
name
272-
);
273-
}
274-
Some(Parent::Contextual(expected_parent)) => {
275-
assert!(
276-
event.is_contextual(),
277-
"[{}] expected {:?} to have a contextual parent",
278-
self.name,
279-
name
280-
);
281-
let stack = self.current.lock().unwrap();
282-
let actual_parent =
283-
stack.last().and_then(|id| spans.get(id)).map(|s| s.name);
284-
assert_eq!(
285-
Some(expected_parent.as_ref()),
286-
actual_parent,
287-
"[{}] expected {:?} to have contextual parent {:?}",
288-
self.name,
289-
name,
290-
expected_parent,
291-
);
292-
}
293-
None => {}
294-
}
237+
let get_parent_name = || {
238+
let stack = self.current.lock().unwrap();
239+
let spans = self.spans.lock().unwrap();
240+
event
241+
.parent()
242+
.and_then(|id| spans.get(id))
243+
.or_else(|| stack.last().and_then(|id| spans.get(id)))
244+
.map(|s| s.name.to_string())
245+
};
246+
expected.check(event, get_parent_name, &self.name);
295247
}
296-
Some(ex) => ex.bad(
297-
&self.name,
298-
format_args!("[{}] observed event {:?}", self.name, event),
299-
),
248+
Some(ex) => ex.bad(&self.name, format_args!("observed event {:#?}", event)),
300249
}
301250
}
302251

@@ -320,70 +269,14 @@ where
320269
let mut spans = self.spans.lock().unwrap();
321270
if was_expected {
322271
if let Expect::NewSpan(mut expected) = expected.pop_front().unwrap() {
323-
let name = meta.name();
324-
expected
325-
.span
326-
.metadata
327-
.check(meta, format_args!("span `{}`", name));
328-
let mut checker = expected.fields.checker(name.to_string());
329-
span.record(&mut checker);
330-
checker.finish();
331-
match expected.parent {
332-
Some(Parent::ExplicitRoot) => {
333-
assert!(
334-
span.is_root(),
335-
"[{}] expected {:?} to be an explicit root span",
336-
self.name,
337-
name
338-
);
339-
}
340-
Some(Parent::Explicit(expected_parent)) => {
341-
let actual_parent =
342-
span.parent().and_then(|id| spans.get(id)).map(|s| s.name);
343-
assert_eq!(
344-
Some(expected_parent.as_ref()),
345-
actual_parent,
346-
"[{}] expected {:?} to have explicit parent {:?}",
347-
self.name,
348-
name,
349-
expected_parent,
350-
);
351-
}
352-
Some(Parent::ContextualRoot) => {
353-
assert!(
354-
span.is_contextual(),
355-
"[{}] expected {:?} to have a contextual parent",
356-
self.name,
357-
name
358-
);
359-
assert!(
360-
self.current.lock().unwrap().last().is_none(),
361-
"[{}] expected {:?} to be a root, but we were inside a span",
362-
self.name,
363-
name
364-
);
365-
}
366-
Some(Parent::Contextual(expected_parent)) => {
367-
assert!(
368-
span.is_contextual(),
369-
"[{}] expected {:?} to have a contextual parent",
370-
self.name,
371-
name
372-
);
373-
let stack = self.current.lock().unwrap();
374-
let actual_parent =
375-
stack.last().and_then(|id| spans.get(id)).map(|s| s.name);
376-
assert_eq!(
377-
Some(expected_parent.as_ref()),
378-
actual_parent,
379-
"[{}] expected {:?} to have contextual parent {:?}",
380-
self.name,
381-
name,
382-
expected_parent,
383-
);
384-
}
385-
None => {}
386-
}
272+
let get_parent_name = || {
273+
let stack = self.current.lock().unwrap();
274+
span.parent()
275+
.and_then(|id| spans.get(id))
276+
.or_else(|| stack.last().and_then(|id| spans.get(id)))
277+
.map(|s| s.name.to_string())
278+
};
279+
expected.check(span, get_parent_name, &self.name);
387280
}
388281
}
389282
spans.insert(
@@ -537,11 +430,15 @@ where
537430
}
538431

539432
impl MockHandle {
433+
pub fn new(expected: Arc<Mutex<VecDeque<Expect>>>, name: String) -> Self {
434+
Self(expected, name)
435+
}
436+
540437
pub fn assert_finished(&self) {
541438
if let Ok(ref expected) = self.0.lock() {
542439
assert!(
543440
!expected.iter().any(|thing| thing != &Expect::Nothing),
544-
"[{}] more notifications expected: {:?}",
441+
"\n[{}] more notifications expected: {:#?}",
545442
self.1,
546443
**expected
547444
);
@@ -550,26 +447,44 @@ impl MockHandle {
550447
}
551448

552449
impl Expect {
553-
fn bad(&self, name: impl AsRef<str>, what: fmt::Arguments<'_>) {
450+
pub fn bad(&self, name: impl AsRef<str>, what: fmt::Arguments<'_>) {
554451
let name = name.as_ref();
555452
match self {
556-
Expect::Event(e) => panic!("[{}] expected event {}, but {} instead", name, e, what,),
557-
Expect::Enter(e) => panic!("[{}] expected to enter {} but {} instead", name, e, what,),
558-
Expect::Exit(e) => panic!("[{}] expected to exit {} but {} instead", name, e, what,),
453+
Expect::Event(e) => panic!(
454+
"\n[{}] expected event {}\n[{}] but instead {}",
455+
name, e, name, what,
456+
),
457+
Expect::Enter(e) => panic!(
458+
"\n[{}] expected to enter {}\n[{}] but instead {}",
459+
name, e, name, what,
460+
),
461+
Expect::Exit(e) => panic!(
462+
"\n[{}] expected to exit {}\n[{}] but instead {}",
463+
name, e, name, what,
464+
),
559465
Expect::CloneSpan(e) => {
560-
panic!("[{}] expected to clone {} but {} instead", name, e, what,)
466+
panic!(
467+
"\n[{}] expected to clone {}\n[{}] but instead {}",
468+
name, e, name, what,
469+
)
561470
}
562471
Expect::DropSpan(e) => {
563-
panic!("[{}] expected to drop {} but {} instead", name, e, what,)
472+
panic!(
473+
"\n[{}] expected to drop {}\n[{}] but instead {}",
474+
name, e, name, what,
475+
)
564476
}
565477
Expect::Visit(e, fields) => panic!(
566-
"[{}] expected {} to record {} but {} instead",
567-
name, e, fields, what,
478+
"\n[{}] expected {} to record {}\n[{}] but instead {}",
479+
name, e, fields, name, what,
480+
),
481+
Expect::NewSpan(e) => panic!(
482+
"\n[{}] expected {}\n[{}] but instead {}",
483+
name, e, name, what
568484
),
569-
Expect::NewSpan(e) => panic!("[{}] expected {} but {} instead", name, e, what),
570485
Expect::Nothing => panic!(
571-
"[{}] expected nothing else to happen, but {} instead",
572-
name, what,
486+
"\n[{}] expected nothing else to happen\n[{}] but {} instead",
487+
name, name, what,
573488
),
574489
}
575490
}

tracing-mock/src/event.rs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
#![allow(missing_docs)]
2-
use super::{field, metadata, Parent};
2+
use super::{field, metadata, span, Parent};
33

44
use std::fmt;
55

66
/// A mock event.
77
///
88
/// This is intended for use with the mock subscriber API in the
99
/// `subscriber` module.
10-
#[derive(Debug, Default, Eq, PartialEq)]
10+
#[derive(Default, Eq, PartialEq)]
1111
pub struct MockEvent {
1212
pub fields: Option<field::Expect>,
1313
pub(crate) parent: Option<Parent>,
14+
in_spans: Vec<span::MockSpan>,
1415
metadata: metadata::Expect,
1516
}
1617

@@ -20,6 +21,10 @@ pub fn mock() -> MockEvent {
2021
}
2122
}
2223

24+
pub fn msg(message: impl fmt::Display) -> MockEvent {
25+
mock().with_fields(field::msg(message))
26+
}
27+
2328
impl MockEvent {
2429
pub fn named<I>(self, name: I) -> Self
2530
where
@@ -78,17 +83,49 @@ impl MockEvent {
7883
}
7984
}
8085

81-
pub(crate) fn check(&mut self, event: &tracing::Event<'_>) {
86+
pub fn check(
87+
&mut self,
88+
event: &tracing::Event<'_>,
89+
get_parent_name: impl FnOnce() -> Option<String>,
90+
collector_name: &str,
91+
) {
8292
let meta = event.metadata();
8393
let name = meta.name();
8494
self.metadata
85-
.check(meta, format_args!("event \"{}\"", name));
86-
assert!(meta.is_event(), "expected {}, but got {:?}", self, event);
95+
.check(meta, format_args!("event \"{}\"", name), collector_name);
96+
assert!(
97+
meta.is_event(),
98+
"[{}] expected {}, but got {:?}",
99+
collector_name,
100+
self,
101+
event
102+
);
87103
if let Some(ref mut expected_fields) = self.fields {
88-
let mut checker = expected_fields.checker(name.to_string());
104+
let mut checker = expected_fields.checker(name, collector_name);
89105
event.record(&mut checker);
90106
checker.finish();
91107
}
108+
109+
if let Some(ref expected_parent) = self.parent {
110+
let actual_parent = get_parent_name();
111+
expected_parent.check_parent_name(
112+
actual_parent.as_deref(),
113+
event.parent().cloned(),
114+
event.metadata().name(),
115+
collector_name,
116+
)
117+
}
118+
}
119+
120+
pub fn in_scope(self, spans: impl IntoIterator<Item = span::MockSpan>) -> Self {
121+
Self {
122+
in_spans: spans.into_iter().collect(),
123+
..self
124+
}
125+
}
126+
127+
pub fn scope_mut(&mut self) -> &mut [span::MockSpan] {
128+
&mut self.in_spans[..]
92129
}
93130
}
94131

@@ -97,3 +134,35 @@ impl fmt::Display for MockEvent {
97134
write!(f, "an event{}", self.metadata)
98135
}
99136
}
137+
138+
impl fmt::Debug for MockEvent {
139+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140+
let mut s = f.debug_struct("MockEvent");
141+
142+
if let Some(ref name) = self.metadata.name {
143+
s.field("name", name);
144+
}
145+
146+
if let Some(ref target) = self.metadata.target {
147+
s.field("target", target);
148+
}
149+
150+
if let Some(ref level) = self.metadata.level {
151+
s.field("level", &format_args!("{:?}", level));
152+
}
153+
154+
if let Some(ref fields) = self.fields {
155+
s.field("fields", fields);
156+
}
157+
158+
if let Some(ref parent) = self.parent {
159+
s.field("parent", &format_args!("{:?}", parent));
160+
}
161+
162+
if !self.in_spans.is_empty() {
163+
s.field("in_spans", &self.in_spans);
164+
}
165+
166+
s.finish()
167+
}
168+
}

0 commit comments

Comments
 (0)