const QUIET: &[&str] = &[
"wgpu",
"wgpu_core",
"wgpu_hal",
"naga",
"winit",
"calloop",
"sctk",
"smithay_client_toolkit",
"cosmic_text",
"fontdb",
"accesskit_windows",
"accesskit_unix",
];
const SILENT: &[&str] = &["wgpu_core::instance"];
const NOT_THE_APP: &[&str] = &[
"frus",
"frus_shell",
"frus_widgets",
"frus_core",
"frus_layout",
"frus_text",
"frus_gpu",
"frus_test",
"core",
"alloc",
"std",
];
static NAMED: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
pub fn name_app(module_path: &'static str) {
let _ = NAMED.set(module_path.split("::").next().unwrap_or(module_path));
}
pub(crate) fn app_crate<A>() -> &'static str {
NAMED
.get()
.copied()
.unwrap_or_else(|| crate_of(std::any::type_name::<A>()))
}
fn crate_of(type_name: &'static str) -> &'static str {
let crates = type_name
.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == ':'))
.filter(|path| path.contains("::"))
.filter_map(|path| path.split("::").next())
.filter(|name| !name.is_empty());
let mut first = None;
for name in crates {
first.get_or_insert(name);
if !NOT_THE_APP.contains(&name) {
return name;
}
}
first.unwrap_or(type_name)
}
pub(crate) fn default_filter(app_crate: &str, debug_build: bool) -> String {
let mut filter = String::from("info");
for quiet in QUIET {
filter.push_str(&format!(",{quiet}=warn"));
}
for silent in SILENT {
filter.push_str(&format!(",{silent}=error"));
}
if debug_build && !app_crate.is_empty() {
filter.push_str(&format!(",{app_crate}=debug"));
}
filter
}
#[cfg_attr(not(any(web, test)), allow(dead_code))]
pub(crate) struct Filter {
default: log::LevelFilter,
rules: Vec<(String, log::LevelFilter)>,
}
#[cfg_attr(not(any(web, test)), allow(dead_code))]
impl Filter {
pub(crate) fn parse(spec: &str) -> Self {
let mut filter = Filter {
default: log::LevelFilter::Error,
rules: Vec::new(),
};
for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
match part.split_once('=') {
Some((target, level)) => {
if let Ok(level) = level.trim().parse() {
filter.rules.push((target.trim().to_string(), level));
}
}
None => {
if let Ok(level) = part.parse() {
filter.default = level;
}
}
}
}
filter
}
pub(crate) fn max(&self) -> log::LevelFilter {
self.rules
.iter()
.map(|(_, level)| *level)
.fold(self.default, std::cmp::max)
}
pub(crate) fn enabled(&self, target: &str, level: log::Level) -> bool {
let rule = self
.rules
.iter()
.filter(|(prefix, _)| {
target == prefix
|| target
.strip_prefix(prefix.as_str())
.is_some_and(|rest| rest.starts_with("::"))
})
.max_by_key(|(prefix, _)| prefix.len())
.map(|(_, level)| *level)
.unwrap_or(self.default);
level <= rule
}
}
#[cfg(android)]
fn log_panics() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let place = info
.location()
.map(|at| format!(" at {}:{}", at.file(), at.line()))
.unwrap_or_default();
let what = info
.payload()
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "a panic".to_string());
log::error!(target: "panic", "{what}{place}");
previous(info);
}));
}
#[cfg(desktop)]
pub(crate) fn init<A>() {
let mut builder = env_logger::Builder::new();
builder.parse_filters(&default_filter(app_crate::<A>(), cfg!(debug_assertions)));
if let Ok(env) = std::env::var("RUST_LOG") {
builder.parse_filters(&env);
}
let _ = builder.try_init();
}
#[cfg(android)]
pub(crate) fn init<A>() {
let app = app_crate::<A>();
let filter = default_filter(app, cfg!(debug_assertions));
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Trace)
.with_tag(app)
.with_filter(android_logger::FilterBuilder::new().parse(&filter).build()),
);
log_panics();
}
#[cfg(web)]
pub(crate) fn init<A>() {
struct Console(Filter);
impl log::Log for Console {
fn enabled(&self, metadata: &log::Metadata) -> bool {
self.0.enabled(metadata.target(), metadata.level())
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let line = wasm_bindgen::JsValue::from_str(&format!(
"[{} {}] {}",
record.level(),
record.target(),
record.args()
));
match record.level() {
log::Level::Error => web_sys::console::error_1(&line),
log::Level::Warn => web_sys::console::warn_1(&line),
log::Level::Info => web_sys::console::info_1(&line),
log::Level::Debug | log::Level::Trace => web_sys::console::debug_1(&line),
}
}
fn flush(&self) {}
}
let filter = Filter::parse(&default_filter(app_crate::<A>(), cfg!(debug_assertions)));
let max = filter.max();
if log::set_logger(Box::leak(Box::new(Console(filter)))).is_ok() {
log::set_max_level(max);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MyApp;
#[test]
fn the_app_crate_is_the_first_that_is_not_the_frameworks() {
assert_eq!(
crate_of("frus_shell::component::Host<my_app::screens::Root>"),
"my_app"
);
assert_eq!(crate_of("my_app::Model"), "my_app");
assert_eq!(
crate_of("alloc::boxed::Box<dyn frus_widgets::Widget<my_app::Msg>>"),
"my_app"
);
assert_eq!(crate_of("frus_shell::logging::tests::MyApp"), "frus_shell");
assert_eq!(app_crate::<MyApp>(), "frus_shell");
}
#[test]
fn the_gpu_layers_launch_report_is_held_back() {
let filter = Filter::parse(&default_filter("my_app", false));
assert!(!filter.enabled("wgpu_core::instance", log::Level::Warn));
assert!(filter.enabled("wgpu_core::instance", log::Level::Error));
assert!(filter.enabled("wgpu_core::device", log::Level::Warn));
}
#[test]
fn the_default_shows_info_and_the_apps_debug() {
let debug = Filter::parse(&default_filter("my_app", true));
assert!(debug.enabled("my_app", log::Level::Debug));
assert!(debug.enabled("my_app::screens", log::Level::Debug));
assert!(!debug.enabled("my_app", log::Level::Trace));
assert!(debug.enabled("frus_widgets", log::Level::Info));
assert!(!debug.enabled("frus_widgets", log::Level::Debug));
assert!(!debug.enabled("wgpu_core::device", log::Level::Info));
assert!(debug.enabled("wgpu_core::device", log::Level::Warn));
assert!(!debug.enabled("my_application", log::Level::Debug));
let release = Filter::parse(&default_filter("my_app", false));
assert!(!release.enabled("my_app", log::Level::Debug));
assert!(release.enabled("my_app", log::Level::Info));
assert_eq!(debug.max(), log::LevelFilter::Debug);
}
#[test]
fn a_broken_part_is_skipped() {
let filter = Filter::parse("warn,,my_app=loud,other=trace");
assert!(filter.enabled("anything", log::Level::Warn));
assert!(!filter.enabled("anything", log::Level::Info));
assert!(!filter.enabled("my_app", log::Level::Info));
assert!(filter.enabled("other", log::Level::Trace));
}
}