snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
224
zerver/webhooks/grafana/view.py
Normal file
224
zerver/webhooks/grafana/view.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from zerver.decorator import webhook_view
|
||||
from zerver.lib.exceptions import AnomalousWebhookPayloadError
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.timestamp import datetime_to_global_time
|
||||
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
|
||||
from zerver.lib.validator import (
|
||||
WildValue,
|
||||
check_anything,
|
||||
check_float,
|
||||
check_int,
|
||||
check_none_or,
|
||||
check_string,
|
||||
check_string_in,
|
||||
check_union,
|
||||
)
|
||||
from zerver.lib.webhooks.common import check_send_webhook_message
|
||||
from zerver.models import UserProfile
|
||||
|
||||
OLD_TOPIC_TEMPLATE = "{alert_title}"
|
||||
|
||||
ALERT_STATUS_TEMPLATE = "{alert_icon} **{alert_state}**\n\n"
|
||||
|
||||
OLD_MESSAGE_TEMPLATE = "{alert_status}[{rule_name}]({rule_url})\n\n{alert_message}{eval_matches}"
|
||||
|
||||
NEW_TOPIC_TEMPLATE = "[{alertname}]"
|
||||
|
||||
START_TIME_TEMPLATE = "This alert was fired at {start_time}."
|
||||
|
||||
END_TIME_TEMPLATE = "\n\nThis alert was resolved at {end_time}."
|
||||
|
||||
MESSAGE_LABELS_TEMPLATE = "\n\nLabels:\n{label_information}\n"
|
||||
|
||||
MESSAGE_VALUES_TEMPLATE = "Values:\n{value_information}\n"
|
||||
|
||||
MESSAGE_ANNOTATIONS_TEMPLATE = "Annotations:\n{annotation_information}"
|
||||
|
||||
MESSAGE_GENERATOR_TEMPLATE = "\n[Generator]({generator_url})"
|
||||
|
||||
MESSAGE_SILENCE_TEMPLATE = "\n[Silence]({silence_url})"
|
||||
|
||||
MESSAGE_IMAGE_TEMPLATE = "\n[Image]({image_url})"
|
||||
|
||||
LEGACY_EVENT_TYPES = ["ok", "pending", "alerting", "paused"]
|
||||
|
||||
NEW_EVENT_TYPES = ["firing", "resolved"]
|
||||
|
||||
ALL_EVENT_TYPES = LEGACY_EVENT_TYPES + NEW_EVENT_TYPES
|
||||
|
||||
|
||||
def get_global_time(dt_str: str) -> str:
|
||||
dt = datetime.fromisoformat(dt_str)
|
||||
return datetime_to_global_time(dt)
|
||||
|
||||
|
||||
# The fields that this webhook needs from a Grafana 7.0 alert payload.
|
||||
@dataclass
|
||||
class GrafanaLegacyAlert:
|
||||
title: str
|
||||
state: str
|
||||
rule_name: str
|
||||
rule_url: str
|
||||
|
||||
|
||||
def parse_legacy_alert(payload: WildValue) -> GrafanaLegacyAlert:
|
||||
"""Parses the fields that this webhook needs from a Grafana 7.0
|
||||
alert payload, raising ValidationError if any is missing or is
|
||||
not the type that we expect.
|
||||
"""
|
||||
title = payload["title"].tame(check_string)
|
||||
state = payload["state"].tame(
|
||||
check_string_in(["no_data", "paused", "alerting", "ok", "pending", "unknown"])
|
||||
)
|
||||
rule_name = payload["ruleName"].tame(check_string)
|
||||
rule_url = payload["ruleUrl"].tame(check_string)
|
||||
return GrafanaLegacyAlert(title, state, rule_name, rule_url)
|
||||
|
||||
|
||||
@webhook_view("Grafana", all_event_types=ALL_EVENT_TYPES)
|
||||
@typed_endpoint
|
||||
def api_grafana_webhook(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
payload: JsonBodyPayload[WildValue],
|
||||
) -> HttpResponse:
|
||||
# Grafana alerting system.
|
||||
if "alerts" in payload:
|
||||
# Grafana 8.0 and above alerting; works for:
|
||||
# - https://grafana.com/docs/grafana/v8.0/alerting/unified-alerting/message-templating/template-data/
|
||||
# - https://grafana.com/docs/grafana/v9.0/alerting/contact-points/notifiers/webhook-notifier/
|
||||
# - https://grafana.com/docs/grafana/v10.0/alerting/alerting-rules/manage-contact-points/webhook-notifier/
|
||||
# - https://grafana.com/docs/grafana/v11.0/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/
|
||||
for alert in payload["alerts"]:
|
||||
status = alert["status"].tame(check_string_in(["firing", "resolved"]))
|
||||
if status == "firing":
|
||||
body = ALERT_STATUS_TEMPLATE.format(
|
||||
alert_icon=":alert:", alert_state=status.upper()
|
||||
)
|
||||
else:
|
||||
body = ALERT_STATUS_TEMPLATE.format(
|
||||
alert_icon=":checkbox:", alert_state=status.upper()
|
||||
)
|
||||
|
||||
if "alertname" in alert["labels"] and alert["labels"]["alertname"]:
|
||||
alertname = alert["labels"]["alertname"].tame(check_string)
|
||||
topic_name = NEW_TOPIC_TEMPLATE.format(alertname=alertname)
|
||||
body += "**" + alertname + "**\n\n"
|
||||
else:
|
||||
# if no alertname, fallback to the alert fingerprint
|
||||
topic_name = NEW_TOPIC_TEMPLATE.format(
|
||||
alertname=alert["fingerprint"].tame(check_string)
|
||||
)
|
||||
|
||||
body += START_TIME_TEMPLATE.format(
|
||||
start_time=get_global_time(alert["startsAt"].tame(check_string))
|
||||
)
|
||||
|
||||
end_time = alert["endsAt"].tame(check_string)
|
||||
if end_time != "0001-01-01T00:00:00Z":
|
||||
body += END_TIME_TEMPLATE.format(end_time=get_global_time(end_time))
|
||||
|
||||
if alert["labels"]:
|
||||
label_information = ""
|
||||
for key, value in alert["labels"].items():
|
||||
label_information += "- " + key + ": " + value.tame(check_string) + "\n"
|
||||
body += MESSAGE_LABELS_TEMPLATE.format(label_information=label_information)
|
||||
|
||||
if alert.get("values"):
|
||||
value_information = ""
|
||||
for key, value in alert["values"].items():
|
||||
value_information += "- " + key + ": " + str(value.tame(check_anything)) + "\n"
|
||||
body += MESSAGE_VALUES_TEMPLATE.format(value_information=value_information)
|
||||
elif alert.get("valueString"):
|
||||
body += (
|
||||
MESSAGE_VALUES_TEMPLATE.format(
|
||||
value_information=alert["valueString"].tame(check_string)
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
if alert.get("annotations"):
|
||||
annotation_information = ""
|
||||
for key, value in alert["annotations"].items():
|
||||
annotation_information += "- " + key + ": " + value.tame(check_string) + "\n"
|
||||
body += MESSAGE_ANNOTATIONS_TEMPLATE.format(
|
||||
annotation_information=annotation_information
|
||||
)
|
||||
|
||||
if alert.get("generatorURL"):
|
||||
body += MESSAGE_GENERATOR_TEMPLATE.format(
|
||||
generator_url=alert["generatorURL"].tame(check_string)
|
||||
)
|
||||
|
||||
if alert.get("silenceURL"):
|
||||
body += MESSAGE_SILENCE_TEMPLATE.format(
|
||||
silence_url=alert["silenceURL"].tame(check_string)
|
||||
)
|
||||
|
||||
if alert.get("imageURL"):
|
||||
body += MESSAGE_IMAGE_TEMPLATE.format(
|
||||
image_url=alert["imageURL"].tame(check_string)
|
||||
)
|
||||
|
||||
body += "\n"
|
||||
|
||||
check_send_webhook_message(request, user_profile, topic_name, body, status)
|
||||
|
||||
return json_success(request)
|
||||
|
||||
try:
|
||||
# Grafana 7.0 alerts:
|
||||
# https://grafana.com/docs/grafana/v7.0/alerting/notifications/#webhook
|
||||
legacy_alert = parse_legacy_alert(payload)
|
||||
except ValidationError:
|
||||
# The payload did not include the expected fields for either
|
||||
# current or legacy Grafana webhooks.
|
||||
raise AnomalousWebhookPayloadError
|
||||
|
||||
topic_name = OLD_TOPIC_TEMPLATE.format(alert_title=legacy_alert.title)
|
||||
|
||||
eval_matches_text = ""
|
||||
if "evalMatches" in payload and payload["evalMatches"] is not None:
|
||||
for match in payload["evalMatches"]:
|
||||
eval_matches_text += "**{}:** {}\n".format(
|
||||
match["metric"].tame(check_string),
|
||||
match["value"].tame(check_none_or(check_union([check_int, check_float]))),
|
||||
)
|
||||
|
||||
message_text = ""
|
||||
if "message" in payload:
|
||||
message_text = payload["message"].tame(check_string) + "\n\n"
|
||||
|
||||
state = legacy_alert.state
|
||||
if state == "alerting":
|
||||
alert_status = ALERT_STATUS_TEMPLATE.format(alert_icon=":alert:", alert_state=state.upper())
|
||||
elif state == "ok":
|
||||
alert_status = ALERT_STATUS_TEMPLATE.format(
|
||||
alert_icon=":squared_ok:", alert_state=state.upper()
|
||||
)
|
||||
else:
|
||||
alert_status = ALERT_STATUS_TEMPLATE.format(alert_icon=":info:", alert_state=state.upper())
|
||||
|
||||
body = OLD_MESSAGE_TEMPLATE.format(
|
||||
alert_message=message_text,
|
||||
alert_status=alert_status,
|
||||
rule_name=legacy_alert.rule_name,
|
||||
rule_url=legacy_alert.rule_url,
|
||||
eval_matches=eval_matches_text,
|
||||
)
|
||||
|
||||
if "imageUrl" in payload:
|
||||
body += "\n[Click to view visualization]({visualization})".format(
|
||||
visualization=payload["imageUrl"].tame(check_string)
|
||||
)
|
||||
|
||||
body = body.strip()
|
||||
check_send_webhook_message(request, user_profile, topic_name, body, state)
|
||||
return json_success(request)
|
||||
Reference in New Issue
Block a user