diff --git a/Cargo.toml b/Cargo.toml index bcd8f51acd505..056406469124c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -657,6 +657,16 @@ description = "Illustrate how to use generate log output" category = "Application" wasm = true +[[example]] +name = "log_layers" +path = "examples/app/log_layers.rs" + +[package.metadata.example.log_layers] +name = "Log layers" +description = "Illustrate how to add custom log layers" +category = "Application" +wasm = false + [[example]] name = "plugin" path = "examples/app/plugin.rs" diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index 7265f4fd62170..9a515235e89fb 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -25,6 +25,7 @@ pub mod prelude { }; } +use bevy_utils::tracing::Subscriber; pub use bevy_utils::tracing::{ debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, Level, @@ -32,6 +33,7 @@ pub use bevy_utils::tracing::{ use bevy_app::{App, Plugin}; use tracing_log::LogTracer; +pub use tracing_subscriber; #[cfg(feature = "tracing-chrome")] use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; @@ -91,6 +93,21 @@ pub struct LogPlugin { /// Filters out logs that are "less than" the given level. /// This can be further filtered using the `filter` setting. pub level: Level, + + // todo: better way of passing layers to plugin + pub extra_layers: std::sync::Arc< + std::sync::Mutex< + Option< + Vec< + Box< + dyn tracing_subscriber::layer::Layer> + + Send + + Sync, + >, + >, + >, + >, + >, } impl Default for LogPlugin { @@ -98,6 +115,7 @@ impl Default for LogPlugin { Self { filter: "wgpu=error".to_string(), level: Level::INFO, + extra_layers: Default::default(), } } } @@ -187,8 +205,18 @@ impl Plugin for LogPlugin { } let logger_already_set = LogTracer::init().is_err(); - let subscriber_already_set = - bevy_utils::tracing::subscriber::set_global_default(finished_subscriber).is_err(); + + let extra_layers = self.extra_layers.lock().unwrap().take(); + + let subscriber_already_set = if let Some(layers) = extra_layers { + let subscriber = Box::new(finished_subscriber); + let subscriber = layers.with_subscriber(subscriber); + // let subscriber = subscriber.with(self.extra_layers); + bevy_utils::tracing::subscriber::set_global_default(subscriber) + } else { + bevy_utils::tracing::subscriber::set_global_default(finished_subscriber) + } + .is_err(); match (logger_already_set, subscriber_already_set) { (true, true) => warn!( diff --git a/examples/app/log_layers.rs b/examples/app/log_layers.rs new file mode 100644 index 0000000000000..1f2547c44810a --- /dev/null +++ b/examples/app/log_layers.rs @@ -0,0 +1,116 @@ +//! This example illustrates how to add custom log layers in bevy. + +use bevy::{ + log::tracing_subscriber::Layer, + prelude::*, + utils::tracing::{field::Visit, Subscriber}, +}; +use std::sync::{Arc, Mutex}; + +struct DebugLogToStdErrLayer; + +impl Layer for DebugLogToStdErrLayer { + fn on_event( + &self, + event: &bevy::utils::tracing::Event<'_>, + _ctx: bevy::log::tracing_subscriber::layer::Context<'_, S>, + ) { + // pretty print received events to std err, including metadata + eprintln!("logged my way: {event:#?}"); + } +} + +struct ChannelLayer { + sender: crossbeam_channel::Sender, + max_level: bevy::log::Level, +} + +#[derive(Resource)] +struct ErrorMessageReceiver(crossbeam_channel::Receiver); + +#[derive(Component)] +struct LastErrorText; + +impl Layer for ChannelLayer { + fn on_event( + &self, + event: &bevy::utils::tracing::Event<'_>, + _ctx: bevy::log::tracing_subscriber::layer::Context<'_, S>, + ) { + if event.metadata().level() <= &self.max_level { + let mut visitor = ChannelSendVisitor(self.sender.clone()); + event.record(&mut visitor); + } + } +} + +struct ChannelSendVisitor(crossbeam_channel::Sender); + +impl Visit for ChannelSendVisitor { + fn record_debug( + &mut self, + _field: &bevy::utils::tracing::field::Field, + value: &dyn std::fmt::Debug, + ) { + // will fail if the receiver is dropped. In that case, we do nothing. + _ = self.0.try_send(format!("{value:?}")); + } +} + +fn main() { + let (sender, receiver) = crossbeam_channel::unbounded(); + + let log_to_screen_layer = ChannelLayer { + sender, + max_level: bevy::log::Level::ERROR, + }; + + App::new() + .insert_resource(ErrorMessageReceiver(receiver)) + .add_plugins(DefaultPlugins.set(bevy::log::LogPlugin { + // todo: fix horrible hack + extra_layers: Arc::new(Mutex::new(Some(vec![ + log_to_screen_layer.boxed(), + DebugLogToStdErrLayer.boxed(), + ]))), + ..default() + })) + .add_startup_system(log_system) + .add_startup_system(text_setup) + .add_system(update_screen_text) + .run(); +} + +fn text_setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + + commands.spawn(( + TextBundle::from_sections([TextSection::from_style(TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 60.0, + color: Color::MAROON, + })]), + LastErrorText, + )); +} + +fn log_system() { + // here is how you write new logs at each "log level" (in "most import" to + // "least important" order) + error!("something failed"); + warn!("something bad happened that isn't a failure, but thats worth calling out"); + info!("helpful information that is worth printing by default"); + debug!("helpful for debugging"); + trace!("very noisy"); +} + +fn update_screen_text( + errors: Res, + mut query: Query<&mut Text, With>, +) { + for error in errors.0.try_iter() { + if let Ok(mut text) = query.get_single_mut() { + text.sections[0].value = error; + } + } +}