diff --git a/Cargo.toml b/Cargo.toml index c146352a6d..ec5d4ac297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1105,6 +1105,16 @@ description = "Illustrate how to add custom log layers" category = "Application" wasm = false +[[example]] +name = "log_layers_ecs" +path = "examples/app/log_layers_ecs.rs" + +[package.metadata.example.log_layers_ecs] +name = "Advanced log layers" +description = "Illustrate how to transfer data between log layers and Bevy's ECS" +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 4815418640..601b10198f 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -106,9 +106,12 @@ pub struct LogPlugin { /// This can be further filtered using the `filter` setting. pub level: Level, - /// Optionally apply extra transformations to the tracing subscriber. - /// For example add [`Layers`](tracing_subscriber::layer::Layer) - pub update_subscriber: Option BoxedSubscriber>, + /// Optionally apply extra transformations to the tracing subscriber, + /// such as adding [`Layer`](tracing_subscriber::layer::Layer)s. + /// + /// Access to [`App`] is also provided to allow for communication between the [`Subscriber`] + /// and the [`App`]. + pub update_subscriber: Option BoxedSubscriber>, } /// Alias for a boxed [`Subscriber`]. @@ -193,7 +196,7 @@ impl Plugin for LogPlugin { let subscriber = subscriber.with(tracy_layer); if let Some(update_subscriber) = self.update_subscriber { - finished_subscriber = update_subscriber(Box::new(subscriber)); + finished_subscriber = update_subscriber(app, Box::new(subscriber)); } else { finished_subscriber = Box::new(subscriber); } diff --git a/examples/README.md b/examples/README.md index 9354b36290..1dde7f5e7b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -172,6 +172,7 @@ Example | Description Example | Description --- | --- +[Advanced log layers](../examples/app/log_layers_ecs.rs) | Illustrate how to transfer data between log layers and Bevy's ECS [Custom Loop](../examples/app/custom_loop.rs) | Demonstrates how to create a custom runner (to update an app manually) [Drag and Drop](../examples/app/drag_and_drop.rs) | An example that shows how to handle drag and drop in an app [Empty](../examples/app/empty.rs) | An empty application (does nothing) diff --git a/examples/app/log_layers.rs b/examples/app/log_layers.rs index 144fac28bd..10b090d988 100644 --- a/examples/app/log_layers.rs +++ b/examples/app/log_layers.rs @@ -22,7 +22,9 @@ impl Layer for CustomLayer { } } -fn update_subscriber(subscriber: BoxedSubscriber) -> BoxedSubscriber { +// We don't need App for this example, as we are just printing log information. +// For an example that uses App, see log_layers_ecs. +fn update_subscriber(_: &mut App, subscriber: BoxedSubscriber) -> BoxedSubscriber { Box::new(subscriber.with(CustomLayer)) } @@ -30,6 +32,14 @@ fn main() { App::new() .add_plugins(DefaultPlugins.set(bevy::log::LogPlugin { update_subscriber: Some(update_subscriber), + // You can chain multiple subscriber updates like this: + // + // update_subscriber: Some(|app, subscriber| { + // let subscriber = update_subscriber_a(app, subscriber); + // let subscriber = update_subscriber_b(app, subscriber); + // + // update_subscriber_c(app, subscriber) + // }), ..default() })) .add_systems(Update, log_system) @@ -37,7 +47,7 @@ fn main() { } fn log_system() { - // here is how you write new logs at each "log level" (in "most import" to + // here is how you write new logs at each "log level" (in "most important" to // "least important" order) error!("something failed"); warn!("something bad happened that isn't a failure, but thats worth calling out"); diff --git a/examples/app/log_layers_ecs.rs b/examples/app/log_layers_ecs.rs new file mode 100644 index 0000000000..c8ad424184 --- /dev/null +++ b/examples/app/log_layers_ecs.rs @@ -0,0 +1,151 @@ +//! This example illustrates how to transfer log events from the [`Layer`] to Bevy's ECS. +//! +//! The way we will do this is via a [`mpsc`] channel. [`mpsc`] channels allow 2 unrelated +//! parts of the program to communicate (in this case, [`Layer`]s and Bevy's ECS). +//! +//! Inside the [`update_subscriber`] function we will create a [`mpsc::Sender`] and a [`mpsc::Receiver`] from a +//! [`mpsc::channel`]. The [`Sender`](mpsc::Sender) will go into the [`AdvancedLayer`] and the [`Receiver`](mpsc::Receiver) will +//! go into a non-send resource called [`LogEvents`] (It has to be non-send because [`Receiver`](mpsc::Receiver) is [`!Sync`](Sync)). +//! From there we will use [`transfer_log_events`] to transfer log events from [`LogEvents`] to an ECS event called [`LogEvent`]. +//! +//! Finally, after all that we can access the [`LogEvent`] event from our systems and use it. +//! In this example we build a simple log viewer. + +use std::sync::mpsc; + +use bevy::{ + log::tracing_subscriber::{self, layer::SubscriberExt, Layer}, + log::BoxedSubscriber, + prelude::*, + utils::tracing, + utils::tracing::Subscriber, +}; + +/// A basic message. This is what we will be sending from the [`CaptureLayer`] to [`CapturedLogEvents`] non-send resource. +#[derive(Debug, Event)] +struct LogEvent { + message: String, +} + +/// This non-send resource temporarily stores [`LogEvent`]s before they are +/// written to [`Events`] by [`transfer_log_events`]. +#[derive(Deref, DerefMut)] +struct CapturedLogEvents(mpsc::Receiver); + +/// Transfers information from the [`LogEvents`] resource to [`Events`](LogEvent). +fn transfer_log_events( + reciever: NonSend, + mut log_events: EventWriter, +) { + // Make sure to use `try_iter()` and not `iter()` to prevent blocking. + log_events.send_batch(reciever.try_iter()); +} + +/// This is the [`Layer`] that we will use to capture log events and then send them to Bevy's +/// ECS via it's [`mpsc::Sender`]. +struct CaptureLayer { + sender: mpsc::Sender, +} +impl Layer for CaptureLayer { + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + // In order to obtain the log message, we have to create a struct that implements + // Visit and holds a reference to our string. Then we use the `record` method and + // the struct to modify the reference to hold the message string. + let mut message = None; + event.record(&mut CaptureLayerVisitor(&mut message)); + if let Some(message) = message { + // You can obtain metadata like this, but we wont use it for this example. + let _metadata = event.metadata(); + self.sender + .send(LogEvent { message }) + .expect("LogEvents resource no longer exists!"); + } + } +} + +/// A [`Visit`](tracing::field::Visit)or that records log messages that are transfered to [`CaptureLayer`]. +struct CaptureLayerVisitor<'a>(&'a mut Option); +impl tracing::field::Visit for CaptureLayerVisitor<'_> { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + // This if statement filters out unneeded events sometimes show up + if field.name() == "message" { + *self.0 = Some(format!("{value:?}")); + } + } +} +fn update_subscriber(app: &mut App, subscriber: BoxedSubscriber) -> BoxedSubscriber { + let (sender, reciever) = mpsc::channel(); + + let layer = CaptureLayer { sender }; + let resource = CapturedLogEvents(reciever); + + app.insert_non_send_resource(resource); + app.add_event::(); + app.add_systems(Update, transfer_log_events); + + Box::new(subscriber.with(layer)) +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(bevy::log::LogPlugin { + update_subscriber: Some(update_subscriber), + ..default() + })) + .add_systems(Startup, (log_system, setup)) + .add_systems(Update, print_logs) + .run(); +} + +fn log_system() { + // here is how you write new logs at each "log level" (in "most important" 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"); +} + +#[derive(Component)] +struct LogViewerRoot; + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + + commands.spawn(( + NodeBundle { + style: Style { + width: Val::Vw(100.0), + height: Val::Vh(100.0), + flex_direction: FlexDirection::Column, + ..default() + }, + ..default() + }, + LogViewerRoot, + )); +} + +// This is how we can read our LogEvents. +// In this example we are reading the LogEvents and inserting them as text into our log viewer. +fn print_logs( + mut events: EventReader, + mut commands: Commands, + log_viewer_root: Query>, +) { + let root_entity = log_viewer_root.single(); + + commands.entity(root_entity).with_children(|child| { + for event in events.read() { + child.spawn(TextBundle::from_section( + &event.message, + TextStyle::default(), + )); + } + }); +}