Open leshow opened 4 years ago
Is there a reason why we can't do this? Why consume
self
for each step in the build.
Many of the builder methods change a type parameter on the builder; we can't do this through a mutable reference, since these functions return a value of a different type. For example, the json
and compact
methods in your example both consume the builder and return a new builder with different values for some of its type parameters, neither of them would work through a mutable reference.
Ah yeah, it looks like some of the configuration is lifted to the type level.
How do you feel about adding a with_format
option or something like that? I'd be happy to do it.
How do you feel about adding a
with_format
option or something like that? I'd be happy to do it.
I made a mistake in my first example you can't actually have this:
use tracing::{info, Level};
use tracing_subscriber::fmt::{format, Subscriber};
fn other(cond: bool) {
let b = Subscriber::builder();
if cond {
b = b.json();
} else {
b = b.compact();
}
let b = b.with_max_level(Level::TRACE).finish();
}
^^^^^^^^ expected struct `tracing_subscriber::fmt::format::DefaultFields`, found struct `tracing_subscriber::fmt::format::json::JsonFields`
|
= note: expected struct `tracing_subscriber::fmt::SubscriberBuilder<tracing_subscriber::fmt::format::DefaultFields, tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::Full>>`
found struct `tracing_subscriber::fmt::SubscriberBuilder<tracing_subscriber::fmt::format::json::JsonFields, tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::json::Json>>`
Perhaps I'm misunderstanding, but I don't see how event_format
helps, for example:
fn other(cond: bool) {
let b = Subscriber::builder();
let b = b.event_format(if cond {
format::Format::default().json()
} else {
format::Format::default().compact()
});
let b = b.with_max_level(Level::TRACE).finish();
}
This still produces an error, because the branches passed to event_format
return a different type.
I would really like to figure out a nicer story for this, especially since there is probably an 0.3 release of tracing-subscriber
on the horizon & we will have the opportunity to make breaking changes.
However, there are a few issues that complicate this. First, there is the issue that neither the FormatEvent
trait nor the FormatFields
trait are currently object-safe, because they are generic. Unfortunately, I think the generics cannot easily be removed. Parameterizing FormatEvent
over a Subscriber
type is a necessary part of the interface between the Registry
and the format layer, as it allows the layer to require that the underlying subscriber provide access to span data. We can't easily pass the subscriber as a trait object for that reason.
Second, there are few places where other code relies on having concrete named types for fmt
components. In particular, they are used to allow downcasting from a fmt::Subscriber
or fmt::Layer
to access the event formatter, writer, or field formatter, here:
https://github.com/tokio-rs/tracing/blob/bcb7326e59c3efb91af1624df32ed21fca9edc90/tracing-subscriber/src/fmt/fmt_layer.rs#L495-L505
and by the FormattedFields
type, which remembers the type of the formatter that created it, to allow multiple formatters to store formatted fields for the same span without clobbering one-another:
https://github.com/tokio-rs/tracing/blob/62e0cc0a03ab6ef9297efceabf89aa02a0258fe9/tracing-subscriber/src/fmt/fmt_layer.rs#L370-L384
The second is particularly important, as it allows multiple formatted representations of a span's fields to co-exist, and allows other layers which also need formatted representations of fields to reuse the formatted fields produced by the fmt::Layer
. For example, this makes the tracing-error
crate's ErrorLayer
much cheaper, because it only needs to format fields if a fmt::Layer
doesn't already exist, or if different formatters are in use by the fmt::Layer
and the ErrorLayer
.
So, I think it's theoretically possible to fix this, but it might require a significant amount of internal refactoring. I'll give it some more thought.
Actually...while I was writing all that up, I think I came up with a fairly simple and obvious solution. Stay tuned!
Just saw this, I was under the impression that you had lifted configuration to the type level solely so that you couldn't misconfigure a Subscriber
or something. I don't think I fully grasp all of the design decisions in the crate but I'm happy to be your rubber duck and help you come up with something!
I was under the impression that you had lifted configuration to the type level solely so that you couldn't misconfigure a
Subscriber
or something.
@leshow that's definitely part of the motivation (e.g. it would be nice if the JSON event formatter makes sure that you are also using the JSON field formatter) but the main motivation is really that we want Layer
to be generic over Subscriber
s, so that Layer
s can place more trait bounds on the Subscriber
as needed if they require additional functionality. Everything being generic rather than trait-object based kind of falls naturally from there.
I'm happy to be your rubber duck and help you come up with something!
The solution I've come up with is adding an erased
combinator to Subscriber
s that allow wrapping them in a Box
to get back a type-erased subscriber. So, you could hypothetically write code like this
use tracing_subscriber::{SubscriberExt, erased};
/// Returns a new `Subscriber` configured to output events in human-readable
/// text format, or as JSON, depending on the provided config.
fn new_subscriber(cfg: &Config) -> erased::Subscriber {
// Shared configuration regardless of whether JSON formatting is used.
let subscriber = tracing_subscriber::fmt()
.with_env_filter(cfg.filter())
.with_ansi(cfg.is_ansi());
// The `erased` combinator allows each branch of this `if` statement
// (and the function itself) to have the same type.
if cfg.is_json() {
subscriber.json().erased()
} else if cfg.is_compact() {
subscriber.compact().erased()
} else {
subscriber.erased()
}
}
The downside to this is that it is rather difficult to forward additional trait impls through the erased
type, such as LookupSpan
, which is necessary to use an erased::Subscriber
with layers that require LookupSpan
. I've found a couple of solutions, but they both have downsides: one is a breaking change, and would have to wait for tracing-subscriber
v0.3 to be released, and the other would only work with the tracing_subscriber::Registry
type (and not other subscribers implementing LookupSpan
).
We could probably land the non-breaking version in the near future, and then consider the more general option in 0.3.
A note on the above: this won't work with cases where you want to continue calling SubscriberBuilder
methods on a builder, as in this case that @leshow mentioned in https://github.com/tokio-rs/tracing/issues/575#issuecomment-584155410:
fn other(cond: bool) { let b = Subscriber::builder(); if cond { b = b.json(); } else { b = b.compact(); } let b = b.with_max_level(Level::TRACE).finish(); }
This is because an erased::Subscriber
type would be a subscriber trait object, and not a SubscriberBuilder
.
However, this specific example could be fixed fairly easily by just moving all the shared configuration (in this case, the call to with_max_level
) before the dynamic configuration (which formatter to use). For example, this code should compile:
fn other(cond: bool) {
let b = Subscriber::builder()
.with_max_level(Level::TRACE);
let subscriber = if cond {
b.json().finish().erased()
} else {
b.compact().finish().erased()
};
// ...
}
Would that work similarly to how erased-serde works? something like:
trait ErasedSubscriber {
fn erased(&self, sub: &mut dyn Subscriber) -> Result<Ok, Error>;
}
My particular use case is a bunch of configuration values come in at runtime, this is what we have right now to support this:
fn configure_tracing(&self) -> Result<()> {
let json_builder = Subscriber::builder();
let full_builder = Subscriber::builder();
let log_lvl = self.log_lvl.clone();
match self.log_frmt.as_str() {
"json" => {
let json_builder = json_builder
.with_max_level(log_lvl)
.event_format(format::Format::default().json());
tracing::subscriber::set_global_default(json_builder.finish())
}
_ => {
let full_builder = full_builder
.with_max_level(log_lvl)
.event_format(format::Format::default());
tracing::subscriber::set_global_default(full_builder.finish())
}
}
.expect("setting default tracing subscriber failed");
Ok(())
}
Simplifying to what you've got above would be a big improvement
Would that work similarly to how erased-serde works? something like:
trait ErasedSubscriber { fn erased(&self, sub: &mut dyn Subscriber) -> Result<Ok, Error>; }
My particular use case is a bunch of configuration values come in at runtime, this is what we have right now to support this:
fn configure_tracing(&self) -> Result<()> { let json_builder = Subscriber::builder(); let full_builder = Subscriber::builder(); let log_lvl = self.log_lvl.clone(); match self.log_frmt.as_str() { "json" => { let json_builder = json_builder .with_max_level(log_lvl) .event_format(format::Format::default().json()); tracing::subscriber::set_global_default(json_builder.finish()) } _ => { let full_builder = full_builder .with_max_level(log_lvl) .event_format(format::Format::default()); tracing::subscriber::set_global_default(full_builder.finish()) } } .expect("setting default tracing subscriber failed"); Ok(()) }
Simplifying to what you've got above would be a big improvement
same issue
Got a ping on this from the last message, thought I would add some more context. There are obvious workarounds to this, but if you want to see how I currently solve it in a real project: https://github.com/leshow/nailgun/blob/master/bin/src/logs.rs#L27
One use case (as above) is if you've got a cli that wants to output json/pretty formatted/etc output depending on some input. I'm actually totally OK with the status quo, because this is the kind of setup that you write once and don't often touch. But at the same time I could see how it could get much worse if you wanted to expose more options at runtime, you could end up with many repetitive branches
I think my question https://github.com/tokio-rs/tracing/discussions/2261 has something to do with this problem.
Any news on this?
Your question in #2261 does, in fact, have to do with this issue. I think that the .boxed()
combinator should resolve this issue for you.
Circling back on this, I think boxed
does alleviate the original issue somewhat with dynamic configuration
before:
pub fn init_tracing(args: &Args) {
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
match args.output {
LogStructure::Pretty => {
tracing_subscriber::registry()
.with(filter_layer)
.with(
fmt::layer()
.event_format(Format::default().with_source_location(false))
.fmt_fields(PrettyFields::new())
.with_target(false),
)
.init();
}
LogStructure::Debug => {
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt::layer().fmt_fields(Pretty::default()))
.init();
}
LogStructure::Json => {
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt::layer().json())
.init();
}
}
}
after
pub fn init_tracing(args: &Args) {
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
let output = match args.output {
LogStructure::Pretty => fmt::layer()
.event_format(Format::default().with_source_location(false))
.fmt_fields(PrettyFields::new())
.with_target(false)
.boxed(),
LogStructure::Debug => fmt::layer().fmt_fields(Pretty::default()).boxed(),
LogStructure::Json => fmt::layer().json().boxed(),
};
tracing_subscriber::registry()
.with(filter_layer)
.with(output)
.init();
}
so long as you don't need to dynamically create a different number of layers. I think it's safe to mark this as resolved. Thank you!
Not sure this is exactly the same issue, but I ran into something similar trying to config layers with cargo features and Registry.
I couldn't figure out a good way to do it without repeating .init()
in multible blocks.
It would be good for there to be a example and recommended way to do this.
Eventually found the answer: just wrap in an Option.
https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#runtime-configuration-with-layers
Also #2499 may be related
Feature Request
Crates
none
Motivation
Doing anything dynamic can get pretty hairy:
Proposal
Make the various builders use functions of the form:
fn foo(&mut self, arg: T) -> &mut Self
Alternative
Could accept a
with_x
for every option so at least re-assignment on conditionals could be worked around (there's nothing forFormat
currently)Is there a reason why we can't do this? Why consume
self
for each step in the build.I'm happy to implement this change if it's something you're interested in.