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]*\]?\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}