59 lines
2.2 KiB
Python
59 lines
2.2 KiB
Python
"""Check every alert rule against the alerting contract (see the header of
|
|||
|
|
k8s/prometheus/rules/ledgerline.rules.yml).
|
||
|
|
|
||
|
|
python observability/alerts/check_contract.py
|
||
|
|
|
||
|
|
Needs PyYAML. Skips scratch.* and host.* rule files.
|
||
|
|
"""
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[2]
|
||
|
|
RULES = ROOT / "k8s" / "prometheus" / "rules"
|
||
|
|
|
||
|
|
SEVERITIES = {"critical", "warning", "info"}
|
||
|
|
TEAMS = {"platform", "payments", "risk", "data"} # placeholders: edit to taste
|
||
|
|
ANNOTATIONS = ("summary", "description", "runbook_url", "dashboard")
|
||
|
|
RUNBOOK_MARKER = "/docs/runbooks/"
|
||
|
|
|
||
|
|
|
||
|
|
def check(path: Path) -> list[str]:
|
||
|
|
problems = []
|
||
|
|
for group in yaml.safe_load(path.read_text())["groups"]:
|
||
|
|
for rule in group["rules"]:
|
||
|
|
name = rule.get("alert")
|
||
|
|
if name is None: # recording rule
|
||
|
|
continue
|
||
|
|
where = f"{path.name}:{name}"
|
||
|
|
labels, ann = rule.get("labels", {}), rule.get("annotations", {})
|
||
|
|
if labels.get("severity") not in SEVERITIES:
|
||
|
|
problems.append(f"{where}: severity must be one of {sorted(SEVERITIES)}")
|
||
|
|
if labels.get("team") not in TEAMS:
|
||
|
|
problems.append(f"{where}: team must be one of {sorted(TEAMS)}")
|
||
|
|
for key in ANNOTATIONS:
|
||
|
|
if not ann.get(key):
|
||
|
|
problems.append(f"{where}: missing annotation {key}")
|
||
|
|
url = ann.get("runbook_url", "")
|
||
|
|
if RUNBOOK_MARKER in url:
|
||
|
|
local = ROOT / "docs" / "runbooks" / url.rsplit("/", 1)[-1]
|
||
|
|
if not local.exists():
|
||
|
|
problems.append(f"{where}: runbook {local.relative_to(ROOT)} does not exist")
|
||
|
|
elif url:
|
||
|
|
problems.append(f"{where}: runbook_url should point under {RUNBOOK_MARKER}")
|
||
|
|
return problems
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
files = [p for p in sorted(RULES.glob("*.yml")) if p.name.split(".")[0] not in ("scratch", "host")]
|
||
|
|
problems = [msg for p in files for msg in check(p)]
|
||
|
|
for msg in problems:
|
||
|
|
print(msg)
|
||
|
|
print(f"{len(files)} file(s) checked, {len(problems)} problem(s)")
|
||
|
|
return 1 if problems else 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|