"""Syslog pattern alerts (#6001): a pattern read from its form, the line test it makes, its metric key, and the per-minute tally its count keeps.""" from datetime import datetime, timedelta, timezone from types import SimpleNamespace as NS import pytest from werkzeug.datastructures import MultiDict from plugins.syslog.patterns import PRESETS, PatternError, Tally, key_for, matches, parse_form def test_a_form_reads_into_a_pattern(): v = parse_form(MultiDict({"name": " NFS ", "phrases": "nfsd:\n\n lockd: \n", "max_severity": "3", "source_id": "", "window_minutes": "60", "enabled": "on"})) assert v == {"name": "NFS", "phrases": ["nfsd:", "lockd:"], "max_severity": 3, "source_id": None, "window_minutes": 60, "enabled": True} # A window or severity not offered falls back rather than failing. v = parse_form(MultiDict({"name": "x", "phrases": "a", "max_severity": "9", "window_minutes": "7"})) assert (v["max_severity"], v["window_minutes"], v["enabled"]) == (None, 15, False) @pytest.mark.parametrize("form, says", [ ({"phrases": "a"}, "name"), ({"name": "x", "phrases": " \n "}, "at least one phrase"), ({"name": "x", "phrases": "\n".join(str(i) for i in range(21))}, "At most 20"), ({"name": "x", "phrases": "a" * 201}, "200 characters"), ]) def test_a_form_that_does_not_read_says_what_to_fix(form, says): with pytest.raises(PatternError, match=says): parse_form(MultiDict(form)) def test_a_line_matches_any_phrase_in_any_case_at_the_severity(): p = NS(phrases=["I/O error", "medium error"], max_severity=None) assert matches(p, 6, "blk_update_request: i/o ERROR, dev sda") assert not matches(p, 6, "all well") sev = NS(phrases=[], max_severity=3) assert matches(sev, 3, "anything") and matches(sev, 0, "") and not matches(sev, 4, "anything") both = NS(phrases=["nfsd"], max_severity=3) assert matches(both, 2, "nfsd: too many") and not matches(both, 5, "nfsd: too many") def test_a_key_is_the_name_in_metric_form(): assert key_for("Disk I/O errors") == "disk_i_o_errors" assert key_for(" --- ") == "pattern" and len(key_for("x" * 90)) == 48 assert len({p.key for p in PRESETS}) == len(PRESETS) assert all(p.phrases or p.max_severity is not None for p in PRESETS) def test_a_tally_counts_its_window_by_the_minute_and_forgets_older(): now = datetime(2026, 10, 19, 12, 30, 40, tzinfo=timezone.utc) t = Tally(sig=()) for minutes in (0, 5, 14, 16, 120): t.add("s1", now - timedelta(minutes=minutes)) assert t.count("s1", now - timedelta(minutes=15)) == 3 assert t.count("s1", now - timedelta(hours=3)) == 3 # what was dropped stays dropped assert t.count("s2", now) == 0