bevy_log/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc(
3    html_logo_url = "https://bevyengine.org/assets/icon.png",
4    html_favicon_url = "https://bevyengine.org/assets/icon.png"
5)]
6
7//! This crate provides logging functions and configuration for [Bevy](https://bevyengine.org)
8//! apps, and automatically configures platform specific log handlers (i.e. Wasm or Android).
9//!
10//! The macros provided for logging are reexported from [`tracing`](https://docs.rs/tracing),
11//! and behave identically to it.
12//!
13//! By default, the [`LogPlugin`] from this crate is included in Bevy's `DefaultPlugins`
14//! and the logging macros can be used out of the box, if used.
15//!
16//! For more fine-tuned control over logging behavior, set up the [`LogPlugin`] or
17//! `DefaultPlugins` during app initialization.
18
19extern crate alloc;
20
21use core::error::Error;
22
23#[cfg(target_os = "android")]
24mod android_tracing;
25mod once;
26
27#[cfg(feature = "trace_tracy_memory")]
28#[global_allocator]
29static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
30    tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
31
32/// The log prelude.
33///
34/// This includes the most common types in this crate, re-exported for your convenience.
35pub mod prelude {
36    #[doc(hidden)]
37    pub use tracing::{
38        debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span,
39    };
40
41    #[doc(hidden)]
42    pub use crate::{debug_once, error_once, info_once, trace_once, warn_once};
43
44    #[doc(hidden)]
45    pub use bevy_utils::once;
46}
47
48pub use bevy_utils::once;
49pub use tracing::{
50    self, debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn,
51    warn_span, Level,
52};
53pub use tracing_subscriber;
54
55use bevy_app::{App, Plugin};
56use tracing_log::LogTracer;
57use tracing_subscriber::{
58    filter::{FromEnvError, ParseError},
59    prelude::*,
60    registry::Registry,
61    EnvFilter, Layer,
62};
63#[cfg(feature = "tracing-chrome")]
64use {
65    bevy_ecs::resource::Resource,
66    bevy_utils::synccell::SyncCell,
67    tracing_subscriber::fmt::{format::DefaultFields, FormattedFields},
68};
69
70/// Wrapper resource for `tracing-chrome`'s flush guard.
71/// When the guard is dropped the chrome log is written to file.
72#[cfg(feature = "tracing-chrome")]
73#[expect(
74    dead_code,
75    reason = "`FlushGuard` never needs to be read, it just needs to be kept alive for the `App`'s lifetime."
76)]
77#[derive(Resource)]
78pub(crate) struct FlushGuard(SyncCell<tracing_chrome::FlushGuard>);
79
80/// Adds logging to Apps. This plugin is part of the `DefaultPlugins`. Adding
81/// this plugin will setup a collector appropriate to your target platform:
82/// * Using [`tracing-subscriber`](https://crates.io/crates/tracing-subscriber) by default,
83///   logging to `stdout`.
84/// * Using [`android_log-sys`](https://crates.io/crates/android_log-sys) on Android,
85///   logging to Android logs.
86/// * Using [`tracing-wasm`](https://crates.io/crates/tracing-wasm) in Wasm, logging
87///   to the browser console.
88///
89/// You can configure this plugin.
90/// ```no_run
91/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup};
92/// # use bevy_log::LogPlugin;
93/// # use tracing::Level;
94/// fn main() {
95///     App::new()
96///         .add_plugins(DefaultPlugins.set(LogPlugin {
97///             level: Level::DEBUG,
98///             filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(),
99///             custom_layer: |_| None,
100///         }))
101///         .run();
102/// }
103/// ```
104///
105/// Log level can also be changed using the `RUST_LOG` environment variable.
106/// For example, using `RUST_LOG=wgpu=error,bevy_render=info,bevy_ecs=trace cargo run ..`
107///
108/// It has the same syntax as the field [`LogPlugin::filter`], see [`EnvFilter`].
109/// If you define the `RUST_LOG` environment variable, the [`LogPlugin`] settings
110/// will be ignored.
111///
112/// Also, to disable color terminal output (ANSI escape codes), you can
113/// set the environment variable `NO_COLOR` to any value. This common
114/// convention is documented at [no-color.org](https://no-color.org/).
115/// For example:
116/// ```no_run
117/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup};
118/// # use bevy_log::LogPlugin;
119/// fn main() {
120/// #   // SAFETY: Single-threaded
121/// #   unsafe {
122///     std::env::set_var("NO_COLOR", "1");
123/// #   }
124///     App::new()
125///        .add_plugins(DefaultPlugins)
126///        .run();
127/// }
128/// ```
129///
130/// If you want to setup your own tracing collector, you should disable this
131/// plugin from `DefaultPlugins`:
132/// ```no_run
133/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup};
134/// # use bevy_log::LogPlugin;
135/// fn main() {
136///     App::new()
137///         .add_plugins(DefaultPlugins.build().disable::<LogPlugin>())
138///         .run();
139/// }
140/// ```
141/// # Example Setup
142///
143/// For a quick setup that enables all first-party logging while not showing any of your dependencies'
144/// log data, you can configure the plugin as shown below.
145///
146/// ```no_run
147/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup};
148/// # use bevy_log::*;
149/// App::new()
150///     .add_plugins(DefaultPlugins.set(LogPlugin {
151///         filter: "warn,my_crate=trace".to_string(), //specific filters
152///         level: Level::TRACE,//Change this to be globally change levels
153///         ..Default::default()
154///         }))
155///     .run();
156/// ```
157/// The filter (in this case an `EnvFilter`) chooses whether to print the log. The most specific filters apply with higher priority.
158/// Let's start with an example: `filter: "warn".to_string()` will only print logs with level `warn` level or greater.
159/// From here, we can change to `filter: "warn,my_crate=trace".to_string()`. Logs will print at level `warn` unless it's in `mycrate`,
160/// which will instead print at `trace` level because `my_crate=trace` is more specific.
161///
162///
163/// ## Log levels
164/// Events can be logged at various levels of importance.
165/// Only events at your configured log level and higher will be shown.
166/// ```no_run
167/// # use bevy_log::*;
168/// // here is how you write new logs at each "log level" (in "most important" to
169/// // "least important" order)
170/// error!("something failed");
171/// warn!("something bad happened that isn't a failure, but that's worth calling out");
172/// info!("helpful information that is worth printing by default");
173/// debug!("helpful for debugging");
174/// trace!("very noisy");
175/// ```
176/// In addition to `format!` style arguments, you can print a variable's debug
177/// value by using syntax like: `trace(?my_value)`.
178///
179/// ## Per module logging levels
180/// Modules can have different logging levels using syntax like `crate_name::module_name=debug`.
181///
182///
183/// ```no_run
184/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup};
185/// # use bevy_log::*;
186/// App::new()
187///     .add_plugins(DefaultPlugins.set(LogPlugin {
188///         filter: "warn,my_crate=trace,my_crate::my_module=debug".to_string(), // Specific filters
189///         level: Level::TRACE, // Change this to be globally change levels
190///         ..Default::default()
191///     }))
192///     .run();
193/// ```
194/// The idea is that instead of deleting logs when they are no longer immediately applicable,
195/// you just disable them. If you do need to log in the future, then you can enable the logs instead of having to rewrite them.
196///
197/// ## Further reading
198///
199/// The `tracing` crate has much more functionality than these examples can show.
200/// Much of this configuration can be done with "layers" in the `log` crate.
201/// Check out:
202/// - Using spans to add more fine grained filters to logs
203/// - Adding instruments to capture more function information
204/// - Creating layers to add additional context such as line numbers
205/// # Panics
206///
207/// This plugin should not be added multiple times in the same process. This plugin
208/// sets up global logging configuration for **all** Apps in a given process, and
209/// rerunning the same initialization multiple times will lead to a panic.
210///
211/// # Performance
212///
213/// Filters applied through this plugin are computed at _runtime_, which will
214/// have a non-zero impact on performance.
215/// To achieve maximum performance, consider using
216/// [_compile time_ filters](https://docs.rs/log/#compile-time-filters)
217/// provided by the [`log`](https://crates.io/crates/log) crate.
218///
219/// ```toml
220/// # cargo.toml
221/// [dependencies]
222/// log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
223/// ```
224pub struct LogPlugin {
225    /// Filters logs using the [`EnvFilter`] format
226    pub filter: String,
227
228    /// Filters out logs that are "less than" the given level.
229    /// This can be further filtered using the `filter` setting.
230    pub level: Level,
231
232    /// Optionally add an extra [`Layer`] to the tracing subscriber
233    ///
234    /// This function is only called once, when the plugin is built.
235    ///
236    /// Because [`BoxedLayer`] takes a `dyn Layer`, `Vec<Layer>` is also an acceptable return value.
237    ///
238    /// Access to [`App`] is also provided to allow for communication between the
239    /// [`Subscriber`](tracing::Subscriber) and the [`App`].
240    ///
241    /// Please see the `examples/log_layers.rs` for a complete example.
242    pub custom_layer: fn(app: &mut App) -> Option<BoxedLayer>,
243}
244
245/// A boxed [`Layer`] that can be used with [`LogPlugin`].
246pub type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
247
248/// The default [`LogPlugin`] [`EnvFilter`].
249pub const DEFAULT_FILTER: &str = "wgpu=error,naga=warn";
250
251impl Default for LogPlugin {
252    fn default() -> Self {
253        Self {
254            filter: DEFAULT_FILTER.to_string(),
255            level: Level::INFO,
256            custom_layer: |_| None,
257        }
258    }
259}
260
261impl Plugin for LogPlugin {
262    #[expect(clippy::print_stderr, reason = "Allowed during logger setup")]
263    fn build(&self, app: &mut App) {
264        #[cfg(feature = "trace")]
265        {
266            let old_handler = std::panic::take_hook();
267            std::panic::set_hook(Box::new(move |infos| {
268                eprintln!("{}", tracing_error::SpanTrace::capture());
269                old_handler(infos);
270            }));
271        }
272
273        let finished_subscriber;
274        let subscriber = Registry::default();
275
276        // add optional layer provided by user
277        let subscriber = subscriber.with((self.custom_layer)(app));
278
279        let default_filter = { format!("{},{}", self.level, self.filter) };
280        let filter_layer = EnvFilter::try_from_default_env()
281            .or_else(|from_env_error| {
282                _ = from_env_error
283                    .source()
284                    .and_then(|source| source.downcast_ref::<ParseError>())
285                    .map(|parse_err| {
286                        // we cannot use the `error!` macro here because the logger is not ready yet.
287                        eprintln!("LogPlugin failed to parse filter from env: {}", parse_err);
288                    });
289
290                Ok::<EnvFilter, FromEnvError>(EnvFilter::builder().parse_lossy(&default_filter))
291            })
292            .unwrap();
293        let subscriber = subscriber.with(filter_layer);
294
295        #[cfg(feature = "trace")]
296        let subscriber = subscriber.with(tracing_error::ErrorLayer::default());
297
298        #[cfg(all(
299            not(target_arch = "wasm32"),
300            not(target_os = "android"),
301            not(target_os = "ios")
302        ))]
303        {
304            #[cfg(feature = "tracing-chrome")]
305            let chrome_layer = {
306                let mut layer = tracing_chrome::ChromeLayerBuilder::new();
307                if let Ok(path) = std::env::var("TRACE_CHROME") {
308                    layer = layer.file(path);
309                }
310                let (chrome_layer, guard) = layer
311                    .name_fn(Box::new(|event_or_span| match event_or_span {
312                        tracing_chrome::EventOrSpan::Event(event) => event.metadata().name().into(),
313                        tracing_chrome::EventOrSpan::Span(span) => {
314                            if let Some(fields) =
315                                span.extensions().get::<FormattedFields<DefaultFields>>()
316                            {
317                                format!("{}: {}", span.metadata().name(), fields.fields.as_str())
318                            } else {
319                                span.metadata().name().into()
320                            }
321                        }
322                    }))
323                    .build();
324                app.insert_resource(FlushGuard(SyncCell::new(guard)));
325                chrome_layer
326            };
327
328            #[cfg(feature = "tracing-tracy")]
329            let tracy_layer = tracing_tracy::TracyLayer::default();
330
331            // note: the implementation of `Default` reads from the env var NO_COLOR
332            // to decide whether to use ANSI color codes, which is common convention
333            // https://no-color.org/
334            let fmt_layer = tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr);
335
336            // bevy_render::renderer logs a `tracy.frame_mark` event every frame
337            // at Level::INFO. Formatted logs should omit it.
338            #[cfg(feature = "tracing-tracy")]
339            let fmt_layer =
340                fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| {
341                    meta.fields().field("tracy.frame_mark").is_none()
342                }));
343
344            let subscriber = subscriber.with(fmt_layer);
345
346            #[cfg(feature = "tracing-chrome")]
347            let subscriber = subscriber.with(chrome_layer);
348            #[cfg(feature = "tracing-tracy")]
349            let subscriber = subscriber.with(tracy_layer);
350            finished_subscriber = subscriber;
351        }
352
353        #[cfg(target_arch = "wasm32")]
354        {
355            finished_subscriber = subscriber.with(tracing_wasm::WASMLayer::new(
356                tracing_wasm::WASMLayerConfig::default(),
357            ));
358        }
359
360        #[cfg(target_os = "android")]
361        {
362            finished_subscriber = subscriber.with(android_tracing::AndroidLayer::default());
363        }
364
365        #[cfg(target_os = "ios")]
366        {
367            finished_subscriber = subscriber.with(tracing_oslog::OsLogger::default());
368        }
369
370        let logger_already_set = LogTracer::init().is_err();
371        let subscriber_already_set =
372            tracing::subscriber::set_global_default(finished_subscriber).is_err();
373
374        match (logger_already_set, subscriber_already_set) {
375            (true, true) => error!(
376                "Could not set global logger and tracing subscriber as they are already set. Consider disabling LogPlugin."
377            ),
378            (true, false) => error!("Could not set global logger as it is already set. Consider disabling LogPlugin."),
379            (false, true) => error!("Could not set global tracing subscriber as it is already set. Consider disabling LogPlugin."),
380            (false, false) => (),
381        }
382    }
383}