-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add personal webhook notification backend
- Loading branch information
Showing
20 changed files
with
561 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import typing | ||
|
||
from django.core.exceptions import ObjectDoesNotExist | ||
|
||
from apps.base.messaging import BaseMessagingBackend | ||
|
||
if typing.TYPE_CHECKING: | ||
from apps.alerts.models import AlertGroup | ||
from apps.base.models import UserNotificationPolicy | ||
from apps.user_management.models import User | ||
|
||
|
||
class PersonalWebhookBackend(BaseMessagingBackend): | ||
backend_id = "WEBHOOK" | ||
label = "Webhook" | ||
short_label = "Webhook" | ||
available_for_use = True | ||
|
||
def serialize_user(self, user: "User"): | ||
try: | ||
personal_webhook = user.personal_webhook | ||
except ObjectDoesNotExist: | ||
return {"name": None} | ||
return {"name": personal_webhook.webhook.name} | ||
|
||
def notify_user( | ||
self, user: "User", alert_group: "AlertGroup", notification_policy: typing.Optional["UserNotificationPolicy"] | ||
): | ||
from apps.webhooks.tasks import notify_user_async | ||
|
||
notify_user_async.delay( | ||
user_pk=user.pk, | ||
alert_group_pk=alert_group.pk, | ||
notification_policy_pk=notification_policy.pk if notification_policy else None, | ||
) |
35 changes: 35 additions & 0 deletions
35
engine/apps/webhooks/migrations/0018_alter_webhook_trigger_type_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Generated by Django 4.2.16 on 2025-01-27 18:46 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import mirage.fields | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('user_management', '0029_remove_organization_general_log_channel_id_db'), | ||
('webhooks', '0017_alter_webhook_trigger_type_and_more'), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name='webhook', | ||
name='trigger_type', | ||
field=models.IntegerField(choices=[(0, 'Manual or escalation step'), (1, 'Alert Group Created'), (2, 'Acknowledged'), (3, 'Resolved'), (4, 'Silenced'), (5, 'Unsilenced'), (6, 'Unresolved'), (7, 'Unacknowledged'), (8, 'Status change'), (9, 'Personal notification')], default=0, null=True), | ||
), | ||
migrations.AlterField( | ||
model_name='webhookresponse', | ||
name='trigger_type', | ||
field=models.IntegerField(choices=[(0, 'Manual or escalation step'), (1, 'Alert Group Created'), (2, 'Acknowledged'), (3, 'Resolved'), (4, 'Silenced'), (5, 'Unsilenced'), (6, 'Unresolved'), (7, 'Unacknowledged'), (8, 'Status change'), (9, 'Personal notification')]), | ||
), | ||
migrations.CreateModel( | ||
name='PersonalNotificationWebhook', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('additional_context_data', mirage.fields.EncryptedTextField(null=True)), | ||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='personal_webhook', to='user_management.user')), | ||
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='personal_channels', to='webhooks.webhook')), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
from .webhook import Webhook, WebhookResponse # noqa: F401 | ||
from .webhook import PersonalNotificationWebhook, Webhook, WebhookResponse # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
from .alert_group_status import alert_group_created, alert_group_status_change # noqa: F401 | ||
from .notify_user import notify_user_async # noqa: F401 | ||
from .trigger_webhook import execute_webhook, send_webhook_event # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
from celery.utils.log import get_task_logger | ||
from django.conf import settings | ||
from django.core.exceptions import ObjectDoesNotExist | ||
|
||
from apps.alerts.models import AlertGroup | ||
from apps.user_management.models import User | ||
from apps.webhooks.models import Webhook | ||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task | ||
|
||
MAX_RETRIES = 1 if settings.DEBUG else 10 | ||
logger = get_task_logger(__name__) | ||
|
||
|
||
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=MAX_RETRIES) | ||
def notify_user_async(user_pk, alert_group_pk, notification_policy_pk): | ||
# imported here to avoid circular import error | ||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord | ||
from apps.webhooks.tasks import execute_webhook | ||
|
||
try: | ||
user = User.objects.get(pk=user_pk) | ||
except User.DoesNotExist: | ||
logger.warning(f"User {user_pk} does not exist") | ||
return | ||
|
||
try: | ||
alert_group = AlertGroup.objects.get(pk=alert_group_pk) | ||
except AlertGroup.DoesNotExist: | ||
logger.warning(f"Alert group {alert_group_pk} does not exist") | ||
return | ||
|
||
try: | ||
notification_policy = UserNotificationPolicy.objects.get(pk=notification_policy_pk) | ||
except UserNotificationPolicy.DoesNotExist: | ||
logger.warning(f"User notification policy {notification_policy_pk} does not exist") | ||
return | ||
|
||
try: | ||
personal_webhook = user.personal_webhook | ||
except ObjectDoesNotExist: | ||
logger.warning(f"Personal webhook is not set for user {user_pk}") | ||
# record log notification error | ||
UserNotificationPolicyLogRecord.objects.create( | ||
author=user, | ||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, | ||
notification_policy=notification_policy, | ||
alert_group=alert_group, | ||
notification_step=notification_policy.step, | ||
notification_channel=notification_policy.notify_by, | ||
) | ||
return | ||
|
||
# trigger webhook via task | ||
execute_webhook.apply_async( | ||
(personal_webhook.pk, alert_group.pk, user.pk, notification_policy.pk), | ||
kwargs={"trigger_type": Webhook.TRIGGER_PERSONAL_NOTIFICATION}, | ||
) | ||
|
||
# record log notification success | ||
UserNotificationPolicyLogRecord.objects.create( | ||
author=user, | ||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS, | ||
notification_policy=notification_policy, | ||
alert_group=alert_group, | ||
notification_step=notification_policy.step, | ||
notification_channel=notification_policy.notify_by, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
from django.conf import settings | ||
|
||
from apps.base.models import UserNotificationPolicy | ||
from apps.webhooks.backend import PersonalWebhookBackend | ||
from apps.webhooks.models import Webhook | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_serialize_user( | ||
make_organization, make_user_for_organization, make_custom_webhook, make_personal_notification_webhook | ||
): | ||
organization = make_organization() | ||
user = make_user_for_organization(organization) | ||
|
||
backend = PersonalWebhookBackend() | ||
|
||
# by default, there is no personal webhook set | ||
assert backend.serialize_user(user) == {"name": None} | ||
|
||
# set personal webhook | ||
webhook = make_custom_webhook( | ||
organization=organization, | ||
trigger_type=Webhook.TRIGGER_PERSONAL_NOTIFICATION, | ||
) | ||
make_personal_notification_webhook(user=user, webhook=webhook) | ||
|
||
assert backend.serialize_user(user) == {"name": user.personal_webhook.webhook.name} | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_notify_user_triggers_task( | ||
make_organization, | ||
make_user_for_organization, | ||
make_user_notification_policy, | ||
make_alert_receive_channel, | ||
make_alert_group, | ||
make_custom_webhook, | ||
make_personal_notification_webhook, | ||
): | ||
organization = make_organization() | ||
user = make_user_for_organization(organization) | ||
alert_receive_channel = make_alert_receive_channel(organization) | ||
alert_group = make_alert_group(alert_receive_channel) | ||
|
||
backend = PersonalWebhookBackend() | ||
# set personal webhook | ||
webhook = make_custom_webhook( | ||
organization=organization, | ||
trigger_type=Webhook.TRIGGER_PERSONAL_NOTIFICATION, | ||
) | ||
make_personal_notification_webhook(user=user, webhook=webhook) | ||
|
||
notification_policy = make_user_notification_policy( | ||
user, | ||
UserNotificationPolicy.Step.NOTIFY, | ||
notify_by=settings.PERSONAL_WEBHOOK_BACKEND_ID, | ||
important=False, | ||
) | ||
|
||
with patch("apps.webhooks.tasks.notify_user_async") as mock_notify_user_async: | ||
backend.notify_user(user, alert_group, notification_policy) | ||
|
||
mock_notify_user_async.delay.assert_called_once_with( | ||
user_pk=user.pk, | ||
alert_group_pk=alert_group.pk, | ||
notification_policy_pk=notification_policy.pk, | ||
) |
Oops, something went wrong.