linux-kernel-panic-parser 0.1.1

Lossless, architecture-neutral parsing of Linux kernel panic logs
Documentation
use jiff::{SignedDuration, Timestamp};
use linux_kernel_panic_parser::{LineKind, PanicLog, parse};

const FIRST: &str = include_str!("../example-logs/1.log");
const SECOND: &str = include_str!("../example-logs/2.log");

fn line<'a>(log: &'a PanicLog, prefix: &str) -> &'a linux_kernel_panic_parser::LogLine {
    let Some(line) = log
        .lines()
        .iter()
        .find(|line| line.message().starts_with(prefix))
    else {
        panic!("missing fixture line with prefix {prefix:?}");
    };
    line
}

#[test]
fn real_logs_are_structured_and_lossless() {
    for (text, cpu, pid, frames, version) in [
        (
            FIRST,
            3,
            1757,
            10,
            "6.12.67-6.12.2.3-amd64-dee77ff0713177fc",
        ),
        (SECOND, 78, 1589370, 65, "6.12.67-amd64-9efba7083c7"),
    ] {
        let log = parse(text);
        assert_eq!(log.to_string(), text);
        assert_eq!(parse(log.to_string()), log);
        assert_eq!(
            log.panic_messages().collect::<Vec<_>>(),
            ["Fatal exception"]
        );
        for line in log.lines() {
            assert!(line.timestamp().is_some(), "{}", line.as_str());
            assert!(line.uptime().is_some(), "{}", line.as_str());
            // Even the less common real records must have a useful classification.
            if !line.message().is_empty() {
                assert_ne!(line.kind(), &LineKind::Unknown, "{}", line.as_str());
            }
        }
        let LineKind::Cpu {
            cpu: actual_cpu,
            uid,
            pid: actual_pid,
            details,
        } = line(&log, "CPU:").kind()
        else {
            panic!()
        };
        assert_eq!((*actual_cpu, *uid, *actual_pid), (cpu, Some(0), pid));
        assert!(details.contains(version));
        assert_eq!(
            log.lines()
                .iter()
                .filter(|l| matches!(l.kind(), LineKind::Frame(_)))
                .count(),
            frames
        );
        assert_eq!(
            line(&log, "Rebooting").kind(),
            &LineKind::Reboot {
                delay: SignedDuration::from_secs(30)
            }
        );
    }
}

#[test]
fn wall_time_is_distinct_from_uptime() -> Result<(), jiff::Error> {
    let log = parse(FIRST);
    assert_eq!(
        log.lines()[0].timestamp(),
        Some(Timestamp::from_second(1791049802)?)
    );
    assert_eq!(
        log.lines()[0].uptime(),
        Some(SignedDuration::new(694, 613_706_000))
    );
    assert!(log.lines()[0].message().starts_with("Oops:"));
    let log = parse(SECOND);
    let tail = line(&log, "Kernel Offset:");
    assert_eq!(tail.timestamp(), Some(Timestamp::from_second(1791034612)?));
    assert_eq!(
        tail.uptime(),
        Some(SignedDuration::new(3873810, 448_508_000))
    );
    // A single bracketed integer remains elapsed time, not an epoch timestamp.
    let log = parse("[1791049802] message");
    assert_eq!(log.lines()[0].timestamp(), None);
    assert_eq!(
        log.lines()[0].uptime(),
        Some(SignedDuration::from_secs(1791049802))
    );
    Ok(())
}

#[test]
fn fault_symbols_register_decorations_and_modules() {
    let log = parse(FIRST);
    let LineKind::Symbol {
        label,
        segment,
        frame,
    } = line(&log, "RIP:").kind()
    else {
        panic!()
    };
    assert_eq!(label, "RIP");
    assert_eq!(segment.as_deref(), Some("0010"));
    assert_eq!(frame.symbol, "decay_va_pool_node");
    assert_eq!(frame.offset, "0x10f");
    assert_eq!(frame.size, "0x410");
    let LineKind::Registers(registers) = line(&log, "RSP:").kind() else {
        panic!()
    };
    assert_eq!(registers[0].value, "0018:ffffd1bc80413d98");
    assert_eq!(registers[1].name, "EFLAGS");
    let LineKind::Registers(registers) = line(&log, "FS:").kind() else {
        panic!()
    };
    assert_eq!(registers[0].value, "0000000000000000(0000)");
    assert_eq!(registers[1].value, "ffff8f58a5ac0000(0000)");
    assert_eq!(registers[2].name, "knlGS");
    let LineKind::Modules {
        modules,
        continuation,
        ..
    } = line(&log, "Modules linked in:").kind()
    else {
        panic!()
    };
    assert!(!continuation);
    assert!(modules.iter().any(|m| m == "zfs(O)"));
    let LineKind::Modules {
        modules,
        continuation,
        ..
    } = line(&log, "i2c_core raid_class").kind()
    else {
        panic!()
    };
    assert!(*continuation);
    assert_eq!(modules.last().map(String::as_str), Some("ipmi_msghandler"));
    let log = parse(SECOND);
    let LineKind::Modules {
        last_unloaded,
        continuation,
        ..
    } = line(&log, "ipmi_devintf").kind()
    else {
        panic!()
    };
    assert!(*continuation);
    assert_eq!(last_unloaded.as_deref(), Some("kvm"));
    let LineKind::Frame(frame) = line(&log, "pmem_do_write+").kind() else {
        panic!()
    };
    assert_eq!(frame.suffix, "[nd_pmem]");
    let LineKind::Registers(entries) = line(&log, "PGD ").kind() else {
        panic!()
    };
    assert_eq!(entries.len(), 5);
    assert_eq!(entries[0].name, "PGD");
    assert_eq!(entries[4].value, "800fffc32fcf5062");
}

#[test]
fn malformed_new_formats_and_context_boundaries() {
    for text in [
        "RSP: 0018:",
        "FS: abc(0000",
        "CPU: 3 UID: x PID: 1 Comm: task",
        "Rebooting in 9999999999999999999999 seconds..",
    ] {
        let log = parse(text);
        assert_eq!(log.lines()[0].kind(), &LineKind::Unknown);
        assert_eq!(log.to_string(), text);
        assert_eq!(parse(log.to_string()), log);
    }
    let log = parse("Modules linked in: zfs(O)\nspl(O) [last unloaded: kvm]\n\nplain words\n");
    assert!(matches!(
        log.lines()[1].kind(),
        LineKind::Modules {
            continuation: true,
            ..
        }
    ));
    assert_eq!(log.lines()[3].kind(), &LineKind::Unknown);
    let log = parse("ipmi_devintf ipmi_msghandler [last unloaded: kvm]");
    assert_eq!(log.lines()[0].kind(), &LineKind::Unknown);
}