Accept mulitple input files
This commit is contained in:
31
src/main.rs
31
src/main.rs
@@ -38,18 +38,33 @@ fn run_app(
|
|||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
if args.len() != 2 {
|
if args.len() < 2 {
|
||||||
eprintln!("Usage: {} <postfix-log-file>", args[0]);
|
eprintln!("Usage: {} <postfix-log-file> [additional-files...]", args[0]);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the log file
|
// Process all log files
|
||||||
let log_content = fs::read_to_string(&args[1])
|
let mut all_sessions = Vec::new();
|
||||||
.with_context(|| format!("Failed to read file: {}", args[1]))?;
|
for filename in &args[1..] {
|
||||||
|
// Read and parse the log file with its path for better timestamp handling
|
||||||
|
let log_content = fs::read_to_string(filename)
|
||||||
|
.with_context(|| format!("Failed to read file: {}", filename))?;
|
||||||
|
|
||||||
// Parse the log file
|
// Parse the log file with the file path for better timestamp handling
|
||||||
let sessions = parser::parse_log_file(&log_content)
|
let mut sessions = parser::parse_log_file_with_path(&log_content, Some(filename))
|
||||||
.context("Failed to parse log file")?;
|
.with_context(|| format!("Failed to parse log file: {}", filename))?;
|
||||||
|
|
||||||
|
all_sessions.append(&mut sessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all sessions by timestamp (newest first)
|
||||||
|
all_sessions.sort_by(|a, b| {
|
||||||
|
let a_time = a.start_time.unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::MIN_UTC.naive_local());
|
||||||
|
let b_time = b.start_time.unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::MIN_UTC.naive_local());
|
||||||
|
b_time.cmp(&a_time)
|
||||||
|
});
|
||||||
|
|
||||||
|
let sessions = all_sessions;
|
||||||
|
|
||||||
if sessions.is_empty() {
|
if sessions.is_empty() {
|
||||||
println!("No SMTP sessions found in the log file.");
|
println!("No SMTP sessions found in the log file.");
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::models::{LogEntry, SMTPSession};
|
use crate::models::{LogEntry, SMTPSession};
|
||||||
use chrono::{Datelike, NaiveDateTime};
|
use chrono::{Datelike, NaiveDateTime};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, path::Path, time::SystemTime};
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref LOG_LINE_RE: Regex = Regex::new(
|
static ref LOG_LINE_RE: Regex = Regex::new(
|
||||||
@@ -9,8 +10,22 @@ lazy_static::lazy_static! {
|
|||||||
).unwrap();
|
).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_log_year(file_path: Option<&str>) -> Option<i32> {
|
||||||
|
file_path.and_then(|path| {
|
||||||
|
let metadata = std::fs::metadata(path).ok()?;
|
||||||
|
let modified = metadata.modified().ok()?;
|
||||||
|
let datetime: chrono::DateTime<chrono::Local> = modified.into();
|
||||||
|
Some(datetime.year())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_log_file(contents: &str) -> anyhow::Result<Vec<SMTPSession>> {
|
pub fn parse_log_file(contents: &str) -> anyhow::Result<Vec<SMTPSession>> {
|
||||||
|
parse_log_file_with_path(contents, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyhow::Result<Vec<SMTPSession>> {
|
||||||
let mut sessions: HashMap<String, SMTPSession> = HashMap::new();
|
let mut sessions: HashMap<String, SMTPSession> = HashMap::new();
|
||||||
|
let log_year = get_log_year(file_path).unwrap_or_else(|| chrono::Local::now().year());
|
||||||
|
|
||||||
for line in contents.lines() {
|
for line in contents.lines() {
|
||||||
if let Some(captures) = LOG_LINE_RE.captures(line) {
|
if let Some(captures) = LOG_LINE_RE.captures(line) {
|
||||||
@@ -22,13 +37,19 @@ pub fn parse_log_file(contents: &str) -> anyhow::Result<Vec<SMTPSession>> {
|
|||||||
let message = captures["message"].to_string();
|
let message = captures["message"].to_string();
|
||||||
|
|
||||||
// Create a timestamp string in the format that chrono can parse
|
// Create a timestamp string in the format that chrono can parse
|
||||||
let year = chrono::Local::now().year();
|
let timestamp_str = format!("{} {} {} {}", log_year, month, day, time);
|
||||||
let timestamp_str = format!("{} {} {} {}", year, month, day, time);
|
let mut timestamp = match NaiveDateTime::parse_from_str(×tamp_str, "%Y %b %d %H:%M:%S") {
|
||||||
let timestamp = match NaiveDateTime::parse_from_str(×tamp_str, "%Y %b %d %H:%M:%S") {
|
|
||||||
Ok(ts) => ts,
|
Ok(ts) => ts,
|
||||||
Err(_) => continue, // Skip lines with invalid timestamps
|
Err(_) => continue, // Skip lines with invalid timestamps
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle year rollover (if log entry is from next year but file is from previous year)
|
||||||
|
if timestamp.month() == 1 && log_year != chrono::Local::now().year() {
|
||||||
|
if let Some(prev_year_timestamp) = timestamp.with_year(log_year + 1) {
|
||||||
|
timestamp = prev_year_timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let entry = LogEntry {
|
let entry = LogEntry {
|
||||||
timestamp,
|
timestamp,
|
||||||
hostname,
|
hostname,
|
||||||
|
|||||||
Reference in New Issue
Block a user