2016-03-13 13:15:21 +01:00
|
|
|
# Webhooks for external integrations.
|
2023-11-18 05:11:51 +01:00
|
|
|
|
|
|
|
from django.core.exceptions import ValidationError
|
2016-06-05 23:18:47 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2016-05-25 15:02:02 +02:00
|
|
|
|
2020-08-20 00:32:15 +02:00
|
|
|
from zerver.decorator import webhook_view
|
2021-06-30 18:35:50 +02:00
|
|
|
from zerver.lib.response import json_success
|
2023-09-27 19:01:31 +02:00
|
|
|
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
|
2022-07-31 09:07:42 +02:00
|
|
|
from zerver.lib.validator import (
|
|
|
|
WildValue,
|
2023-11-18 05:11:51 +01:00
|
|
|
check_float,
|
2022-07-31 09:07:42 +02:00
|
|
|
check_int,
|
|
|
|
check_list,
|
|
|
|
check_none_or,
|
|
|
|
check_string,
|
2023-11-18 05:11:51 +01:00
|
|
|
check_string_in,
|
2022-07-31 09:07:42 +02:00
|
|
|
check_union,
|
|
|
|
)
|
2020-11-24 21:26:05 +01:00
|
|
|
from zerver.lib.webhooks.common import check_send_webhook_message, unix_milliseconds_to_timestamp
|
2019-02-02 23:53:55 +01:00
|
|
|
from zerver.models import UserProfile
|
2016-03-13 13:15:21 +01:00
|
|
|
|
2023-11-18 05:11:51 +01:00
|
|
|
MISSING_FIELDS_NOTIFICATION = """
|
|
|
|
:danger: A New Relic [incident]({url}) updated
|
2022-07-22 11:37:33 +02:00
|
|
|
|
2023-11-18 05:11:51 +01:00
|
|
|
**Warning**: Unable to use the default notification format because at least one expected field was missing from the incident payload. See [New Relic integration documentation](/integrations/doc/newrelic).
|
|
|
|
|
|
|
|
**Missing fields**: {formatted_missing_fields}
|
|
|
|
"""
|
|
|
|
|
|
|
|
NOTIFICATION_TEMPLATE = """
|
|
|
|
{priority_symbol} **[{title}]({incident_url})**
|
|
|
|
|
|
|
|
```quote
|
|
|
|
**Priority**: {priority}
|
|
|
|
**State**: {state}
|
|
|
|
**Updated at**: {time_updated}
|
|
|
|
{owner}
|
2019-04-17 23:26:31 +02:00
|
|
|
```
|
|
|
|
|
2023-11-18 05:11:51 +01:00
|
|
|
```spoiler :file: Incident details
|
2022-07-22 11:37:33 +02:00
|
|
|
{details}
|
|
|
|
```
|
2023-11-18 05:11:51 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
NOTIFICATION_DETAILS = """
|
|
|
|
- **Alert policies**: {alert_policy}
|
|
|
|
- **Conditions**: {conditions}
|
|
|
|
- **Total incidents**: {total_incidents}
|
|
|
|
- **Incident created at**: {time_created}
|
|
|
|
"""
|
|
|
|
|
|
|
|
ALL_EVENT_TYPES = ["CREATED", "ACTIVATED", "CLOSED"]
|
|
|
|
|
|
|
|
PRIORITIES = {
|
|
|
|
"CRITICAL": ":red_circle:",
|
|
|
|
"HIGH": ":orange_circle:",
|
|
|
|
"MEDIUM": ":yellow:",
|
|
|
|
"LOW": ":blue_circle:",
|
|
|
|
}
|
|
|
|
|
|
|
|
DEFAULT_NEWRELIC_URL = "https://one.newrelic.com/alerts-ai"
|
|
|
|
|
|
|
|
|
|
|
|
EXPECTED_FIELDS = [
|
|
|
|
"issueUrl",
|
|
|
|
"title",
|
|
|
|
"priority",
|
|
|
|
"totalIncidents",
|
|
|
|
"state",
|
|
|
|
"createdAt",
|
|
|
|
"updatedAt",
|
|
|
|
"alertPolicyNames",
|
|
|
|
"alertConditionNames",
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def get_timestamp_string(payload: WildValue, event_type: str) -> str:
|
|
|
|
# This function is intended to be used only for the "updatedAt"
|
|
|
|
# and "createdAt" fields. Theoretically, neither field can be
|
|
|
|
# None at any time.
|
|
|
|
unix_time = payload[event_type].tame(check_union([check_int, check_string]))
|
|
|
|
timestamp = str(unix_milliseconds_to_timestamp(unix_time, "newrelic"))
|
|
|
|
return f"<time: {timestamp} >"
|
|
|
|
|
|
|
|
|
|
|
|
def parse_payload(payload: WildValue) -> dict[str, str]:
|
|
|
|
priority = payload["priority"].tame(check_string_in(PRIORITIES.keys()))
|
|
|
|
priority_symbol = PRIORITIES.get(priority, ":alert:")
|
|
|
|
conditions_list = payload.get("alertConditionNames", ["Unknown condition"]).tame(
|
|
|
|
check_list(check_string)
|
|
|
|
)
|
|
|
|
conditions = ", ".join([f"`{c}`" for c in conditions_list])
|
|
|
|
policy_list = payload.get("alertPolicyNames", ["Unknown policy"]).tame(check_list(check_string))
|
|
|
|
alert_policy = ", ".join([f"`{p}`" for p in policy_list])
|
|
|
|
|
|
|
|
owner = payload.get("owner").tame(check_none_or(check_string))
|
|
|
|
acknowledged = ""
|
|
|
|
if owner and owner != "N/A":
|
|
|
|
acknowledged = f"**Acknowledged by**: {owner}"
|
|
|
|
|
|
|
|
message_context: dict[str, str] = {
|
|
|
|
"title": payload["title"].tame(check_string),
|
|
|
|
"incident_url": payload.get("issueUrl", DEFAULT_NEWRELIC_URL).tame(check_string),
|
|
|
|
"total_incidents": str(payload["totalIncidents"].tame(check_int)),
|
|
|
|
"state": payload["state"].tame(check_string_in(ALL_EVENT_TYPES)),
|
|
|
|
"time_created": get_timestamp_string(payload, "createdAt"),
|
|
|
|
"time_updated": get_timestamp_string(payload, "updatedAt"),
|
|
|
|
"priority": priority,
|
|
|
|
"priority_symbol": priority_symbol,
|
|
|
|
"conditions": conditions,
|
|
|
|
"alert_policy": alert_policy,
|
|
|
|
"owner": acknowledged,
|
|
|
|
}
|
|
|
|
|
|
|
|
return message_context
|
|
|
|
|
|
|
|
|
|
|
|
def format_zulip_custom_fields(payload: WildValue) -> str:
|
|
|
|
body_custom_field_detail: str = ""
|
|
|
|
zulip_custom_fields = payload.get("zulipCustomFields", {})
|
|
|
|
|
|
|
|
for key, value in zulip_custom_fields.items():
|
|
|
|
custom_field_name = key.capitalize()
|
|
|
|
try:
|
|
|
|
details = value.tame(
|
|
|
|
check_none_or(
|
|
|
|
check_union(
|
|
|
|
[
|
|
|
|
check_int,
|
|
|
|
check_float,
|
|
|
|
check_string,
|
|
|
|
check_list(
|
|
|
|
check_none_or(check_union([check_int, check_float, check_string]))
|
|
|
|
),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if isinstance(details, list):
|
|
|
|
custom_field_detail = ", ".join([f"{detail}" for detail in details])
|
|
|
|
else:
|
|
|
|
custom_field_detail = f"{details}"
|
|
|
|
|
|
|
|
custom_field_message = f"- **{custom_field_name}**: {custom_field_detail}\n"
|
|
|
|
body_custom_field_detail += custom_field_message
|
|
|
|
except ValidationError:
|
|
|
|
invalid_field_message = (
|
|
|
|
f"- **{custom_field_name}**: *Value is not a supported data type*\n"
|
|
|
|
)
|
|
|
|
body_custom_field_detail += invalid_field_message
|
|
|
|
return body_custom_field_detail
|
2019-04-17 23:26:31 +02:00
|
|
|
|
|
|
|
|
2023-11-18 05:11:51 +01:00
|
|
|
def check_for_expected_fields(payload: WildValue) -> list[str]:
|
|
|
|
return [key for key in EXPECTED_FIELDS if key not in payload]
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-07-16 11:40:46 +02:00
|
|
|
|
|
|
|
@webhook_view("NewRelic", all_event_types=ALL_EVENT_TYPES)
|
2023-08-12 09:34:31 +02:00
|
|
|
@typed_endpoint
|
2020-11-19 01:19:08 +01:00
|
|
|
def api_newrelic_webhook(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2023-08-12 09:34:31 +02:00
|
|
|
*,
|
2023-09-27 19:01:31 +02:00
|
|
|
payload: JsonBodyPayload[WildValue],
|
2020-11-19 01:19:08 +01:00
|
|
|
) -> HttpResponse:
|
2023-11-18 05:11:51 +01:00
|
|
|
missing_fields = check_for_expected_fields(payload)
|
|
|
|
if missing_fields:
|
|
|
|
formatted_missing_fields = ", ".join([f"`{fields}`" for fields in missing_fields])
|
|
|
|
content = MISSING_FIELDS_NOTIFICATION.format(
|
|
|
|
url=DEFAULT_NEWRELIC_URL,
|
|
|
|
formatted_missing_fields=formatted_missing_fields,
|
2022-07-31 09:07:42 +02:00
|
|
|
)
|
2023-11-18 05:11:51 +01:00
|
|
|
topic = "New Relic incident alerts"
|
|
|
|
check_send_webhook_message(request, user_profile, topic, content)
|
2022-07-22 11:37:33 +02:00
|
|
|
return json_success(request)
|
|
|
|
|
2023-11-18 05:11:51 +01:00
|
|
|
message_context = parse_payload(payload)
|
|
|
|
incident_details = NOTIFICATION_DETAILS.format(**message_context)
|
|
|
|
incident_details += format_zulip_custom_fields(payload)
|
|
|
|
content = NOTIFICATION_TEMPLATE.format(details=incident_details, **message_context)
|
|
|
|
topic = message_context["title"]
|
|
|
|
check_send_webhook_message(request, user_profile, topic, content, message_context["state"])
|
|
|
|
return json_success(request)
|