use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorGroup {
pub fingerprint: String,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorAggregationSnapshot {
pub total: u64,
pub groups: Vec<ErrorGroup>,
}
#[derive(Debug, Clone, Default)]
pub struct ErrorAggregator {
groups: Arc<Mutex<BTreeMap<String, u64>>>,
}
impl ErrorAggregator {
pub fn new() -> Self {
Self::default()
}
pub fn record(&self, message: &str) {
let fingerprint = fingerprint_error(message);
let mut groups = self.groups.lock().expect("aggregator mutex");
*groups.entry(fingerprint).or_default() += 1;
}
pub fn snapshot(&self) -> ErrorAggregationSnapshot {
let groups = self.groups.lock().expect("aggregator mutex");
ErrorAggregationSnapshot {
total: groups.values().sum(),
groups: groups
.iter()
.map(|(fingerprint, count)| ErrorGroup {
fingerprint: fingerprint.clone(),
count: *count,
})
.collect(),
}
}
}
fn fingerprint_error(message: &str) -> String {
let tokens = message.split_whitespace().collect::<Vec<_>>();
let mut output = Vec::with_capacity(tokens.len());
let mut redact_next = false;
for token in tokens {
if redact_next {
output.push("*".to_string());
redact_next = false;
continue;
}
let lower = token.to_ascii_lowercase();
if matches!(lower.as_str(), "token" | "password" | "secret" | "key") {
output.push(lower);
redact_next = true;
continue;
}
output.push(normalize_dynamic_token(token));
}
output.join(" ")
}
fn normalize_dynamic_token(token: &str) -> String {
if token.chars().any(|value| value.is_ascii_digit()) {
"#".to_string()
} else {
token.to_ascii_lowercase()
}
}