Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to create a Registry with a dynamic number of Layers #1708

Closed
futile opened this issue Nov 4, 2021 · 9 comments · Fixed by #2027
Closed

Unable to create a Registry with a dynamic number of Layers #1708

futile opened this issue Nov 4, 2021 · 9 comments · Fixed by #2027

Comments

@futile
Copy link

futile commented Nov 4, 2021

Feature Request

Crates

Probably at least tracing-subscriber, maybe also others (tracing-core?).

Motivation

I would like to use a dynamic number of layers with a Registry in order to configure (at startup-time) the set of output files and of what goes where.

So something like this:

let mut registry = tracing_subscriber::registry();

for (file, filter) in &log_settings {
  registry = registry.with(
    fmt::layer()
      .with_writer(Arc::new(file))
      .with_filter(filter)
  )
}

registry.init();

Proposal

No real idea, the issue seems to be that every SubscriberExt::with() call returns a different type, and so this can't be made dynamic.

While not a solution, this issue seems to run into similar problems: #575

Edit: Maybe something like impl Layer for Vec<Box<dyn Layer>> could work? Then such a Vec could be added to the registry with SubscriberExt::with().

Alternatives

I tried doing this with a form of type-erased objects, but this doesn't work:

trait RegistrySupertrait: SubscriberExt + SubscriberInitExt {}
impl<T: SubscriberExt + SubscriberInitExt> RegistrySupertrait for T {}

let mut registry: Box<dyn RegistrySupertrait> = tracing_subscriber::registry();

...

registry.init();

Fails because SubscriberInitExt is not object-safe. I also tried with Into<Dispatch> instead of SubscriberInitExt (which can be converted automatically), but Into<Dispatch> is also not object-safe, probably because core::convert::Into<T> requires Self: Sized (note: std::convert::Into does not show this bound, but it actually seems to be the same type as core::convert::Into, and thus seems to require it as well).

Is this possible somehow/am I missing something?

@hawkw
Copy link
Member

hawkw commented Nov 4, 2021

Edit: Maybe something like impl Layer for Vec<Box<dyn Layer>> could work? Then such a Vec could be added to the registry with SubscriberExt::with().

There was previously a discussion on discord about something like this, where I suggested that the user implement a type like

pub struct DynLayerList<S>(Vec<Box<dyn Layer<S> + Send + Sync + 'static>>);

impl<S> Layer<S> for DynLayerList<S> {
    fn on_event(&self, event: Event<'_>, cx: Context<'_, S>) {
         for layer in self.0 {
             layer.on_event(event, cx);
         }
    }
    
    // and then do this for every other layer callback...
}

and, I believe that solved his problem. So, a solution like that should work for you right now.

If we added it in tracing-subscriber, we could probably have a direct impl Layer<S> for Vec<Box<dyn Layer<S> + Send + Sync + 'static>, rather than for a newtype wrapping a Vec (which is required in other crates due to the orphan rules). I'd be willing to consider adding something like that.

@futile
Copy link
Author

futile commented Nov 4, 2021

Yeah, just came to basically the same solution. Took a while to get the + Send + Sync bounds figured out (but seems to work without 'static for me, dunno why).

Thanks for the quick reply! I'll see if I can whip up a PR once I got it working.

@steveocr
Copy link

This solution/workaround seems to break per-layer filtering. Is that expected? Or have I missed something?

@hawkw
Copy link
Member

hawkw commented Jan 10, 2022

@steveocr

This solution/workaround seems to break per-layer filtering. Is that expected? Or have I missed something?

In what way does it break per-layer filtering --- do you mean that when a per-layer filter is added to a Layer in a vector of Layers, it doesn't get its per-layer applied?

Are you able to share your implementation? Theoretically, I think should be possible to implement a dynamic list of Layers that doesn't break per-layer filters, but it's possible your implementation isn't doing the right thing...that might be a good motivation for providing a first-party implementation of this in tracing-subscriber, so that others don't have this problem...

@steveocr
Copy link

I wasn't sure if all of the methods needed to be implemented. I had the same behaviour before and after overriding the default implementations (except for maybe the max_level_hint())

pub struct DynLayerList<S>(Vec<Box<dyn Layer<S> + Send + Sync + 'static>>);

impl<S> Layer<S> for DynLayerList<S>
where
    S: Subscriber,
{
    fn on_layer(&mut self, subscriber: &mut S) {
        for layer in &mut self.0 {
            layer.on_layer(subscriber);
        }
    }

    fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
        // Return highest level of interest.
        let mut interest = Interest::never();
        for layer in &self.0 {
            let new_interest = layer.register_callsite(metadata);
            if (interest.is_sometimes() && new_interest.is_always())
                || (interest.is_never() && !new_interest.is_never())
            {
                interest = new_interest;
            }
        }
        interest
    }

    fn enabled(
        &self,
        metadata: &Metadata<'_>,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) -> bool {
        for layer in &self.0 {
            if layer.enabled(metadata, ctx.clone()) {
                return true;
            }
        }
        false
    }

    fn on_new_span(
        &self,
        attrs: &Attributes<'_>,
        id: &Id,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        for layer in &self.0 {
            layer.on_new_span(attrs, id, ctx.clone());
        }
    }

    fn max_level_hint(&self) -> Option<LevelFilter> {
        for layer in &self.0 {
            if let Some(x) = layer.max_level_hint() {
                return Some(x);
            }
        }
        None
    }

    fn on_record(
        &self,
        span: &Id,
        values: &Record<'_>,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        for layer in &self.0 {
            layer.on_record(span, values, ctx.clone())
        }
    }

    fn on_follows_from(
        &self,
        span: &Id,
        follows: &Id,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        for layer in &self.0 {
            layer.on_follows_from(span, follows, ctx.clone());
        }
    }

    fn on_event(&self, event: &Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
        for layer in &self.0 {
            layer.on_event(event, ctx.clone());
        }
    }

    fn on_enter(&self, id: &Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
        for layer in &self.0 {
            layer.on_enter(id, ctx.clone());
        }
    }

    fn on_exit(&self, id: &Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
        for layer in &self.0 {
            layer.on_exit(id, ctx.clone());
        }
    }

    fn on_close(&self, id: Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
        for layer in &self.0 {
            layer.on_close(id.clone(), ctx.clone());
        }
    }

    fn on_id_change(&self, old: &Id, new: &Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
        for layer in &self.0 {
            layer.on_id_change(old, new, ctx.clone());
        }
    }
}

Then adding to the list like so:

let mut dyn_layers = DynLayerList(vec![]);

let target_filter = filter::Targets::new().with_targets(targets); // targets is HashMap<String, LevelFilter>

let l = tracing_subscriber::fmt::layer().with_filter(target_filter);
dyn_layers.0.push(Box::new(l));

let registry = Registry::default().with(dyn_layers);
set_global_default(registry)?;

A first-party implementation would be awesome. I'm finding it really difficult to add anything conditionally.

@hawkw
Copy link
Member

hawkw commented Jan 10, 2022

@steveocr I think the max_level_hint implementation is the problem here. Your code is returning the hint from the first layer that returns Some.

For example, suppose we have a list of Layers with per-layer filters like this:

[
    <enables up to INFO>,
    <enables up to DEBUG>,
    <enables all levels>,
]

your code will return the Some(INFO) returned by the first layer in the list. That means that the global max enabled level will be INFO. This means that the second and third layers, which would enable more events, will never see those events.

Rather than returning the first hint any layer gives us, we need to always iterate over the entire list, and return the least restrictive hint that any layer returns. The layers with more restrictive per-layer filters will still be able to filter out things they don't care about, but the layers with less restrictive filters will get the opportunity to record everything they're interested in. I believe the correct behavior is:

  • if any layer returns None, return None.
  • otherwise, return the maximum level returned by any layer in the list.

I still think we should probably add a version of this to tracing-subscriber, but in the meantime, that should hopefully give you the information to get your code working?

@steveocr
Copy link

steveocr commented Jan 10, 2022

Aha, that makes sense. Thank you!

After some more experimenting it turned out the main issue was with enabled(). This seems to work (ie. it can't short-circuit):

    fn enabled(
        &self,
        metadata: &Metadata<'_>,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) -> bool {
        let mut enabled = false;
        for layer in &self.0 {
            if layer.enabled(metadata, ctx.clone()) {
                enabled = true;
            }
        }
        enabled
    }

I also changed max_level_hint() to:

fn max_level_hint(&self) -> Option<LevelFilter> {
        let mut max_level = None;
        for layer in &self.0 {
            let x = layer.max_level_hint()?;
            match max_level {
                Some(y) => {
                    if x > y {
                        max_level = Some(x);
                    }
                }
                None => max_level = Some(x),
            }
        }
        max_level
    }

I think having this functionality built-in would be awesome.

@hawkw
Copy link
Member

hawkw commented Jan 10, 2022

@steveocr ah, yeah, that looks right, I missed that the enabled implementation was also a bit backwards.

FWIW, I think both of these can be written a little bit simpler:

    fn enabled(
        &self,
        metadata: &Metadata<'_>,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) -> bool {
        for layer in &self.0 {
            if layer.enabled(metadata, ctx.clone()) {
                return true;
            }
        }
        false
    }

    fn max_level_hint(&self) -> Option<LevelFilter> {
        let mut max_level = LevelFilter::ERROR;
        for layer in &self.0 {
            let x = layer.max_level_hint()?;
            max_level = std::cmp::max(x, max_level);
        }
        Some(max_level)
    }

@steveocr
Copy link

@hawkw That works great! Thanks!

hawkw added a commit that referenced this issue Mar 29, 2022
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>
hawkw added a commit that referenced this issue Apr 1, 2022
Depends on #2028

## Motivation

In many cases, it is desirable to have a variable number of `Layer`s
at runtime (such as when reading a logging configuration from a config
file). The current approach, where types implementing `Layer` are
composed at the type level into `Layered` layers, doesn't work well
when the number of layers varies at runtime.

## Solution

To solve this, this branch adds a `Layer` implementation for
`Vec<L> where L: Layer`. This allows variable-length lists of
layers to be added to a subscriber. Although the impl for `Vec<L>`
requires all the layers to be the same type, it can also be used in
 onjunction with `Box<dyn Layer<S> + ...>` trait objects to
implement a variable-length list of layers 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
`Layer`s. This would have one primary usability benefit, which is
that we could have a `push` operation that takes an `impl Layer`
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 layers that are all the same type.

Closes #1708

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
hawkw added a commit that referenced this issue Apr 1, 2022
Depends on #2028

## Motivation

In many cases, it is desirable to have a variable number of `Layer`s
at runtime (such as when reading a logging configuration from a config
file). The current approach, where types implementing `Layer` are
composed at the type level into `Layered` layers, doesn't work well
when the number of layers varies at runtime.

## Solution

To solve this, this branch adds a `Layer` implementation for
`Vec<L> where L: Layer`. This allows variable-length lists of
layers to be added to a subscriber. Although the impl for `Vec<L>`
requires all the layers to be the same type, it can also be used in
 onjunction with `Box<dyn Layer<S> + ...>` trait objects to
implement a variable-length list of layers 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
`Layer`s. This would have one primary usability benefit, which is
that we could have a `push` operation that takes an `impl Layer`
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 layers that are all the same type.

Closes #1708

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
hawkw added a commit that referenced this issue Apr 1, 2022
Depends on #2028

## Motivation

In many cases, it is desirable to have a variable number of `Layer`s
at runtime (such as when reading a logging configuration from a config
file). The current approach, where types implementing `Layer` are
composed at the type level into `Layered` layers, doesn't work well
when the number of layers varies at runtime.

## Solution

To solve this, this branch adds a `Layer` implementation for
`Vec<L> where L: Layer`. This allows variable-length lists of
layers to be added to a subscriber. Although the impl for `Vec<L>`
requires all the layers to be the same type, it can also be used in
 onjunction with `Box<dyn Layer<S> + ...>` trait objects to
implement a variable-length list of layers 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
`Layer`s. This would have one primary usability benefit, which is
that we could have a `push` operation that takes an `impl Layer`
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 layers that are all the same type.

Closes #1708

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
hawkw added a commit that referenced this issue Apr 1, 2022
Depends on #2028

## Motivation

In many cases, it is desirable to have a variable number of `Layer`s
at runtime (such as when reading a logging configuration from a config
file). The current approach, where types implementing `Layer` are
composed at the type level into `Layered` layers, doesn't work well
when the number of layers varies at runtime.

## Solution

To solve this, this branch adds a `Layer` implementation for
`Vec<L> where L: Layer`. This allows variable-length lists of
layers to be added to a subscriber. Although the impl for `Vec<L>`
requires all the layers to be the same type, it can also be used in
 onjunction with `Box<dyn Layer<S> + ...>` trait objects to
implement a variable-length list of layers 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
`Layer`s. This would have one primary usability benefit, which is
that we could have a `push` operation that takes an `impl Layer`
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 layers that are all the same type.

Closes #1708

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
kaffarell pushed a commit to kaffarell/tracing that referenced this issue May 22, 2024
Depends on tokio-rs#2028

## Motivation

In many cases, it is desirable to have a variable number of `Layer`s
at runtime (such as when reading a logging configuration from a config
file). The current approach, where types implementing `Layer` are
composed at the type level into `Layered` layers, doesn't work well
when the number of layers varies at runtime.

## Solution

To solve this, this branch adds a `Layer` implementation for
`Vec<L> where L: Layer`. This allows variable-length lists of
layers to be added to a subscriber. Although the impl for `Vec<L>`
requires all the layers to be the same type, it can also be used in
 onjunction with `Box<dyn Layer<S> + ...>` trait objects to
implement a variable-length list of layers 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
`Layer`s. This would have one primary usability benefit, which is
that we could have a `push` operation that takes an `impl Layer`
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 layers that are all the same type.

Closes tokio-rs#1708

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants