error_stack/fmt/
hook.rs

1// We allow `unreachable_pub` on no-std, because in that case we do not export (`pub`) the
2// structures contained in here, but still use them, otherwise we would need to have two redundant
3// implementation: `pub(crate)` and `pub`.
4#![cfg_attr(not(feature = "std"), allow(unreachable_pub))]
5
6use alloc::{boxed::Box, string::String, vec::Vec};
7use core::{any::TypeId, mem};
8
9pub(crate) use default::install_builtin_hooks;
10
11use crate::fmt::{ColorMode, Frame, charset::Charset};
12
13pub(crate) struct Format {
14    alternate: bool,
15
16    color: ColorMode,
17    charset: Charset,
18
19    body: Vec<String>,
20    appendix: Vec<String>,
21}
22
23impl Format {
24    pub(crate) const fn new(alternate: bool, color: ColorMode, charset: Charset) -> Self {
25        Self {
26            alternate,
27
28            color,
29            charset,
30
31            body: Vec::new(),
32            appendix: Vec::new(),
33        }
34    }
35
36    fn appendix(&self) -> &[String] {
37        &self.appendix
38    }
39
40    fn take_body(&mut self) -> Vec<String> {
41        mem::take(&mut self.body)
42    }
43}
44
45crate::hook::context::impl_hook_context! {
46    /// Carrier for contextual information used across hook invocations.
47    ///
48    /// `HookContext` has two fundamental use-cases:
49    /// 1) Adding body entries and appendix entries
50    /// 2) Storage
51    ///
52    /// ## Adding body entries and appendix entries
53    ///
54    /// A [`Debug`] backtrace consists of two different sections, a rendered tree of objects (the
55    /// **body**) and additional text/information that is too large to fit into the tree (the
56    /// **appendix**).
57    ///
58    /// Entries for the body can be attached to the rendered tree of objects via
59    /// [`HookContext::push_body`]. An appendix entry can be attached via
60    /// [`HookContext::push_appendix`].
61    ///
62    /// [`Debug`]: core::fmt::Debug
63    ///
64    /// ### Example
65    ///
66    /// ```rust
67    /// # // we only test with nightly, which means that `render()` is unused on earlier version
68    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
69    /// use std::io::{Error, ErrorKind};
70    ///
71    /// use error_stack::Report;
72    ///
73    /// struct Warning(&'static str);
74    /// struct HttpResponseStatusCode(u64);
75    /// struct Suggestion(&'static str);
76    /// # #[allow(dead_code)]
77    /// struct Secret(&'static str);
78    ///
79    /// Report::install_debug_hook::<HttpResponseStatusCode>(|HttpResponseStatusCode(value), context| {
80    ///     // Create a new appendix, which is going to be displayed when someone requests the alternate
81    ///     // version (`:#?`) of the report.
82    ///     if context.alternate() {
83    ///         context.push_appendix(format!("error {value}: {} error", if *value < 500 {"client"} else {"server"}))
84    ///     }
85    ///
86    ///     // This will push a new entry onto the body with the specified value
87    ///     context.push_body(format!("error code: {value}"));
88    /// });
89    ///
90    /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
91    ///     let idx = context.increment_counter();
92    ///
93    ///     // Create a new appendix, which is going to be displayed when someone requests the alternate
94    ///     // version (`:#?`) of the report.
95    ///     if context.alternate() {
96    ///         context.push_body(format!("suggestion {idx}:\n  {value}"));
97    ///     }
98    ///
99    ///     // This will push a new entry onto the body with the specified value
100    ///     context.push_body(format!("suggestion ({idx})"));
101    /// });
102    ///
103    /// Report::install_debug_hook::<Warning>(|Warning(value), context| {
104    ///     // You can add multiples entries to the body (and appendix) in the same hook.
105    ///     context.push_body("abnormal program execution detected");
106    ///     context.push_body(format!("warning: {value}"));
107    /// });
108    ///
109    /// // By not adding anything you are able to hide an attachment
110    /// // (it will still be counted towards opaque attachments)
111    /// Report::install_debug_hook::<Secret>(|_, _| {});
112    ///
113    /// let report = Report::new(Error::from(ErrorKind::InvalidInput))
114    ///     .attach_opaque(HttpResponseStatusCode(404))
115    ///     .attach_opaque(Suggestion("do you have a connection to the internet?"))
116    ///     .attach_opaque(HttpResponseStatusCode(405))
117    ///     .attach_opaque(Warning("unable to determine environment"))
118    ///     .attach_opaque(Secret("pssst, don't tell anyone else c;"))
119    ///     .attach_opaque(Suggestion("execute the program from the fish shell"))
120    ///     .attach_opaque(HttpResponseStatusCode(501))
121    ///     .attach_opaque(Suggestion("try better next time!"));
122    ///
123    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
124    /// # fn render(value: String) -> String {
125    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
126    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
127    /// #
128    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
129    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
130    /// #
131    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
132    /// # }
133    /// #
134    /// # #[cfg(nightly)]
135    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__emit.snap")].assert_eq(&render(format!("{report:?}")));
136    /// #
137    /// println!("{report:?}");
138    ///
139    /// # #[cfg(nightly)]
140    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__emit_alt.snap")].assert_eq(&render(format!("{report:#?}")));
141    /// #
142    /// println!("{report:#?}");
143    /// ```
144    ///
145    /// The output of `println!("{report:?}")`:
146    ///
147    /// <pre>
148    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__emit.snap"))]
149    /// </pre>
150    ///
151    /// The output of `println!("{report:#?}")`:
152    ///
153    /// <pre>
154    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__emit_alt.snap"))]
155    /// </pre>
156    ///
157    /// ## Storage
158    ///
159    /// `HookContext` can be used to store and retrieve values that are going to be used on multiple
160    /// hook invocations in a single [`Debug`] call.
161    ///
162    /// Every hook can request their corresponding `HookContext`.
163    /// This is especially useful for incrementing/decrementing values, but can also be used to store
164    /// any arbitrary value for the duration of the [`Debug`] invocation.
165    ///
166    /// All data stored in `HookContext` is completely separated from all other hooks and can store
167    /// any arbitrary data of any type, and even data of multiple types at the same time.
168    ///
169    /// ### Example
170    ///
171    /// ```rust
172    /// # // we only test with nightly, which means that `render()` is unused on earlier version
173    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
174    /// use std::io::ErrorKind;
175    ///
176    /// use error_stack::Report;
177    ///
178    /// struct Computation(u64);
179    ///
180    /// Report::install_debug_hook::<Computation>(|Computation(value), context| {
181    ///     // Get a value of type `u64`, if we didn't insert one yet, default to 0
182    ///     let mut acc = context.get::<u64>().copied().unwrap_or(0);
183    ///     acc += *value;
184    ///
185    ///     // Get a value of type `f64`, if we didn't insert one yet, default to 1.0
186    ///     let mut div = context.get::<f32>().copied().unwrap_or(1.0);
187    ///     div /= *value as f32;
188    ///
189    ///     // Insert the calculated `u64` and `f32` back into storage, so that we can use them
190    ///     // in the invocations following this one (for the same `Debug` call)
191    ///     context.insert(acc);
192    ///     context.insert(div);
193    ///
194    ///     context.push_body(format!(
195    ///         "computation for {value} (acc = {acc}, div = {div})"
196    ///     ));
197    /// });
198    ///
199    /// let report = Report::new(std::io::Error::from(ErrorKind::InvalidInput))
200    ///     .attach_opaque(Computation(2))
201    ///     .attach_opaque(Computation(3));
202    ///
203    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
204    /// # fn render(value: String) -> String {
205    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
206    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
207    /// #
208    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
209    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
210    /// #
211    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
212    /// # }
213    /// #
214    /// # #[cfg(nightly)]
215    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_storage.snap")].assert_eq(&render(format!("{report:?}")));
216    /// #
217    /// println!("{report:?}");
218    /// ```
219    ///
220    /// <pre>
221    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_storage.snap"))]
222    /// </pre>
223    ///
224    /// [`Debug`]: core::fmt::Debug
225    pub struct HookContext<Format> { .. }
226}
227
228impl<T> HookContext<T> {
229    pub(crate) fn appendix(&self) -> &[String] {
230        self.inner().extra().appendix()
231    }
232
233    /// The requested [`ColorMode`] for this invocation of hooks.
234    ///
235    /// Hooks can be invoked in different color modes, which represent the preferences of an
236    /// end-user.
237    #[must_use]
238    pub const fn color_mode(&self) -> ColorMode {
239        self.inner().extra().color
240    }
241
242    /// The requested [`Charset`] for this invocation of hooks
243    ///
244    /// Hooks can be invoked in using different charsets, which reflect the capabilities of the
245    /// terminal.
246    #[must_use]
247    pub const fn charset(&self) -> Charset {
248        self.inner().extra().charset
249    }
250
251    /// The contents of the appendix are going to be displayed after the body in the order they have
252    /// been pushed into the [`HookContext`].
253    ///
254    /// This is useful for dense information like backtraces, or span traces.
255    ///
256    /// # Example
257    ///
258    /// ```rust
259    /// # // we only test with nightly, which means that `render()` is unused on earlier version
260    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
261    /// use std::io::ErrorKind;
262    ///
263    /// use error_stack::Report;
264    ///
265    /// struct Error {
266    ///     code: usize,
267    ///     reason: &'static str,
268    /// }
269    ///
270    /// Report::install_debug_hook::<Error>(|Error { code, reason }, context| {
271    ///     if context.alternate() {
272    ///         // Add an entry to the appendix
273    ///         context.push_appendix(format!("error {code}:\n  {reason}"));
274    ///     }
275    ///
276    ///     context.push_body(format!("error {code}"));
277    /// });
278    ///
279    /// let report = Report::new(std::io::Error::from(ErrorKind::InvalidInput))
280    ///     .attach_opaque(Error {
281    ///         code: 404,
282    ///         reason: "not found - server cannot find requested resource",
283    ///     })
284    ///     .attach_opaque(Error {
285    ///         code: 405,
286    ///         reason: "bad request - server cannot or will not process request",
287    ///     });
288    ///
289    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
290    /// # fn render(value: String) -> String {
291    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
292    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
293    /// #
294    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
295    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
296    /// #
297    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
298    /// # }
299    /// #
300    /// # #[cfg(nightly)]
301    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_emit.snap")].assert_eq(&render(format!("{report:#?}")));
302    /// #
303    /// println!("{report:#?}");
304    /// ```
305    ///
306    /// <pre>
307    #[cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__hookcontext_emit.snap")))]
308    /// </pre>
309    pub fn push_appendix(&mut self, content: impl Into<String>) {
310        self.inner_mut().extra_mut().appendix.push(content.into());
311    }
312
313    /// Add a new entry to the body.
314    ///
315    /// # Example
316    ///
317    /// ```rust
318    /// # // we only test with nightly, which means that `render()` is unused on earlier version
319    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
320    /// use std::io;
321    ///
322    /// use error_stack::Report;
323    ///
324    /// struct Suggestion(&'static str);
325    ///
326    /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
327    ///     context.push_body(format!("suggestion: {value}"));
328    ///     // We can push multiples entries in a single hook, these lines will be added one after
329    ///     // another.
330    ///     context.push_body("sorry for the inconvenience!");
331    /// });
332    ///
333    /// let report = Report::new(io::Error::from(io::ErrorKind::InvalidInput))
334    ///     .attach_opaque(Suggestion("try better next time"));
335    ///
336    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
337    /// # fn render(value: String) -> String {
338    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
339    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
340    /// #
341    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
342    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
343    /// #
344    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
345    /// # }
346    /// #
347    /// # #[cfg(nightly)]
348    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__diagnostics_add.snap")].assert_eq(&render(format!("{report:?}")));
349    /// #
350    /// println!("{report:?}");
351    /// ```
352    ///
353    /// <pre>
354    #[cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__diagnostics_add.snap")))]
355    /// </pre>
356    pub fn push_body(&mut self, content: impl Into<String>) {
357        self.inner_mut().extra_mut().body.push(content.into());
358    }
359
360    /// Returns if the currently requested format should render the alternate representation.
361    ///
362    /// This corresponds to the output of [`std::fmt::Formatter::alternate`].
363    #[must_use]
364    pub const fn alternate(&self) -> bool {
365        self.inner().extra().alternate
366    }
367
368    pub(crate) fn take_body(&mut self) -> Vec<String> {
369        self.inner_mut().extra_mut().take_body()
370    }
371}
372
373type BoxedHook = Box<dyn Fn(&Frame, &mut HookContext<Frame>) -> bool + Send + Sync>;
374
375fn into_boxed_hook<T: Send + Sync + 'static>(
376    hook: impl Fn(&T, &mut HookContext<T>) + Send + Sync + 'static,
377) -> BoxedHook {
378    Box::new(move |frame: &Frame, context: &mut HookContext<Frame>| {
379        #[cfg(nightly)]
380        {
381            frame
382                .request_ref::<T>()
383                .map(|value| hook(value, context.cast()))
384                .or_else(|| {
385                    frame
386                        .request_value::<T>()
387                        .map(|ref value| hook(value, context.cast()))
388                })
389                .is_some()
390        }
391
392        // emulate the behavior from nightly by searching for
393        //  - `Context::provide`: not available
394        //  - `Attachment`s: provide themself, emulated by `downcast_ref`
395        #[cfg(not(nightly))]
396        matches!(frame.kind(), crate::FrameKind::Attachment(_))
397            .then_some(frame)
398            .and_then(Frame::downcast_ref::<T>)
399            .map(|value| hook(value, context.cast()))
400            .is_some()
401    })
402}
403
404/// Holds list of hooks.
405///
406/// These are used to augment the [`Debug`] information of attachments and contexts, which are
407/// normally not printable.
408///
409/// Hooks are added via [`.insert()`], which will wrap the function in an additional closure.
410/// This closure will downcast/request the [`Frame`] to the requested type.
411///
412/// If not set, opaque attachments (added via [`.attach_opaque()`]) won't be rendered in the
413/// [`Debug`] output.
414///
415/// The default implementation provides supports for [`Backtrace`] and [`SpanTrace`],
416/// if their necessary features have been enabled.
417///
418/// [`Backtrace`]: std::backtrace::Backtrace
419/// [`SpanTrace`]: tracing_error::SpanTrace
420/// [`Display`]: core::fmt::Display
421/// [`Debug`]: core::fmt::Debug
422/// [`.insert()`]: Hooks::insert
423/// [`.attach_opaque()`]: crate::Report::attach_opaque
424#[expect(clippy::field_scoped_visibility_modifiers)]
425pub(crate) struct Hooks {
426    // We use `Vec`, instead of `HashMap` or `BTreeMap`, so that ordering is consistent with the
427    // insertion order of types.
428    pub(crate) inner: Vec<(TypeId, BoxedHook)>,
429}
430
431impl Hooks {
432    pub(crate) fn insert<T: Send + Sync + 'static>(
433        &mut self,
434        hook: impl Fn(&T, &mut HookContext<T>) + Send + Sync + 'static,
435    ) {
436        let type_id = TypeId::of::<T>();
437
438        // make sure that previous hooks of the same TypeId are deleted.
439        self.inner.retain(|(id, _)| *id != type_id);
440        // push new hook onto the stack
441        self.inner.push((type_id, into_boxed_hook(hook)));
442    }
443
444    pub(crate) fn call(&self, frame: &Frame, context: &mut HookContext<Frame>) -> bool {
445        let mut hit = false;
446
447        for (_, hook) in &self.inner {
448            hit = hook(frame, context) || hit;
449        }
450
451        hit
452    }
453}
454
455mod default {
456    #[cfg(any(feature = "backtrace", feature = "spantrace"))]
457    use alloc::format;
458    use alloc::string::ToString as _;
459    use core::{
460        panic::Location,
461        sync::atomic::{AtomicBool, Ordering},
462    };
463    #[cfg(feature = "backtrace")]
464    use std::backtrace::Backtrace;
465    #[cfg(feature = "std")]
466    use std::sync::Once;
467
468    #[cfg(all(not(feature = "std"), feature = "hooks"))]
469    use spin::once::Once;
470    #[cfg(feature = "spantrace")]
471    use tracing_error::SpanTrace;
472
473    use crate::{
474        Report,
475        fmt::{hook::HookContext, location::LocationAttachment},
476    };
477
478    pub(crate) fn install_builtin_hooks() {
479        // We could in theory remove this and replace it with a single AtomicBool.
480        static INSTALL_BUILTIN: Once = Once::new();
481
482        // This static makes sure that we only run once, if we wouldn't have this guard we would
483        // deadlock, as `install_debug_hook` calls `install_builtin_hooks`, and according to the
484        // docs:
485        //
486        // > If the given closure recursively invokes call_once on the same Once instance the exact
487        // > behavior is not specified, allowed outcomes are a panic or a deadlock.
488        //
489        // This limitation is not present for the implementation from the spin crate, but for
490        // simplicity and readability the extra guard is kept.
491        static INSTALL_BUILTIN_RUNNING: AtomicBool = AtomicBool::new(false);
492
493        // This has minimal overhead, as `Once::call_once` calls `.is_completed` as the short path
494        // we just move it out here, so that we're able to check `INSTALL_BUILTIN_RUNNING`
495        if INSTALL_BUILTIN.is_completed() || INSTALL_BUILTIN_RUNNING.load(Ordering::Acquire) {
496            return;
497        }
498
499        INSTALL_BUILTIN.call_once(|| {
500            INSTALL_BUILTIN_RUNNING.store(true, Ordering::Release);
501
502            Report::install_debug_hook::<Location>(location);
503
504            #[cfg(feature = "backtrace")]
505            Report::install_debug_hook::<Backtrace>(backtrace);
506
507            #[cfg(feature = "spantrace")]
508            Report::install_debug_hook::<SpanTrace>(span_trace);
509        });
510    }
511
512    fn location(location: &Location<'static>, context: &mut HookContext<Location<'static>>) {
513        context.push_body(LocationAttachment::new(location, context.color_mode()).to_string());
514    }
515
516    #[cfg(feature = "backtrace")]
517    fn backtrace(backtrace: &Backtrace, context: &mut HookContext<Backtrace>) {
518        let idx = context.increment_counter();
519
520        context.push_appendix(format!("backtrace no. {}\n{backtrace}", idx + 1));
521        #[cfg(nightly)]
522        context.push_body(format!(
523            "backtrace with {} frames ({})",
524            backtrace.frames().len(),
525            idx + 1
526        ));
527        #[cfg(not(nightly))]
528        context.push_body(format!("backtrace ({})", idx + 1));
529    }
530
531    #[cfg(feature = "spantrace")]
532    fn span_trace(span_trace: &SpanTrace, context: &mut HookContext<SpanTrace>) {
533        let idx = context.increment_counter();
534
535        let mut span = 0;
536        span_trace.with_spans(|_, _| {
537            span += 1;
538            true
539        });
540
541        context.push_appendix(format!("span trace No. {}\n{span_trace}", idx + 1));
542        context.push_body(format!("span trace with {span} frames ({})", idx + 1));
543    }
544}