Log (failed) attempt to notify a user with viewer role
This commit is contained in:
parent
9a60c29eb6
commit
a92579da2c
7 changed files with 208 additions and 10 deletions
|
|
@ -266,7 +266,7 @@ class EscalationPolicySnapshot:
|
|||
escalation_policy_step=self.step,
|
||||
)
|
||||
else:
|
||||
notify_to_users_list = list_users_to_notify_from_ical(on_call_schedule)
|
||||
notify_to_users_list = list_users_to_notify_from_ical(on_call_schedule, include_viewers=True)
|
||||
if notify_to_users_list is None:
|
||||
log_record = AlertGroupLogRecord(
|
||||
type=AlertGroupLogRecord.TYPE_ESCALATION_FAILED,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,13 @@ def notify_user_task(
|
|||
|
||||
if not user.is_notification_allowed:
|
||||
task_logger.info(f"notify_user_task: user {user.pk} notification is not allowed for role {user.role}")
|
||||
UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
reason=f"notification is not allowed for user with role {user.role}",
|
||||
alert_group=alert_group,
|
||||
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE,
|
||||
).save()
|
||||
return
|
||||
|
||||
user_has_notification, _ = UserHasNotification.objects.get_or_create(
|
||||
|
|
@ -257,6 +264,16 @@ def perform_notification(log_record_pk):
|
|||
).save()
|
||||
return
|
||||
|
||||
if not user.is_notification_allowed:
|
||||
UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
reason=f"notification is not allowed for user with role {user.role}",
|
||||
alert_group=alert_group,
|
||||
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE,
|
||||
).save()
|
||||
return
|
||||
|
||||
if notification_channel == UserNotificationPolicy.NotificationChannel.SMS:
|
||||
SMSMessage.send_sms(user, alert_group, notification_policy)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from apps.alerts.escalation_snapshot.utils import eta_for_escalation_step_notify
|
|||
from apps.alerts.models import AlertGroupLogRecord, EscalationPolicy
|
||||
from apps.schedules.ical_utils import list_users_to_notify_from_ical
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar
|
||||
from common.constants.role import Role
|
||||
|
||||
|
||||
def get_escalation_policy_snapshot_from_model(escalation_policy):
|
||||
|
|
@ -200,6 +201,55 @@ def test_escalation_step_notify_on_call_schedule(
|
|||
assert mocked_execute_tasks.called
|
||||
|
||||
|
||||
@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_step_notify_on_call_schedule_viewer_user(
|
||||
mocked_execute_tasks,
|
||||
escalation_step_test_setup,
|
||||
make_user_for_organization,
|
||||
make_escalation_policy,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup
|
||||
viewer = make_user_for_organization(organization=organization, role=Role.VIEWER)
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
|
||||
# create on_call_shift with user to notify
|
||||
data = {
|
||||
"start": timezone.datetime.now().replace(microsecond=0),
|
||||
"duration": timezone.timedelta(seconds=7200),
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, **data
|
||||
)
|
||||
on_call_shift.users.add(viewer)
|
||||
schedule.custom_on_call_shifts.add(on_call_shift)
|
||||
|
||||
notify_schedule_step = make_escalation_policy(
|
||||
escalation_chain=channel_filter.escalation_chain,
|
||||
escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE,
|
||||
notify_schedule=schedule,
|
||||
)
|
||||
escalation_policy_snapshot = get_escalation_policy_snapshot_from_model(notify_schedule_step)
|
||||
expected_eta = timezone.now() + timezone.timedelta(seconds=NEXT_ESCALATION_DELAY)
|
||||
result = escalation_policy_snapshot.execute(alert_group, reason)
|
||||
expected_result = EscalationPolicySnapshot.StepExecutionResultData(
|
||||
eta=result.eta,
|
||||
stop_escalation=False,
|
||||
pause_escalation=False,
|
||||
start_from_beginning=False,
|
||||
)
|
||||
assert expected_eta + timezone.timedelta(seconds=15) > result.eta > expected_eta - timezone.timedelta(seconds=15)
|
||||
assert result == expected_result
|
||||
assert notify_schedule_step.log_records.filter(type=AlertGroupLogRecord.TYPE_ESCALATION_TRIGGERED).exists()
|
||||
assert list(escalation_policy_snapshot.notify_to_users_queue) == list(
|
||||
list_users_to_notify_from_ical(schedule, include_viewers=True)
|
||||
)
|
||||
assert list(escalation_policy_snapshot.notify_to_users_queue) == [viewer]
|
||||
assert mocked_execute_tasks.called
|
||||
|
||||
|
||||
@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_step_notify_user_group(
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from apps.alerts.tasks.notify_user import perform_notification
|
||||
from apps.alerts.tasks.notify_user import notify_user_task, perform_notification
|
||||
from apps.base.models.user_notification_policy import UserNotificationPolicy
|
||||
from apps.base.models.user_notification_policy_log_record import UserNotificationPolicyLogRecord
|
||||
from common.constants.role import Role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -118,3 +119,62 @@ def test_notify_user_missing_data_errors(
|
|||
assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
assert error_log_record.reason == "Expected data is missing"
|
||||
assert error_log_record.notification_error_code is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notify_user_perform_notification_error_if_viewer(
|
||||
make_organization,
|
||||
make_user,
|
||||
make_user_notification_policy,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_user_notification_policy_log_record,
|
||||
):
|
||||
organization = make_organization()
|
||||
user_1 = make_user(organization=organization, role=Role.VIEWER, _verified_phone_number="1234567890")
|
||||
user_notification_policy = make_user_notification_policy(
|
||||
user=user_1,
|
||||
step=UserNotificationPolicy.Step.NOTIFY,
|
||||
notify_by=UserNotificationPolicy.NotificationChannel.SMS,
|
||||
)
|
||||
alert_receive_channel = make_alert_receive_channel(organization=organization)
|
||||
alert_group = make_alert_group(alert_receive_channel=alert_receive_channel)
|
||||
log_record = make_user_notification_policy_log_record(
|
||||
author=user_1,
|
||||
alert_group=alert_group,
|
||||
notification_policy=user_notification_policy,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_TRIGGERED,
|
||||
)
|
||||
|
||||
perform_notification(log_record.pk)
|
||||
|
||||
error_log_record = UserNotificationPolicyLogRecord.objects.last()
|
||||
assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
assert error_log_record.reason == f"notification is not allowed for user with role {user_1.role}"
|
||||
assert (
|
||||
error_log_record.notification_error_code
|
||||
== UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notify_user_error_if_viewer(
|
||||
make_organization,
|
||||
make_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
):
|
||||
organization = make_organization()
|
||||
user_1 = make_user(organization=organization, role=Role.VIEWER, _verified_phone_number="1234567890")
|
||||
alert_receive_channel = make_alert_receive_channel(organization=organization)
|
||||
alert_group = make_alert_group(alert_receive_channel=alert_receive_channel)
|
||||
|
||||
notify_user_task(user_1.pk, alert_group.pk)
|
||||
|
||||
error_log_record = UserNotificationPolicyLogRecord.objects.last()
|
||||
assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
assert error_log_record.reason == f"notification is not allowed for user with role {user_1.role}"
|
||||
assert (
|
||||
error_log_record.notification_error_code
|
||||
== UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE
|
||||
)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ class UserNotificationPolicyLogRecord(models.Model):
|
|||
ERROR_NOTIFICATION_IN_SLACK_CHANNEL_IS_ARCHIVED,
|
||||
ERROR_NOTIFICATION_IN_SLACK_RATELIMIT,
|
||||
ERROR_NOTIFICATION_MESSAGING_BACKEND_ERROR,
|
||||
) = range(25)
|
||||
ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE,
|
||||
) = range(26)
|
||||
|
||||
# for this errors we want to send message to general log channel
|
||||
ERRORS_TO_SEND_IN_SLACK_CHANNEL = [
|
||||
|
|
@ -266,6 +267,10 @@ class UserNotificationPolicyLogRecord(models.Model):
|
|||
result += f"failed to notify {user_verbal} in Slack, because channel is archived"
|
||||
elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK_RATELIMIT:
|
||||
result += f"failed to notify {user_verbal} in Slack due to Slack rate limit"
|
||||
elif (
|
||||
self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE
|
||||
):
|
||||
result += f"failed to notify {user_verbal}, not allowed role"
|
||||
else:
|
||||
# TODO: handle specific backend errors
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -26,14 +26,18 @@ if TYPE_CHECKING:
|
|||
from apps.user_management.models import User
|
||||
|
||||
|
||||
def users_in_ical(usernames_from_ical, organization):
|
||||
def users_in_ical(usernames_from_ical, organization, include_viewers=False):
|
||||
"""
|
||||
Parse ical file and return list of users found
|
||||
"""
|
||||
# Only grafana username will be used, consider adding grafana email and id
|
||||
|
||||
users_found_in_ical = organization.users.filter(
|
||||
Q(role__in=(Role.ADMIN, Role.EDITOR)) & (Q(username__in=usernames_from_ical) | Q(email__in=usernames_from_ical))
|
||||
users_found_in_ical = organization.users
|
||||
if not include_viewers:
|
||||
users_found_in_ical = users_found_in_ical.filter(role__in=(Role.ADMIN, Role.EDITOR))
|
||||
|
||||
users_found_in_ical = users_found_in_ical.filter(
|
||||
(Q(username__in=usernames_from_ical) | Q(email__in=usernames_from_ical))
|
||||
).distinct()
|
||||
|
||||
# Here is the example how we extracted users previously, using slack fields too
|
||||
|
|
@ -260,15 +264,17 @@ def list_of_empty_shifts_in_schedule(schedule, start_date, end_date):
|
|||
return sorted(empty_shifts, key=lambda dt: dt.start)
|
||||
|
||||
|
||||
def list_users_to_notify_from_ical(schedule, events_datetime=None):
|
||||
def list_users_to_notify_from_ical(schedule, events_datetime=None, include_viewers=False):
|
||||
"""
|
||||
Retrieve on-call users for the current time
|
||||
"""
|
||||
events_datetime = events_datetime if events_datetime else timezone.datetime.now(timezone.utc)
|
||||
return list_users_to_notify_from_ical_for_period(schedule, events_datetime, events_datetime)
|
||||
return list_users_to_notify_from_ical_for_period(
|
||||
schedule, events_datetime, events_datetime, include_viewers=include_viewers
|
||||
)
|
||||
|
||||
|
||||
def list_users_to_notify_from_ical_for_period(schedule, start_datetime, end_datetime):
|
||||
def list_users_to_notify_from_ical_for_period(schedule, start_datetime, end_datetime, include_viewers=False):
|
||||
# get list of iCalendars from current iCal files. If there is more than one calendar, primary calendar will always
|
||||
# be the first
|
||||
calendars = schedule.get_icalendars()
|
||||
|
|
@ -286,7 +292,7 @@ def list_users_to_notify_from_ical_for_period(schedule, start_datetime, end_date
|
|||
parsed_ical_events.setdefault(current_priority, []).extend(current_usernames)
|
||||
# find users by usernames. if users are not found for shift, get users from lower priority
|
||||
for _, usernames in sorted(parsed_ical_events.items(), reverse=True):
|
||||
users_found_in_ical = users_in_ical(usernames, schedule.organization)
|
||||
users_found_in_ical = users_in_ical(usernames, schedule.organization, include_viewers=include_viewers)
|
||||
if users_found_in_ical:
|
||||
break
|
||||
if users_found_in_ical:
|
||||
|
|
|
|||
60
engine/apps/schedules/tests/test_ical_utils.py
Normal file
60
engine/apps/schedules/tests/test_ical_utils.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import pytest
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.schedules.ical_utils import list_users_to_notify_from_ical, users_in_ical
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar
|
||||
from common.constants.role import Role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"include_viewers",
|
||||
[True, False],
|
||||
)
|
||||
def test_users_in_ical_viewers_inclusion(make_organization_and_user, make_user_for_organization, include_viewers):
|
||||
organization, user = make_organization_and_user()
|
||||
viewer = make_user_for_organization(organization, Role.VIEWER)
|
||||
|
||||
usernames = [user.username, viewer.username]
|
||||
result = users_in_ical(usernames, organization, include_viewers=include_viewers)
|
||||
if include_viewers:
|
||||
assert set(result) == {user, viewer}
|
||||
else:
|
||||
assert set(result) == {user}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"include_viewers",
|
||||
[True, False],
|
||||
)
|
||||
def test_list_users_to_notify_from_ical_viewers_inclusion(
|
||||
make_organization_and_user, make_user_for_organization, make_schedule, make_on_call_shift, include_viewers
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
viewer = make_user_for_organization(organization, Role.VIEWER)
|
||||
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
|
||||
date = timezone.now().replace(tzinfo=None, microsecond=0)
|
||||
data = {
|
||||
"priority_level": 1,
|
||||
"start": date,
|
||||
"duration": timezone.timedelta(seconds=10800),
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, **data
|
||||
)
|
||||
on_call_shift.users.add(user)
|
||||
on_call_shift.users.add(viewer)
|
||||
schedule.custom_on_call_shifts.add(on_call_shift)
|
||||
|
||||
# get users on-call
|
||||
date = date + timezone.timedelta(minutes=5)
|
||||
users_on_call = list_users_to_notify_from_ical(schedule, date, include_viewers=include_viewers)
|
||||
|
||||
if include_viewers:
|
||||
assert len(users_on_call) == 2
|
||||
assert set(users_on_call) == {user, viewer}
|
||||
else:
|
||||
assert len(users_on_call) == 1
|
||||
assert set(users_on_call) == {user}
|
||||
Loading…
Add table
Reference in a new issue