Search code examples
rustrust-tracing

How to combine a collection of tracing subscribers in one?


Let's say I have a function that returns a Vec<Box<dyn Subscriber>> and I want to combine them into a single one to be set as the default subscriber. How to do this?

I'm trying to do something like this, but I cannot make the types match:

pub fn init_log(subscribers: Vec<Box<dyn Subscriber>>) -> Result<(), Error> {
    use tracing_subscriber::prelude::*;
    let mut layers: Vec<Box<dyn Layer<dyn Subscriber>>> = Vec::new();

    for subscriber in subscribers.iter().drain(..) {
        let layer: Box<dyn Layer<dyn Subscriber>> =
          Box::new(tracing_subscriber::layer::Identity::new().with_subscriber(subscriber));

        layers.push(layer);
    }

    let init_layer: Box<dyn Layer<dyn Subscriber>> =
        Box::new(tracing_subscriber::layer::Identity::new());

    let acc_subscriber: Layered<Box<dyn Layer<dyn Subscriber>>, _> =
        tracing_subscriber::fmt().finish().with(init_layer);
    
    let composed = layers
        .drain(..)
        .into_iter()
        .fold(acc_subscriber, |acc, layer| acc.with(layer));
    
    tracing::subscriber::set_global_default(composed);

    Ok(())
}
error[E0277]: the trait bound `Box<dyn tracing::Subscriber>: tracing::Subscriber` is not satisfied
  --> jormungandr\src\settings\logging.rs:97:85
   |
97 |                 Box::new(tracing_subscriber::layer::Identity::new().with_subscriber(subscriber));
   |                                                                                     ^^^^^^^^^^ the trait `tracing::Subscriber` is not implemented for `Box<dyn tracing::Subscriber>`

Solution

  • I think this is fundamentally at odds with what a Subscriber is tasked with handling. I think the module documentation for Layers puts it clearly:

    The Subscriber trait in tracing-core represents the complete set of functionality required to consume tracing instrumentation. This means that a single Subscriber instance is a self-contained implementation of a complete strategy for collecting traces; but it also means that the Subscriber trait cannot easily be composed with other Subscribers.

    In particular, Subscribers are responsible for generating span IDs and assigning them to spans. Since these IDs must uniquely identify a span within the context of the current trace, this means that there may only be a single Subscriber for a given thread at any point in time — otherwise, there would be no authoritative source of span IDs.

    On the other hand, the majority of the Subscriber trait’s functionality is composable: any number of subscribers may observe events, span entry and exit, and so on, provided that there is a single authoritative source of span IDs. The Layer trait represents this composable subset of the Subscriber behavior; it can observe events and spans, but does not assign IDs.

    So if at all possible, try to generalize your logging configuration over Layers instead of Subscribers. There is even a impl Layer for Vec<L> where L: Layer implementation already.

    I think this expressibility and composiblity is the reason why most Related Crates are implemented as Layers instead of Subscribers. Even some of those listed as "implements a subscriber" use layers (like tracing-gelf or tracing-forest).