From 9b41ab82a023f42c0c0439c9eef229b30840ad59 Mon Sep 17 00:00:00 2001
From: Benedikt Ziemons
Date: Fri, 3 Apr 2020 16:32:17 +0200
Subject: [PATCH] Add basic prometheus webhook support
with type=prometheus and secret url arguments.
Also update project files.
---
.idea/inspectionProfiles/Project_Default.xml | 31 +++++++
.idea/misc.xml | 2 +-
.idea/webhook-matrix-notifier.iml | 5 +-
wmn.py | 98 +++++++++++++++++++-
4 files changed, 126 insertions(+), 10 deletions(-)
create mode 100644 .idea/inspectionProfiles/Project_Default.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..876cfb3
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 7e1538d..17c1b54 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/webhook-matrix-notifier.iml b/.idea/webhook-matrix-notifier.iml
index 446968a..a09991b 100644
--- a/.idea/webhook-matrix-notifier.iml
+++ b/.idea/webhook-matrix-notifier.iml
@@ -4,10 +4,7 @@
-
+
-
-
-
\ No newline at end of file
diff --git a/wmn.py b/wmn.py
index e19d464..6cc3c94 100644
--- a/wmn.py
+++ b/wmn.py
@@ -1,5 +1,8 @@
import json
import re
+import sys
+import traceback
+import typing
from datetime import datetime
import yaml
@@ -136,9 +139,9 @@ def process_jenkins_request():
if len(change_message) > 0:
htimestamp = datetime.fromtimestamp(change['timestamp'] / 1000).strftime("%d. %b %y %H:%M")
return f"{shorten(change_message)} " \
- f"({shorten(change['commitId'], 7, appendix='')}) " \
- f"by {change['author']} " \
- f"at {htimestamp}"
+ f"({shorten(change['commitId'], 7, appendix='')}) " \
+ f"by {change['author']} " \
+ f"at {htimestamp}"
else:
return shorten(json.dumps(change), appendix="...}")
@@ -155,8 +158,8 @@ def process_jenkins_request():
f"{len(change_messages)} commits
\n"
"" + (f"\n" if len(change_messages) > 0 else ""),
body=f"**Build {build_name} on project {project_name} complete: {result_type}**, "
- f"{len(change_messages)} commits\n"
- "" + (f"{text_changes}\n" if len(change_messages) > 0 else ""),
+ f"{len(change_messages)} commits\n"
+ "" + (f"{text_changes}\n" if len(change_messages) > 0 else ""),
msgtype=msgtype)
except MatrixRequestError as e:
return matrix_error(e)
@@ -165,11 +168,96 @@ def process_jenkins_request():
return "", 204
+def process_prometheus_request():
+ secret = request.args.get('secret')
+ if secret != cfg['secret']:
+ abort(401)
+
+ msgtype = get_msg_type()
+ room = get_a_room()
+
+ # written for version 4 of the alertmanager webhook JSON
+ # https://prometheus.io/docs/alerting/configuration/#webhook_config
+
+ def color_status_html(status: str, text: typing.Optional[str] = None):
+ _status_colors = {"resolved": "34A91D", "firing": "EF2929"}
+ if text is None:
+ text = status
+ if status in _status_colors:
+ return f'{text}'
+ else:
+ return text
+
+ def extract_alert_message(alert: typing.Dict[str, typing.Any]) -> typing.Tuple[str, str]:
+ """Takes the alert object and returns (text, html) as a string tuple."""
+
+ alert_status = alert.get("status", "None")
+ alert_labels = str(alert.get("labels", None))
+ alert_annotations = str(alert.get("annotations", None))
+ alert_start = alert.get("startsAt", None)
+ alert_end = alert.get("endsAt", None)
+ alert_daterange = []
+ if alert_start is not None:
+ alert_start = datetime.fromisoformat(alert_start).strftime("%d. %b %y %H:%M %Z").rstrip()
+ alert_daterange.append(f'Started at {alert_start}')
+ if alert_end is not None:
+ alert_end = datetime.fromisoformat(alert_end).strftime("%d. %b %y %H:%M %Z").rstrip()
+ alert_daterange.append(f'Ended at {alert_end}')
+ alert_daterange = "" if len(alert_daterange) == 0 else f'({", ".join(alert_daterange)})'
+ alert_generator_url = alert.get("generatorURL", "None")
+
+ return (
+ f'[{alert_status}] Labels: {alert_labels}, Annotations: {alert_annotations} - {alert_daterange} | Generator: {alert_generator_url}',
+ f'{color_status_html(alert_status)} Labels: {alert_labels}, Annotations: {alert_annotations} - {alert_daterange} | Generator: {alert_generator_url}',
+ )
+
+ def extract_prometheus_message() -> typing.Tuple[str, str]:
+ """Dissects the request's JSON and returns (text, html) as a string tuple."""
+
+ group_key = request.json.get("groupKey", "None")
+ status = request.json.get("status", "None")
+ receiver = request.json.get("receiver", "None")
+ group_labels = str(request.json.get("groupLabels", None))
+ common_labels = str(request.json.get("commonLabels", None))
+ common_annotations = str(request.json.get("commonAnnotations", None))
+ ext_url = request.json.get("externalURL", "None")
+ alerts = request.json.get("alerts", []) # type: typing.List[typing.Dict[str, typing.Any]]
+
+ text_alerts, html_alerts = zip(*map(extract_alert_message, alerts))
+ text_alerts = "\n" + "\n".join((f"- {msg}" for msg in text_alerts))
+ html_alerts = "
\n\n" + "\n".join((f" - {msg}
" for msg in html_alerts)) + "\n
"
+
+ return (
+ f'*{status.title()} alert for group {group_key}*\n Receiver: {receiver}\n Labels: {group_labels} | {common_labels}\n Annotations: {common_annotations}\n External URL: {ext_url}\nAlerts:{text_alerts}',
+ f'{color_status_html(status, f"{status.title()} alert for group {group_key}")}
\n Receiver: {receiver}
\n Labels: {group_labels} | {common_labels}
\n Annotations: {common_annotations}
\n External URL: {ext_url}
\nAlerts:{html_alerts}',
+ )
+
+ try:
+ html, body = extract_prometheus_message()
+ except (LookupError, ValueError, TypeError):
+ print("Error parsing JSON and forming message:", file=sys.stderr)
+ traceback.print_exc()
+ return "Error parsing JSON and forming message", 500
+
+ try:
+ client = MatrixClient(cfg["matrix"]["server"])
+ client.login(username=cfg["matrix"]["username"], password=cfg["matrix"]["password"])
+ room = client.join_room(room_id_or_alias=room)
+ room.send_html(html=html, body=body, msgtype=msgtype)
+ except MatrixRequestError as e:
+ return matrix_error(e)
+
+ # see Flask.make_response, this is interpreted as (body, status)
+ return "", 204
+
+
@application.route('/matrix', methods=("POST",))
def notify():
if 'X-Gitlab-Token' in request.headers:
return process_gitlab_request()
elif 'X-Jenkins-Token' in request.headers:
return process_jenkins_request()
+ elif 'type' in request.args and request.args.get('type') == "prometheus":
+ return process_prometheus_request()
else:
return "Cannot determine the request's webhook cause", 400