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