Skip to main content

linera_base/
panic_hook.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Process-wide reporting of panics.
5//!
6//! Tokio catches a panic at the task boundary and hands it to whoever joins the task, so a
7//! panicking task neither stops the runtime nor, on its own, produces anything a monitoring
8//! system can act on: the default hook writes to standard error and nothing else. The hook
9//! installed here reports the panic through `tracing` and the metrics registry first, so
10//! that panics are visible wherever the process's other logs and metrics are collected.
11
12use std::{
13    panic::PanicHookInfo,
14    sync::{Once, OnceLock},
15};
16
17#[cfg(with_metrics)]
18pub(crate) mod metrics {
19    use prometheus::IntCounter;
20
21    use crate::prometheus_util::register_int_counter;
22
23    crate::declare_metrics! {
24        /// Panics observed by the hook installed by [`super::init`].
25        ///
26        /// A panic does not stop the process, so this counter is often the only durable signal
27        /// that one happened: whatever the panicking task was responsible for has stopped, and
28        /// the effect on the rest of the process depends entirely on who was joining it. Any
29        /// increase deserves investigation.
30        pub(crate) static PANICS: IntCounter =
31            register_int_counter("panics_total", "Number of panics observed");
32    }
33}
34
35/// A panic hook, in the form [`std::panic::take_hook`] returns it.
36type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Sync + Send>;
37
38/// The hook that was installed before ours, which we delegate to so that the standard
39/// message and the `RUST_BACKTRACE` backtrace are still printed.
40///
41/// A `OnceLock` rather than a lock, so that reading it from inside a panic costs nothing
42/// and cannot be contended.
43static PREVIOUS_HOOK: OnceLock<PanicHook> = OnceLock::new();
44
45static INIT: Once = Once::new();
46
47/// Installs a panic hook that reports panics through `tracing` and the metrics registry
48/// before delegating to the hook that was previously installed.
49///
50/// Calling this more than once has no further effect. It does not change what a panic
51/// *does* — the process still unwinds the panicking task and keeps running — only what is
52/// recorded about it.
53pub fn init() {
54    INIT.call_once(|| {
55        #[cfg(with_metrics)]
56        metrics::init_metrics();
57        PREVIOUS_HOOK
58            .set(std::panic::take_hook())
59            .unwrap_or_else(|_| unreachable!("`call_once` runs this at most once"));
60        std::panic::set_hook(Box::new(report_panic));
61    });
62}
63
64/// The panic message, for the two payload types `panic!` produces.
65///
66/// `PanicHookInfo::payload_as_str` does the same thing, but is not yet stable in the
67/// toolchain the release branches pin, and this code is backported to them.
68fn payload_message<'a>(info: &'a PanicHookInfo<'_>) -> &'a str {
69    let payload = info.payload();
70    payload
71        .downcast_ref::<&str>()
72        .copied()
73        .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
74        .unwrap_or("<non-string payload>")
75}
76
77fn report_panic(info: &PanicHookInfo<'_>) {
78    #[cfg(with_metrics)]
79    metrics::PANICS.inc();
80
81    let thread = std::thread::current();
82    tracing::error!(
83        thread = thread.name().unwrap_or("<unnamed>"),
84        location = info.location().map(tracing::field::display),
85        message = payload_message(info),
86        "Panic",
87    );
88
89    if let Some(previous) = PREVIOUS_HOOK.get() {
90        previous(info);
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::{
97        panic::AssertUnwindSafe,
98        sync::atomic::{AtomicUsize, Ordering},
99    };
100
101    use super::*;
102
103    /// Installing the hook twice must not chain it to itself, which would make every panic
104    /// report grow by one line per call.
105    #[test]
106    fn test_init_is_idempotent() {
107        static DELEGATIONS: AtomicUsize = AtomicUsize::new(0);
108
109        std::panic::set_hook(Box::new(|_| {
110            DELEGATIONS.fetch_add(1, Ordering::SeqCst);
111        }));
112        init();
113        init();
114
115        let panicked = std::panic::catch_unwind(AssertUnwindSafe(|| panic!("boom"))).is_err();
116
117        assert!(panicked);
118        assert_eq!(
119            DELEGATIONS.load(Ordering::SeqCst),
120            1,
121            "the hook installed before `init` ran exactly once",
122        );
123    }
124
125    /// `register_int_counter` applies the `linera` namespace itself, so a name that already
126    /// carries the prefix is exported twice over, as `linera_linera_panics_total`.
127    #[cfg(with_metrics)]
128    #[test]
129    fn the_counter_is_exported_under_a_single_linera_prefix() {
130        crate::init_metrics();
131
132        let names = prometheus::gather()
133            .iter()
134            .map(|family| family.get_name().to_owned())
135            .collect::<Vec<_>>();
136
137        assert!(names.iter().any(|name| name == "linera_panics_total"));
138        assert!(!names.iter().any(|name| name.contains("linera_linera")));
139    }
140}