1use once_cell::sync::Lazy;
2use parking_lot::RwLock;
3use serde_json::Value;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Severity {
10 Warning,
12 Error,
14 Fatal,
16}
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct ErrorReport {
21 pub error: String,
23 pub handled: bool,
25 pub severity: Severity,
27 pub context: HashMap<String, Value>,
29}
30
31pub trait ErrorSubscriber: Send + Sync {
33 fn report(&self, report: &ErrorReport);
35}
36
37#[derive(Default)]
39pub struct ErrorReporter {
40 subscribers: RwLock<Vec<Arc<dyn ErrorSubscriber>>>,
41}
42
43impl ErrorReporter {
44 #[must_use]
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn subscribe<S>(&self, subscriber: S)
52 where
53 S: ErrorSubscriber + 'static,
54 {
55 self.subscribers.write().push(Arc::new(subscriber));
56 }
57
58 pub fn report(&self, error: impl ToString, handled: bool, context: HashMap<String, Value>) {
60 let severity = if handled {
61 Severity::Warning
62 } else {
63 Severity::Error
64 };
65 self.report_with_severity(error, handled, severity, context);
66 }
67
68 pub fn report_with_severity(
70 &self,
71 error: impl ToString,
72 handled: bool,
73 severity: Severity,
74 context: HashMap<String, Value>,
75 ) {
76 let report = ErrorReport {
77 error: error.to_string(),
78 handled,
79 severity,
80 context,
81 };
82 let subscribers = self.subscribers.read().clone();
83 for subscriber in subscribers {
84 subscriber.report(&report);
85 }
86 }
87}
88
89static GLOBAL_ERROR_REPORTER: Lazy<ErrorReporter> = Lazy::new(ErrorReporter::new);
90
91#[must_use]
93pub fn global_reporter() -> &'static ErrorReporter {
94 &GLOBAL_ERROR_REPORTER
95}
96
97pub fn subscribe<S>(subscriber: S)
99where
100 S: ErrorSubscriber + 'static,
101{
102 global_reporter().subscribe(subscriber);
103}
104
105pub(crate) fn reset_global_reporter() {
106 GLOBAL_ERROR_REPORTER.subscribers.write().clear();
107}
108
109#[cfg(test)]
110mod tests {
111 use super::{
112 ErrorReport, ErrorReporter, ErrorSubscriber, Severity, global_reporter,
113 reset_global_reporter, subscribe,
114 };
115 use parking_lot::Mutex;
116 use serde_json::json;
117 use std::collections::HashMap;
118 use std::sync::Arc;
119
120 #[derive(Default)]
121 struct RecordingSubscriber {
122 reports: Arc<Mutex<Vec<ErrorReport>>>,
123 }
124
125 impl RecordingSubscriber {
126 fn new() -> Self {
127 Self::default()
128 }
129
130 fn handle(&self) -> Arc<Mutex<Vec<ErrorReport>>> {
131 Arc::clone(&self.reports)
132 }
133 }
134
135 impl ErrorSubscriber for RecordingSubscriber {
136 fn report(&self, report: &ErrorReport) {
137 self.reports.lock().push(report.clone());
138 }
139 }
140
141 #[test]
142 fn report_delivers_to_registered_subscriber() {
143 let reporter = ErrorReporter::new();
144 let subscriber = RecordingSubscriber::new();
145 let reports = subscriber.handle();
146 reporter.subscribe(subscriber);
147
148 reporter.report("boom", false, HashMap::new());
149
150 let reports = reports.lock();
151 assert_eq!(reports.len(), 1);
152 assert_eq!(reports[0].error, "boom");
153 }
154
155 #[test]
156 fn handled_reports_are_warnings() {
157 let reporter = ErrorReporter::new();
158 let subscriber = RecordingSubscriber::new();
159 let reports = subscriber.handle();
160 reporter.subscribe(subscriber);
161
162 reporter.report("handled", true, HashMap::new());
163
164 assert_eq!(reports.lock()[0].severity, Severity::Warning);
165 }
166
167 #[test]
168 fn unhandled_reports_are_errors() {
169 let reporter = ErrorReporter::new();
170 let subscriber = RecordingSubscriber::new();
171 let reports = subscriber.handle();
172 reporter.subscribe(subscriber);
173
174 reporter.report("unhandled", false, HashMap::new());
175
176 assert_eq!(reports.lock()[0].severity, Severity::Error);
177 }
178
179 #[test]
180 fn explicit_fatal_severity_is_preserved() {
181 let reporter = ErrorReporter::new();
182 let subscriber = RecordingSubscriber::new();
183 let reports = subscriber.handle();
184 reporter.subscribe(subscriber);
185
186 reporter.report_with_severity("fatal", false, Severity::Fatal, HashMap::new());
187
188 assert_eq!(reports.lock()[0].severity, Severity::Fatal);
189 }
190
191 #[test]
192 fn report_preserves_handled_flag() {
193 let reporter = ErrorReporter::new();
194 let subscriber = RecordingSubscriber::new();
195 let reports = subscriber.handle();
196 reporter.subscribe(subscriber);
197
198 reporter.report("handled", true, HashMap::new());
199
200 assert!(reports.lock()[0].handled);
201 }
202
203 #[test]
204 fn report_clones_context_for_subscribers() {
205 let reporter = ErrorReporter::new();
206 let subscriber = RecordingSubscriber::new();
207 let reports = subscriber.handle();
208 reporter.subscribe(subscriber);
209
210 let context = HashMap::from([(String::from("request_id"), json!("abc123"))]);
211 reporter.report("boom", false, context);
212
213 assert_eq!(
214 reports.lock()[0].context.get("request_id"),
215 Some(&json!("abc123"))
216 );
217 }
218
219 #[test]
220 fn multiple_subscribers_receive_the_same_report() {
221 let reporter = ErrorReporter::new();
222 let first = RecordingSubscriber::new();
223 let second = RecordingSubscriber::new();
224 let first_reports = first.handle();
225 let second_reports = second.handle();
226 reporter.subscribe(first);
227 reporter.subscribe(second);
228
229 reporter.report("boom", false, HashMap::new());
230
231 assert_eq!(first_reports.lock().len(), 1);
232 assert_eq!(second_reports.lock().len(), 1);
233 }
234
235 #[test]
236 fn subscribers_receive_reports_in_subscription_order() {
237 struct OrderedSubscriber {
238 name: &'static str,
239 calls: Arc<Mutex<Vec<&'static str>>>,
240 }
241
242 impl ErrorSubscriber for OrderedSubscriber {
243 fn report(&self, _: &ErrorReport) {
244 self.calls.lock().push(self.name);
245 }
246 }
247
248 let reporter = ErrorReporter::new();
249 let calls = Arc::new(Mutex::new(Vec::new()));
250 reporter.subscribe(OrderedSubscriber {
251 name: "first",
252 calls: Arc::clone(&calls),
253 });
254 reporter.subscribe(OrderedSubscriber {
255 name: "second",
256 calls: Arc::clone(&calls),
257 });
258
259 reporter.report("boom", false, HashMap::new());
260
261 assert_eq!(&*calls.lock(), &["first", "second"]);
262 }
263
264 #[test]
265 fn report_accepts_non_string_errors() {
266 let reporter = ErrorReporter::new();
267 let subscriber = RecordingSubscriber::new();
268 let reports = subscriber.handle();
269 reporter.subscribe(subscriber);
270
271 reporter.report(404, false, HashMap::new());
272
273 assert_eq!(reports.lock()[0].error, "404");
274 }
275
276 #[test]
277 fn global_reporter_returns_the_same_instance() {
278 let first = global_reporter() as *const ErrorReporter;
279 let second = global_reporter() as *const ErrorReporter;
280
281 assert_eq!(first, second);
282 }
283
284 #[test]
285 fn global_subscribe_registers_subscribers() {
286 reset_global_reporter();
287 let subscriber = RecordingSubscriber::new();
288 let reports = subscriber.handle();
289 subscribe(subscriber);
290
291 global_reporter().report("boom", false, HashMap::new());
292
293 assert_eq!(reports.lock().len(), 1);
294 }
295
296 #[test]
297 fn global_reporter_can_emit_fatal_reports() {
298 reset_global_reporter();
299 let subscriber = RecordingSubscriber::new();
300 let reports = subscriber.handle();
301 subscribe(subscriber);
302
303 global_reporter().report_with_severity("fatal", false, Severity::Fatal, HashMap::new());
304
305 assert_eq!(reports.lock()[0].severity, Severity::Fatal);
306 }
307}