diff --git a/Cargo.lock b/Cargo.lock index 975c190a20..6a5efde57a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1454,6 +1454,7 @@ dependencies = [ "bytemuck", "bytes", "csscolorparser", + "directories", "document-features", "egui", "egui_plot", @@ -1484,6 +1485,10 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-appender", + "tracing-subscriber", + "tracing-tracy", + "tracing-wasm", "ttf-parser", "unic-langid", ] @@ -3096,6 +3101,20 @@ dependencies = [ "synstructure 0.13.1", ] +[[package]] +name = "generator" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186014d53bc231d0090ef8d6f03e0920c54d85a5ed22f4f2f74315ec56cf83fb" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.54.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -4325,6 +4344,19 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.12.4" @@ -6379,6 +6411,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -7397,6 +7435,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "parking_lot", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -7458,6 +7509,17 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "tracing-tracy" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a90519f16f55e5c62ffd5976349f10744435a919ecff83d918300575dfb69b" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracy-client", +] + [[package]] name = "tracing-wasm" version = "0.2.1" @@ -7469,6 +7531,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "tracy-client" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373db47331c3407b343538df77eea2516884a0b126cdfb4b135acfd400015dd7" +dependencies = [ + "loom", + "once_cell", + "tracy-client-sys", +] + +[[package]] +name = "tracy-client-sys" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cf0064dcb31c99aa1244c1b93439359e53f72ed217eef5db50abd442241e9a" +dependencies = [ + "cc", +] + [[package]] name = "triple_buffer" version = "8.0.0" diff --git a/demos/assets_minimal/src/main.rs b/demos/assets_minimal/src/main.rs index 39bc1185c6..092f0dd94c 100644 --- a/demos/assets_minimal/src/main.rs +++ b/demos/assets_minimal/src/main.rs @@ -19,6 +19,9 @@ struct GameMeta { } fn main() { + // Setup logging + setup_logs!(); + // First create bones game. let mut game = Game::new(); diff --git a/demos/features/src/main.rs b/demos/features/src/main.rs index 754c9930cc..c97a421f01 100644 --- a/demos/features/src/main.rs +++ b/demos/features/src/main.rs @@ -84,6 +84,9 @@ struct TileMeta { struct PersistedTextData(String); fn main() { + // Setup logging + setup_logs!(); + // Register persistent data's schema so that it can be loaded by the storage loader. PersistedTextData::register_schema(); diff --git a/demos/hello_world/src/main.rs b/demos/hello_world/src/main.rs index d2c9ed7283..3428194cbb 100644 --- a/demos/hello_world/src/main.rs +++ b/demos/hello_world/src/main.rs @@ -2,6 +2,9 @@ use bones_bevy_renderer::BonesBevyRenderer; use bones_framework::prelude::*; fn main() { + // Setup logging + setup_logs!(); + // First create bones game. let mut game = Game::new(); diff --git a/demos/scripting/src/main.rs b/demos/scripting/src/main.rs index f0d40cbe49..24ec4f5b42 100644 --- a/demos/scripting/src/main.rs +++ b/demos/scripting/src/main.rs @@ -12,6 +12,9 @@ struct GameMeta { } fn main() { + // Setup logging + setup_logs!(); + let mut game = Game::new(); game.install_plugin(DefaultGamePlugin); game.shared_resource_mut::() diff --git a/framework_crates/bones_bevy_renderer/src/lib.rs b/framework_crates/bones_bevy_renderer/src/lib.rs index 657ba91af3..403999809c 100644 --- a/framework_crates/bones_bevy_renderer/src/lib.rs +++ b/framework_crates/bones_bevy_renderer/src/lib.rs @@ -24,7 +24,7 @@ use render::*; mod ui; use ui::*; mod rumble; -use bevy::prelude::*; +use bevy::{log::LogPlugin, prelude::*}; use bones::GamepadsRumble; use bones_framework::prelude as bones; use rumble::*; @@ -145,6 +145,7 @@ impl BonesBevyRenderer { }), ..default() }) + .disable::() .build(); if self.pixel_art { plugins = plugins.set(ImagePlugin::default_nearest()); diff --git a/framework_crates/bones_framework/Cargo.toml b/framework_crates/bones_framework/Cargo.toml index f4ea8b991e..2541bb80ef 100644 --- a/framework_crates/bones_framework/Cargo.toml +++ b/framework_crates/bones_framework/Cargo.toml @@ -11,7 +11,7 @@ categories.workspace = true keywords.workspace = true [features] -default = ["image_png", "ui", "localization", "audio", "audio_ogg", "scripting"] +default = ["image_png", "ui", "localization", "logging", "audio", "audio_ogg", "scripting"] #! Cargo feature supported in `bones_framework`. ## Enable the `ui` module, powered by [`egui`]. @@ -19,6 +19,8 @@ ui = ["dep:egui", "dep:ttf-parser"] ## Enable the localization module, powered by [`fluent`](https://github.com/projectfluent/fluent-rs). localization = ["dep:fluent", "dep:fluent-langneg", "dep:intl-memoizer", "dep:unic-langid", "dep:sys-locale"] +logging = ["dep:tracing-subscriber", "dep:tracing-wasm", "dep:tracing-appender"] + ## Enable the audio system. audio = ["dep:kira"] @@ -71,6 +73,12 @@ image_bmp = ["image/bmp"] ## Simulate dramatic network latency by inserting random sleeps into the networking code. This is extremely cheap and hacky but may be useful. debug-network-slowdown = [] +# Enables tracy tracing subscriber to capture tracing spans for profiling with Tracy. +# +# Note that bones is primarily instrumented with puffin scopes, tracy only captures tracing spans. +# This flag only enables span capture in logging plugin, `bevy/trace_tracy` may be used to enable tracy. +tracing-tracy = ["logging", "dep:tracing-tracy"] + document-features = ["dep:document-features"] [dependencies] @@ -92,7 +100,12 @@ instant = { version = "0.1", features = ["wasm-bindgen"] } noise = "0.9" once_cell = "1.17" thiserror = "1.0" -tracing = "0.1" + +# Tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", optional = true, features = ["env-filter"] } +tracing-appender = { version = "0.2", optional = true, features = ["parking_lot"] } +tracing-tracy = { version = "0.11.0", optional = true, default-features = false } # Render csscolorparser = "0.6" @@ -139,3 +152,9 @@ smallvec = "1.10" iroh-quinn = { version = "0.10" } iroh-net = { version = "0.22" } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + + +directories = "5.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tracing-wasm = { version = "0.2.1", optional = true } diff --git a/framework_crates/bones_framework/src/lib.rs b/framework_crates/bones_framework/src/lib.rs index ef7ddc499c..d76efb3075 100644 --- a/framework_crates/bones_framework/src/lib.rs +++ b/framework_crates/bones_framework/src/lib.rs @@ -42,6 +42,9 @@ pub mod prelude { #[cfg(feature = "localization")] pub use crate::localization::*; + + #[cfg(feature = "logging")] + pub use crate::logging::prelude::*; } pub mod animation; @@ -67,6 +70,9 @@ pub use bones_scripting as scripting; #[cfg(feature = "localization")] pub mod localization; +#[cfg(feature = "logging")] +pub mod logging; + /// External crate documentation. /// /// This module only exists during docs builds and serves to make it eaiser to link to relevant diff --git a/framework_crates/bones_framework/src/logging.rs b/framework_crates/bones_framework/src/logging.rs new file mode 100644 index 0000000000..1c22f5e450 --- /dev/null +++ b/framework_crates/bones_framework/src/logging.rs @@ -0,0 +1,482 @@ +//! Logging module for bones. Provides implementation of global tracing subscriber and panic hook. +//! +//! Enabled with feature "logging". See docs of [`setup_logging`] for details + usage. +#![allow(clippy::needless_doctest_main)] + +use std::{ + backtrace::{Backtrace, BacktraceStatus}, + error::Error, + panic::PanicInfo, + path::PathBuf, +}; + +#[allow(unused_imports)] +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::{ + filter::{FromEnvError, ParseError}, + layer::SubscriberExt, + EnvFilter, Layer, Registry, +}; + +#[allow(unused_imports)] +use tracing::{error, warn, Level}; + +use bones_asset::HasSchema; +use bones_lib::prelude::Deref; + +/// Logging prelude +pub mod prelude { + pub use super::{ + macros::setup_logs, setup_logging, setup_logging_default, LogFileConfig, LogFileError, + LogFileRotation, LogPath, LogSettings, + }; +} + +pub use macros::setup_logs; + +/// A boxed [`Layer`] that can be used with [`setup_logging`]. +pub type BoxedLayer = Box + Send + Sync + 'static>; + +/// Plugin to enable tracing. Configures global tracing subscriber. +pub struct LogSettings { + /// Filters logs using the [`EnvFilter`] format + pub filter: String, + + /// Filters out logs that are "less than" the given level. + /// This can be further filtered using the `filter` setting. + pub level: tracing::Level, + + /// Optionally add an extra [`Layer`] to the tracing subscriber + /// + /// This function is only called once, when logging is initialized. + /// + /// Because [`BoxedLayer`] takes a `dyn Layer`, `Vec` is also an acceptable return value. + pub custom_layer: fn() -> Option, + + /// The (qualifier, organization, application) that will be used to pick a persistent storage + /// location for the game. + /// + /// For example: `("org", "fishfolk", "jumpy")` + /// + /// Used to determine directory to write log files if + // pub app_namespace: Option<(String, String, String)>, + + /// Set to write log output to file system. Not supported on wasm. + pub log_file: Option, +} + +impl Default for LogSettings { + fn default() -> Self { + Self { + filter: "wgpu=error,naga=warn".to_string(), + level: Level::INFO, + custom_layer: || None, + log_file: None, + } + } +} + +/// How often to rotate log file. +#[derive(Copy, Clone, Default)] +#[allow(missing_docs)] +pub enum LogFileRotation { + Minutely, + Hourly, + #[default] + Daily, + Never, +} + +impl From for tracing_appender::rolling::Rotation { + fn from(value: LogFileRotation) -> Self { + match value { + LogFileRotation::Minutely => Rotation::MINUTELY, + LogFileRotation::Hourly => Rotation::HOURLY, + LogFileRotation::Daily => Rotation::DAILY, + LogFileRotation::Never => Rotation::NEVER, + } + } +} + +/// Error for file logging. +#[derive(Debug, thiserror::Error)] +pub enum LogFileError { + /// Failed to determine a log directory. + #[error("Could not determine log dir: {0}")] + LogDirFail(String), + /// Attempted to setup file logging on unsupported platform. + #[error("Logging to file system is unsupported on platform: {0}")] + Unsupported(String), +} + +/// Path to save log files. [`LogPath::find_app_data_dir`] may be used to +/// to automatically find OS appropriate app data path from app namespace strings, e.g. ("org", "fishfolk", "jumpy") +#[derive(Clone, Deref)] +pub struct LogPath(pub PathBuf); + +impl LogPath { + /// Find OS app data path for provided app namespace (e.g. ("org", "fishfolk", "jumpy")) + /// + /// Will error if failed to resole this directory for OS or on unsupported platform such as wasm. + /// + /// i.e. ~/.local/share/org.fishfolk.jumpy/logs, + // C:\Users\\Appdata\Roaming\org.fishfolk.jumpy\logs, + /// ~/Library/Application Support/org.fishfolk.jumpy/logs + #[allow(unused_variables)] + pub fn find_app_data_dir(app_namespace: (&str, &str, &str)) -> Result { + // Don't run this during tests, as Miri CI does not support the syscall. + #[cfg(not(target_arch = "wasm32"))] + { + directories::ProjectDirs::from(app_namespace.0, app_namespace.1, app_namespace.2) + // error message from `ProjectDirs::from` docs + .ok_or(LogFileError::LogDirFail( + "no valid home directory path could be retrieved from the operating system" + .to_string(), + )) + .map(|dirs| LogPath(dirs.data_dir().join("logs"))) + } + + #[cfg(target_arch = "wasm32")] + { + Err(LogFileError::Unsupported("wasm32".to_string())) + } + } +} + +/// Settings to enable writing tracing output to files. +pub struct LogFileConfig { + /// Path to store log files - use [`LogPath`]'s helper function to find good default path. + pub log_path: LogPath, + + /// How often to rotate log file. + pub rotation: LogFileRotation, + + /// Beginning of log file name (e.g. "Jumpy.log"), timestamp will be appended to this + /// if using rotatig logs. + pub file_name_prefix: String, + + /// If set, will cleanup the oldest log files in directory that match `file_name_prefix` until under max + /// file count. Otherwise no log files will be cleaned up. + pub max_log_files: Option, +} + +/// Guard for file logging thread, this should be held onto for duration of app, if dropped +/// writing to log file will stop. +/// +/// It is recommended to hold onto this in main() to ensure all logs are flushed when app is +/// exiting. See [`tracing_appender::non_blocking::WorkerGuard`] docs for details. +#[derive(HasSchema)] +#[schema(no_clone, no_default)] +#[allow(dead_code)] +pub struct LogFileGuard(tracing_appender::non_blocking::WorkerGuard); + +impl Drop for LogFileGuard { + fn drop(&mut self) { + warn!("LogFileGuard dropped - flushing buffered tracing to file, no further tracing will be written to file. If unexpected, make sure bones logging init is done in root scope of app."); + } +} + +/// Setup the global tracing subscriber, add hook for tracing panics, and optionally enable logging to file system. +/// +/// This function sets panic hook to call [`tracing_panic_hook`], and then call previous hook. This writes panics to +/// tracing subscribers. This is helpful for recording panics when logging to file system. +/// +/// if [`LogFileConfig`] was provided in settings and is supported on this platform (cannot log to file system on wasm), +/// this function will return a [`LogFileGuard`]. This must be kept alive for duration of process to capture all logs, +/// see [`LogFileGuard`] docs. +/// +/// Examples below show direct usage and short-hand with [`setup_logs`] macro. +/// +/// # Examples +/// +/// ### Default without logging to file +/// ``` +/// use bones_framework::logging::prelude::*; +/// fn main() { +/// let _log_guard = bones_framework::logging::setup_logging(LogSettings::default()); +/// } +/// ``` +/// or +/// ``` +/// use bones_framework::logging::prelude::*; +/// fn main() { +/// setup_logs!(); +/// } +/// ``` +/// +/// ### Enable logging including logging to files: +/// ```no_run +/// use bones_framework::prelude::*; +/// fn main() { +/// let log_file = +/// match LogPath::find_app_data_dir(("org", "fishfolk", "jumpy")) { +/// Ok(log_path) => Some(LogFileConfig { +/// log_path, +/// rotation: LogFileRotation::Daily, +/// file_name_prefix: "Jumpy.log".to_string(), +/// max_log_files: Some(7), +/// }), +/// Err(err) => { +/// // Cannot use error! macro as logging not configured yet. +/// eprintln!("Failed to configure file logging: {err}"); +/// None +/// } +/// }; +/// +/// // _log_guard will be dropped when main exits, remains alive for duration of program. +/// let _log_guard = bones_framework::logging::setup_logging(LogSettings { +/// log_file, +/// ..default() +/// }); +/// } +/// ``` +/// or logging to file with defaults: +/// ```no_run +/// use bones_framework::logging::prelude::*; +/// fn main() { +/// let _log_guard = bones_framework::logging::setup_logging_default(("org", "fishfolk", "jumpy")); +/// } +/// ``` +/// same with [`macros::setup_logs`] macro: +/// ```no_run +/// use bones_framework::prelude::*; +/// fn main() { +/// setup_logs!("org", "fishfolk", "jumpy"); +/// } +/// ``` +/// +/// +#[must_use] +pub fn setup_logging(settings: LogSettings) -> Option { + // Preserve current panic hook, and call `tracing_panic_hook` to send panic and possibly backtrace to + // tracing subscribers, and not just stderr. + let prev_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + tracing_panic_hook(panic_info); + prev_hook(panic_info); + })); + + let finished_subscriber; + let subscriber = Registry::default(); + + // add optional layer provided by user + let subscriber = subscriber.with((settings.custom_layer)()); + + let default_filter = { format!("{},{}", settings.level, settings.filter) }; + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|from_env_error| { + _ = from_env_error + .source() + .and_then(|source| source.downcast_ref::()) + .map(|parse_err| { + // we cannot use the `error!` macro here because the logger is not ready yet. + eprintln!( + "setup_logging() failed to parse filter from env: {}", + parse_err + ); + }); + + Ok::(EnvFilter::builder().parse_lossy(&default_filter)) + }) + .unwrap(); + let subscriber = subscriber.with(filter_layer); + + let log_file_guard; + #[cfg(not(target_arch = "wasm32"))] + { + let (file_layer, file_guard) = match &settings.log_file { + Some(log_file) => { + let LogFileConfig { + log_path, + rotation, + file_name_prefix, + max_log_files, + } = log_file; + + let file_appender = RollingFileAppender::builder() + .filename_prefix(file_name_prefix) + .rotation((*rotation).into()); + + let file_appender = match *max_log_files { + Some(max) => file_appender.max_log_files(max), + None => file_appender, + }; + + match file_appender.build(&**log_path) { + Ok(file_appender) => { + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let file_layer = + tracing_subscriber::fmt::Layer::default().with_writer(non_blocking); + (Some(file_layer), Some(LogFileGuard(_guard))) + } + Err(err) => { + // we cannot use the `error!` macro here because the logger is not ready yet. + eprintln!("Failed to configure tracing_appender layer for logging to file system - {err}"); + (None, None) + } + } + } + None => (None, None), + }; + let subscriber = subscriber.with(file_layer); + log_file_guard = file_guard; + + #[cfg(feature = "tracing-tracy")] + let tracy_layer = tracing_tracy::TracyLayer::default(); + + // note: the implementation of `Default` reads from the env var NO_COLOR + // to decide whether to use ANSI color codes, which is common convention + // https://no-color.org/ + let fmt_layer = tracing_subscriber::fmt::Layer::default(); + + // bevy_render::renderer logs a `tracy.frame_mark` event every frame + // at Level::INFO. Formatted logs should omit it. + #[cfg(feature = "tracing-tracy")] + let fmt_layer = fmt_layer.with_filter(tracing_subscriber::filter::FilterFn::new(|meta| { + meta.fields().field("tracy.frame_mark").is_none() + })); + + let subscriber = subscriber.with(fmt_layer); + + #[cfg(feature = "tracing-tracy")] + let subscriber = subscriber.with(tracy_layer); + finished_subscriber = subscriber; + } + + #[cfg(target_arch = "wasm32")] + { + finished_subscriber = subscriber.with(tracing_wasm::WASMLayer::new( + tracing_wasm::WASMLayerConfig::default(), + )); + log_file_guard = None; + } + + if let Err(err) = tracing::subscriber::set_global_default(finished_subscriber) { + error!("{err} - `setup_logging` was called and configures global subscriber. Game may either setup subscriber itself, or call `setup_logging` from bones, but not both."); + } + + #[cfg(target_arch = "wasm32")] + { + if settings.log_file.is_some() { + // Report this warning after setting up tracing subscriber so it will show up on wasm. + warn!("bones_framework::setup_logging() - `LogFileConfig` provided, however logging to file system is not supported in wasm."); + } + } + + log_file_guard +} + +/// Helper to call [`setup_logging`] conciseably with reasonable defaults for logging to console and file system. +/// +/// This uses default [`LogSettings`] with addition of enabling logging to files. If logging to file is not desired, +/// you can call `setup_logging(LogSettings::default())` instead. +/// +/// See [`setup_logging`] docs for details. +#[must_use] +pub fn setup_logging_default(app_namespace: (&str, &str, &str)) -> Option { + let file_name_prefix = format!("{}.log", app_namespace.2); + let log_file = + match LogPath::find_app_data_dir((app_namespace.0, app_namespace.1, app_namespace.2)) { + Ok(log_path) => Some(LogFileConfig { + log_path, + rotation: LogFileRotation::Daily, + file_name_prefix, + max_log_files: Some(7), + }), + Err(err) => { + // Cannot use error! macro as logging not configured yet. + eprintln!("Failed to configure file logging: {err}"); + None + } + }; + + // _log_guard will be dropped when main exits, remains alive for duration of program. + setup_logging(LogSettings { + log_file, + ..Default::default() + }) +} + +/// Logging macros +#[macro_use] +pub mod macros { + + /// [`setup_logs`] is a macro for initializing logging in bones. + /// + /// It wraps a call to [`super::setup_logging`] (see docs for details on configuration options). + /// + /// Warning: There may be issues if not called in root scope of app (e.g. in `main()`). + /// Macro expands to: `let _guard = setup_logging(...);`, if `_guard` is dropped, any logging to + /// file system will stop (console logging unimpacted). + /// + /// Usage for log defaults (logging to file system included): + /// ```no_run + /// use bones_framework::prelude::*; + /// setup_logs!("org", "fishfolk", "jumpy"); + /// ``` + /// + /// Usage for log defaults (without logging to file system): + /// ``` + /// use bones_framework::prelude::*; + /// setup_logs!(); + /// ``` + #[macro_export] + macro_rules! setup_logs { + // LogSettings::default() - + // setup_logs!(); + () => { + use bones_framework::logging::setup_logging; + use bones_framework::logging::LogSettings; + let _log_file_guard = setup_logging(LogSettings::default()); + }; + // With LogSettings identifier - + // let settings = LogSettings::{...}; + // setup_logs!(settings); + ($settings:ident) => { + use bones_framework::logging::setup_logging; + let _log_file_guard = setup_logging($settings); + }; + // setup_logging_default from app namespace - + // setup_logs!(("org", "fishfolk", "jumpy")); + ($app_namespace:expr) => { + use bones_framework::logging::setup_logging_default; + let _log_file_guard = setup_logging_default($app_namespace); + }; + // setup_logging_default from app namespace - + // setup_logs!("org", "fishfolk", "jumpy"); + ($app_ns1:expr, $app_ns2:expr, $app_ns3:expr) => { + use bones_framework::logging::setup_logging_default; + let _log_file_guard = setup_logging_default(($app_ns1, $app_ns2, $app_ns3)); + }; + } + pub use setup_logs; +} + +/// Panic hook that sends panic payload to [`tracing::error`], and backtrace if available. +/// +/// This hook is enabled in [`setup_logging`] to make sure panics are traced. +pub fn tracing_panic_hook(panic_info: &PanicInfo) { + let payload = panic_info.payload(); + + let payload = if let Some(s) = payload.downcast_ref::<&str>() { + Some(*s) + } else { + payload.downcast_ref::().map(|s| s.as_str()) + }; + + let location = panic_info.location().map(|l| l.to_string()); + let (backtrace, note) = { + let backtrace = Backtrace::capture(); + let note = (backtrace.status() == BacktraceStatus::Disabled) + .then_some("run with RUST_BACKTRACE=1 environment variable to display a backtrace"); + (Some(backtrace), note) + }; + + tracing::error!( + panic.payload = payload, + panic.location = location, + panic.backtrace = backtrace.map(tracing::field::display), + panic.note = note, + "A panic occurred", + ); +}