http_problem/
reporter.rs

1use std::{
2    error::Error,
3    fmt::{self, Debug},
4    ops::Deref,
5    panic::Location,
6};
7
8use backtrace::Backtrace;
9use chrono::{DateTime, Local};
10use http::Extensions;
11use once_cell::sync::OnceCell;
12use parking_lot::RwLock;
13
14use crate::Problem;
15
16static REPORTER: OnceCell<Box<dyn ProblemReporter + Send + Sync>> = OnceCell::new();
17
18#[track_caller]
19pub(crate) fn capture_handler(error: &(dyn Error + 'static)) -> Box<dyn eyre::EyreHandler> {
20    let mut report = Box::default();
21
22    if let Some(reporter) = global_reporter() {
23        reporter.capture_error_context(&mut report, error);
24    }
25
26    report
27}
28
29pub(crate) fn global_reporter() -> Option<&'static dyn ProblemReporter> {
30    REPORTER.get().map(|r| &**r as &'static dyn ProblemReporter)
31}
32
33/// Sets the current global reporter.
34///
35/// # Panics
36///
37/// This function panics if it is called twice.
38pub fn set_global_reporter<R>(reporter: R)
39where
40    R: ProblemReporter + Send + Sync + 'static,
41{
42    // Can't use `.expect` as `R` is not (and should not) be `Debug`.
43    if REPORTER.set(Box::new(reporter)).is_err() {
44        panic!("Global problem reporter set twice! Did you call `problem::reporter::set_global_reporter` twice?");
45    }
46}
47
48/// A type responsible for capturing and report [`Problem`] errors.
49///
50/// The crate doesn't provide a default implementation of this trait,
51/// as how you report your service's errors depends on your infrastructure.
52pub trait ProblemReporter {
53    /// Capture extra information about the error context and saves it
54    /// inside the given report.
55    ///
56    /// Note that this will be called even for client-related errors,
57    /// like NotFound or UnprocessableEntity, during the error _creation_
58    /// (this is why we don't pass a [`Problem`] instance here). So try
59    /// to be conservative on what you do in this method to prevent making
60    /// error handling an expensive operation.
61    fn capture_error_context(&'static self, report: &mut Report, error: &(dyn Error + 'static));
62
63    /// Says if we should report the error or not.
64    fn should_report_error(&'static self, problem: &Problem) -> bool;
65
66    /// Reports the error.
67    ///
68    /// This will only be called if [`ProblemReporter::should_report_error`]
69    /// returns `true`.
70    fn report_error(&'static self, problem: &Problem);
71}
72
73/// Location dependent problem report content.
74///
75/// This type contains information about a given error, primarily the
76/// backtrace, location and timestamp of the error, although reporters may
77/// include extra information.
78///
79/// Note that the [`Backtrace`] is NOT resolved during creation, to prevent
80/// wasting time on the creation of non-reported errors.
81pub struct Report {
82    backtrace: RwLock<Backtrace>,
83    location: &'static Location<'static>,
84    timestamp: DateTime<Local>,
85    extensions: Extensions,
86}
87
88impl Default for Report {
89    #[track_caller]
90    fn default() -> Self {
91        Self {
92            backtrace: RwLock::new(Backtrace::new_unresolved()),
93            location: Location::caller(),
94            timestamp: Local::now(),
95            extensions: Extensions::new(),
96        }
97    }
98}
99
100impl Report {
101    /// Resolves and returns a reference to the error backtrace.
102    pub fn backtrace(&self) -> impl Deref<Target = Backtrace> + '_ {
103        self.backtrace.write().resolve();
104        self.backtrace.read()
105    }
106
107    /// Returns a reference to the _unresolved_ backtrace.
108    #[inline(always)]
109    pub fn backtrace_unresolved(&self) -> impl Deref<Target = Backtrace> + '_ {
110        self.backtrace.read()
111    }
112
113    /// Returns the location where the error happened.
114    ///
115    /// We try our best to fetch the correct location of the error by
116    /// marking everything that may create a [`Problem`] with `#[track_caller]`.
117    #[inline(always)]
118    pub fn location(&self) -> &'static Location<'static> {
119        self.location
120    }
121
122    /// Returns the timestamp of when the error was captured.
123    #[inline(always)]
124    pub fn timestamp(&self) -> DateTime<Local> {
125        self.timestamp
126    }
127
128    /// Inserts an arbitrary data into the report.
129    #[inline(always)]
130    pub fn insert<T: Send + Sync + 'static>(&mut self, val: T) {
131        self.extensions.insert(val);
132    }
133
134    /// Get data inserted in the report via [`Self::insert`].
135    ///
136    /// Returns `None` if no data with the given type is found.
137    #[inline(always)]
138    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
139        self.extensions.get()
140    }
141}
142
143impl eyre::EyreHandler for Report {
144    fn debug(&self, error: &(dyn Error + 'static), f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        write!(
146            f,
147            "Error at {} ({}, {}): ",
148            self.location.file(),
149            self.location.line(),
150            self.location.column()
151        )?;
152
153        write!(f, "{error}")?;
154
155        // Causes.
156        if error.source().is_some() {
157            writeln!(f, "\n\nCaused by:")?;
158
159            let mut curr = error.source();
160            let mut idx = 0;
161            while let Some(err) = curr {
162                writeln!(f, "  {idx}: {err}")?;
163                curr = err.source();
164                idx += 1;
165            }
166        }
167
168        // TODO: print backtrace based on backtrace style.
169        // TODO(internal): Open-Source Backtrace cleaning solution.
170        //
171        // Initially, this used to print a cleaned-up version of the
172        // backtrace, but the process had very TM-specific filters and was
173        // specific to a format that our error reporting infrastructure understood.
174        //
175        // We'll one day open source it when we've a more general solution to
176        // the problem, as this is definitively _extremely_ useful.
177        (*self.backtrace()).fmt(f)
178    }
179
180    fn track_caller(&mut self, location: &'static Location<'static>) {
181        self.location = location;
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_report() {
191        let rep = Report::default();
192
193        assert_eq!(rep.location().line(), line!() - 2);
194        assert_eq!(rep.location().file(), file!());
195
196        std::thread::sleep(std::time::Duration::from_millis(10));
197
198        assert!(rep.timestamp() < Local::now());
199        assert!(!rep.backtrace_unresolved().frames().is_empty());
200
201        let symbols_count = rep
202            .backtrace()
203            .frames()
204            .iter()
205            .flat_map(|f| f.symbols())
206            .count();
207
208        assert!(symbols_count > 0);
209    }
210
211    #[test]
212    fn test_report_extensions() {
213        let mut rep = Report::default();
214
215        rep.insert(2usize);
216
217        assert_eq!(rep.get(), Some(&2usize));
218
219        assert!(rep.get::<String>().is_none());
220    }
221}