rs_zero/core/logging/
aggregation.rs1use std::{
2 collections::BTreeMap,
3 sync::{Arc, Mutex},
4};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ErrorGroup {
9 pub fingerprint: String,
11 pub count: u64,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ErrorAggregationSnapshot {
18 pub total: u64,
20 pub groups: Vec<ErrorGroup>,
22}
23
24#[derive(Debug, Clone, Default)]
26pub struct ErrorAggregator {
27 groups: Arc<Mutex<BTreeMap<String, u64>>>,
28}
29
30impl ErrorAggregator {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn record(&self, message: &str) {
38 let fingerprint = fingerprint_error(message);
39 let mut groups = self.groups.lock().expect("aggregator mutex");
40 *groups.entry(fingerprint).or_default() += 1;
41 }
42
43 pub fn snapshot(&self) -> ErrorAggregationSnapshot {
45 let groups = self.groups.lock().expect("aggregator mutex");
46 ErrorAggregationSnapshot {
47 total: groups.values().sum(),
48 groups: groups
49 .iter()
50 .map(|(fingerprint, count)| ErrorGroup {
51 fingerprint: fingerprint.clone(),
52 count: *count,
53 })
54 .collect(),
55 }
56 }
57}
58
59fn fingerprint_error(message: &str) -> String {
60 let tokens = message.split_whitespace().collect::<Vec<_>>();
61 let mut output = Vec::with_capacity(tokens.len());
62 let mut redact_next = false;
63 for token in tokens {
64 if redact_next {
65 output.push("*".to_string());
66 redact_next = false;
67 continue;
68 }
69 let lower = token.to_ascii_lowercase();
70 if matches!(lower.as_str(), "token" | "password" | "secret" | "key") {
71 output.push(lower);
72 redact_next = true;
73 continue;
74 }
75 output.push(normalize_dynamic_token(token));
76 }
77 output.join(" ")
78}
79
80fn normalize_dynamic_token(token: &str) -> String {
81 if token.chars().any(|value| value.is_ascii_digit()) {
82 "#".to_string()
83 } else {
84 token.to_ascii_lowercase()
85 }
86}