monitoring: Resolved hakt die Firing-Nachricht per Edit ab statt neu zu posten
Ein Alarm = eine Nachricht: Firing-Event-IDs werden je Alertmanager-Fingerprint gemerkt (State-Datei, ueberlebt Restarts), Resolved ersetzt die Originalnachricht per m.replace mit durchgestrichenem Text + Haken. Ohne bekannte Zuordnung Fallback auf eigenstaendige Resolved-Nachricht. Re-Notifies desselben Alarms erzeugen keine Doppelnachrichten mehr. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
This commit is contained in:
co-authored by
Claude Fable 5
parent
5b09a12287
commit
ea33c3b7b0
@@ -1,42 +1,99 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Minimaler Alertmanager-Webhook -> Matrix-Raum (gitops#32). Gleiche Machart wie
|
# Minimaler Alertmanager-Webhook -> Matrix-Raum (gitops#32). Gleiche Machart wie
|
||||||
# maintenance-notify (Issue #24 im gitops-Repo): purer Stdlib-HTTP-Server, Bot-Token
|
# maintenance-notify (Issue #24 im gitops-Repo): purer Stdlib-HTTP-Server, Bot-Token
|
||||||
# aus der Umgebung, Nachricht per Client-Server-API. Kein Framework, kein State.
|
# aus der Umgebung, Nachricht per Client-Server-API.
|
||||||
import json, os, urllib.request, uuid
|
#
|
||||||
|
# Ein Alarm = EINE Nachricht: beim Firing wird pro Alarm (Alertmanager-Fingerprint)
|
||||||
|
# eine Nachricht gesendet und deren Event-ID gemerkt; beim Resolved wird dieselbe
|
||||||
|
# Nachricht per m.replace-Edit durchgestrichen und abgehakt statt eine neue zu
|
||||||
|
# posten (Wunsch sorb 2026-08-01: append-only wird unuebersichtlich). Die Zuordnung
|
||||||
|
# ueberlebt Container-Restarts via State-Datei; ohne Zuordnung (z.B. nach Neubau)
|
||||||
|
# faellt Resolved auf eine eigenstaendige ✅-Nachricht zurueck.
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
import uuid
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
|
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
|
||||||
ROOM_ID = os.environ["MATRIX_ROOM_ID"]
|
ROOM_ID = os.environ["MATRIX_ROOM_ID"]
|
||||||
TOKEN = os.environ["MATRIX_TOKEN"]
|
TOKEN = os.environ["MATRIX_TOKEN"]
|
||||||
|
STATE_FILE = os.environ.get("MATRIX_STATE_FILE", "/tmp/matrix-alerts-state.json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE) as f:
|
||||||
|
state = json.load(f) # fingerprint -> {"event_id":..., "text":...}
|
||||||
|
except Exception:
|
||||||
|
state = {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state():
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"state save failed: {e}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def matrix_put(content):
|
||||||
|
url = (f"{HOMESERVER}/_matrix/client/v3/rooms/{urllib.parse.quote(ROOM_ID)}"
|
||||||
|
f"/send/m.room.message/{uuid.uuid4()}")
|
||||||
|
req = urllib.request.Request(url, data=json.dumps(content).encode(), method="PUT",
|
||||||
|
headers={"Authorization": f"Bearer {TOKEN}",
|
||||||
|
"Content-Type": "application/json"})
|
||||||
|
return json.loads(urllib.request.urlopen(req, timeout=10).read()).get("event_id")
|
||||||
|
|
||||||
|
|
||||||
|
def send_text(text):
|
||||||
|
return matrix_put({"msgtype": "m.text", "body": text})
|
||||||
|
|
||||||
|
|
||||||
|
def edit_resolved(event_id, old_text):
|
||||||
|
# m.replace-Edit: Original wird in Element in-place ersetzt (durchgestrichen + Haken)
|
||||||
|
new_body = f"✅ ~~{old_text}~~"
|
||||||
|
new_html = f"✅ <del>{html.escape(old_text)}</del>"
|
||||||
|
matrix_put({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": f"* {new_body}",
|
||||||
|
"m.new_content": {"msgtype": "m.text", "body": new_body,
|
||||||
|
"format": "org.matrix.custom.html", "formatted_body": new_html},
|
||||||
|
"m.relates_to": {"rel_type": "m.replace", "event_id": event_id},
|
||||||
|
})
|
||||||
|
|
||||||
def send_matrix(text):
|
|
||||||
url = f"{HOMESERVER}/_matrix/client/v3/rooms/{urllib.parse.quote(ROOM_ID)}/send/m.room.message/{uuid.uuid4()}"
|
|
||||||
body = json.dumps({"msgtype": "m.text", "body": text}).encode()
|
|
||||||
req = urllib.request.Request(url, data=body, method="PUT", headers={
|
|
||||||
"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"})
|
|
||||||
urllib.request.urlopen(req, timeout=10).read()
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if self.path != "/alert":
|
if self.path != "/alert":
|
||||||
self.send_response(404); self.end_headers(); return
|
self.send_response(404); self.end_headers(); return
|
||||||
data = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
|
data = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
|
||||||
lines = []
|
try:
|
||||||
for a in data.get("alerts", []):
|
for a in data.get("alerts", []):
|
||||||
icon = "\U0001F534" if a.get("status") == "firing" else "✅"
|
name = a.get("labels", {}).get("alertname", "?")
|
||||||
name = a.get("labels", {}).get("alertname", "?")
|
summary = a.get("annotations", {}).get("summary", "")
|
||||||
summary = a.get("annotations", {}).get("summary", "")
|
fp = a.get("fingerprint", "")
|
||||||
lines.append(f"{icon} [{a.get('status')}] {name}: {summary}")
|
if a.get("status") == "firing":
|
||||||
if lines:
|
text = f"\U0001F534 [firing] {name}: {summary}"
|
||||||
try:
|
if fp in state: # re-notify desselben Alarms -> keine Doppelnachricht
|
||||||
send_matrix("\n".join(lines))
|
continue
|
||||||
except Exception as e:
|
event_id = send_text(text)
|
||||||
self.log_message("matrix send failed: %s", e)
|
if fp and event_id:
|
||||||
self.send_response(502); self.end_headers(); return
|
state[fp] = {"event_id": event_id, "text": text}
|
||||||
|
else:
|
||||||
|
known = state.pop(fp, None)
|
||||||
|
if known:
|
||||||
|
edit_resolved(known["event_id"], known["text"])
|
||||||
|
else:
|
||||||
|
send_text(f"✅ [resolved] {name}: {summary}")
|
||||||
|
save_state()
|
||||||
|
except Exception as e:
|
||||||
|
self.log_message("matrix send failed: %s", e)
|
||||||
|
self.send_response(502); self.end_headers(); return
|
||||||
self.send_response(200); self.end_headers()
|
self.send_response(200); self.end_headers()
|
||||||
|
|
||||||
def log_message(self, fmt, *args):
|
def log_message(self, fmt, *args):
|
||||||
print(fmt % args, flush=True)
|
print(fmt % args, flush=True)
|
||||||
|
|
||||||
import urllib.parse
|
|
||||||
ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
|
ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
|
||||||
|
|||||||
Reference in New Issue
Block a user