Send demo alert with dynamic payload and get demo payload example on private api (#1700)

# What this PR does

## Which issue(s) this PR fixes

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Ildar Iskhakov 2023-04-18 10:48:11 +08:00 committed by GitHub
parent e93347e75b
commit f825fdf1a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 69 additions and 18 deletions

View file

@ -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)

View file

@ -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,
},

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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()

View file

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