Episode 16: Kuma alerts sorted by rules, a local agent and a script
ops-readonly gets kuma_alerts (host export kuma-export.py, one more read-only mount). kuma-triage.py sorts the same data without a model. Flow H runs the agent with skill alert-triage.md. Flow B: one open incident per monitor, an UP closes all of them, titles name start and end and stay under Outline's limit. B and D accept Kuma only with a header token. Monitoring notes: ntfy rejects Kuma messages for monitors without a URL, resend for long outages.
This commit is contained in:
parent
54ab43dac7
commit
1655d04d7d
10 changed files with 705 additions and 67 deletions
28
ops-readonly/kuma-triage.py
Normal file
28
ops-readonly/kuma-triage.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env python3
|
||||
# kuma-triage.py < kuma_alerts.json (episode 16): the same answer as the agent, no model.
|
||||
# Down now = alert (still_down if it started before the window). 3+ drops in the window = flapping.
|
||||
# Recovered drops are reported once. Cert < 14 days. Same host + same kind + start within 90 s = one alert.
|
||||
import json, sys, datetime as dt
|
||||
d = json.load(sys.stdin); t0 = d["window"]["from"][:19]; out = []
|
||||
ts = lambda s: dt.datetime.fromisoformat(s.replace("T", " ")[:19])
|
||||
for m in d["monitors"]:
|
||||
if m["type"] == "group" or not m["active"]:
|
||||
continue
|
||||
downs = [c["t"] for c in m["changes"] if c["status"] == "DOWN"]
|
||||
host = m["target"].split("//")[-1].split("/")[0].split(":")[0]
|
||||
if m["now"]["status"] == "DOWN":
|
||||
out.append(dict(kind="still_down" if m["now"]["since"] < t0 else "outage", start=m["now"]["since"], host=host, monitors=[m["name"]], summary="down since %s: %s" % (m["now"]["since"], m["now"]["msg"][:80])))
|
||||
elif len(downs) >= 3:
|
||||
out.append(dict(kind="flapping", start=downs[0], host=host, monitors=[m["name"]], summary="%d drops in the window" % len(downs)))
|
||||
elif downs:
|
||||
out.append(dict(kind="recovered", start=downs[0], host=host, monitors=[m["name"]], summary="was down from %s, up again" % downs[0]))
|
||||
if m["tls_days_left"] is not None and m["tls_days_left"] < 14:
|
||||
out.append(dict(kind="cert_expiring", start="", host=host, monitors=[m["name"]], summary="certificate expires in %d days" % m["tls_days_left"]))
|
||||
merged = []
|
||||
for a in sorted(out, key=lambda a: (a["kind"], a["host"], a["start"])):
|
||||
p = merged[-1] if merged else None
|
||||
if p and a["kind"] == p["kind"] != "cert_expiring" and a["host"] == p["host"] and abs((ts(a["start"]) - ts(p["start"])).total_seconds()) <= 90:
|
||||
p["monitors"] += a["monitors"]
|
||||
else:
|
||||
merged.append(a)
|
||||
print(json.dumps({"alerts": [{k: a[k] for k in ("monitors", "kind", "summary")} for a in merged]}, indent=1))
|
||||
Loading…
Add table
Add a link
Reference in a new issue