1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Span and panic isolation tracking (feature `observability`).
//!
//! [`PanicTracker::install`] installs a global panic hook that logs every
//! panic through [`tracing`] inside a `mytheclipse_panic_task` span — with the
//! panic message and source location — without stopping the application.
//! [`PanicTracker::catch`] wraps a closure so a panic is captured and reported
//! as a [`PanicInfo`] instead of unwinding across a boundary.
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
/// Information about a captured panic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PanicInfo {
/// Best-effort rendering of the panic payload.
pub message: String,
/// The source location of the panic, if available.
pub location: Option<String>,
}
#[allow(deprecated)]
type PanicHookFn = dyn Fn(&std::panic::PanicInfo<'_>) + Send + Sync + 'static;
/// A RAII guard that, while held, keeps the mytheclipse panic hook installed,
/// restoring the previously-installed hook on drop.
///
/// Returned by [`PanicTracker::install`].
#[must_use = "dropping the guard restores the previous panic hook"]
pub struct PanicGuard {
previous: Arc<PanicHookFn>,
}
/// Installs a panic hook that logs panics through [`tracing`].
pub struct PanicTracker;
impl PanicTracker {
/// Installs a global panic hook that records each panic via
/// `tracing::error!` inside a `mytheclipse_panic_task` span, then re-invokes
/// the previously-installed hook so default panic output (and any user
/// hook) still runs.
///
/// The application does not stop; the panic still unwinds normally, but a
/// trace is captured first. Returns a [`PanicGuard`] that restores the
/// previous hook when dropped.
#[allow(deprecated)]
pub fn install() -> PanicGuard {
let previous: Box<PanicHookFn> = std::panic::take_hook();
let previous: Arc<PanicHookFn> = Arc::from(previous);
let for_hook = Arc::clone(&previous);
std::panic::set_hook(Box::new(move |info| {
let span = tracing::error_span!("mytheclipse_panic_task");
let message = payload_to_string(info.payload());
let location = info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()));
let _guard = span.enter();
match location {
Some(loc) => tracing::error!("panic caught: {message} at {loc}"),
None => tracing::error!("panic caught: {message}"),
}
for_hook(info);
}));
PanicGuard { previous }
}
/// Runs `f`, catching any panic and returning it as an
/// [`Err(PanicInfo)`](PanicInfo) rather than unwinding across the caller.
///
/// Useful to isolate a panic inside a worker thread so it cannot bring
/// down the rest of the process. The caller is responsible for deciding
/// whether to continue after a caught panic.
pub fn catch<T>(f: impl FnOnce() -> T) -> Result<T, PanicInfo> {
let wrapped = AssertUnwindSafe(f);
catch_unwind(wrapped).map_err(|payload| PanicInfo {
message: payload_to_string(&*payload),
location: None,
})
}
}
impl Drop for PanicGuard {
fn drop(&mut self) {
// Take out the hook we installed so we don't call ourselves, then
// restore the one that was active before it.
let _current = std::panic::take_hook();
let previous = Arc::clone(&self.previous);
std::panic::set_hook(Box::new(move |info| previous(info)));
}
}
/// Renders a panic payload (a `&str`, `String`, or fallback) to a string.
fn payload_to_string(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"panic with a non-string payload".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn catch_returns_ok_on_success() {
let result = PanicTracker::catch(|| 1 + 1);
assert_eq!(result.unwrap(), 2);
}
#[test]
fn catch_captures_panic_as_err() {
let result: Result<u32, PanicInfo> = PanicTracker::catch(|| panic!("boom"));
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "boom");
}
#[test]
fn catch_captures_string_payload() {
let err = PanicTracker::catch(|| panic!("{}", String::from("stringy"))).unwrap_err();
assert_eq!(err.message, "stringy");
}
#[test]
fn hook_is_restored_after_guard_drop() {
// Install then drop — previous hook (default) restored.
let _guard = PanicTracker::install();
drop(_guard);
let _ = PanicTracker::catch(|| panic!("irrelevant"));
}
#[test]
fn hook_runs_without_and_panics_are_still_catchable() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_hook = Arc::clone(&calls);
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
calls_hook.fetch_add(1, Ordering::SeqCst);
let _ = info.location().is_some();
}));
let _guard = PanicTracker::install();
// A panic in another thread triggers the hook but not the main thread.
std::thread::spawn(|| panic!("worker boom"))
.join()
.unwrap_err();
drop(_guard);
std::panic::set_hook(prev);
assert!(calls.load(Ordering::SeqCst) >= 1);
}
}