diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aac851c..65048da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Helm chart: add the option to use a helm hook for the migration job ([1386](https://github.com/grafana/oncall/pull/1386)) +- Send demo alert with dynamic payload and get demo payload example on private api ([1700](https://github.com/grafana/oncall/pull/1700)) ## v1.2.11 (2023-04-14) diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 412193c3..80e6ccb5 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -501,19 +501,26 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): return getattr(heartbeat, self.INTEGRATIONS_TO_REVERSE_URL_MAP[self.integration], None) # Demo alerts - def send_demo_alert(self, force_route_id=None): + def send_demo_alert(self, force_route_id=None, payload=None): logger.info(f"send_demo_alert integration={self.pk} force_route_id={force_route_id}") + if payload is None: + payload = self.config.example_payload if self.is_demo_alert_enabled: if self.has_alertmanager_payload_structure: - for alert in self.config.example_payload.get("alerts", []): - create_alertmanager_alerts.apply_async( - [], - { - "alert_receive_channel_pk": self.pk, - "alert": alert, - "is_demo": True, - "force_route_id": force_route_id, - }, + if (alerts := payload.get("alerts", None)) and type(alerts) == list and len(alerts): + for alert in alerts: + create_alertmanager_alerts.apply_async( + [], + { + "alert_receive_channel_pk": self.pk, + "alert": alert, + "is_demo": True, + "force_route_id": force_route_id, + }, + ) + else: + raise UnableToSendDemoAlert( + "Unable to send demo alert as payload has no 'alerts' key, it is not array, or it is empty." ) else: create_alert.apply_async( @@ -525,7 +532,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): "link_to_upstream_details": None, "alert_receive_channel_pk": self.pk, "integration_unique_data": None, - "raw_request_data": self.config.example_payload, + "raw_request_data": payload, "is_demo": True, "force_route_id": force_route_id, }, diff --git a/engine/apps/alerts/models/channel_filter.py b/engine/apps/alerts/models/channel_filter.py index 7ea55abe..677f3a19 100644 --- a/engine/apps/alerts/models/channel_filter.py +++ b/engine/apps/alerts/models/channel_filter.py @@ -94,6 +94,8 @@ class ChannelFilter(OrderedModel): @classmethod def select_filter(cls, alert_receive_channel, raw_request_data, force_route_id=None): # Try to find force route first if force_route_id is given + # Force route was used to send demo alerts to specific route. + # It is deprecated and may be used by older versions of the plugins if force_route_id is not None: logger.info( f"start select_filter with force_route_id={force_route_id} alert_receive_channel={alert_receive_channel.pk}." @@ -164,6 +166,7 @@ class ChannelFilter(OrderedModel): raise Exception("Unknown filtering term") def send_demo_alert(self): + """Deprecated. May be used in the older versions of the plugin""" integration = self.alert_receive_channel integration.send_demo_alert(force_route_id=self.pk) diff --git a/engine/apps/alerts/tests/test_alert_receiver_channel.py b/engine/apps/alerts/tests/test_alert_receiver_channel.py index cdc204de..12aa63db 100644 --- a/engine/apps/alerts/tests/test_alert_receiver_channel.py +++ b/engine/apps/alerts/tests/test_alert_receiver_channel.py @@ -90,14 +90,25 @@ def test_get_default_template_attribute_fallback_to_web(make_organization, make_ @mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None) @pytest.mark.django_db -def test_send_demo_alert(mocked_create_alert, make_organization, make_alert_receive_channel): +@pytest.mark.parametrize( + "payload", + [ + None, + {"foo": "bar"}, + ], +) +def test_send_demo_alert(mocked_create_alert, make_organization, make_alert_receive_channel, payload): organization = make_organization() alert_receive_channel = make_alert_receive_channel( organization, integration=AlertReceiveChannel.INTEGRATION_WEBHOOK ) - alert_receive_channel.send_demo_alert() + alert_receive_channel.send_demo_alert(payload=payload) assert mocked_create_alert.called assert mocked_create_alert.call_args.args[1]["is_demo"] + assert ( + mocked_create_alert.call_args.args[1]["raw_request_data"] == payload + or alert_receive_channel.config.example_payload + ) assert mocked_create_alert.call_args.args[1]["force_route_id"] is None @@ -111,14 +122,26 @@ def test_send_demo_alert(mocked_create_alert, make_organization, make_alert_rece AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING, ], ) +@pytest.mark.parametrize( + "payload", + [ + None, + {"alerts": [{"foo": "bar"}]}, + ], +) def test_send_demo_alert_alertmanager_payload_shape( - mocked_create_alert, make_organization, make_alert_receive_channel, integration + mocked_create_alert, make_organization, make_alert_receive_channel, integration, payload ): organization = make_organization() alert_receive_channel = make_alert_receive_channel(organization, integration=integration) - alert_receive_channel.send_demo_alert() + alert_receive_channel.send_demo_alert(payload=payload) assert mocked_create_alert.called assert mocked_create_alert.call_args.args[1]["is_demo"] + assert ( + mocked_create_alert.call_args.args[1]["alert"] == payload["alerts"][0] + if payload + else alert_receive_channel.config.example_payload["alerts"][0] + ) assert mocked_create_alert.call_args.args[1]["force_route_id"] is None diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index 7bd2ced4..64341317 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -50,6 +50,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ maintenance_till = serializers.ReadOnlyField(source="till_maintenance_timestamp") heartbeat = serializers.SerializerMethodField() allow_delete = serializers.SerializerMethodField() + demo_alert_payload = serializers.SerializerMethodField() # integration heartbeat is in PREFETCH_RELATED not by mistake. # With using of select_related ORM builds strange join @@ -82,6 +83,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ "heartbeat", "is_available_for_integration_heartbeat", "allow_delete", + "demo_alert_payload", ] read_only_fields = [ "created_at", @@ -92,6 +94,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ "instructions", "demo_alert_enabled", "maintenance_mode", + "demo_alert_payload", ] extra_kwargs = {"integration": {"required": True}} @@ -153,6 +156,9 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ def get_alert_groups_count(self, obj): return 0 + def get_demo_alert_payload(self, obj): + return obj.config.example_payload + class AlertReceiveChannelUpdateSerializer(AlertReceiveChannelSerializer): class Meta(AlertReceiveChannelSerializer.Meta): diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 8675d95f..ff401793 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -149,9 +149,19 @@ class AlertReceiveChannelView( @action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler]) def send_demo_alert(self, request, pk): - instance = AlertReceiveChannel.objects.get(public_primary_key=pk) + alert_receive_channel = AlertReceiveChannel.objects.get(public_primary_key=pk) + demo_alert_payload = request.data.get("demo_alert_payload", None) + + if not demo_alert_payload: + # If no payload provided, use the demo payload for backword compatibility + payload = alert_receive_channel.config.example_payload + else: + if type(demo_alert_payload) != dict: + raise BadRequest(detail="Payload for demo alert must be a valid json object") + payload = demo_alert_payload + try: - instance.send_demo_alert() + alert_receive_channel.send_demo_alert(payload=payload) except UnableToSendDemoAlert as e: raise BadRequest(detail=str(e)) return Response(status=status.HTTP_200_OK) diff --git a/engine/apps/api/views/channel_filter.py b/engine/apps/api/views/channel_filter.py index adfe534f..1ba3bb1e 100644 --- a/engine/apps/api/views/channel_filter.py +++ b/engine/apps/api/views/channel_filter.py @@ -137,6 +137,7 @@ class ChannelFilterView( @action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler]) def send_demo_alert(self, request, pk): + """Deprecated action. May be used in the older version of the plugin.""" instance = ChannelFilter.objects.get(public_primary_key=pk) try: instance.send_demo_alert() diff --git a/engine/common/exceptions/exceptions.py b/engine/common/exceptions/exceptions.py index 9adf0b47..15f70520 100644 --- a/engine/common/exceptions/exceptions.py +++ b/engine/common/exceptions/exceptions.py @@ -1,6 +1,6 @@ class OperationCouldNotBePerformedError(Exception): """ - Indicates that operation could not be performed due to to application logic. + Indicates that operation could not be performed due to application logic. E.g. you can't ack resolved AlertGroup """