use regex::Regex;
use std::collections::{HashMap, HashSet};
pub struct LogAnalyzer {
level_regex: Regex,
timestamp_regex: Regex,
error_type_regex: Regex,
}
pub struct AnalysisResult {
pub matched_lines: Vec<String>,
pub count: usize,
pub time_trends: HashMap<String, usize>,
pub levels_count: HashMap<String, usize>,
pub error_types: HashMap<String, usize>,
pub unique_messages: HashSet<String>,
}
impl Default for LogAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl LogAnalyzer {
pub fn new() -> Self {
LogAnalyzer {
level_regex: Regex::new(r"\[((?i)ERROR|WARN|INFO|DEBUG|TRACE|SEVERE|WARNING|FINE)]|(?i:ERROR|WARN|INFO|DEBUG|TRACE|SEVERE|WARNING|FINE):").expect("Failed to create level regex"),
timestamp_regex: Regex::new(r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})").expect("Failed to create timestamp regex"),
error_type_regex: Regex::new(r"([A-Za-z]+Exception|[A-Za-z]+Error|[A-Za-z]+\s+timeout|Connection timeout|500 Internal Server Error|401 Unauthorized|503 Service Unavailable)").expect("Failed to create error regex"),
}
}
pub fn analyze_line(
&self,
line: &str,
pattern: Option<&Regex>,
level_filter: Option<&str>,
collect_trends: bool,
_collect_stats: bool,
) -> Option<(String, String, Option<String>)> {
let mut found_level = String::new();
if let Some(caps) = self.level_regex.captures(line) {
found_level = caps
.get(1)
.map_or_else(|| caps.get(0).map_or("", |m| m.as_str()), |m| m.as_str())
.to_uppercase();
}
let level_matches = level_filter.is_none_or(|filter_level| {
!found_level.is_empty() && found_level == filter_level.to_uppercase()
});
let pattern_matches = pattern.is_none_or(|re| re.is_match(line));
if level_matches && pattern_matches {
let timestamp = if collect_trends {
self.extract_timestamp(line)
} else {
None
};
return Some((line.to_string(), found_level, timestamp));
}
None
}
fn extract_timestamp(&self, line: &str) -> Option<String> {
if let Some(caps) = self.timestamp_regex.captures(line) {
if let Some(timestamp) = caps.get(1) {
let timestamp_str = timestamp.as_str();
if timestamp_str.len() >= 13 {
return Some(timestamp_str[0..13].to_string());
} else {
return Some(timestamp_str.to_string());
}
}
}
None
}
pub fn extract_error_type(&self, line: &str) -> Option<String> {
if let Some(caps) = self.error_type_regex.captures(line) {
if let Some(error_type) = caps.get(1) {
return Some(error_type.as_str().to_string());
}
}
if line.contains("Failed to") {
let parts: Vec<&str> = line.split("Failed to ").collect();
if parts.len() > 1 {
let action_parts: Vec<&str> = parts[1].split(":").collect();
if !action_parts.is_empty() {
let action = action_parts[0].trim();
return Some(format!("Failed to {}", action));
}
}
}
None
}
fn extract_message(&self, line: &str) -> String {
let parts: Vec<&str> = line.splitn(3, " - ").collect();
if parts.len() >= 3 {
return parts[2].to_string();
} else if parts.len() == 2 {
return parts[1].to_string();
}
line.to_string()
}
pub fn analyze_lines<I>(
&self,
lines: I,
pattern: Option<&Regex>,
level_filter: Option<&str>,
collect_trends: bool,
collect_stats: bool,
) -> AnalysisResult
where
I: Iterator<Item = String>,
{
let mut result = AnalysisResult {
matched_lines: Vec::new(),
count: 0,
time_trends: HashMap::new(),
levels_count: HashMap::new(),
error_types: HashMap::new(),
unique_messages: HashSet::new(),
};
for line in lines {
if let Some((matched_line, level, timestamp)) =
self.analyze_line(&line, pattern, level_filter, collect_trends, collect_stats)
{
result.matched_lines.push(matched_line.clone());
result.count += 1;
if collect_stats {
if !level.is_empty() {
*result.levels_count.entry(level).or_insert(0) += 1;
}
if let Some(error_type) = self.extract_error_type(&matched_line) {
*result.error_types.entry(error_type).or_insert(0) += 1;
}
let message_content = self.extract_message(&matched_line);
result.unique_messages.insert(message_content);
}
if collect_trends {
if let Some(hour) = timestamp {
*result.time_trends.entry(hour).or_insert(0) += 1;
}
}
}
}
result
}
}