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}