1use std::cmp::Reverse;
16use std::collections::HashMap;
17use std::time::{Duration, SystemTime};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum ErrorSeverity {
22 Low,
23 Medium,
24 High,
25 Critical,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub enum ErrorCategory {
31 Validation,
32 Authorization,
33 Network,
34 Database,
35 Internal,
36 Timeout,
37 RateLimit,
38 Unknown,
39}
40
41#[derive(Debug, Clone)]
43pub struct ErrorOccurrence {
44 pub timestamp: SystemTime,
45 pub error_message: String,
46 pub error_type: String,
47 pub stack_trace: Option<String>,
48 pub query_id: Option<String>,
49 pub user_id: Option<String>,
50 pub field_path: Option<Vec<String>>,
51 pub context: HashMap<String, String>,
52}
53
54#[derive(Debug, Clone)]
56pub struct ErrorGroup {
57 pub id: String,
58 pub fingerprint: String,
59 pub error_type: String,
60 pub category: ErrorCategory,
61 pub severity: ErrorSeverity,
62 pub first_seen: SystemTime,
63 pub last_seen: SystemTime,
64 pub occurrences: Vec<ErrorOccurrence>,
65 pub count: usize,
66 pub affected_users: usize,
67 pub affected_queries: usize,
68 pub sample_message: String,
69}
70
71impl ErrorGroup {
72 pub fn error_rate(&self) -> f64 {
74 if self.occurrences.len() < 2 {
75 return 0.0;
76 }
77
78 let duration = self
79 .last_seen
80 .duration_since(self.first_seen)
81 .unwrap_or(Duration::ZERO);
82
83 let minutes = duration.as_secs_f64() / 60.0;
84 if minutes > 0.0 {
85 self.count as f64 / minutes
86 } else {
87 0.0
88 }
89 }
90
91 pub fn is_trending_up(&self, window: Duration) -> bool {
93 let now = SystemTime::now();
94 let cutoff = now - window;
95
96 let recent_count = self
97 .occurrences
98 .iter()
99 .filter(|e| e.timestamp >= cutoff)
100 .count();
101
102 let total_duration = now
103 .duration_since(self.first_seen)
104 .unwrap_or(Duration::ZERO);
105 let window_fraction = window.as_secs_f64() / total_duration.as_secs_f64();
106
107 recent_count as f64 > self.count as f64 * window_fraction * 1.5
108 }
109}
110
111pub struct ErrorAggregator {
113 groups: HashMap<String, ErrorGroup>,
114 max_occurrences_per_group: usize,
115}
116
117impl ErrorAggregator {
118 pub fn new(max_occurrences_per_group: usize) -> Self {
120 Self {
121 groups: HashMap::new(),
122 max_occurrences_per_group,
123 }
124 }
125
126 pub fn record_error(&mut self, occurrence: ErrorOccurrence) {
128 let fingerprint = Self::generate_fingerprint(&occurrence);
129 let category = Self::categorize_error(&occurrence);
130 let severity = Self::assess_severity(&occurrence, &category);
131
132 if let Some(group) = self.groups.get_mut(&fingerprint) {
133 group.last_seen = occurrence.timestamp;
135 group.count += 1;
136
137 if let Some(ref user_id) = occurrence.user_id {
139 if !group
140 .occurrences
141 .iter()
142 .any(|e| e.user_id.as_ref() == Some(user_id))
143 {
144 group.affected_users += 1;
145 }
146 }
147
148 if let Some(ref query_id) = occurrence.query_id {
149 if !group
150 .occurrences
151 .iter()
152 .any(|e| e.query_id.as_ref() == Some(query_id))
153 {
154 group.affected_queries += 1;
155 }
156 }
157
158 group.occurrences.push(occurrence);
160
161 if group.occurrences.len() > self.max_occurrences_per_group {
163 group.occurrences.drain(0..1);
164 }
165 } else {
166 let id = format!("group-{}", self.groups.len());
168 let sample_message = occurrence.error_message.clone();
169 let first_seen = occurrence.timestamp;
170
171 let occurrences = vec![occurrence];
172 let affected_users = occurrences[0].user_id.as_ref().map(|_| 1).unwrap_or(0);
173 let affected_queries = occurrences[0].query_id.as_ref().map(|_| 1).unwrap_or(0);
174
175 self.groups.insert(
176 fingerprint.clone(),
177 ErrorGroup {
178 id,
179 fingerprint,
180 error_type: occurrences[0].error_type.clone(),
181 category,
182 severity,
183 first_seen,
184 last_seen: first_seen,
185 occurrences,
186 count: 1,
187 affected_users,
188 affected_queries,
189 sample_message,
190 },
191 );
192 }
193 }
194
195 pub fn get_all_groups(&self) -> Vec<&ErrorGroup> {
197 self.groups.values().collect()
198 }
199
200 pub fn get_groups_by_category(&self, category: ErrorCategory) -> Vec<&ErrorGroup> {
202 self.groups
203 .values()
204 .filter(|g| g.category == category)
205 .collect()
206 }
207
208 pub fn get_groups_by_severity(&self, severity: ErrorSeverity) -> Vec<&ErrorGroup> {
210 self.groups
211 .values()
212 .filter(|g| g.severity == severity)
213 .collect()
214 }
215
216 pub fn get_top_errors(&self, limit: usize) -> Vec<&ErrorGroup> {
218 let mut groups: Vec<_> = self.groups.values().collect();
219 groups.sort_by_key(|g| Reverse(g.count));
220 groups.into_iter().take(limit).collect()
221 }
222
223 pub fn get_trending_errors(&self, window: Duration) -> Vec<&ErrorGroup> {
225 self.groups
226 .values()
227 .filter(|g| g.is_trending_up(window))
228 .collect()
229 }
230
231 pub fn get_statistics(&self) -> ErrorStatistics {
233 let total_errors: usize = self.groups.values().map(|g| g.count).sum();
234 let total_groups = self.groups.len();
235
236 let mut by_category: HashMap<ErrorCategory, usize> = HashMap::new();
237 let mut by_severity: HashMap<ErrorSeverity, usize> = HashMap::new();
238
239 for group in self.groups.values() {
240 *by_category.entry(group.category.clone()).or_insert(0) += group.count;
241 *by_severity.entry(group.severity).or_insert(0) += group.count;
242 }
243
244 ErrorStatistics {
245 total_errors,
246 total_groups,
247 by_category,
248 by_severity,
249 }
250 }
251
252 pub fn clear(&mut self) {
254 self.groups.clear();
255 }
256
257 fn generate_fingerprint(occurrence: &ErrorOccurrence) -> String {
259 let normalized_message = Self::normalize_message(&occurrence.error_message);
261
262 let mut hash: u64 = 5381;
264 for byte in occurrence.error_type.bytes() {
265 hash = hash.wrapping_mul(33).wrapping_add(byte as u64);
266 }
267 for byte in normalized_message.bytes() {
268 hash = hash.wrapping_mul(33).wrapping_add(byte as u64);
269 }
270
271 format!("{:x}", hash)
272 }
273
274 fn normalize_message(message: &str) -> String {
276 let mut normalized = message.to_string();
278
279 normalized = normalized
281 .split_whitespace()
282 .map(|word| {
283 if word.chars().all(|c| c.is_numeric()) {
284 "<num>"
285 } else {
286 word
287 }
288 })
289 .collect::<Vec<_>>()
290 .join(" ");
291
292 normalized.to_lowercase()
294 }
295
296 fn categorize_error(occurrence: &ErrorOccurrence) -> ErrorCategory {
298 let message_lower = occurrence.error_message.to_lowercase();
299 let type_lower = occurrence.error_type.to_lowercase();
300
301 if type_lower.contains("validation") || message_lower.contains("invalid") {
302 ErrorCategory::Validation
303 } else if type_lower.contains("auth") || message_lower.contains("unauthorized") {
304 ErrorCategory::Authorization
305 } else if type_lower.contains("database")
306 || type_lower.contains("db")
307 || message_lower.contains("database")
308 || message_lower.contains("query")
309 {
310 ErrorCategory::Database
311 } else if message_lower.contains("network") || message_lower.contains("connection") {
312 ErrorCategory::Network
313 } else if message_lower.contains("timeout") {
314 ErrorCategory::Timeout
315 } else if message_lower.contains("rate limit") {
316 ErrorCategory::RateLimit
317 } else if type_lower.contains("internal") {
318 ErrorCategory::Internal
319 } else {
320 ErrorCategory::Unknown
321 }
322 }
323
324 fn assess_severity(occurrence: &ErrorOccurrence, category: &ErrorCategory) -> ErrorSeverity {
326 match category {
327 ErrorCategory::Authorization => ErrorSeverity::High,
328 ErrorCategory::Internal => ErrorSeverity::Critical,
329 ErrorCategory::Database => ErrorSeverity::High,
330 ErrorCategory::Timeout => ErrorSeverity::Medium,
331 ErrorCategory::RateLimit => ErrorSeverity::Low,
332 ErrorCategory::Validation => ErrorSeverity::Low,
333 ErrorCategory::Network => ErrorSeverity::Medium,
334 ErrorCategory::Unknown => {
335 let message_lower = occurrence.error_message.to_lowercase();
337 if message_lower.contains("critical") || message_lower.contains("fatal") {
338 ErrorSeverity::Critical
339 } else if message_lower.contains("error") {
340 ErrorSeverity::High
341 } else {
342 ErrorSeverity::Medium
343 }
344 }
345 }
346 }
347
348 pub fn generate_report(&self) -> String {
350 let mut report = String::from("=== Error Aggregation Report ===\n\n");
351
352 let stats = self.get_statistics();
353 report.push_str(&format!("Total Errors: {}\n", stats.total_errors));
354 report.push_str(&format!("Error Groups: {}\n\n", stats.total_groups));
355
356 report.push_str("Errors by Category:\n");
358 for (category, count) in &stats.by_category {
359 report.push_str(&format!(" {:?}: {}\n", category, count));
360 }
361 report.push('\n');
362
363 report.push_str("Errors by Severity:\n");
365 for (severity, count) in &stats.by_severity {
366 report.push_str(&format!(" {:?}: {}\n", severity, count));
367 }
368 report.push('\n');
369
370 report.push_str("Top 5 Errors:\n");
372 for (i, group) in self.get_top_errors(5).iter().enumerate() {
373 report.push_str(&format!(
374 "{}. {} (count: {}, users: {})\n",
375 i + 1,
376 group.sample_message,
377 group.count,
378 group.affected_users
379 ));
380 }
381
382 report
383 }
384}
385
386#[derive(Debug)]
388pub struct ErrorStatistics {
389 pub total_errors: usize,
390 pub total_groups: usize,
391 pub by_category: HashMap<ErrorCategory, usize>,
392 pub by_severity: HashMap<ErrorSeverity, usize>,
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_record_error() {
401 let mut aggregator = ErrorAggregator::new(100);
402
403 let occurrence = ErrorOccurrence {
404 timestamp: SystemTime::now(),
405 error_message: "Invalid input".to_string(),
406 error_type: "ValidationError".to_string(),
407 stack_trace: None,
408 query_id: Some("query-1".to_string()),
409 user_id: Some("user-1".to_string()),
410 field_path: None,
411 context: HashMap::new(),
412 };
413
414 aggregator.record_error(occurrence);
415
416 assert_eq!(aggregator.get_all_groups().len(), 1);
417 }
418
419 #[test]
420 fn test_error_grouping() {
421 let mut aggregator = ErrorAggregator::new(100);
422
423 for i in 0..3 {
425 aggregator.record_error(ErrorOccurrence {
426 timestamp: SystemTime::now(),
427 error_message: format!("Invalid input {}", i),
428 error_type: "ValidationError".to_string(),
429 stack_trace: None,
430 query_id: Some(format!("query-{}", i)),
431 user_id: None,
432 field_path: None,
433 context: HashMap::new(),
434 });
435 }
436
437 let groups = aggregator.get_all_groups();
439 assert_eq!(groups.len(), 1);
440 assert_eq!(groups[0].count, 3);
441 }
442
443 #[test]
444 fn test_error_categorization() {
445 let mut aggregator = ErrorAggregator::new(100);
446
447 aggregator.record_error(ErrorOccurrence {
448 timestamp: SystemTime::now(),
449 error_message: "Unauthorized access".to_string(),
450 error_type: "AuthError".to_string(),
451 stack_trace: None,
452 query_id: None,
453 user_id: None,
454 field_path: None,
455 context: HashMap::new(),
456 });
457
458 let groups = aggregator.get_groups_by_category(ErrorCategory::Authorization);
459 assert_eq!(groups.len(), 1);
460 }
461
462 #[test]
463 fn test_error_severity() {
464 let mut aggregator = ErrorAggregator::new(100);
465
466 aggregator.record_error(ErrorOccurrence {
467 timestamp: SystemTime::now(),
468 error_message: "Database connection failed".to_string(),
469 error_type: "DatabaseError".to_string(),
470 stack_trace: None,
471 query_id: None,
472 user_id: None,
473 field_path: None,
474 context: HashMap::new(),
475 });
476
477 let groups = aggregator.get_groups_by_severity(ErrorSeverity::High);
478 assert_eq!(groups.len(), 1);
479 }
480
481 #[test]
482 fn test_top_errors() {
483 let mut aggregator = ErrorAggregator::new(100);
484
485 for _ in 0..5 {
487 aggregator.record_error(ErrorOccurrence {
488 timestamp: SystemTime::now(),
489 error_message: "Error A".to_string(),
490 error_type: "ErrorA".to_string(),
491 stack_trace: None,
492 query_id: None,
493 user_id: None,
494 field_path: None,
495 context: HashMap::new(),
496 });
497 }
498
499 for _ in 0..2 {
501 aggregator.record_error(ErrorOccurrence {
502 timestamp: SystemTime::now(),
503 error_message: "Error B".to_string(),
504 error_type: "ErrorB".to_string(),
505 stack_trace: None,
506 query_id: None,
507 user_id: None,
508 field_path: None,
509 context: HashMap::new(),
510 });
511 }
512
513 let top = aggregator.get_top_errors(1);
514 assert_eq!(top.len(), 1);
515 assert_eq!(top[0].count, 5);
516 }
517
518 #[test]
519 fn test_affected_users_tracking() {
520 let mut aggregator = ErrorAggregator::new(100);
521
522 aggregator.record_error(ErrorOccurrence {
523 timestamp: SystemTime::now(),
524 error_message: "Error".to_string(),
525 error_type: "Error".to_string(),
526 stack_trace: None,
527 query_id: None,
528 user_id: Some("user-1".to_string()),
529 field_path: None,
530 context: HashMap::new(),
531 });
532
533 aggregator.record_error(ErrorOccurrence {
534 timestamp: SystemTime::now(),
535 error_message: "Error".to_string(),
536 error_type: "Error".to_string(),
537 stack_trace: None,
538 query_id: None,
539 user_id: Some("user-2".to_string()),
540 field_path: None,
541 context: HashMap::new(),
542 });
543
544 let groups = aggregator.get_all_groups();
545 assert_eq!(groups[0].affected_users, 2);
546 }
547
548 #[test]
549 fn test_statistics() {
550 let mut aggregator = ErrorAggregator::new(100);
551
552 aggregator.record_error(ErrorOccurrence {
553 timestamp: SystemTime::now(),
554 error_message: "Invalid input".to_string(),
555 error_type: "ValidationError".to_string(),
556 stack_trace: None,
557 query_id: None,
558 user_id: None,
559 field_path: None,
560 context: HashMap::new(),
561 });
562
563 aggregator.record_error(ErrorOccurrence {
564 timestamp: SystemTime::now(),
565 error_message: "Unauthorized".to_string(),
566 error_type: "AuthError".to_string(),
567 stack_trace: None,
568 query_id: None,
569 user_id: None,
570 field_path: None,
571 context: HashMap::new(),
572 });
573
574 let stats = aggregator.get_statistics();
575 assert_eq!(stats.total_errors, 2);
576 assert_eq!(stats.total_groups, 2);
577 }
578
579 #[test]
580 fn test_clear() {
581 let mut aggregator = ErrorAggregator::new(100);
582
583 aggregator.record_error(ErrorOccurrence {
584 timestamp: SystemTime::now(),
585 error_message: "Error".to_string(),
586 error_type: "Error".to_string(),
587 stack_trace: None,
588 query_id: None,
589 user_id: None,
590 field_path: None,
591 context: HashMap::new(),
592 });
593
594 assert_eq!(aggregator.get_all_groups().len(), 1);
595
596 aggregator.clear();
597 assert_eq!(aggregator.get_all_groups().len(), 0);
598 }
599
600 #[test]
601 fn test_generate_report() {
602 let mut aggregator = ErrorAggregator::new(100);
603
604 aggregator.record_error(ErrorOccurrence {
605 timestamp: SystemTime::now(),
606 error_message: "Test error".to_string(),
607 error_type: "TestError".to_string(),
608 stack_trace: None,
609 query_id: None,
610 user_id: None,
611 field_path: None,
612 context: HashMap::new(),
613 });
614
615 let report = aggregator.generate_report();
616 assert!(report.contains("Error Aggregation Report"));
617 assert!(report.contains("Total Errors: 1"));
618 }
619
620 #[test]
621 fn test_error_rate_calculation() {
622 let mut aggregator = ErrorAggregator::new(100);
623
624 let first_time = SystemTime::now();
625
626 aggregator.record_error(ErrorOccurrence {
627 timestamp: first_time,
628 error_message: "Error".to_string(),
629 error_type: "Error".to_string(),
630 stack_trace: None,
631 query_id: None,
632 user_id: None,
633 field_path: None,
634 context: HashMap::new(),
635 });
636
637 aggregator.record_error(ErrorOccurrence {
638 timestamp: first_time + Duration::from_secs(60),
639 error_message: "Error".to_string(),
640 error_type: "Error".to_string(),
641 stack_trace: None,
642 query_id: None,
643 user_id: None,
644 field_path: None,
645 context: HashMap::new(),
646 });
647
648 let groups = aggregator.get_all_groups();
649 let rate = groups[0].error_rate();
650 assert!(rate > 0.0);
651 }
652
653 #[test]
654 fn test_normalize_message() {
655 let msg1 = "Error with ID 12345";
656 let msg2 = "Error with ID 67890";
657
658 let norm1 = ErrorAggregator::normalize_message(msg1);
659 let norm2 = ErrorAggregator::normalize_message(msg2);
660
661 assert_eq!(norm1, norm2);
663 }
664
665 #[test]
666 fn test_max_occurrences_per_group() {
667 let mut aggregator = ErrorAggregator::new(2);
668
669 for _ in 0..5 {
670 aggregator.record_error(ErrorOccurrence {
671 timestamp: SystemTime::now(),
672 error_message: "Error".to_string(),
673 error_type: "Error".to_string(),
674 stack_trace: None,
675 query_id: None,
676 user_id: None,
677 field_path: None,
678 context: HashMap::new(),
679 });
680 }
681
682 let groups = aggregator.get_all_groups();
683 assert_eq!(groups[0].occurrences.len(), 2); assert_eq!(groups[0].count, 5); }
686}