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