1pub 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#[allow(dead_code)]
55#[derive(Resource)]
56struct LoggerGuard(tracing_appender::non_blocking::WorkerGuard);
57
58#[cfg(feature = "puffin")]
59use crate::puffin_layer::PuffinLayer;
60
61type 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
72type BoxedFmtLayer = Box<dyn Layer<PreFmtSubscriber> + Send + Sync + 'static>;
74
75const 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
88pub struct LogPlugin {
104 pub filter: String,
106
107 pub level: LogLevel,
109
110 pub custom_layer: fn(app: &mut App) -> Option<BoxedLayer>,
112
113 pub fmt_layer: fn(app: &mut App) -> Option<BoxedFmtLayer>,
117
118 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 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 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_panic_hook();
174
175 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 let logger_already_set = LogTracer::init().is_err();
188 let subscriber_already_set = tracing::subscriber::set_global_default(subscriber).is_err();
189
190 info!("Starting WaterDropEngine.");
192 info!("Logs will be written to {}.", self.log_file.display());
193
194 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 #[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 let subscriber = subscriber.with((custom_layer)(app));
248
249 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 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 #[cfg(feature = "tracing")]
267 let subscriber = subscriber.with(tracing_error::ErrorLayer::default());
268
269 let fmt_layer = {
271 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 #[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 let subscriber = subscriber.with(panic_report_layer::PanicReportLogLayer);
287
288 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 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 #[cfg(feature = "editor")]
322 let subscriber = subscriber.with(editor_layer::EditorLogLayer);
323
324 #[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 #[cfg(feature = "tracing")]
335 let subscriber = subscriber.with(tracing_tracy::TracyLayer::default());
336
337 (subscriber, _guard)
338}