#![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;
pub mod prelude {
pub use super::{
macros::setup_logs, setup_logging, setup_logging_default, LogFileConfig, LogFileError,
LogFileRotation, LogPath, LogSettings,
};
}
pub use macros::setup_logs;
pub type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
pub struct LogSettings {
pub filter: String,
pub level: tracing::Level,
pub custom_layer: fn() -> Option<BoxedLayer>,
pub log_file: Option<LogFileConfig>,
}
impl Default for LogSettings {
fn default() -> Self {
Self {
filter: "wgpu=error,naga=warn".to_string(),
level: Level::INFO,
custom_layer: || None,
log_file: None,
}
}
}
#[derive(Copy, Clone, Default)]
#[allow(missing_docs)]
pub enum LogFileRotation {
Minutely,
Hourly,
#[default]
Daily,
Never,
}
impl From<LogFileRotation> 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,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LogFileError {
#[error("Could not determine log dir: {0}")]
LogDirFail(String),
#[error("Logging to file system is unsupported on platform: {0}")]
Unsupported(String),
}
#[derive(Clone, Deref)]
pub struct LogPath(pub PathBuf);
impl LogPath {
#[allow(unused_variables)]
pub fn find_app_data_dir(app_namespace: (&str, &str, &str)) -> Result<Self, LogFileError> {
#[cfg(not(target_arch = "wasm32"))]
{
directories::ProjectDirs::from(app_namespace.0, app_namespace.1, app_namespace.2)
.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()))
}
}
}
pub struct LogFileConfig {
pub log_path: LogPath,
pub rotation: LogFileRotation,
pub file_name_prefix: String,
pub max_log_files: Option<usize>,
}
#[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.");
}
}
#[must_use]
pub fn setup_logging(settings: LogSettings) -> Option<LogFileGuard> {
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();
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::<ParseError>())
.map(|parse_err| {
eprintln!(
"setup_logging() failed to parse filter from env: {}",
parse_err
);
});
Ok::<EnvFilter, FromEnvError>(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) => {
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();
let fmt_layer = tracing_subscriber::fmt::Layer::default();
#[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() {
warn!("bones_framework::setup_logging() - `LogFileConfig` provided, however logging to file system is not supported in wasm.");
}
}
log_file_guard
}
#[must_use]
pub fn setup_logging_default(app_namespace: (&str, &str, &str)) -> Option<LogFileGuard> {
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) => {
eprintln!("Failed to configure file logging: {err}");
None
}
};
setup_logging(LogSettings {
log_file,
..Default::default()
})
}
#[macro_use]
pub mod macros {
#[macro_export]
macro_rules! setup_logs {
() => {
use bones_framework::logging::setup_logging;
use bones_framework::logging::LogSettings;
let _log_file_guard = setup_logging(LogSettings::default());
};
($settings:ident) => {
use bones_framework::logging::setup_logging;
let _log_file_guard = setup_logging($settings);
};
($app_namespace:expr) => {
use bones_framework::logging::setup_logging_default;
let _log_file_guard = setup_logging_default($app_namespace);
};
($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;
}
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::<String>().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",
);
}