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