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}