tracing_panic/
lib.rs

1//! A panic hook that emits an error-level `tracing` event when a panic occurs.
2//!
3//! Check out [`panic_hook`]'s documentation for more information.
4use std::{
5    backtrace::{Backtrace, BacktraceStatus},
6    panic::PanicInfo,
7};
8
9/// A panic hook that emits an error-level `tracing` event when a panic occurs.
10///
11/// The default panic hook prints the panic information to stderr, which might or
12/// might not be picked up by your telemetry system.
13///
14/// This hook, instead, makes sure that panic information goes through the `tracing`
15/// pipeline you've configured.
16///
17/// # Usage
18///
19/// ```rust
20/// use tracing_panic::panic_hook;
21///
22/// # #[allow(clippy::needless_doctest_main)]
23/// fn main() {
24///     // Initialize your `tracing` subscriber however you like.
25///     // [...]
26///     // Then set the panic hook.
27///     // This should be done only once, at the beginning of your program.
28///     std::panic::set_hook(Box::new(panic_hook));
29/// }
30/// ```
31///
32/// # Backtrace
33///
34/// If the `capture-backtrace` feature flag is enabled, `tracing-panic` will try to
35/// capture a backtrace.  
36/// Whether a backtrace is actually captured depends on the value of a few
37/// environment variables, defined in Rust's standard library.
38/// Check out [`std`'s documentation](https://doc.rust-lang.org/std/backtrace/#environment-variables)
39/// for more details.
40///
41/// # Preserving previous hook
42///
43/// Sometimes it's desirable to preserve the previous panic hook, because other crates
44/// might rely on their panic hook integration to function properly.
45///
46/// For this behavior, you can do the following:
47///
48/// ```rust
49/// use tracing_panic::panic_hook;
50///
51/// # #[allow(clippy::needless_doctest_main)]
52/// fn main() {
53///     let prev_hook = std::panic::take_hook();
54///     std::panic::set_hook(Box::new(move |panic_info| {
55///         panic_hook(panic_info);
56///         prev_hook(panic_info);
57///     }));
58/// }
59/// ```
60pub fn panic_hook(panic_info: &PanicInfo) {
61    let payload = panic_info.payload();
62
63    #[allow(clippy::manual_map)]
64    let payload = if let Some(s) = payload.downcast_ref::<&str>() {
65        Some(&**s)
66    } else if let Some(s) = payload.downcast_ref::<String>() {
67        Some(s.as_str())
68    } else {
69        None
70    };
71
72    let location = panic_info.location().map(|l| l.to_string());
73    let (backtrace, note) = if cfg!(feature = "capture-backtrace") {
74        let backtrace = Backtrace::capture();
75        let note = (backtrace.status() == BacktraceStatus::Disabled)
76            .then_some("run with RUST_BACKTRACE=1 environment variable to display a backtrace");
77        (Some(backtrace), note)
78    } else {
79        (None, None)
80    };
81
82    tracing::error!(
83        panic.payload = payload,
84        panic.location = location,
85        panic.backtrace = backtrace.map(tracing::field::display),
86        panic.note = note,
87        "A panic occurred",
88    );
89}
90
91#[cfg(test)]
92mod tests {
93    use tracing::subscriber::DefaultGuard;
94
95    use super::panic_hook;
96    use std::io;
97    use std::sync::{Arc, Mutex, MutexGuard, TryLockError};
98
99    #[test]
100    fn test_static_panic_message() {
101        let buffer = Arc::new(Mutex::new(vec![]));
102        let _guard = init_subscriber(buffer.clone());
103        let _ = std::panic::catch_unwind(|| {
104            std::panic::set_hook(Box::new(panic_hook));
105            panic!("This is a static panic message");
106        });
107
108        let logs = String::from_utf8(buffer.lock().unwrap().clone()).unwrap();
109        assert!(logs.contains("This is a static panic message"));
110    }
111
112    #[cfg(feature = "capture-backtrace")]
113    #[test]
114    fn panic_has_backtrace() {
115        let buffer = Arc::new(Mutex::new(vec![]));
116        let _guard = init_subscriber(buffer.clone());
117        let _ = std::panic::catch_unwind(|| {
118            std::panic::set_hook(Box::new(panic_hook));
119            panic!("This is a static panic message");
120        });
121
122        let logs = String::from_utf8(buffer.lock().unwrap().clone()).unwrap();
123        assert!(logs.contains("backtrace"));
124    }
125
126    #[cfg(not(feature = "capture-backtrace"))]
127    #[test]
128    fn panic_has_no_backtrace() {
129        let buffer = Arc::new(Mutex::new(vec![]));
130        let _guard = init_subscriber(buffer.clone());
131        let _ = std::panic::catch_unwind(|| {
132            std::panic::set_hook(Box::new(panic_hook));
133            panic!("This is a static panic message");
134        });
135
136        let logs = String::from_utf8(buffer.lock().unwrap().clone()).unwrap();
137        assert!(!logs.contains("backtrace"));
138    }
139
140    #[test]
141    fn test_interpolated_panic_message() {
142        let buffer = Arc::new(Mutex::new(vec![]));
143        let _guard = init_subscriber(buffer.clone());
144
145        let _ = std::panic::catch_unwind(|| {
146            std::panic::set_hook(Box::new(panic_hook));
147            panic!("This is an {} panic message", "interpolated");
148        });
149
150        let logs = String::from_utf8(buffer.lock().unwrap().clone()).unwrap();
151        assert!(logs.contains("This is an interpolated panic message"));
152    }
153
154    fn init_subscriber(buffer: Arc<Mutex<Vec<u8>>>) -> DefaultGuard {
155        let subscriber = tracing_subscriber::fmt()
156            .with_writer(move || MockWriter::new(buffer.clone()))
157            .finish();
158        tracing::subscriber::set_default(subscriber)
159    }
160
161    /// Use a vector of bytes behind a Arc<Mutex> as writer in order to inspect the tracing output
162    /// for testing purposes.
163    pub struct MockWriter {
164        buf: Arc<Mutex<Vec<u8>>>,
165    }
166
167    impl MockWriter {
168        pub fn new(buf: Arc<Mutex<Vec<u8>>>) -> Self {
169            Self { buf }
170        }
171
172        pub fn map_error<Guard>(err: TryLockError<Guard>) -> io::Error {
173            match err {
174                TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock),
175                TryLockError::Poisoned(_) => io::Error::from(io::ErrorKind::Other),
176            }
177        }
178
179        pub fn buf(&self) -> io::Result<MutexGuard<'_, Vec<u8>>> {
180            self.buf.try_lock().map_err(Self::map_error)
181        }
182    }
183
184    impl io::Write for MockWriter {
185        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
186            self.buf()?.write(buf)
187        }
188
189        fn flush(&mut self) -> io::Result<()> {
190            self.buf()?.flush()
191        }
192    }
193}