Log (failed) attempt to notify a user with viewer role

This commit is contained in:
Matias Bordese 2022-06-08 13:40:45 -03:00
parent 9a60c29eb6
commit a92579da2c
7 changed files with 208 additions and 10 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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}