Skip to main content

lean_ctx/core/patterns/
log_dedup.rs

1use regex::Regex;
2use std::sync::OnceLock;
3
4static TIMESTAMP_RE: OnceLock<Regex> = OnceLock::new();
5
6fn timestamp_re() -> &'static Regex {
7    TIMESTAMP_RE.get_or_init(|| {
8        Regex::new(r"^\[?\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]\s]*\]?\s*").unwrap()
9    })
10}
11
12pub fn compress(output: &str) -> Option<String> {
13    let lines: Vec<&str> = output.lines().collect();
14    if lines.len() <= 10 {
15        return None;
16    }
17
18    let mut deduped: Vec<(String, u32)> = Vec::new();
19    let mut error_lines = Vec::new();
20
21    for line in &lines {
22        let stripped = timestamp_re().replace(line, "").trim().to_string();
23        if stripped.is_empty() {
24            continue;
25        }
26
27        let lower = stripped.to_lowercase();
28        if lower.contains("error")
29            || lower.contains("critical")
30            || lower.contains("fatal")
31            || lower.contains("panic")
32            || lower.contains("exception")
33        {
34            error_lines.push(stripped.clone());
35        }
36
37        if let Some(last) = deduped.last_mut() {
38            if last.0 == stripped {
39                last.1 += 1;
40                continue;
41            }
42        }
43        deduped.push((stripped, 1));
44    }
45
46    let result: Vec<String> = deduped
47        .iter()
48        .map(|(line, count)| {
49            if *count > 1 {
50                format!("{line} (x{count})")
51            } else {
52                line.clone()
53            }
54        })
55        .collect();
56
57    let mut parts = Vec::new();
58    parts.push(format!("{} lines → {} unique", lines.len(), deduped.len()));
59
60    if !error_lines.is_empty() {
61        parts.push(format!("{} errors:", error_lines.len()));
62        for e in error_lines.iter().take(5) {
63            parts.push(format!("  {e}"));
64        }
65        if error_lines.len() > 5 {
66            parts.push(format!("  ... +{} more errors", error_lines.len() - 5));
67        }
68    }
69
70    if result.len() > 30 {
71        let tail = &result[result.len() - 15..];
72        parts.push(format!("last 15 unique lines:\n{}", tail.join("\n")));
73    } else {
74        parts.push(result.join("\n"));
75    }
76
77    Some(parts.join("\n"))
78}