87 lines
3.0 KiB
Rust
87 lines
3.0 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use std::time::{Duration, Instant};
|
|
use tracing::debug;
|
|
|
|
/// Deduplicates and formats diagnosis results to avoid spam
|
|
#[derive(Clone)]
|
|
pub struct DiagnosisFormatter {
|
|
// Hash of diagnosis content -> (timestamp, count)
|
|
seen_diagnoses: Arc<RwLock<HashMap<String, (Instant, usize)>>>,
|
|
}
|
|
|
|
impl DiagnosisFormatter {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
seen_diagnoses: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Checks if this diagnosis is a duplicate and should be suppressed
|
|
/// Returns: (should_display, display_suffix)
|
|
pub async fn should_display(&self, diagnosis: &str) -> (bool, Option<String>) {
|
|
let diagnosis_hash = Self::hash_diagnosis(diagnosis);
|
|
let mut seen = self.seen_diagnoses.write().await;
|
|
|
|
// Clean old entries (older than 10 minutes)
|
|
seen.retain(|_, (timestamp, _)| timestamp.elapsed() < Duration::from_secs(600));
|
|
|
|
if let Some((last_seen, count)) = seen.get_mut(&diagnosis_hash) {
|
|
// Similar diagnosis seen recently
|
|
if last_seen.elapsed() < Duration::from_secs(300) {
|
|
// Within 5 minutes - increment count and suppress
|
|
*count += 1;
|
|
*last_seen = Instant::now();
|
|
debug!(
|
|
hash = %diagnosis_hash,
|
|
count = *count,
|
|
"Suppressing duplicate diagnosis"
|
|
);
|
|
return (false, Some(format!(" (seen {} times in last 5min)", count)));
|
|
} else {
|
|
// More than 5 minutes - reset and show
|
|
*last_seen = Instant::now();
|
|
*count = 1;
|
|
}
|
|
} else {
|
|
// First time seeing this diagnosis
|
|
seen.insert(diagnosis_hash.clone(), (Instant::now(), 1));
|
|
}
|
|
|
|
(true, None)
|
|
}
|
|
|
|
/// Creates a simplified hash of diagnosis to detect duplicates
|
|
/// Focuses on root cause rather than resource names
|
|
fn hash_diagnosis(diagnosis: &str) -> String {
|
|
// Extract key phrases from diagnosis
|
|
let normalized = diagnosis
|
|
.to_lowercase()
|
|
.lines()
|
|
.filter(|line| {
|
|
line.contains("root cause:")
|
|
|| line.contains("cause:")
|
|
|| line.contains("problem:")
|
|
|| line.contains("severity:")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
|
|
// Simple hash based on content
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
let mut hasher = DefaultHasher::new();
|
|
normalized.hash(&mut hasher);
|
|
format!("{:x}", hasher.finish())
|
|
}
|
|
|
|
/// Periodically clean old entries
|
|
pub async fn cleanup(&self) {
|
|
let mut seen = self.seen_diagnoses.write().await;
|
|
seen.retain(|_, (timestamp, _)| timestamp.elapsed() < Duration::from_secs(600));
|
|
debug!("Cleaned up diagnosis cache, {} entries remain", seen.len());
|
|
}
|
|
}
|