use std::sync::Once;
pub fn install_panic_logger() {
static INSTALLED: Once = Once::new();
INSTALLED.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(wrap_hook(previous));
});
}
type Hook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send + 'static>;
fn wrap_hook(previous: Hook) -> Hook {
Box::new(move |info| {
let payload = info
.payload_as_str()
.unwrap_or("<non-string panic payload>");
let location = info.location().map_or_else(
|| "<unknown>".to_string(),
|l| format!("{}:{}:{}", l.file(), l.line(), l.column()),
);
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>").to_string();
let backtrace = std::backtrace::Backtrace::force_capture();
tracing::error!(
panic_payload = payload,
panic_location = %location,
panic_thread = %thread_name,
"PANIC in thread '{thread_name}' at {location}: {payload}\n\
backtrace:\n{backtrace}"
);
previous(info);
})
}
#[cfg(test)]
mod tests {
use super::*;
fn hook_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn logger_install_is_idempotent() {
let _guard = hook_lock();
install_panic_logger();
install_panic_logger();
let caught = std::panic::catch_unwind(|| panic!("idempotence probe"));
assert!(
caught.is_err(),
"panic must still propagate to catch_unwind"
);
}
#[test]
fn panic_payload_reaches_the_tracing_subscriber() {
use tracing_subscriber::layer::SubscriberExt;
let _guard = hook_lock();
let buffer = crate::log_buffer::LogBuffer::new(32);
let subscriber = tracing_subscriber::registry()
.with(crate::log_buffer::LogBufferLayer::new(buffer.clone()));
install_panic_logger();
tracing::subscriber::with_default(subscriber, || {
let _ = std::panic::catch_unwind(|| panic!("payload probe 4764"));
});
let captured = buffer.tail(32).join("\n");
assert!(
captured.contains("payload probe 4764"),
"panic payload missing from subscriber output:\n{captured}"
);
assert!(
captured.contains("PANIC"),
"panic marker missing from subscriber output:\n{captured}"
);
assert!(
captured.contains("panic_hook.rs"),
"panic location missing from subscriber output:\n{captured}"
);
}
#[test]
fn logger_preserves_previous_hook() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let _guard = hook_lock();
let ran = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&ran);
let saved = std::panic::take_hook();
std::panic::set_hook(wrap_hook(Box::new(move |_| {
flag.store(true, Ordering::SeqCst);
})));
let _ = std::panic::catch_unwind(|| panic!("delegation probe"));
std::panic::set_hook(saved);
assert!(ran.load(Ordering::SeqCst), "previous hook must still run");
}
}