Skip to main content

lean_ctx/core/patterns/
log_dedup.rs

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