Skip to main content

wde_logger/
lib.rs

1//! Logging functions and configuration for WaterDropEngine using [`tracing`](https://docs.rs/tracing).
2//!
3//! The logging behavior can be configured using the [`LogPlugin`], which allows you to set the log level, filter logs using `EnvFilter` syntax, and add custom layers to the tracing subscriber. The `RUST_LOG` environment variable can also be used to override plugin settings and configure log filtering.
4//!
5//! This crate provides multiple macros for logging, including:
6//! - [`trace!()`](crate::trace) - Verbose tracing information, typically only useful for debugging.
7//! - [`debug!()`](crate::debug) - Debug information, useful for development and debugging.
8//! - [`info!()`](crate::info) - General information about the application's operation.
9//! - [`warn!()`](crate::warn) - Important warnings that may indicate potential issues.
10//! - [`error!()`](crate::error) - Critical errors that indicate failures in the application.
11//!
12//! Each of these macros also has a corresponding `_once` variant (e.g. [`info_once!()`](crate::info_once)) that will only log the message once per call site, which is useful for logging within systems that are called every frame without spamming the logs.
13//!
14//! Lastly, span tracing is supported using the `*_span!()` macros (e.g. [`info_span!()`](crate::info_span)) which can be used to create spans for better structuring of logs and performance profiling.
15
16pub mod editor_layer;
17mod panic_handler;
18mod panic_report_layer;
19mod puffin_layer;
20
21extern crate alloc;
22
23use core::error::Error;
24
25mod once;
26pub use once::OnceFlag;
27
28#[doc(hidden)]
29pub mod prelude {
30    pub use crate::LogLevel;
31    pub use crate::{debug_once, error_once, info_once, trace_once, warn_once};
32    pub use tracing::event;
33    pub use tracing::{
34        debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span
35    };
36}
37
38pub use tracing::{
39    self, Level as LogLevel, debug, debug_span, error, error_span, info, info_span, trace,
40    trace_span, warn, warn_span
41};
42
43use bevy::prelude::*;
44use tracing_log::LogTracer;
45use tracing_subscriber::{
46    EnvFilter, Layer,
47    filter::{FromEnvError, ParseError},
48    layer::Layered,
49    prelude::*,
50    registry::Registry
51};
52
53// Store the guard for the non-blocking file appender to ensure logs are flushed on drop and to prevent it from being dropped while the subscriber is still active.
54#[allow(dead_code)]
55#[derive(Resource)]
56struct LoggerGuard(tracing_appender::non_blocking::WorkerGuard);
57
58#[cfg(feature = "puffin")]
59use crate::puffin_layer::PuffinLayer;
60
61/// A boxed [`Layer`] that can be used with [`LogPlugin::custom_layer`].
62type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
63#[cfg(feature = "tracing")]
64type BaseSubscriber =
65    Layered<EnvFilter, Layered<Option<Box<dyn Layer<Registry> + Send + Sync>>, Registry>>;
66#[cfg(feature = "tracing")]
67type PreFmtSubscriber = Layered<tracing_error::ErrorLayer<BaseSubscriber>, BaseSubscriber>;
68#[cfg(not(feature = "tracing"))]
69type PreFmtSubscriber =
70    Layered<EnvFilter, Layered<Option<Box<dyn Layer<Registry> + Send + Sync>>, Registry>>;
71
72/// A boxed [`Layer`] that can be used with [`LogPlugin::fmt_layer`].
73type BoxedFmtLayer = Box<dyn Layer<PreFmtSubscriber> + Send + Sync + 'static>;
74
75/// The default [`LogPlugin`] [`EnvFilter`].
76const DEFAULT_FILTER: &str = concat!(
77    "wgpu_hal=warn,",
78    "wgpu_hal::vulkan::instance=error,",
79    "wgpu_core=warn,",
80    "naga=warn,",
81    "egui_wgpu=error,",
82    "winit=warn,",
83    "calloop=debug,",
84    "notify_debouncer_full=debug,",
85    "notify::inotify=debug,",
86);
87
88/// Plugin that configures logging for WaterDropEngine applications.
89///
90/// # Configuration
91///
92/// The `RUST_LOG` environment variable overrides plugin settings and uses [`EnvFilter`] syntax.
93/// Set `NO_COLOR=1` to disable colored output (see [no-color.org](https://no-color.org/)).
94///
95/// # Log Levels
96///
97/// Available log levels (most to least important):
98/// - `error!()` - Critical failures
99/// - `warn!()` - Important warnings
100/// - `info!()` - General information
101/// - `debug!()` - Debug information
102/// - `trace!()` - Verbose tracing
103pub struct LogPlugin {
104    /// Filters logs using the [`EnvFilter`] format
105    pub filter: String,
106
107    /// Filters out logs that are "less than" the given level. This can be further filtered using the `filter` setting.
108    pub level: LogLevel,
109
110    /// Optionally add an extra [`Layer`] to the tracing subscriber.
111    pub custom_layer: fn(app: &mut App) -> Option<BoxedLayer>,
112
113    /// Override the default [`tracing_subscriber::fmt::Layer`] with a custom one.
114    /// For example, you can use [`tracing_subscriber::fmt::Layer::without_time`] to remove the
115    /// timestamp from the log output.
116    pub fmt_layer: fn(app: &mut App) -> Option<BoxedFmtLayer>,
117
118    /// Path of the file logs are written to. The previous log file, if any, is renamed
119    /// alongside it with a `-old` suffix before its extension (e.g. `log.txt` -> `log-old.txt`).
120    ///
121    /// Defaults to `<temp_dir>/waterdropengine/log.txt`.
122    pub log_file: std::path::PathBuf
123}
124impl Default for LogPlugin {
125    fn default() -> Self {
126        Self {
127            filter: DEFAULT_FILTER.to_string(),
128            level: LogLevel::INFO,
129            custom_layer: |_| None,
130            fmt_layer: |_| None,
131            log_file: std::env::temp_dir().join("waterdropengine").join("log.txt")
132        }
133    }
134}
135impl LogPlugin {
136    /// Sets the log level based on debug/release mode.
137    pub fn auto_level(mut self) -> Self {
138        #[cfg(debug_assertions)]
139        {
140            self.level = if cfg!(feature = "tracing") {
141                LogLevel::TRACE
142            } else {
143                LogLevel::DEBUG
144            };
145        }
146        #[cfg(not(debug_assertions))]
147        {
148            let args = std::env::args().collect::<Vec<_>>();
149            self.level = if args.iter().any(|arg| arg == "--debug") {
150                LogLevel::DEBUG
151            } else {
152                LogLevel::INFO
153            };
154        }
155        self
156    }
157
158    /// Overrides the log level for a specific crate/module target, regardless of the global `level`.
159    ///
160    /// This is useful to keep the engine's own logs quiet (e.g. `LogLevel::INFO`) while enabling
161    /// more verbose logging (e.g. `LogLevel::DEBUG`) for your own game/application crate.
162    ///
163    /// `target` should be the crate's module path as used by `tracing` (i.e. the crate name with
164    /// `-` replaced by `_`), such as `"my_game"`.
165    pub fn with_crate_level(mut self, target: &str, level: LogLevel) -> Self {
166        self.filter = format!("{},{target}={level}", self.filter);
167        self
168    }
169}
170impl Plugin for LogPlugin {
171    fn build(&self, app: &mut App) {
172        // Configure the panic hook
173        configure_panic_hook();
174
175        // Build the tracing subscriber with the configured layers and filters
176        let (subscriber, _guard) = configure_subscriber(
177            app,
178            self.level,
179            &self.filter,
180            self.custom_layer,
181            self.fmt_layer,
182            &self.log_file
183        );
184        app.insert_resource(LoggerGuard(_guard));
185
186        // Set the global logger and subscriber based on the different features enabled
187        let logger_already_set = LogTracer::init().is_err();
188        let subscriber_already_set = tracing::subscriber::set_global_default(subscriber).is_err();
189
190        // Initial log message
191        info!("Starting WaterDropEngine.");
192        info!("Logs will be written to {}.", self.log_file.display());
193
194        // Log errors if we failed to set the global logger or subscriber, likely due to another logger/subscriber already being set.
195        match (logger_already_set, subscriber_already_set) {
196            (true, true) => error!(
197                "Could not set global logger and tracing subscriber as they are already set. Consider disabling LogPlugin."
198            ),
199            (true, false) => error!(
200                "Could not set global logger as it is already set. Consider disabling LogPlugin."
201            ),
202            (false, true) => error!(
203                "Could not set global tracing subscriber as it is already set. Consider disabling LogPlugin."
204            ),
205            (false, false) => ()
206        }
207        debug!(
208            "Logger and tracing subscriber initialized successfully with filter '{}' and log level '{}'.",
209            self.filter, self.level
210        );
211
212        // Log warnings about features that may increase memory usage and potential conflicts with existing loggers/subscribers
213        #[cfg(feature = "tracing")]
214        warn!(
215            "Tracing with Tracy is active, memory consumption will grow until a client is connected."
216        );
217        #[cfg(feature = "puffin")]
218        debug!("Tracing with Puffin is active.");
219        #[cfg(feature = "editor")]
220        debug!("Tracing with editor log layer is active.");
221    }
222}
223
224fn configure_panic_hook() {
225    let custom_hook = panic_handler::PanicHook::get();
226    std::panic::set_hook(Box::new(move |infos| {
227        #[cfg(feature = "tracing")]
228        eprintln!("{}", tracing_error::SpanTrace::capture());
229        custom_hook(infos);
230    }));
231}
232
233fn configure_subscriber(
234    app: &mut App,
235    level: LogLevel,
236    filter: &str,
237    custom_layer: fn(app: &mut App) -> Option<BoxedLayer>,
238    fmt_layer: fn(app: &mut App) -> Option<BoxedFmtLayer>,
239    log_file: &std::path::Path
240) -> (
241    impl tracing::Subscriber + Send + Sync,
242    tracing_appender::non_blocking::WorkerGuard
243) {
244    let subscriber = Registry::default();
245
246    // Add optional layer provided by user
247    let subscriber = subscriber.with((custom_layer)(app));
248
249    // Set up filter corresponding to the env variable RUST_LOG, or use the default filter if the env variable is not set or invalid
250    let default_filter = { format!("{},{}", level, filter) };
251    let filter_layer = EnvFilter::try_from_default_env()
252        .or_else(|from_env_error| {
253            _ = from_env_error
254                .source()
255                .and_then(|source| source.downcast_ref::<ParseError>())
256                .map(|parse_err| {
257                    // We cannot use the `error!` macro here because the logger is not ready yet.
258                    eprintln!("Invalid RUST_LOG environment variable: {parse_err}. Falling back to default filter: {default_filter}");
259                });
260            Ok::<EnvFilter, FromEnvError>(EnvFilter::builder().parse_lossy(&default_filter))
261        })
262        .unwrap();
263    let subscriber = subscriber.with(filter_layer);
264
265    // Register default error layer for capturing backtraces if the `tracing` feature is enabled.
266    #[cfg(feature = "tracing")]
267    let subscriber = subscriber.with(tracing_error::ErrorLayer::default());
268
269    // Set up writer layer
270    let fmt_layer = {
271        // By default, use the default fmt layer with stderr as the output
272        let fmt_layer = (fmt_layer)(app).unwrap_or_else(|| {
273            Box::new(tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr))
274        });
275
276        // Ignore some logs when using tracy
277        #[cfg(feature = "tracing")]
278        let fmt_layer = fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| {
279            meta.fields().field("tracy.frame_mark").is_none()
280        }));
281        fmt_layer
282    };
283    let subscriber = subscriber.with(fmt_layer);
284
285    // Keep an in-memory ring buffer of recent logs for panic reports.
286    let subscriber = subscriber.with(panic_report_layer::PanicReportLogLayer);
287
288    // Rename old log file
289    let path = log_file
290        .parent()
291        .unwrap_or_else(|| std::path::Path::new("."));
292    let _ = std::fs::create_dir_all(path);
293    let file_name = log_file
294        .file_name()
295        .unwrap_or_else(|| std::ffi::OsStr::new("log.txt"));
296    if log_file.exists() {
297        let archived_log_file = {
298            let stem = log_file.file_stem().unwrap_or(file_name).to_string_lossy();
299            let ext = log_file
300                .extension()
301                .map(|ext| format!(".{}", ext.to_string_lossy()))
302                .unwrap_or_default();
303            path.join(format!("{stem}-old{ext}"))
304        };
305        let _ = std::fs::rename(log_file, &archived_log_file);
306    }
307
308    // Set up file appender layer with a non-blocking writer
309    let file_appender = tracing_appender::rolling::never(path, file_name);
310    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
311    let subscriber = subscriber.with(
312        tracing_subscriber::fmt::Layer::default()
313            .with_writer(non_blocking)
314            .with_ansi(false)
315            .with_filter(tracing_subscriber::filter::FilterFn::new(|meta| {
316                meta.fields().field("tracy.frame_mark").is_none()
317            }))
318    );
319
320    // Register editor log layer
321    #[cfg(feature = "editor")]
322    let subscriber = subscriber.with(editor_layer::EditorLogLayer);
323
324    // Register puffin layer
325    #[cfg(feature = "puffin")]
326    let subscriber = subscriber.with(PuffinLayer::default());
327    #[cfg(feature = "puffin")]
328    {
329        puffin::set_scopes_on(true);
330        app.add_systems(Update, puffin_layer::puffin_new_frame_system);
331    }
332
333    // Register tracy layer
334    #[cfg(feature = "tracing")]
335    let subscriber = subscriber.with(tracing_tracy::TracyLayer::default());
336
337    (subscriber, _guard)
338}