From 02802f7e1f5455f6b94647c1239e6bac410bbd5c Mon Sep 17 00:00:00 2001 From: Zo Bot Date: Tue, 16 Jun 2026 01:34:01 +0000 Subject: [PATCH] parse fractional-second ISO-8601 timestamps in syslog parser SyslogParser.PATTERNS[1] already accepts timestamps with optional fractional seconds and an optional Z or numeric offset: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})? But _parse_timestamp only knew about: '%Y-%m-%dT%H:%M:%S.%f' (no offset) '%Y-%m-%dT%H:%M:%S%z' (no fractional) So an input like '2025-03-20T10:15:32.123Z' or '2025-03-20T10:15:32.123+02:00' was matched by the regex and can_parse() returned True, but parse() then set timestamp=None and the time_distribution, source/time-range reports, and start-time/ end-time filters all silently lost those entries. This format is the default for rsyslog with RFC 5424 templates and for journald exporters, so it is by far the most common modern syslog format. Add '%Y-%m-%dT%H:%M:%S.%f%z' to the format list and normalize the trailing 'Z' to '+00:00' before passing the string to strptime. Two new tests pin the behavior for both the Z suffix and the numeric offset variants. --- src/log_analyzer_cli/parsers/syslog.py | 7 ++++++- tests/test_parsers.py | 28 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/log_analyzer_cli/parsers/syslog.py b/src/log_analyzer_cli/parsers/syslog.py index 99891f3..2641fc2 100644 --- a/src/log_analyzer_cli/parsers/syslog.py +++ b/src/log_analyzer_cli/parsers/syslog.py @@ -118,17 +118,22 @@ def _parse_timestamp(self, ts_str: str) -> Optional[datetime]: formats = [ "%b %d %H:%M:%S", "%Y-%m-%d %H:%M:%S", + "%Y-%m-%dT%H:%M:%S.%f%z", "%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S%z", ] + ts_str_normalized = ts_str + if ts_str_normalized.endswith("Z"): + ts_str_normalized = ts_str_normalized[:-1] + "+00:00" + for fmt in formats: try: if fmt == "%b %d %H:%M:%S": dt = datetime.strptime(ts_str, fmt) dt = dt.replace(year=year) return dt - return datetime.strptime(ts_str, fmt) + return datetime.strptime(ts_str_normalized, fmt) except ValueError: continue return None diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 0248a28..f03ef88 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -50,6 +50,34 @@ def test_parse_syslog_with_level(self): assert entry.level == "ERROR" assert "Database connection failed" in entry.message + def test_parse_iso8601_with_microseconds_and_z(self): + """ISO-8601 timestamps with fractional seconds and a Z suffix should parse.""" + parser = SyslogParser() + line = "2025-03-20T10:15:32.123Z host process[1234]: something happened" + entry = parser.parse(line) + + assert entry is not None + assert entry.timestamp is not None + assert entry.timestamp.year == 2025 + assert entry.timestamp.month == 3 + assert entry.timestamp.day == 20 + assert entry.timestamp.hour == 10 + assert entry.timestamp.minute == 15 + assert entry.timestamp.second == 32 + assert entry.timestamp.microsecond == 123000 + assert entry.timestamp.tzinfo is not None + + def test_parse_iso8601_with_microseconds_and_offset(self): + """ISO-8601 timestamps with fractional seconds and a numeric offset should parse.""" + parser = SyslogParser() + line = "2025-03-20T10:15:32.123+02:00 host process[1234]: something happened" + entry = parser.parse(line) + + assert entry is not None + assert entry.timestamp is not None + assert entry.timestamp.microsecond == 123000 + assert entry.timestamp.utcoffset().total_seconds() == 2 * 3600 + class TestJSONLogParser: """Tests for JSONLogParser."""