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*").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("fatal")
30            || lower.contains("panic")
31            || lower.contains("exception")
32        {
33            error_lines.push(stripped.clone());
34        }
35
36        if let Some(last) = deduped.last_mut() {
37            if last.0 == stripped {
38                last.1 += 1;
39                continue;
40            }
41        }
42        deduped.push((stripped, 1));
43    }
44
45    let result: Vec<String> = deduped
46        .iter()
47        .map(|(line, count)| {
48            if *count > 1 {
49                format!("{line} (x{count})")
50            } else {
51                line.clone()
52            }
53        })
54        .collect();
55
56    let mut parts = Vec::new();
57    parts.push(format!("{} lines → {} unique", lines.len(), deduped.len()));
58
59    if !error_lines.is_empty() {
60        parts.push(format!("{} errors:", error_lines.len()));
61        for e in error_lines.iter().take(5) {
62            parts.push(format!("  {e}"));
63        }
64        if error_lines.len() > 5 {
65            parts.push(format!("  ... +{} more errors", error_lines.len() - 5));
66        }
67    }
68
69    if result.len() > 30 {
70        let tail = &result[result.len() - 15..];
71        parts.push(format!("last 15 unique lines:\n{}", tail.join("\n")));
72    } else {
73        parts.push(result.join("\n"));
74    }
75
76    Some(parts.join("\n"))
77}