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" + + 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