#!/usr/bin/env python3 """Translate a Grafana alert webhook into a Gotify message. Grafana sends its own payload shape: {"receiver": "...", "status": "firing", "alerts": [{"labels": {...}, "annotations": {...}}]} Gotify requires: {"title": "...", "message": "...", "priority": 0..10} Gotify answers 400 "Field 'message' is required" to a Grafana payload, so a conversion step is mandatory. This service is that step and nothing else. Standard library only, so it needs no image build and no dependencies. """ import json import os import urllib.request from http.server import BaseHTTPRequestHandler, HTTPServer GOTIFY_BASE = os.environ.get("GOTIFY_URL", "http://gotify.notifications.svc.cluster.local") GOTIFY_TOKEN = os.environ["GOTIFY_TOKEN"] LISTEN_PORT = int(os.environ.get("PORT", "8080")) # Firing is louder than resolved. Gotify priority runs 0 to 10. PRIORITY = {"firing": 8, "resolved": 3} def to_gotify(payload): alerts = payload.get("alerts") or [] status = (payload.get("status") or "firing").lower() priority = PRIORITY.get(status, 5) if not alerts: title = payload.get("title") or "Grafana notification" return title, json.dumps(payload)[:2000], priority first = alerts[0] labels = first.get("labels") or {} annotations = first.get("annotations") or {} name = labels.get("alertname", "Grafana alert") title = "[%s] %s" % (status.upper(), name) lines = [] summary = annotations.get("summary") or annotations.get("description") if summary: lines.append(summary) for key in ("severity", "namespace", "pod", "instance", "job"): if labels.get(key): lines.append("%s: %s" % (key, labels[key])) if len(alerts) > 1: lines.append("+%d more alert(s) in this notification" % (len(alerts) - 1)) return title, "\n".join(lines) or name, priority class Handler(BaseHTTPRequestHandler): def _reply(self, code, body=b"ok"): self.send_response(code) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(body) def do_GET(self): self._reply(200) def do_POST(self): length = int(self.headers.get("Content-Length") or 0) raw = self.rfile.read(length) if length else b"{}" try: payload = json.loads(raw.decode("utf-8", "replace") or "{}") except ValueError: payload = {} title, message, priority = to_gotify(payload) body = json.dumps({ "title": title[:200], "message": message[:4000], "priority": priority, }).encode() request = urllib.request.Request( GOTIFY_BASE.rstrip("/") + "/message", data=body, headers={"Content-Type": "application/json", "X-Gotify-Key": GOTIFY_TOKEN}, ) try: urllib.request.urlopen(request, timeout=10).read() except Exception as exc: # noqa: BLE001 - log and report to Grafana print("forward to gotify failed: %s" % exc, flush=True) self._reply(502, b"gotify rejected the message") return print("forwarded: %s" % title, flush=True) self._reply(200) def log_message(self, fmt, *args): # keep the log to one line per delivery print("%s - %s" % (self.address_string(), fmt % args), flush=True) if __name__ == "__main__": print("grafana-gotify bridge listening on %d" % LISTEN_PORT, flush=True) HTTPServer(("0.0.0.0", LISTEN_PORT), Handler).serve_forever()