bevy_log: refactor how log layers are wired together (#19248)
# Objective Current way to wire `Layer`s together using `layer.with(new_layer)` in the `bevy_log` plugin is brittle and not flexible. As #17722 demonstrated, the current solution makes it very hard to do any kind of advanced wiring, as the type system of `tracing::Subscriber` gets in the way very quickly (the type of each new layer depends on the type of the previous ones). We want to make it easier to have more complex wiring of `Layers`. It would be hard to solve #19085 without it ## Solution It aims to be functionally equivalent. - Replace of using `layer.with(new_layer)` . We now add `layer.boxed()` to a `Vec<BoxedLayer>`. It is a solution recommended by `tracing_subscriber::Layer` for complex wiring cases (See https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#runtime-configuration-with-layers) - Do some refactoring and clean up that is now enabled by the new solution ## Testing - Ran CI locally on Linux - Ran the logs examples - Need people familiar with the features `trace`, `tracing-chrome`, `tracing-tracy` to check that it still works as expected - Need people with access to `ios`, `android` and `wasm` to check it as well. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
This commit is contained in:
parent
3b25b38bdc
commit
8661e914a5
@ -56,7 +56,6 @@ use bevy_app::{App, Plugin};
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{
|
||||
filter::{FromEnvError, ParseError},
|
||||
layer::Layered,
|
||||
prelude::*,
|
||||
registry::Registry,
|
||||
EnvFilter, Layer,
|
||||
@ -84,6 +83,8 @@ pub(crate) struct FlushGuard(SyncCell<tracing_chrome::FlushGuard>);
|
||||
/// logging to `stdout`.
|
||||
/// * Using [`android_log-sys`](https://crates.io/crates/android_log-sys) on Android,
|
||||
/// logging to Android logs.
|
||||
/// * Using [`tracing_oslog`](https://crates.io/crates/tracing_oslog) on iOS,
|
||||
/// logging to iOS logs.
|
||||
/// * Using [`tracing-wasm`](https://crates.io/crates/tracing-wasm) in Wasm, logging
|
||||
/// to the browser console.
|
||||
///
|
||||
@ -254,26 +255,15 @@ pub struct LogPlugin {
|
||||
/// timestamp from the log output.
|
||||
///
|
||||
/// Please see the `examples/log_layers.rs` for a complete example.
|
||||
pub fmt_layer: fn(app: &mut App) -> Option<BoxedFmtLayer>,
|
||||
///
|
||||
/// Note that this field has no effect when `os_target` is `android`, `ios` or `wasm`, as on those
|
||||
/// platforms we don't use [`tracing_subscriber::fmt::Layer`] but rather the platform default.
|
||||
pub fmt_layer: fn(app: &mut App) -> Option<BoxedLayer>,
|
||||
}
|
||||
|
||||
/// A boxed [`Layer`] that can be used with [`LogPlugin::custom_layer`].
|
||||
pub type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
|
||||
|
||||
#[cfg(feature = "trace")]
|
||||
type BaseSubscriber =
|
||||
Layered<EnvFilter, Layered<Option<Box<dyn Layer<Registry> + Send + Sync>>, Registry>>;
|
||||
|
||||
#[cfg(feature = "trace")]
|
||||
type PreFmtSubscriber = Layered<tracing_error::ErrorLayer<BaseSubscriber>, BaseSubscriber>;
|
||||
|
||||
#[cfg(not(feature = "trace"))]
|
||||
type PreFmtSubscriber =
|
||||
Layered<EnvFilter, Layered<Option<Box<dyn Layer<Registry> + Send + Sync>>, Registry>>;
|
||||
|
||||
/// A boxed [`Layer`] that can be used with [`LogPlugin::fmt_layer`].
|
||||
pub type BoxedFmtLayer = Box<dyn Layer<PreFmtSubscriber> + Send + Sync + 'static>;
|
||||
|
||||
/// The default [`LogPlugin`] [`EnvFilter`].
|
||||
pub const DEFAULT_FILTER: &str = "wgpu=error,naga=warn";
|
||||
|
||||
@ -300,30 +290,25 @@ impl Plugin for LogPlugin {
|
||||
}));
|
||||
}
|
||||
|
||||
let finished_subscriber;
|
||||
let subscriber = Registry::default();
|
||||
// We use a Vec of BoxedLayer instead of adding each layer individually using the
|
||||
// `layer.with(next_layer)`.
|
||||
// Otherwise, the types of each successive layer becomes unwieldy,
|
||||
// as the type of each new layer would depend on the types of the previous layers.
|
||||
let mut layers: Vec<BoxedLayer> = Vec::new();
|
||||
|
||||
// add optional layer provided by user
|
||||
let subscriber = subscriber.with((self.custom_layer)(app));
|
||||
// Add optional layer provided by user
|
||||
// As they are added first, any of the following layers won't be applied.
|
||||
// In particular, it won't be affected by the filtering we put in place next.
|
||||
if let Some(layer) = (self.custom_layer)(app) {
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
let default_filter = { format!("{},{}", self.level, self.filter) };
|
||||
let filter_layer = EnvFilter::try_from_default_env()
|
||||
.or_else(|from_env_error| {
|
||||
_ = from_env_error
|
||||
.source()
|
||||
.and_then(|source| source.downcast_ref::<ParseError>())
|
||||
.map(|parse_err| {
|
||||
// we cannot use the `error!` macro here because the logger is not ready yet.
|
||||
eprintln!("LogPlugin failed to parse filter from env: {}", parse_err);
|
||||
});
|
||||
|
||||
Ok::<EnvFilter, FromEnvError>(EnvFilter::builder().parse_lossy(&default_filter))
|
||||
})
|
||||
.unwrap();
|
||||
let subscriber = subscriber.with(filter_layer);
|
||||
layers.push(Self::build_filter_layer(self.level, &self.filter));
|
||||
|
||||
#[cfg(feature = "trace")]
|
||||
let subscriber = subscriber.with(tracing_error::ErrorLayer::default());
|
||||
layers.push(tracing_error::ErrorLayer::default().boxed());
|
||||
|
||||
layers.push(Self::build_system_output_layer((self.fmt_layer)(app)));
|
||||
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
@ -332,76 +317,19 @@ impl Plugin for LogPlugin {
|
||||
))]
|
||||
{
|
||||
#[cfg(feature = "tracing-chrome")]
|
||||
let chrome_layer = {
|
||||
let mut layer = tracing_chrome::ChromeLayerBuilder::new();
|
||||
if let Ok(path) = std::env::var("TRACE_CHROME") {
|
||||
layer = layer.file(path);
|
||||
}
|
||||
let (chrome_layer, guard) = layer
|
||||
.name_fn(Box::new(|event_or_span| match event_or_span {
|
||||
tracing_chrome::EventOrSpan::Event(event) => event.metadata().name().into(),
|
||||
tracing_chrome::EventOrSpan::Span(span) => {
|
||||
if let Some(fields) =
|
||||
span.extensions().get::<FormattedFields<DefaultFields>>()
|
||||
{
|
||||
format!("{}: {}", span.metadata().name(), fields.fields.as_str())
|
||||
} else {
|
||||
span.metadata().name().into()
|
||||
}
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
{
|
||||
let (chrome_layer, guard) = Self::build_chrome_layer();
|
||||
app.insert_resource(FlushGuard(SyncCell::new(guard)));
|
||||
chrome_layer
|
||||
};
|
||||
|
||||
layers.push(chrome_layer);
|
||||
}
|
||||
#[cfg(feature = "tracing-tracy")]
|
||||
let tracy_layer = tracing_tracy::TracyLayer::default();
|
||||
|
||||
let fmt_layer = (self.fmt_layer)(app).unwrap_or_else(|| {
|
||||
// note: the implementation of `Default` reads from the env var NO_COLOR
|
||||
// to decide whether to use ANSI color codes, which is common convention
|
||||
// https://no-color.org/
|
||||
Box::new(tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr))
|
||||
});
|
||||
|
||||
// bevy_render::renderer logs a `tracy.frame_mark` event every frame
|
||||
// at Level::INFO. Formatted logs should omit it.
|
||||
#[cfg(feature = "tracing-tracy")]
|
||||
let fmt_layer =
|
||||
fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| {
|
||||
meta.fields().field("tracy.frame_mark").is_none()
|
||||
}));
|
||||
|
||||
let subscriber = subscriber.with(fmt_layer);
|
||||
|
||||
#[cfg(feature = "tracing-chrome")]
|
||||
let subscriber = subscriber.with(chrome_layer);
|
||||
#[cfg(feature = "tracing-tracy")]
|
||||
let subscriber = subscriber.with(tracy_layer);
|
||||
finished_subscriber = subscriber;
|
||||
layers.push(tracing_tracy::TracyLayer::default().boxed());
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
finished_subscriber = subscriber.with(tracing_wasm::WASMLayer::new(
|
||||
tracing_wasm::WASMLayerConfig::default(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
finished_subscriber = subscriber.with(android_tracing::AndroidLayer::default());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
finished_subscriber = subscriber.with(tracing_oslog::OsLogger::default());
|
||||
}
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
let logger_already_set = LogTracer::init().is_err();
|
||||
let subscriber_already_set =
|
||||
tracing::subscriber::set_global_default(finished_subscriber).is_err();
|
||||
let subscriber_already_set = tracing::subscriber::set_global_default(subscriber).is_err();
|
||||
|
||||
match (logger_already_set, subscriber_already_set) {
|
||||
(true, true) => error!(
|
||||
@ -413,3 +341,112 @@ impl Plugin for LogPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogPlugin {
|
||||
/// Build a [`BoxedLayer`] that will filter which logs are outputted.
|
||||
/// It will read the `RUST_LOG` env variable to override the settings
|
||||
/// on a given run, the default will fallback to the provided `level` and `filter`
|
||||
fn build_filter_layer(level: Level, filter: &str) -> BoxedLayer {
|
||||
let default_filter = { format!("{},{}", level, filter) };
|
||||
|
||||
EnvFilter::try_from_default_env()
|
||||
.or_else(|from_env_error| {
|
||||
_ = from_env_error
|
||||
.source()
|
||||
.and_then(|source| source.downcast_ref::<ParseError>())
|
||||
.map(|parse_err| {
|
||||
#[expect(
|
||||
clippy::print_stderr,
|
||||
reason = "We cannot use the `error!` macro here because the logger is not ready yet."
|
||||
)]
|
||||
{
|
||||
eprintln!("LogPlugin failed to parse filter from env: {}", parse_err);
|
||||
}
|
||||
});
|
||||
|
||||
Ok::<EnvFilter, FromEnvError>(EnvFilter::builder().parse_lossy(&default_filter))
|
||||
})
|
||||
.unwrap().boxed()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing-chrome")]
|
||||
/// [`BoxedLayer`] to build the necessary output when the `tracing-chrome` feature is enabled.
|
||||
/// The [`tracing_chrome::FlushGuard`] must be kept around till we don't need to output logs
|
||||
/// any more
|
||||
fn build_chrome_layer() -> (BoxedLayer, tracing_chrome::FlushGuard) {
|
||||
let mut layer = tracing_chrome::ChromeLayerBuilder::new();
|
||||
if let Ok(path) = std::env::var("TRACE_CHROME") {
|
||||
layer = layer.file(path);
|
||||
}
|
||||
let (chrome_layer, guard) = layer
|
||||
.name_fn(Box::new(|event_or_span| match event_or_span {
|
||||
tracing_chrome::EventOrSpan::Event(event) => event.metadata().name().into(),
|
||||
tracing_chrome::EventOrSpan::Span(span) => {
|
||||
if let Some(fields) = span.extensions().get::<FormattedFields<DefaultFields>>()
|
||||
{
|
||||
format!("{}: {}", span.metadata().name(), fields.fields.as_str())
|
||||
} else {
|
||||
span.metadata().name().into()
|
||||
}
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
(chrome_layer.boxed(), guard)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::allow_attributes,
|
||||
reason = "We can't switch to `expect` for allow(unused_variables) as we use it if not on those platforms"
|
||||
)]
|
||||
#[allow(unused_variables, reason = "Not used on `wasm32`, `android` or `ios")]
|
||||
/// Build a [`BoxedLayer`] that outputs logs to the system default.
|
||||
/// On most platforms, it will be `stderr` with [`tracing_subscriber::fmt::Layer`], expect on `android`, `ios` and` wasm32` where it
|
||||
/// uses those system default log infrastructure.
|
||||
/// It is possible to override how you output those logs by providing a `custom_format_layer`.
|
||||
/// Note that won't have an effect on platform that don't use [`tracing_subscriber::fmt::Layer`]
|
||||
fn build_system_output_layer(custom_format_layer: Option<BoxedLayer>) -> BoxedLayer {
|
||||
let layer: BoxedLayer;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
layer = tracing_wasm::WASMLayer::new(tracing_wasm::WASMLayerConfig::default()).boxed();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
layer = android_tracing::AndroidLayer::default().boxed();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
layer = tracing_oslog::OsLogger::default().boxed();
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
{
|
||||
layer = {
|
||||
let fmt_layer = custom_format_layer.unwrap_or_else(|| {
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
// note: the implementation of `Default` reads from the env var NO_COLOR
|
||||
// to decide whether to use ANSI color codes, which is common convention
|
||||
// https://no-color.org/
|
||||
.with_writer(std::io::stderr)
|
||||
.boxed()
|
||||
});
|
||||
|
||||
// bevy_render::renderer logs a `tracy.frame_mark` event every frame
|
||||
// at Level::INFO. Formatted logs should omit it.
|
||||
#[cfg(feature = "tracing-tracy")]
|
||||
let fmt_layer =
|
||||
fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| {
|
||||
meta.fields().field("tracy.frame_mark").is_none()
|
||||
}));
|
||||
fmt_layer.boxed()
|
||||
}
|
||||
}
|
||||
layer
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use bevy::{
|
||||
log::{
|
||||
tracing::{self, Subscriber},
|
||||
tracing_subscriber::Layer,
|
||||
BoxedFmtLayer, BoxedLayer,
|
||||
BoxedLayer,
|
||||
},
|
||||
prelude::*,
|
||||
};
|
||||
@ -41,12 +41,13 @@ fn custom_layer(_app: &mut App) -> Option<BoxedLayer> {
|
||||
// `fmt_layer` option.
|
||||
//
|
||||
// In this example, we're disabling the timestamp in the log output.
|
||||
fn fmt_layer(_app: &mut App) -> Option<BoxedFmtLayer> {
|
||||
Some(Box::new(
|
||||
fn fmt_layer(_app: &mut App) -> Option<BoxedLayer> {
|
||||
Some(
|
||||
bevy::log::tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_writer(std::io::stderr),
|
||||
))
|
||||
.with_writer(std::io::stderr)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
Loading…
Reference in New Issue
Block a user