Browse Source

feat(pagerduty): issue alert custom severity (#67158)

Cathy Teng 11 months ago
parent
commit
e4ac25c2af

+ 32 - 4
src/sentry/integrations/pagerduty/actions/notification.py

@@ -2,10 +2,13 @@ from __future__ import annotations
 
 import logging
 from collections.abc import Sequence
+from typing import cast
 
 import sentry_sdk
 
+from sentry import features
 from sentry.integrations.pagerduty.actions import PagerDutyNotifyServiceForm
+from sentry.integrations.pagerduty.client import PAGERDUTY_DEFAULT_SEVERITY, PagerdutySeverity
 from sentry.rules.actions import IntegrationEventAction
 from sentry.shared_integrations.exceptions import ApiError
 
@@ -15,20 +18,35 @@ logger = logging.getLogger("sentry.integrations.pagerduty")
 class PagerDutyNotifyServiceAction(IntegrationEventAction):
     id = "sentry.integrations.pagerduty.notify_action.PagerDutyNotifyServiceAction"
     form_cls = PagerDutyNotifyServiceForm
-    label = "Send a notification to PagerDuty account {account} and service {service}"
+    old_label = "Send a notification to PagerDuty account {account} and service {service}"
+    new_label = "Send a notification to PagerDuty account {account} and service {service} with {severity} severity"
     prompt = "Send a PagerDuty notification"
     provider = "pagerduty"
     integration_key = "account"
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
+        self.has_feature_flag = features.has(
+            "organizations:integrations-custom-alert-priorities", self.project.organization
+        )
         self.form_fields = {
             "account": {
                 "type": "choice",
                 "choices": [(i.id, i.name) for i in self.get_integrations()],
             },
             "service": {"type": "choice", "choices": self.get_services()},
+            "severity": {
+                "type": "choice",
+                "choices": [
+                    ("default", "default"),
+                    ("critical", "critical"),
+                    ("warning", "warning"),
+                    ("error", "error"),
+                    ("info", "info"),
+                ],
+            },
         }
+        self.__class__.label = self.new_label if self.has_feature_flag else self.old_label
 
     def _get_service(self):
         oi = self.get_organization_integration()
@@ -56,6 +74,10 @@ class PagerDutyNotifyServiceAction(IntegrationEventAction):
             logger.info("pagerduty.service_missing", extra=log_context)
             return
 
+        severity = cast(
+            PagerdutySeverity, self.get_option("severity", default=PAGERDUTY_DEFAULT_SEVERITY)
+        )
+
         def send_notification(event, futures):
             installation = integration.get_installation(self.project.organization_id)
             try:
@@ -65,7 +87,9 @@ class PagerDutyNotifyServiceAction(IntegrationEventAction):
                 return
 
             try:
-                resp = client.send_trigger(event, notification_uuid=notification_uuid)
+                resp = client.send_trigger(
+                    event, notification_uuid=notification_uuid, severity=severity
+                )
             except ApiError as e:
                 self.logger.info(
                     "rule.fail.pagerduty_trigger",
@@ -95,7 +119,7 @@ class PagerDutyNotifyServiceAction(IntegrationEventAction):
                 },
             )
 
-        key = f"pagerduty:{integration.id}:{service['id']}"
+        key = f"pagerduty:{integration.id}:{service['id']}:{severity}"
         yield self.future(send_notification, key=key)
 
     def get_services(self) -> Sequence[tuple[int, str]]:
@@ -117,7 +141,11 @@ class PagerDutyNotifyServiceAction(IntegrationEventAction):
         else:
             service_name = "[removed]"
 
-        return self.label.format(account=self.get_integration_name(), service=service_name)
+        severity = self.get_option("severity", default=PAGERDUTY_DEFAULT_SEVERITY)
+
+        return self.label.format(
+            account=self.get_integration_name(), service=service_name, severity=severity
+        )
 
     def get_form_instance(self):
         return self.form_cls(

+ 14 - 3
src/sentry/integrations/pagerduty/client.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Any
+from typing import Any, Literal
 
 from sentry.api.serializers import ExternalEventSerializer, serialize
 from sentry.eventstore.models import Event, GroupEvent
@@ -14,6 +14,8 @@ LEVEL_SEVERITY_MAP = {
     "error": "error",
     "fatal": "critical",
 }
+PAGERDUTY_DEFAULT_SEVERITY = "default"  # represents using LEVEL_SEVERITY_MAP
+PagerdutySeverity = Literal["default", "critical", "warning", "error", "info"]
 
 
 class PagerDutyClient(ApiClient):
@@ -31,7 +33,12 @@ class PagerDutyClient(ApiClient):
             headers = {"Content-Type": "application/json"}
         return self._request(method, *args, headers=headers, **kwargs)
 
-    def send_trigger(self, data, notification_uuid: str | None = None):
+    def send_trigger(
+        self,
+        data,
+        notification_uuid: str | None = None,
+        severity: PagerdutySeverity | None = None,
+    ):
         # expected payload: https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2
         if isinstance(data, (Event, GroupEvent)):
             source = data.transaction or data.culprit or "<unknown>"
@@ -42,13 +49,17 @@ class PagerDutyClient(ApiClient):
             link_params = {"referrer": "pagerduty_integration"}
             if notification_uuid:
                 link_params["notification_uuid"] = notification_uuid
+
+            if severity == PAGERDUTY_DEFAULT_SEVERITY:
+                severity = LEVEL_SEVERITY_MAP[level]
+
             payload = {
                 "routing_key": self.integration_key,
                 "event_action": "trigger",
                 "dedup_key": group.qualified_short_id,
                 "payload": {
                     "summary": summary,
-                    "severity": LEVEL_SEVERITY_MAP[level],
+                    "severity": severity,
                     "source": source,
                     "component": group.project.slug,
                     "custom_details": custom_details,

+ 73 - 15
tests/sentry/integrations/pagerduty/test_client.py

@@ -56,11 +56,7 @@ class PagerDutyClientTest(APITestCase):
         self.installation = self.integration.get_installation(self.organization.id)
         self.min_ago = iso_format(before_now(minutes=1))
 
-    @responses.activate
-    def test_send_trigger(self):
-        integration_key = self.service["integration_key"]
-
-        event = self.store_event(
+        self.event = self.store_event(
             data={
                 "event_id": "a" * 32,
                 "message": "message",
@@ -69,23 +65,85 @@ class PagerDutyClientTest(APITestCase):
             },
             project_id=self.project.id,
         )
-        custom_details = serialize(event, None, ExternalEventSerializer())
-        assert event.group is not None
-        group = event.group
+
+        self.integration_key = self.service["integration_key"]
+        self.custom_details = serialize(self.event, None, ExternalEventSerializer())
+        assert self.event.group is not None
+        self.group = self.event.group
+
+    @responses.activate
+    def test_send_trigger(self):
         expected_data = {
-            "routing_key": integration_key,
+            "routing_key": self.integration_key,
             "event_action": "trigger",
-            "dedup_key": group.qualified_short_id,
+            "dedup_key": self.group.qualified_short_id,
             "payload": {
-                "summary": event.message,
+                "summary": self.event.message,
                 "severity": "error",
-                "source": event.transaction or event.culprit,
+                "source": self.event.transaction or self.event.culprit,
+                "component": self.project.slug,
+                "custom_details": self.custom_details,
+            },
+            "links": [
+                {
+                    "href": self.group.get_absolute_url(
+                        params={"referrer": "pagerduty_integration"}
+                    ),
+                    "text": "View Sentry Issue Details",
+                }
+            ],
+        }
+
+        responses.add(
+            responses.POST,
+            "https://events.pagerduty.com/v2/enqueue/",
+            body=b"{}",
+            match=[
+                matchers.header_matcher(
+                    {
+                        "Content-Type": "application/json",
+                    }
+                ),
+                matchers.json_params_matcher(expected_data),
+            ],
+        )
+
+        client = self.installation.get_keyring_client(self.service["id"])
+        client.send_trigger(self.event, severity="default")
+
+        assert len(responses.calls) == 1
+        request = responses.calls[0].request
+        assert "https://events.pagerduty.com/v2/enqueue/" == request.url
+        assert client.base_url and (client.base_url.lower() in request.url)
+
+        # Check if metrics is generated properly
+        calls = [
+            call(
+                "integrations.http_response",
+                sample_rate=1.0,
+                tags={"integration": "pagerduty", "status": 200},
+            )
+        ]
+        assert self.metrics.incr.mock_calls == calls
+
+    @responses.activate
+    def test_send_trigger_custom_severity(self):
+        expected_data = {
+            "routing_key": self.integration_key,
+            "event_action": "trigger",
+            "dedup_key": self.group.qualified_short_id,
+            "payload": {
+                "summary": self.event.message,
+                "severity": "info",
+                "source": self.event.transaction or self.event.culprit,
                 "component": self.project.slug,
-                "custom_details": custom_details,
+                "custom_details": self.custom_details,
             },
             "links": [
                 {
-                    "href": group.get_absolute_url(params={"referrer": "pagerduty_integration"}),
+                    "href": self.group.get_absolute_url(
+                        params={"referrer": "pagerduty_integration"}
+                    ),
                     "text": "View Sentry Issue Details",
                 }
             ],
@@ -106,7 +164,7 @@ class PagerDutyClientTest(APITestCase):
         )
 
         client = self.installation.get_keyring_client(self.service["id"])
-        client.send_trigger(event)
+        client.send_trigger(self.event, severity="info")
 
         assert len(responses.calls) == 1
         request = responses.calls[0].request

+ 32 - 4
tests/sentry/integrations/pagerduty/test_notify_action.py

@@ -173,21 +173,49 @@ class PagerDutyNotifyActionTest(RuleTestCase, PerformanceIssueTestCase):
         assert data["payload"]["custom_details"]["title"] == group_event.occurrence.issue_title
 
     def test_render_label(self):
-        rule = self.get_rule(data={"account": self.integration.id, "service": self.service["id"]})
+        rule_data = {
+            "account": self.integration.id,
+            "service": self.service["id"],
+            "severity": "warning",
+        }
+        rule = self.get_rule(data=rule_data)
 
         assert (
             rule.render_label()
             == "Send a notification to PagerDuty account Example and service Critical"
         )
 
+        with self.feature("organizations:integrations-custom-alert-priorities"):
+            # reinitialize rule to utilize flag
+            rule = self.get_rule(data=rule_data)
+            assert (
+                rule.render_label()
+                == "Send a notification to PagerDuty account Example and service Critical with warning severity"
+            )
+
     def test_render_label_without_integration(self):
         with assume_test_silo_mode(SiloMode.CONTROL):
             self.integration.delete()
 
-        rule = self.get_rule(data={"account": self.integration.id, "service": self.service["id"]})
+        rule_data = {
+            "account": self.integration.id,
+            "service": self.service["id"],
+            "severity": "default",
+        }
+        rule = self.get_rule(data=rule_data)
 
-        label = rule.render_label()
-        assert label == "Send a notification to PagerDuty account [removed] and service [removed]"
+        assert (
+            rule.render_label()
+            == "Send a notification to PagerDuty account [removed] and service [removed]"
+        )
+
+        with self.feature("organizations:integrations-custom-alert-priorities"):
+            # reinitialize rule to utilize flag
+            rule = self.get_rule(data=rule_data)
+            assert (
+                rule.render_label()
+                == "Send a notification to PagerDuty account [removed] and service [removed] with default severity"
+            )
 
     def test_valid_service_options(self):
         # create new org that has the same pd account but different a service added