openzeppelin_monitor/utils/logging/
error.rs1use chrono::Utc;
8use std::{collections::HashMap, fmt};
9use uuid::Uuid;
10
11#[derive(Debug)]
25pub struct ErrorContext {
26 pub message: String,
28 pub source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
30 pub metadata: Option<HashMap<String, String>>,
32 pub timestamp: String,
34 pub trace_id: String,
36}
37
38impl ErrorContext {
39 pub fn new(
51 message: impl Into<String>,
52 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
53 metadata: Option<HashMap<String, String>>,
54 ) -> Self {
55 let trace_id = if let Some(ref src) = source {
56 TraceableError::trace_id(src.as_ref())
58 } else {
59 Uuid::new_v4().to_string()
60 };
61
62 Self {
63 message: message.into(),
64 source,
65 metadata,
66 timestamp: Utc::now().to_rfc3339(),
67 trace_id,
68 }
69 }
70
71 pub fn new_with_log(
83 message: impl Into<String>,
84 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
85 metadata: Option<HashMap<String, String>>,
86 ) -> Self {
87 let error_context = Self::new(message, source, metadata);
89
90 log_error(&error_context);
92
93 error_context
94 }
95
96 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
109 let metadata = self.metadata.get_or_insert_with(HashMap::new);
110 metadata.insert(key.into(), value.into());
111 self
112 }
113
114 pub fn format_with_metadata(&self) -> String {
123 let mut result = self.message.clone();
124
125 if let Some(metadata) = &self.metadata {
126 if !metadata.is_empty() {
127 let mut parts = Vec::new();
128 let mut keys: Vec<_> = metadata.keys().collect();
130 keys.sort();
131
132 for key in keys {
133 if let Some(value) = metadata.get(key) {
134 parts.push(format!("{}={}", key, value));
135 }
136 }
137
138 if !parts.is_empty() {
139 result.push_str(&format!(" [{}]", parts.join(", ")));
140 }
141 }
142 }
143
144 result
145 }
146}
147
148impl fmt::Display for ErrorContext {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}", self.format_with_metadata())
151 }
152}
153
154impl std::error::Error for ErrorContext {
156 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157 self.source
158 .as_ref()
159 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
160 }
161}
162
163unsafe impl Send for ErrorContext {}
165unsafe impl Sync for ErrorContext {}
166
167pub trait TraceableError: std::error::Error + Send + Sync {
169 fn trace_id(&self) -> String;
171}
172
173impl TraceableError for dyn std::error::Error + Send + Sync + 'static {
174 fn trace_id(&self) -> String {
175 if let Some(id) = try_extract_trace_id(self) {
177 return id;
178 }
179
180 let mut source = self.source();
182 const MAX_DEPTH: usize = 3; let mut depth = 0;
184
185 while let Some(err) = source {
186 depth += 1;
187 if depth > MAX_DEPTH {
188 break;
189 }
190
191 if let Some(id) = try_extract_trace_id(err) {
193 return id;
194 }
195
196 source = err.source();
198 }
199
200 Uuid::new_v4().to_string()
202 }
203}
204
205fn try_extract_trace_id(err: &(dyn std::error::Error + 'static)) -> Option<String> {
207 if let Some(ctx) = err.downcast_ref::<ErrorContext>() {
209 return Some(ctx.trace_id.clone());
210 }
211
212 macro_rules! try_downcast {
214 ($($ty:path),*) => {
215 $(
216 if let Some(e) = err.downcast_ref::<$ty>() {
217 return Some(e.trace_id());
218 }
219 )*
220 }
221 }
222
223 try_downcast!(
225 crate::services::notification::NotificationError,
226 crate::services::trigger::TriggerError,
227 crate::services::filter::FilterError,
228 crate::services::blockwatcher::BlockWatcherError,
229 crate::services::blockchain::BlockChainError,
230 crate::repositories::RepositoryError,
231 crate::services::trigger::ScriptError,
232 crate::models::ConfigError
233 );
234
235 None
237}
238
239fn sanitize_error_message(message: &str) -> String {
241 if message.contains("<html>") || message.contains("<head>") || message.contains("<body>") {
242 if let Some(pos) = message.find('<') {
243 return message[..pos].trim().to_string();
244 }
245 }
246 message.to_string()
247}
248
249fn format_error_chain(err: &dyn std::error::Error) -> String {
251 let mut result = sanitize_error_message(&err.to_string());
252 let mut source = err.source();
253
254 while let Some(err) = source {
255 result.push_str("\n\tCaused by: ");
256 result.push_str(&sanitize_error_message(&err.to_string()));
257 source = err.source();
258 }
259
260 result
261}
262
263pub fn metadata_to_fields(metadata: &Option<HashMap<String, String>>) -> Vec<(&str, &str)> {
265 let mut fields = Vec::new();
266 if let Some(metadata) = metadata {
267 for (key, value) in metadata {
268 fields.push((key.as_str(), value.as_str()));
269 }
270 }
271 fields
272}
273
274fn log_error(error: &ErrorContext) {
276 if let Some(err) = &error.source {
277 tracing::error!(
278 message = error.format_with_metadata(),
279 trace_id = %error.trace_id,
280 timestamp = %error.timestamp,
281 error.chain = %format_error_chain(&**err),
282 "Error occurred"
283 );
284 } else {
285 tracing::error!(
286 message = error.format_with_metadata(),
287 trace_id = %error.trace_id,
288 timestamp = %error.timestamp,
289 "Error occurred"
290 );
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use crate::services::notification::NotificationError;
297
298 use super::*;
299 use std::io;
300
301 #[test]
302 fn test_new_error_context() {
303 let error = ErrorContext::new("Test error", None, None);
304
305 assert_eq!(error.message, "Test error");
306 assert!(error.source.is_none());
307 assert!(error.metadata.is_none());
308 assert!(!error.timestamp.is_empty());
309 assert!(!error.trace_id.is_empty());
310 }
311
312 #[test]
313 fn test_with_metadata() {
314 let error = ErrorContext::new("Test error", None, None)
315 .with_metadata("key1", "value1")
316 .with_metadata("key2", "value2");
317
318 let metadata = error.metadata.unwrap();
319 assert_eq!(metadata.get("key1"), Some(&"value1".to_string()));
320 assert_eq!(metadata.get("key2"), Some(&"value2".to_string()));
321 }
322
323 #[test]
324 fn test_format_with_metadata() {
325 let error = ErrorContext::new("Test error", None, None)
326 .with_metadata("a", "1")
327 .with_metadata("b", "2");
328
329 assert_eq!(error.format_with_metadata(), "Test error [a=1, b=2]");
331 }
332
333 #[test]
334 fn test_display_implementation() {
335 let error = ErrorContext::new("Test error", None, None).with_metadata("key", "value");
336
337 assert_eq!(format!("{}", error), "Test error [key=value]");
338 }
339
340 #[test]
341 fn test_with_source_error() {
342 let source_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
343 let boxed_source = Box::new(source_error) as Box<dyn std::error::Error + Send + Sync>;
344
345 let error = ErrorContext::new("Failed to read config", Some(boxed_source), None);
346
347 assert_eq!(error.message, "Failed to read config");
348 assert!(error.source.is_some());
349 }
350
351 #[test]
352 fn test_metadata_to_fields() {
353 let mut metadata = HashMap::new();
354 metadata.insert("key1".to_string(), "value1".to_string());
355 metadata.insert("key2".to_string(), "value2".to_string());
356
357 let metadata = Some(metadata);
358
359 let fields = metadata_to_fields(&metadata);
360
361 assert_eq!(fields.len(), 2);
363 assert!(fields.contains(&("key1", "value1")));
364 assert!(fields.contains(&("key2", "value2")));
365 }
366
367 #[test]
368 fn test_format_error_chain() {
369 let inner_error = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
371 let middle_error =
372 ErrorContext::new("Failed to open file", Some(Box::new(inner_error)), None);
373 let outer_error =
374 ErrorContext::new("Config loading failed", Some(Box::new(middle_error)), None);
375
376 let formatted = format_error_chain(&outer_error);
377
378 assert!(formatted.contains("Config loading failed"));
379 assert!(formatted.contains("Caused by: Failed to open file"));
380 assert!(formatted.contains("Caused by: Permission denied"));
381 }
382
383 #[test]
384 #[cfg_attr(not(feature = "test-ci-only"), ignore)]
385 fn test_log_error() {
386 use tracing_test::traced_test;
387
388 #[traced_test]
389 fn inner_test() {
390 let error = ErrorContext::new("Test log error", None, None)
391 .with_metadata("test_key", "test_value");
392
393 log_error(&error);
394
395 assert!(logs_contain("Test log error"));
397 assert!(logs_contain(&error.trace_id));
398 assert!(logs_contain(&error.timestamp));
399
400 let source_error = std::io::Error::new(std::io::ErrorKind::Other, "Source error");
402 let error_with_source =
403 ErrorContext::new("Parent error", Some(Box::new(source_error)), None);
404
405 log_error(&error_with_source);
406
407 assert!(logs_contain("Parent error"));
408 assert!(logs_contain("Source error"));
409 }
410
411 inner_test();
412 }
413
414 #[derive(Debug)]
416 struct TestError {
417 message: String,
418 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
419 }
420
421 impl fmt::Display for TestError {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 write!(f, "{}", self.message)
424 }
425 }
426
427 impl std::error::Error for TestError {
428 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
429 self.source
430 .as_ref()
431 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
432 }
433 }
434
435 #[test]
436 fn test_trace_id_propagation() {
437 let inner_error = ErrorContext::new(
439 "Inner error",
440 None,
441 Some(HashMap::from([("key".to_string(), "value".to_string())])),
442 );
443
444 let inner_trace_id = inner_error.trace_id.clone();
446
447 let middle_error = TestError {
449 message: "Middle error".to_string(),
450 source: Some(Box::new(inner_error)),
451 };
452
453 let outer_error = ErrorContext::new("Outer error", Some(Box::new(middle_error)), None);
455
456 let outer_trace_id = outer_error.trace_id.clone();
458
459 assert_eq!(
461 inner_trace_id, outer_trace_id,
462 "Trace IDs should match between inner and outer errors"
463 );
464
465 let dyn_error: &(dyn std::error::Error + Send + Sync) = &outer_error;
467 let trace_id = TraceableError::trace_id(dyn_error);
468
469 assert_eq!(
470 inner_trace_id, trace_id,
471 "Trace ID from TraceableError should match the original trace ID"
472 );
473 }
474
475 #[test]
476 fn test_error_sanitization() {
477 let html_error = "Error occurred<html><body>Some HTML content</body></html>";
479 let sanitized = sanitize_error_message(html_error);
480 assert_eq!(
481 sanitized, "Error occurred",
482 "HTML content should be removed"
483 );
484
485 let normal_error = "This is a normal error message";
487 let sanitized = sanitize_error_message(normal_error);
488 assert_eq!(
489 sanitized, normal_error,
490 "Normal error should remain unchanged"
491 );
492 }
493
494 #[test]
495 fn test_try_extract_trace_id() {
496 let error_ctx = ErrorContext::new("Test error", None, None);
498 let expected_trace_id = error_ctx.trace_id.clone();
499
500 let dyn_error: &(dyn std::error::Error + 'static) = &error_ctx;
501 let extracted = try_extract_trace_id(dyn_error);
502
503 assert_eq!(
504 extracted,
505 Some(expected_trace_id),
506 "Should extract trace ID from ErrorContext"
507 );
508
509 let std_error = io::Error::new(io::ErrorKind::Other, "Standard error");
511 let dyn_error: &(dyn std::error::Error + 'static) = &std_error;
512 let extracted = try_extract_trace_id(dyn_error);
513
514 assert_eq!(
515 extracted, None,
516 "Should return None for non-traceable errors"
517 );
518 }
519
520 #[derive(Debug)]
522 struct MockTraceableError {
523 trace_id: String,
524 }
525
526 impl MockTraceableError {
527 fn new() -> Self {
528 Self {
529 trace_id: Uuid::new_v4().to_string(),
530 }
531 }
532 }
533
534 impl fmt::Display for MockTraceableError {
535 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536 write!(f, "Mock traceable error")
537 }
538 }
539
540 impl std::error::Error for MockTraceableError {}
541
542 impl TraceableError for MockTraceableError {
543 fn trace_id(&self) -> String {
544 self.trace_id.clone()
545 }
546 }
547
548 #[test]
549 fn test_trace_id_extraction_with_custom_implementation() {
550 let mock_error = MockTraceableError::new();
552 let expected_trace_id = mock_error.trace_id.clone();
553
554 let dyn_error: &(dyn std::error::Error + Send + Sync) = &mock_error;
556 let extracted = TraceableError::trace_id(dyn_error);
557
558 assert!(
559 extracted != expected_trace_id,
560 "Should not extract trace ID from custom error types since it's not in the \
561 try_downcast! macro list"
562 );
563 }
564
565 #[test]
566 fn test_trace_id_propagation_through_error_chain() {
567 let mock_error = NotificationError::config_error("Test error", None, None);
568 let expected_trace_id = mock_error.trace_id();
569
570 let boxed_error: Box<dyn std::error::Error + Send + Sync> = Box::new(mock_error);
572
573 let error_ctx = ErrorContext::new("Outer error", Some(boxed_error), None);
575
576 assert_eq!(
578 error_ctx.trace_id, expected_trace_id,
579 "Trace ID should propagate through the error chain"
580 );
581 }
582}