Skip to main content

obs_core/
panic_hook.rs

1//! `install_panic_hook` — emits one `ObsPanicked` event then calls
2//! `Observer::shutdown_blocking(2s)` so the inflight sinks have a
3//! chance to flush before the prior hook (potentially `panic = abort`)
4//! takes the process down.
5//!
6//! Spec 11 § 6.1.
7
8use std::{
9    panic::{self, PanicHookInfo},
10    sync::atomic::{AtomicBool, Ordering},
11    time::Duration,
12};
13
14use obs_proto::obs::v1::{ObsEnvelope, Severity as PSeverity, Tier as PTier};
15
16static INSTALLED: AtomicBool = AtomicBool::new(false);
17
18/// Install the obs panic hook. Idempotent — calling more than once is
19/// a no-op.
20///
21/// The hook chains the previously-installed hook (typically the
22/// default), so `panic!` still produces the standard backtrace; the
23/// added behaviour is one `ObsPanicked` envelope plus a 2-second
24/// best-effort sink flush before the chained hook runs.
25pub fn install_panic_hook() {
26    if INSTALLED.swap(true, Ordering::SeqCst) {
27        return;
28    }
29    let prev = panic::take_hook();
30    panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| {
31        emit_panicked(info);
32        // Best-effort flush before the chain continues.
33        let observer = crate::observer::observer();
34        observer.shutdown_blocking(Duration::from_secs(2));
35        prev(info);
36    }));
37}
38
39fn emit_panicked(info: &PanicHookInfo<'_>) {
40    let message = panic_message(info);
41    let location = info
42        .location()
43        .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
44        .unwrap_or_default();
45
46    let mut env = ObsEnvelope {
47        full_name: "obs.runtime.v1.ObsPanicked".to_string(),
48        tier: ::buffa::EnumValue::Known(PTier::TIER_LOG),
49        sev: ::buffa::EnumValue::Known(PSeverity::SEVERITY_FATAL),
50        ts_ns: now_ns(),
51        sampling_reason: ::buffa::EnumValue::Known(
52            obs_proto::obs::v1::SamplingReason::SAMPLING_REASON_OVERRIDE,
53        ),
54        ..Default::default()
55    };
56    env.labels
57        .insert("message".to_string(), truncate(&message, 1024));
58    env.labels.insert("location".to_string(), location);
59    // Spec 11 § 6.1: capture trace_id / span_id from the active
60    // `obs::scope!` frame, so the panic event correlates with the
61    // request that triggered it.
62    crate::scope::auto_fill_envelope(&mut env);
63
64    let observer = crate::observer::observer();
65    observer.emit_envelope(env);
66}
67
68fn panic_message(info: &PanicHookInfo<'_>) -> String {
69    if let Some(s) = info.payload().downcast_ref::<&'static str>() {
70        return (*s).to_string();
71    }
72    if let Some(s) = info.payload().downcast_ref::<String>() {
73        return s.clone();
74    }
75    "panic with non-string payload".to_string()
76}
77
78fn truncate(s: &str, max: usize) -> String {
79    if s.len() <= max {
80        s.to_string()
81    } else {
82        let mut t = s[..max].to_string();
83        t.push('…');
84        t
85    }
86}
87
88fn now_ns() -> u64 {
89    use std::time::{SystemTime, UNIX_EPOCH};
90    SystemTime::now()
91        .duration_since(UNIX_EPOCH)
92        .map(|d| d.as_nanos() as u64)
93        .unwrap_or(0)
94}