fail2ban-log-parser-core 0.1.2

Fast, zero-copy fail2ban log parser built with winnow
Documentation
use std::net::{IpAddr, Ipv4Addr};

use fail2ban_log_parser_core::{Fail2BanEvent, Fail2BanLevel, parse};

#[test]
fn parse_multiple_lines() {
    let input = "\
2024-01-01 00:00:00,000 fail2ban.filter [1] INFO [sshd] Found 1.2.3.4
2024-01-01 00:00:01,000 fail2ban.actions [2] NOTICE [sshd] Ban 5.6.7.8
2024-01-01 00:00:02,000 fail2ban.actions [3] INFO [sshd] Unban 5.6.7.8";
    let results: Vec<_> = parse(input).collect();
    assert_eq!(results.len(), 3);
    assert!(results.iter().all(|r| r.is_ok()));

    let logs: Vec<_> = results.into_iter().map(|r| r.unwrap()).collect();
    assert_eq!(logs[0].event(), Some(&Fail2BanEvent::Found));
    assert_eq!(logs[1].event(), Some(&Fail2BanEvent::Ban));
    assert_eq!(logs[2].event(), Some(&Fail2BanEvent::Unban));

    assert_eq!(logs[0].ip(), Some(&IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
    assert_eq!(logs[1].ip(), Some(&IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8))));
    assert_eq!(logs[2].ip(), Some(&IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8))));
}

#[test]
fn parse_empty_input_yields_no_results() {
    let results: Vec<_> = parse("").collect();
    assert_eq!(results.len(), 0);
}

#[test]
fn parse_invalid_line_returns_error() {
    let line = "this is not a valid log line";
    let result = parse(line).next().unwrap();
    assert!(result.is_err());
}

#[test]
fn parse_error_contains_line_number_and_text() {
    let input = "bad line\n2024-01-01 00:00:00,000 fail2ban.filter [1] INFO [sshd] Found 1.2.3.4";
    let results: Vec<_> = parse(input).collect();
    assert_eq!(results.len(), 2);

    let err = results[0].as_ref().unwrap_err();
    assert_eq!(err.line_number, 1);
    assert_eq!(err.line, "bad line");
    assert!(results[1].is_ok());
}

#[test]
fn parse_mixed_valid_and_invalid_lines() {
    let input = "\
2024-01-01 00:00:00,000 fail2ban.filter [1] INFO [sshd] Found 1.2.3.4
garbage line here
2024-01-01 00:00:02,000 fail2ban.actions [3] INFO [sshd] Unban 5.6.7.8";
    let results: Vec<_> = parse(input).collect();
    assert_eq!(results.len(), 3);
    assert!(results[0].is_ok());
    assert!(results[1].is_err());
    assert!(results[2].is_ok());
}

#[test]
fn parse_error_display_format() {
    let err = parse("bad").next().unwrap().unwrap_err();
    let msg = format!("{}", err);
    assert!(msg.contains("failed to parse line 1"));
    assert!(msg.contains("bad"));
}

#[test]
fn parse_realistic_log_batch() {
    let input = "\
2024-01-15 14:32:01,847 fail2ban.filter [12345] INFO [sshd] Found 192.168.1.100
2024-01-15 14:32:02,123 fail2ban.filter [12345] INFO [sshd] Found 192.168.1.100
2024-01-15 14:32:03,456 fail2ban.filter [12345] INFO [sshd] Found 192.168.1.100
2024-01-15 14:32:04,789 fail2ban.actions [12345] NOTICE [sshd] Ban 192.168.1.100
2024-01-15 14:42:04,789 fail2ban.actions [12345] NOTICE [sshd] Unban 192.168.1.100";

    let logs: Vec<_> = parse(input).map(|r| r.unwrap()).collect();
    assert_eq!(logs.len(), 5);

    for log in &logs[..3] {
        assert_eq!(log.event(), Some(&Fail2BanEvent::Found));
        assert_eq!(log.ip(), Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))));
    }

    assert_eq!(logs[3].event(), Some(&Fail2BanEvent::Ban));
    assert_eq!(logs[3].level(), Some(&Fail2BanLevel::Notice));
    assert_eq!(logs[4].event(), Some(&Fail2BanEvent::Unban));
}