Files
duty-ai-ops/src/events/formatter.rs
2025-12-24 02:46:22 +00:00

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());
}
}