use crate::config::RuntimeConfig;
use crate::parsers::ParseResult;
use crate::parsers::traits::AdaptiveParallel;
use crate::results::{LogMetadata, MiningResult, Template};
use crate::utils::path_string_helper::{
PlaceholderType, format_placeholder_bracketed_typed, format_placeholder_typed,
};
use crate::utils::typecheck::{
contains_unix_timestamp, parse_date_to_timestamp, parse_timestamp_to_seconds,
};
use anyhow::Result;
use dashmap::DashMap;
use rayon::prelude::*;
use std::collections::BTreeMap;
const TIMESTAMP_SAMPLE_LINES: usize = 50;
const MAX_TIMESTAMP_TOKENS: usize = 6;
#[must_use]
pub fn extract_log_metadata(content: &str, stats: &ParseResult) -> LogMetadata {
let line_count = stats.line_count;
let byte_count = stats.byte_count;
let bytes = content.as_bytes();
let mut lf = 0usize;
let mut crlf = 0usize;
let mut cr = 0usize;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\r' {
if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
crlf += 1;
i += 2;
} else {
cr += 1;
i += 1;
}
} else if bytes[i] == b'\n' {
lf += 1;
i += 1;
} else {
i += 1;
}
}
let line_ending = if lf + crlf + cr == 0 {
None
} else {
let total = lf + crlf + cr;
Some(if crlf == total {
"crlf".to_string()
} else if lf == total {
"lf".to_string()
} else if cr == total {
"cr".to_string()
} else {
"mixed".to_string()
})
};
let max_line_length = content.lines().map(str::len).max();
let blank_line_count = content.lines().filter(|l| l.trim().is_empty()).count();
let has_timestamps = content.lines().take(TIMESTAMP_SAMPLE_LINES).any(|line| {
let tokens: Vec<&str> = line.split_whitespace().collect();
if tokens.is_empty() {
return false;
}
for token in &tokens {
if contains_unix_timestamp(token) {
return true;
}
}
for start in 0..tokens.len().min(MAX_TIMESTAMP_TOKENS) {
for len in 1..=MAX_TIMESTAMP_TOKENS.min(tokens.len().saturating_sub(start)) {
let slice = &tokens[start..start + len];
let candidate = slice.join(" ");
if parse_timestamp_to_seconds(&candidate).is_some()
|| parse_date_to_timestamp(&candidate).is_some()
{
return true;
}
}
}
false
});
LogMetadata {
byte_count,
line_count,
line_ending,
max_line_length,
blank_line_count: if blank_line_count > 0 {
Some(blank_line_count)
} else {
None
},
has_timestamps: Some(has_timestamps),
}
}
pub fn extract_log_templates(
content: &str,
stats: &ParseResult,
config: &RuntimeConfig,
) -> Result<MiningResult> {
let lines: Vec<&str> = content.lines().collect();
let total_lines = lines.len();
if total_lines == 0 {
return Ok(crate::parsers::traits::empty_mining_result(stats));
}
let freq_map: DashMap<usize, DashMap<String, usize>> = DashMap::new();
lines.as_slice().par_iter_adaptive(config).for_each(|line| {
let tokens: Vec<&str> = line.split_whitespace().collect();
for (pos, token) in tokens.iter().enumerate() {
freq_map
.entry(pos)
.or_default()
.entry(token.to_string())
.and_modify(|c| *c += 1)
.or_insert(1);
}
});
let max_pos = freq_map.iter().map(|entry| *entry.key()).max().unwrap_or(0);
let threshold = (total_lines as f64 * config.static_threshold) as usize;
let mut classifications: Vec<(usize, bool, Option<String>)> = Vec::new();
for pos in 0..=max_pos {
if let Some(token_map) = freq_map.get(&pos) {
let (most_common, count) = token_map
.iter()
.max_by_key(|e| *e.value())
.map_or_else(|| (String::new(), 0), |e| (e.key().clone(), *e.value()));
let is_static = count >= threshold && token_map.len() == 1;
classifications.push((
pos,
is_static,
if is_static { Some(most_common) } else { None },
));
} else {
classifications.push((pos, false, None));
}
}
let template_groups: DashMap<String, Vec<&str>> = DashMap::new();
lines.as_slice().par_iter_adaptive(config).for_each(|line| {
let tokens: Vec<&str> = line.split_whitespace().collect();
let pattern = build_pattern(&tokens, &classifications);
template_groups.entry(pattern).or_default().push(line);
});
let templates: Vec<Template> = template_groups
.iter()
.map(|entry| {
let pattern = entry.key().clone();
let matching_lines = entry.value();
let mut examples: BTreeMap<String, Vec<String>> = BTreeMap::new();
for line in matching_lines.iter().take(config.max_sample_lines) {
let tokens: Vec<&str> = line.split_whitespace().collect();
for (pos, token) in tokens.iter().enumerate() {
if let Some((_, is_static, _)) = classifications.get(pos)
&& !is_static
{
let placeholder = format_placeholder_typed(PlaceholderType::Position, pos);
let entry = examples.entry(placeholder).or_insert_with(|| {
Vec::with_capacity(config.max_examples_per_placeholder.min(10))
});
let token_str = token.to_string();
if !entry.contains(&token_str) {
entry.push(token_str);
}
if entry.len() > config.max_examples_per_placeholder {
break;
}
}
}
}
Template {
pattern,
count: matching_lines.len(),
examples,
}
})
.collect();
Ok(crate::parsers::traits::build_mining_result(
templates,
total_lines,
stats,
config,
))
}
fn build_pattern(tokens: &[&str], classifications: &[(usize, bool, Option<String>)]) -> String {
let mut pattern_parts = Vec::new();
for (pos, token) in tokens.iter().enumerate() {
if let Some((_, is_static, static_token)) = classifications.get(pos) {
if *is_static {
pattern_parts.push(static_token.clone().unwrap_or_else(|| token.to_string()));
} else {
pattern_parts.push(format_placeholder_bracketed_typed(
PlaceholderType::Position,
pos,
));
}
} else {
pattern_parts
.push(crate::utils::path_string_helper::format_placeholder_bracketed("POS", pos));
}
}
pattern_parts.join(" ")
}