Skip to main content

bevy_log/
lib.rs

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