lean_ctx/core/patterns/
log_dedup.rs1macro_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}