Skip to content

Commit a3e7944

Browse files
authored
subscriber: add Subscribe impl for Vec<S: Subscribe> (#2027)
Depends on #2028 ## Motivation In many cases, it is desirable to have a variable number of subscribers at runtime (such as when reading a logging configuration from a config file). The current approach, where types implementing `Subscribe` are composed at the type level into `Layered` subscribers, doesn't work well when the number of subscribers varies at runtime. ## Solution To solve this, this branch adds a `Subscribe` implementation for `Vec<S> where S: Subscribe`. This allows variable-length lists of subscribers to be added to a collector. Although the impl for `Vec<S>` requires all the subscribers to be the same type, it can also be used in conjunction with `Box<dyn Subscribe<C> + ...>` trait objects to implement a variable-length list of subscribers of multiple types. I also wrote a bunch of docs examples. ## Notes Alternatively, we could have a separate type defined in `tracing-subscriber` for a variable-length list of type-erased subscribers. This would have one primary usability benefit, which is that we could have a `push` operation that takes an `impl Subscribe` and boxes it automatically. However, I thought the approach used here is nicer, since it's based on composing together existing primitives such as `Vec` and `Box`, rather than adding a whole new API. Additionally, it allows avoiding the additional `Box`ing in the case where the list consists of subscribers that are all the same type. Closes #1708 Signed-off-by: Eliza Weisman <eliza@buoyant.io>
1 parent f58019e commit a3e7944

File tree

4 files changed

+451
-1
lines changed

4 files changed

+451
-1
lines changed

tracing-subscriber/src/subscribe/mod.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,111 @@
269269
//! The [`Subscribe::boxed`] method is provided to make boxing a subscriber
270270
//! more convenient, but [`Box::new`] may be used as well.
271271
//!
272+
//! When the number of subscribers varies at runtime, note that a
273+
//! [`Vec<S> where S: Subscribe` also implements `Subscribe`][vec-impl]. This
274+
//! can be used to add a variable number of subscribers to a collector:
275+
//!
276+
//! ```
277+
//! use tracing_subscriber::{Subscribe, prelude::*};
278+
//! struct MySubscriber {
279+
//! // ...
280+
//! }
281+
//! # impl MySubscriber { fn new() -> Self { Self {} }}
282+
//!
283+
//! impl<C: tracing_core::Collect> Subscribe<C> for MySubscriber {
284+
//! // ...
285+
//! }
286+
//!
287+
//! /// Returns how many subscribers we need
288+
//! fn how_many_subscribers() -> usize {
289+
//! // ...
290+
//! # 3
291+
//! }
292+
//!
293+
//! // Create a variable-length `Vec` of subscribers
294+
//! let mut subscribers = Vec::new();
295+
//! for _ in 0..how_many_subscribers() {
296+
//! subscribers.push(MySubscriber::new());
297+
//! }
298+
//!
299+
//! tracing_subscriber::registry()
300+
//! .with(subscribers)
301+
//! .init();
302+
//! ```
303+
//!
304+
//! If a variable number of subscribers is needed and those subscribers have
305+
//! different types, a `Vec` of [boxed subscriber trait objects][box-impl] may
306+
//! be used. For example:
307+
//!
308+
//! ```
309+
//! use tracing_subscriber::{filter::LevelFilter, Subscribe, prelude::*};
310+
//! use std::fs::File;
311+
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
312+
//! struct Config {
313+
//! enable_log_file: bool,
314+
//! enable_stdout: bool,
315+
//! enable_stderr: bool,
316+
//! // ...
317+
//! }
318+
//! # impl Config {
319+
//! # fn from_config_file()-> Result<Self, Box<dyn std::error::Error>> {
320+
//! # // don't enable the log file so that the example doesn't actually create it
321+
//! # Ok(Self { enable_log_file: false, enable_stdout: true, enable_stderr: true })
322+
//! # }
323+
//! # }
324+
//!
325+
//! let cfg = Config::from_config_file()?;
326+
//!
327+
//! // Based on our dynamically loaded config file, create any number of subscribers:
328+
//! let mut subscribers = Vec::new();
329+
//!
330+
//! if cfg.enable_log_file {
331+
//! let file = File::create("myapp.log")?;
332+
//! let subscriber = tracing_subscriber::fmt::subscriber()
333+
//! .with_thread_names(true)
334+
//! .with_target(true)
335+
//! .json()
336+
//! .with_writer(file)
337+
//! // Box the subscriber as a type-erased trait object, so that it can
338+
//! // be pushed to the `Vec`.
339+
//! .boxed();
340+
//! subscribers.push(subscriber);
341+
//! }
342+
//!
343+
//! if cfg.enable_stdout {
344+
//! let subscriber = tracing_subscriber::fmt::subscriber()
345+
//! .pretty()
346+
//! .with_filter(LevelFilter::INFO)
347+
//! // Box the subscriber as a type-erased trait object, so that it can
348+
//! // be pushed to the `Vec`.
349+
//! .boxed();
350+
//! subscribers.push(subscriber);
351+
//! }
352+
//!
353+
//! if cfg.enable_stdout {
354+
//! let subscriber = tracing_subscriber::fmt::subscriber()
355+
//! .with_target(false)
356+
//! .with_filter(LevelFilter::WARN)
357+
//! // Box the subscriber as a type-erased trait object, so that it can
358+
//! // be pushed to the `Vec`.
359+
//! .boxed();
360+
//! subscribers.push(subscriber);
361+
//! }
362+
//!
363+
//! tracing_subscriber::registry()
364+
//! .with(subscribers)
365+
//! .init();
366+
//!# Ok(()) }
367+
//! ```
368+
//!
369+
//! Finally, if the number of subscribers _changes_ at runtime, a `Vec` of
370+
//! subscribers can be used alongside the [`reload`](crate::reload) module to
371+
//! add or remove subscribers dynamically at runtime.
372+
//!
272373
//! [prelude]: crate::prelude
273374
//! [option-impl]: crate::subscribe::Subscribe#impl-Subscribe<C>-for-Option<S>
274375
//! [box-impl]: Subscribe#impl-Subscribe%3CC%3E-for-Box%3Cdyn%20Subscribe%3CC%3E%20+%20Send%20+%20Sync%20+%20%27static%3E
376+
//! [vec-impl]: Subscribe#impl-Subscribe<C>-for-Vec<S>
275377
//!
276378
//! # Recording Traces
277379
//!
@@ -1487,6 +1589,119 @@ feature! {
14871589
{
14881590
subscriber_impl_body! {}
14891591
}
1592+
1593+
1594+
impl<C, S> Subscribe<C> for Vec<S>
1595+
where
1596+
S: Subscribe<C>,
1597+
C: Collect,
1598+
{
1599+
1600+
fn on_subscribe(&mut self, collector: &mut C) {
1601+
for s in self {
1602+
s.on_subscribe(collector);
1603+
}
1604+
}
1605+
1606+
fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
1607+
// Return highest level of interest.
1608+
let mut interest = Interest::never();
1609+
for s in self {
1610+
let new_interest = s.register_callsite(metadata);
1611+
if (interest.is_sometimes() && new_interest.is_always())
1612+
|| (interest.is_never() && !new_interest.is_never())
1613+
{
1614+
interest = new_interest;
1615+
}
1616+
}
1617+
1618+
interest
1619+
}
1620+
1621+
fn enabled(&self, metadata: &Metadata<'_>, ctx: Context<'_, C>) -> bool {
1622+
self.iter().all(|s| s.enabled(metadata, ctx.clone()))
1623+
}
1624+
1625+
fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, C>) {
1626+
for s in self {
1627+
s.on_new_span(attrs, id, ctx.clone());
1628+
}
1629+
}
1630+
1631+
fn max_level_hint(&self) -> Option<LevelFilter> {
1632+
let mut max_level = LevelFilter::ERROR;
1633+
for s in self {
1634+
// NOTE(eliza): this is slightly subtle: if *any* subscriber
1635+
// returns `None`, we have to return `None`, assuming there is
1636+
// no max level hint, since that particular subscriber cannot
1637+
// provide a hint.
1638+
let hint = s.max_level_hint()?;
1639+
max_level = core::cmp::max(hint, max_level);
1640+
}
1641+
Some(max_level)
1642+
}
1643+
1644+
fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, C>) {
1645+
for s in self {
1646+
s.on_record(span, values, ctx.clone())
1647+
}
1648+
}
1649+
1650+
fn on_follows_from(&self, span: &span::Id, follows: &span::Id, ctx: Context<'_, C>) {
1651+
for s in self {
1652+
s.on_follows_from(span, follows, ctx.clone());
1653+
}
1654+
}
1655+
1656+
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, C>) {
1657+
for s in self {
1658+
s.on_event(event, ctx.clone());
1659+
}
1660+
}
1661+
1662+
fn on_enter(&self, id: &span::Id, ctx: Context<'_, C>) {
1663+
for s in self {
1664+
s.on_enter(id, ctx.clone());
1665+
}
1666+
}
1667+
1668+
fn on_exit(&self, id: &span::Id, ctx: Context<'_, C>) {
1669+
for s in self {
1670+
s.on_exit(id, ctx.clone());
1671+
}
1672+
}
1673+
1674+
fn on_close(&self, id: span::Id, ctx: Context<'_, C>) {
1675+
for s in self {
1676+
s.on_close(id.clone(), ctx.clone());
1677+
}
1678+
}
1679+
1680+
#[doc(hidden)]
1681+
unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
1682+
// If downcasting to `Self`, return a pointer to `self`.
1683+
if id == TypeId::of::<Self>() {
1684+
return Some(NonNull::from(self).cast());
1685+
}
1686+
1687+
// Someone is looking for per-subscriber filters. But, this `Vec`
1688+
// might contain subscribers with per-subscriber filters *and*
1689+
// subscribers without filters. It should only be treated as a
1690+
// per-subscriber-filtered subscriber if *all* its subscribers have
1691+
// per-subscriber filters.
1692+
// XXX(eliza): it's a bummer we have to do this linear search every
1693+
// time. It would be nice if this could be cached, but that would
1694+
// require replacing the `Vec` impl with an impl for a newtype...
1695+
if filter::is_psf_downcast_marker(id) && self.iter().any(|s| s.downcast_raw(id).is_none()) {
1696+
return None;
1697+
}
1698+
1699+
// Otherwise, return the first child of `self` that downcaaasts to
1700+
// the selected type, if any.
1701+
// XXX(eliza): hope this is reasonable lol
1702+
self.iter().find_map(|s| s.downcast_raw(id))
1703+
}
1704+
}
14901705
}
14911706

14921707
// === impl CollectExt ===

tracing-subscriber/tests/subscriber_filters/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ use self::support::*;
55
mod filter_scopes;
66
mod targets;
77
mod trees;
8+
mod vec;
89

910
use tracing::{level_filters::LevelFilter, Level};
10-
use tracing_subscriber::{filter, prelude::*};
11+
use tracing_subscriber::{filter, prelude::*, Subscribe};
1112

1213
#[test]
1314
fn basic_subscriber_filters() {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use super::*;
2+
use tracing::Collect;
3+
4+
#[test]
5+
fn with_filters_unboxed() {
6+
let (trace_subscriber, trace_handle) = subscriber::named("trace")
7+
.event(event::mock().at_level(Level::TRACE))
8+
.event(event::mock().at_level(Level::DEBUG))
9+
.event(event::mock().at_level(Level::INFO))
10+
.done()
11+
.run_with_handle();
12+
let trace_subscriber = trace_subscriber.with_filter(LevelFilter::TRACE);
13+
14+
let (debug_subscriber, debug_handle) = subscriber::named("debug")
15+
.event(event::mock().at_level(Level::DEBUG))
16+
.event(event::mock().at_level(Level::INFO))
17+
.done()
18+
.run_with_handle();
19+
let debug_subscriber = debug_subscriber.with_filter(LevelFilter::DEBUG);
20+
21+
let (info_subscriber, info_handle) = subscriber::named("info")
22+
.event(event::mock().at_level(Level::INFO))
23+
.done()
24+
.run_with_handle();
25+
let info_subscriber = info_subscriber.with_filter(LevelFilter::INFO);
26+
27+
let _collector = tracing_subscriber::registry()
28+
.with(vec![trace_subscriber, debug_subscriber, info_subscriber])
29+
.set_default();
30+
31+
tracing::trace!("hello trace");
32+
tracing::debug!("hello debug");
33+
tracing::info!("hello info");
34+
35+
trace_handle.assert_finished();
36+
debug_handle.assert_finished();
37+
info_handle.assert_finished();
38+
}
39+
40+
#[test]
41+
fn with_filters_boxed() {
42+
let (unfiltered_subscriber, unfiltered_handle) = subscriber::named("unfiltered")
43+
.event(event::mock().at_level(Level::TRACE))
44+
.event(event::mock().at_level(Level::DEBUG))
45+
.event(event::mock().at_level(Level::INFO))
46+
.done()
47+
.run_with_handle();
48+
let unfiltered_subscriber = unfiltered_subscriber.boxed();
49+
50+
let (debug_subscriber, debug_handle) = subscriber::named("debug")
51+
.event(event::mock().at_level(Level::DEBUG))
52+
.event(event::mock().at_level(Level::INFO))
53+
.done()
54+
.run_with_handle();
55+
let debug_subscriber = debug_subscriber.with_filter(LevelFilter::DEBUG).boxed();
56+
57+
let (target_subscriber, target_handle) = subscriber::named("target")
58+
.event(event::mock().at_level(Level::INFO))
59+
.done()
60+
.run_with_handle();
61+
let target_subscriber = target_subscriber
62+
.with_filter(filter::filter_fn(|meta| meta.target() == "my_target"))
63+
.boxed();
64+
65+
let _collector = tracing_subscriber::registry()
66+
.with(vec![
67+
unfiltered_subscriber,
68+
debug_subscriber,
69+
target_subscriber,
70+
])
71+
.set_default();
72+
73+
tracing::trace!("hello trace");
74+
tracing::debug!("hello debug");
75+
tracing::info!(target: "my_target", "hello my target");
76+
77+
unfiltered_handle.assert_finished();
78+
debug_handle.assert_finished();
79+
target_handle.assert_finished();
80+
}
81+
82+
#[test]
83+
fn mixed_max_level_hint() {
84+
let unfiltered = subscriber::named("unfiltered").run().boxed();
85+
let info = subscriber::named("info")
86+
.run()
87+
.with_filter(LevelFilter::INFO)
88+
.boxed();
89+
let debug = subscriber::named("debug")
90+
.run()
91+
.with_filter(LevelFilter::DEBUG)
92+
.boxed();
93+
94+
let collector = tracing_subscriber::registry().with(vec![unfiltered, info, debug]);
95+
96+
assert_eq!(collector.max_level_hint(), None);
97+
}
98+
99+
#[test]
100+
fn all_filtered_max_level_hint() {
101+
let warn = subscriber::named("warn")
102+
.run()
103+
.with_filter(LevelFilter::WARN)
104+
.boxed();
105+
let info = subscriber::named("info")
106+
.run()
107+
.with_filter(LevelFilter::INFO)
108+
.boxed();
109+
let debug = subscriber::named("debug")
110+
.run()
111+
.with_filter(LevelFilter::DEBUG)
112+
.boxed();
113+
114+
let collector = tracing_subscriber::registry().with(vec![warn, info, debug]);
115+
116+
assert_eq!(collector.max_level_hint(), Some(LevelFilter::DEBUG));
117+
}

0 commit comments

Comments
 (0)