From 3c933752445440609318636e99ae4b775b07c19e Mon Sep 17 00:00:00 2001 From: Yulya Artyukhina Date: Wed, 27 Mar 2024 13:37:01 +0100 Subject: [PATCH] Update alert group state by backsync (#4089) # What this PR does Adds method to update alert group state by backsync Related to https://github.com/grafana/oncall-private/issues/2542 Should be merged with https://github.com/grafana/oncall-private/pull/2606 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- engine/apps/alerts/constants.py | 1 + ...alter_alertgrouplogrecord_action_source.py | 18 ++ engine/apps/alerts/models/alert_group.py | 173 +++++++++++++----- .../alerts/models/alert_group_log_record.py | 6 +- .../alerts/tests/test_acknowledge_reminder.py | 2 +- engine/apps/alerts/tests/test_alert_group.py | 67 +++++-- engine/apps/api/tests/test_alert_group.py | 4 +- engine/apps/api/views/alert_group.py | 12 +- .../tests/test_update_metrics_cache.py | 28 +-- engine/apps/public_api/views/incidents.py | 8 +- .../apps/slack/scenarios/distribute_alerts.py | 12 +- .../telegram/tests/test_keyboard_renderer.py | 6 +- .../telegram/tests/test_message_renderer.py | 2 +- .../updates/update_handlers/button_press.py | 12 +- engine/apps/twilioapp/gather.py | 6 +- engine/apps/zvonok/status_callback.py | 2 +- 16 files changed, 253 insertions(+), 106 deletions(-) create mode 100644 engine/apps/alerts/migrations/0049_alter_alertgrouplogrecord_action_source.py diff --git a/engine/apps/alerts/constants.py b/engine/apps/alerts/constants.py index aa5adca1..f5c9f196 100644 --- a/engine/apps/alerts/constants.py +++ b/engine/apps/alerts/constants.py @@ -9,6 +9,7 @@ class ActionSource(IntegerChoices): PHONE = 2, "Phone" TELEGRAM = 3, "Telegram" API = 4, "API" + BACKSYNC = 5, "Backsync" TASK_DELAY_SECONDS = 1 diff --git a/engine/apps/alerts/migrations/0049_alter_alertgrouplogrecord_action_source.py b/engine/apps/alerts/migrations/0049_alter_alertgrouplogrecord_action_source.py new file mode 100644 index 00000000..08b0a017 --- /dev/null +++ b/engine/apps/alerts/migrations/0049_alter_alertgrouplogrecord_action_source.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-03-20 12:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0048_alertgroupexternalid'), + ] + + operations = [ + migrations.AlterField( + model_name='alertgrouplogrecord', + name='action_source', + field=models.SmallIntegerField(default=None, null=True, verbose_name=[(0, 'Slack'), (1, 'Web'), (2, 'Phone'), (3, 'Telegram'), (4, 'API'), (5, 'Backsync')]), + ), + ] diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index a663b571..20c7408a 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -612,11 +612,37 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. """Update metrics cache for response time and state as needed.""" update_metrics_for_alert_group.apply_async((self.id, organization_id, previous_state, state)) - def acknowledge_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None: + def update_state_by_backsync(self, new_state: AlertGroupState) -> None: + if self.state == new_state: + return + logger.debug(f"Update state {self.state} -> {new_state} for alert_group {self.pk}") + if new_state == AlertGroupState.FIRING: + if self.state == AlertGroupState.ACKNOWLEDGED: + self.un_acknowledge_by_user_or_backsync(action_source=ActionSource.BACKSYNC) + elif self.state == AlertGroupState.RESOLVED: + self.un_resolve_by_user_or_backsync(action_source=ActionSource.BACKSYNC) + elif self.state == AlertGroupState.SILENCED: + self.un_silence_by_user_or_backsync(action_source=ActionSource.BACKSYNC) + elif new_state == AlertGroupState.ACKNOWLEDGED: + self.acknowledge_by_user_or_backsync(action_source=ActionSource.BACKSYNC) + elif new_state == AlertGroupState.RESOLVED: + self.resolve_by_user_or_backsync(action_source=ActionSource.BACKSYNC) + elif new_state == AlertGroupState.SILENCED: + self.silence_by_user_or_backsync(action_source=ActionSource.BACKSYNC) + + def acknowledge_by_user_or_backsync( + self, user: typing.Optional[User] = None, action_source: typing.Optional[ActionSource] = None + ) -> None: from apps.alerts.models import AlertGroupLogRecord initial_state = self.state - logger.debug(f"Started acknowledge_by_user for alert_group {self.pk}") + reason = "Acknowledge button" if user else "Backsync signal" + acknowledged_by = AlertGroup.USER if user else AlertGroup.SOURCE + step_specific_info = ( + {"source_integration_name": self.channel.verbal_name} if action_source == ActionSource.BACKSYNC else None + ) + organization_id = user.organization_id if user else self.channel.organization_id + logger.debug(f"Started acknowledge_by_user_or_backsync for alert_group {self.pk}") # if incident was silenced or resolved, unsilence/unresolve it without starting escalation if self.silenced: @@ -625,28 +651,34 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. type=AlertGroupLogRecord.TYPE_UN_SILENCE, author=user, silence_delay=None, - reason="Acknowledge button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) if self.resolved: self.unresolve() self.log_records.create( type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, - reason="Acknowledge button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) - self.acknowledge(acknowledged_by_user=user, acknowledged_by=AlertGroup.USER) + self.acknowledge(acknowledged_by_user=user, acknowledged_by=acknowledged_by) # Update alert group state and response time metrics cache - self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state) + self._update_metrics(organization_id=organization_id, previous_state=initial_state, state=self.state) self.stop_escalation() - self.start_ack_reminder_if_needed() + if user: # ack reminder works only for actions performed by user + self.start_ack_reminder_if_needed() with transaction.atomic(): log_record = self.log_records.create( - type=AlertGroupLogRecord.TYPE_ACK, author=user, action_source=action_source + type=AlertGroupLogRecord.TYPE_ACK, + author=user, + action_source=action_source, + step_specific_info=step_specific_info, ) logger.debug( @@ -657,9 +689,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) for dependent_alert_group in self.dependent_alert_groups.all(): - dependent_alert_group.acknowledge_by_user(user, action_source=action_source) + dependent_alert_group.acknowledge_by_user_or_backsync(user, action_source=action_source) - logger.debug(f"Finished acknowledge_by_user for alert_group {self.pk}") + logger.debug(f"Finished acknowledge_by_user_or_backsync for alert_group {self.pk}") def acknowledge_by_source(self): from apps.alerts.models import AlertGroupLogRecord @@ -694,21 +726,30 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. for dependent_alert_group in self.dependent_alert_groups.all(): dependent_alert_group.acknowledge_by_source() - def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None: + def un_acknowledge_by_user_or_backsync( + self, user: typing.Optional[User] = None, action_source: typing.Optional[ActionSource] = None + ) -> None: from apps.alerts.models import AlertGroupLogRecord initial_state = self.state - logger.debug(f"Started un_acknowledge_by_user for alert_group {self.pk}") + step_specific_info = ( + {"source_integration_name": self.channel.verbal_name} if action_source == ActionSource.BACKSYNC else None + ) + organization_id = user.organization_id if user else self.channel.organization_id + logger.debug(f"Started un_acknowledge_by_user_or_backsync for alert_group {self.pk}") self.unacknowledge() # Update alert group state metric cache - self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state) + self._update_metrics(organization_id=organization_id, previous_state=initial_state, state=self.state) if self.is_root_alert_group: self.start_escalation_if_needed() with transaction.atomic(): log_record = self.log_records.create( - type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, action_source=action_source + type=AlertGroupLogRecord.TYPE_UN_ACK, + author=user, + action_source=action_source, + step_specific_info=step_specific_info, ) logger.debug( @@ -719,14 +760,21 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) for dependent_alert_group in self.dependent_alert_groups.all(): - dependent_alert_group.un_acknowledge_by_user(user, action_source=action_source) - logger.debug(f"Finished un_acknowledge_by_user for alert_group {self.pk}") + dependent_alert_group.un_acknowledge_by_user_or_backsync(user, action_source=action_source) + logger.debug(f"Finished un_acknowledge_by_user_or_backsync for alert_group {self.pk}") - def resolve_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None: + def resolve_by_user_or_backsync( + self, user: typing.Optional[User] = None, action_source: typing.Optional[ActionSource] = None + ) -> None: from apps.alerts.models import AlertGroupLogRecord initial_state = self.state - + reason = "Resolve button" if user else "Backsync signal" + resolved_by = AlertGroup.USER if user else AlertGroup.SOURCE + step_specific_info = ( + {"source_integration_name": self.channel.verbal_name} if action_source == ActionSource.BACKSYNC else None + ) + organization_id = user.organization_id if user else self.channel.organization_id # if incident was silenced, unsilence it without starting escalation if self.silenced: self.un_silence() @@ -734,17 +782,21 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. type=AlertGroupLogRecord.TYPE_UN_SILENCE, author=user, silence_delay=None, - reason="Resolve button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) - self.resolve(resolved_by=AlertGroup.USER, resolved_by_user=user) + self.resolve(resolved_by=resolved_by, resolved_by_user=user) # Update alert group state and response time metrics cache - self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state) + self._update_metrics(organization_id=organization_id, previous_state=initial_state, state=self.state) self.stop_escalation() with transaction.atomic(): log_record = self.log_records.create( - type=AlertGroupLogRecord.TYPE_RESOLVED, author=user, action_source=action_source + type=AlertGroupLogRecord.TYPE_RESOLVED, + author=user, + action_source=action_source, + step_specific_info=step_specific_info, ) logger.debug( @@ -755,7 +807,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) for dependent_alert_group in self.dependent_alert_groups.all(): - dependent_alert_group.resolve_by_user(user, action_source=action_source) + dependent_alert_group.resolve_by_user_or_backsync(user, action_source=action_source) def resolve_by_source(self): from apps.alerts.models import AlertGroupLogRecord @@ -835,18 +887,29 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. for dependent_alert_group in self.dependent_alert_groups.all(): dependent_alert_group.resolve_by_disable_maintenance() - def un_resolve_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None: + def un_resolve_by_user_or_backsync( + self, user: typing.Optional[User] = None, action_source: typing.Optional[ActionSource] = None + ) -> None: from apps.alerts.models import AlertGroupLogRecord if self.wiped_at is None: initial_state = self.state + step_specific_info = ( + {"source_integration_name": self.channel.verbal_name} + if action_source == ActionSource.BACKSYNC + else None + ) + organization_id = user.organization_id if user else self.channel.organization_id self.unresolve() # Update alert group state metric cache - self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state) + self._update_metrics(organization_id=organization_id, previous_state=initial_state, state=self.state) with transaction.atomic(): log_record = self.log_records.create( - type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, action_source=action_source + type=AlertGroupLogRecord.TYPE_UN_RESOLVED, + author=user, + action_source=action_source, + step_specific_info=step_specific_info, ) if self.is_root_alert_group: @@ -861,7 +924,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) for dependent_alert_group in self.dependent_alert_groups.all(): - dependent_alert_group.un_resolve_by_user(user, action_source=action_source) + dependent_alert_group.un_resolve_by_user_or_backsync(user, action_source=action_source) def attach_by_user( self, user: User, root_alert_group: "AlertGroup", action_source: typing.Optional[ActionSource] = None @@ -873,15 +936,15 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. self.save(update_fields=["root_alert_group"]) self.stop_escalation() if root_alert_group.acknowledged and not self.acknowledged: - self.acknowledge_by_user(user, action_source=action_source) + self.acknowledge_by_user_or_backsync(user, action_source=action_source) elif not root_alert_group.acknowledged and self.acknowledged: - self.un_acknowledge_by_user(user, action_source=action_source) + self.un_acknowledge_by_user_or_backsync(user, action_source=action_source) if root_alert_group.silenced and not self.silenced: - self.silence_by_user(user, action_source=action_source, silence_delay=None) + self.silence_by_user_or_backsync(user, action_source=action_source, silence_delay=None) if not root_alert_group.silenced and self.silenced: - self.un_silence_by_user(user, action_source=action_source) + self.un_silence_by_user_or_backsync(user, action_source=action_source) with transaction.atomic(): log_record = self.log_records.create( @@ -997,26 +1060,39 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) - def silence_by_user( - self, user: User, silence_delay: typing.Optional[int], action_source: typing.Optional[ActionSource] = None + def silence_by_user_or_backsync( + self, + user: typing.Optional[User] = None, + silence_delay: typing.Optional[int] = None, + action_source: typing.Optional[ActionSource] = None, ) -> None: from apps.alerts.models import AlertGroupLogRecord initial_state = self.state + reason = "Silence button" if user else "Backsync signal" + step_specific_info = ( + {"source_integration_name": self.channel.verbal_name} if action_source == ActionSource.BACKSYNC else None + ) + organization_id = user.organization_id if user else self.channel.organization_id if self.resolved: self.unresolve() self.log_records.create( type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, - reason="Silence button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) if self.acknowledged: self.unacknowledge() self.log_records.create( - type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, reason="Silence button", action_source=action_source + type=AlertGroupLogRecord.TYPE_UN_ACK, + author=user, + reason=reason, + action_source=action_source, + step_specific_info=step_specific_info, ) if self.silenced: @@ -1025,8 +1101,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. type=AlertGroupLogRecord.TYPE_UN_SILENCE, author=user, silence_delay=None, - reason="Silence button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) now = timezone.now() @@ -1048,15 +1125,16 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. raw_escalation_snapshot=self.raw_escalation_snapshot, ) # Update alert group state and response time metrics cache - self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state) + self._update_metrics(organization_id=organization_id, previous_state=initial_state, state=self.state) with transaction.atomic(): log_record = self.log_records.create( type=AlertGroupLogRecord.TYPE_SILENCE, author=user, silence_delay=silence_delay_timedelta, - reason="Silence button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) logger.debug( @@ -1068,16 +1146,22 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) for dependent_alert_group in self.dependent_alert_groups.all(): - dependent_alert_group.silence_by_user(user, silence_delay, action_source) + dependent_alert_group.silence_by_user_or_backsync(user, silence_delay, action_source) - def un_silence_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None: + def un_silence_by_user_or_backsync( + self, user: typing.Optional[User] = None, action_source: typing.Optional[ActionSource] = None + ) -> None: from apps.alerts.models import AlertGroupLogRecord initial_state = self.state - + reason = "Unsilence button" if user else "Backsync signal" + step_specific_info = ( + {"source_integration_name": self.channel.verbal_name} if action_source == ActionSource.BACKSYNC else None + ) + organization_id = user.organization_id if user else self.channel.organization_id self.un_silence() # Update alert group state metric cache - self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state) + self._update_metrics(organization_id=organization_id, previous_state=initial_state, state=self.state) if self.is_root_alert_group: self.start_escalation_if_needed() @@ -1088,8 +1172,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. author=user, silence_delay=None, # 2.Look like some time ago there was no TYPE_UN_SILENCE - reason="Unsilence button", + reason=reason, action_source=action_source, + step_specific_info=step_specific_info, ) logger.debug( @@ -1101,7 +1186,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. transaction.on_commit(partial(send_alert_group_signal.delay, log_record.pk)) for dependent_alert_group in self.dependent_alert_groups.all(): - dependent_alert_group.un_silence_by_user(user, action_source=action_source) + dependent_alert_group.un_silence_by_user_or_backsync(user, action_source=action_source) def wipe_by_user(self, user: User) -> None: from apps.alerts.models import AlertGroupLogRecord diff --git a/engine/apps/alerts/models/alert_group_log_record.py b/engine/apps/alerts/models/alert_group_log_record.py index 64d6876a..1ea9cefc 100644 --- a/engine/apps/alerts/models/alert_group_log_record.py +++ b/engine/apps/alerts/models/alert_group_log_record.py @@ -224,7 +224,7 @@ class AlertGroupLogRecord(models.Model): escalation_policy_step = models.IntegerField(null=True, default=None) step_specific_info = JSONField(null=True, default=None) - STEP_SPECIFIC_INFO_KEYS = ["schedule_name", "custom_button_name", "usergroup_handle"] + STEP_SPECIFIC_INFO_KEYS = ["schedule_name", "custom_button_name", "usergroup_handle", "source_integration_name"] def render_log_line_json(self): time = humanize.naturaldelta(self.alert_group.started_at - self.created_at) @@ -271,6 +271,8 @@ class AlertGroupLogRecord(models.Model): if self.action_source == ActionSource.API: author_name = "API" + elif self.action_source == ActionSource.BACKSYNC: + author_name = "source integration " + step_specific_info.get("source_integration_name", "") elif self.author: if substitute_author_with_tag: author_name = "{{author}}" @@ -390,7 +392,7 @@ class AlertGroupLogRecord(models.Model): else: result += f"silenced by {author_name} for {humanize.naturaldelta(self.silence_delay)}" elif self.type == AlertGroupLogRecord.TYPE_UN_SILENCE: - if self.author is not None: + if author_name is not None: result += f"unsilenced by {author_name}" else: result += "alert group unsilenced" diff --git a/engine/apps/alerts/tests/test_acknowledge_reminder.py b/engine/apps/alerts/tests/test_acknowledge_reminder.py index 46ba9412..3b53350b 100644 --- a/engine/apps/alerts/tests/test_acknowledge_reminder.py +++ b/engine/apps/alerts/tests/test_acknowledge_reminder.py @@ -71,7 +71,7 @@ def test_acknowledge_by_user_invokes_start_ack_reminder(ack_reminder_test_setup) organization, alert_group, user = ack_reminder_test_setup(acknowledged=False) with patch.object(alert_group, "start_ack_reminder_if_needed") as mock_start_ack_reminder: - alert_group.acknowledge_by_user(user, ActionSource.SLACK) + alert_group.acknowledge_by_user_or_backsync(user, ActionSource.SLACK) mock_start_ack_reminder.assert_called_once_with() diff --git a/engine/apps/alerts/tests/test_alert_group.py b/engine/apps/alerts/tests/test_alert_group.py index 8101b31c..566ecc48 100644 --- a/engine/apps/alerts/tests/test_alert_group.py +++ b/engine/apps/alerts/tests/test_alert_group.py @@ -2,7 +2,7 @@ from unittest.mock import call, patch import pytest -from apps.alerts.constants import ActionSource +from apps.alerts.constants import ActionSource, AlertGroupState from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertGroupPhoneCallRenderer from apps.alerts.models import Alert, AlertGroup, AlertGroupLogRecord from apps.alerts.tasks import wipe @@ -327,7 +327,7 @@ def test_silence_by_user_for_period( author=user, ).exists() - alert_group.silence_by_user(user, silence_delay=silence_delay) + alert_group.silence_by_user_or_backsync(user, silence_delay=silence_delay) assert alert_group.log_records.filter( type=AlertGroupLogRecord.TYPE_SILENCE, @@ -364,7 +364,7 @@ def test_silence_by_user_forever( author=user, ).exists() - alert_group.silence_by_user(user, silence_delay=None) + alert_group.silence_by_user_or_backsync(user, silence_delay=None) assert alert_group.log_records.filter( type=AlertGroupLogRecord.TYPE_SILENCE, @@ -476,32 +476,32 @@ def test_alert_group_log_record_action_source( root_alert_group = make_alert_group(alert_receive_channel) # Silence alert group - alert_group.silence_by_user(user, 42, action_source=action_source) + alert_group.silence_by_user_or_backsync(user, 42, action_source=action_source) log_record = alert_group.log_records.last() assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_SILENCE, action_source) # Unsilence alert group - alert_group.un_silence_by_user(user, action_source=action_source) + alert_group.un_silence_by_user_or_backsync(user, action_source=action_source) log_record = alert_group.log_records.last() assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_SILENCE, action_source) # Acknowledge alert group - alert_group.acknowledge_by_user(user, action_source=action_source) + alert_group.acknowledge_by_user_or_backsync(user, action_source=action_source) log_record = alert_group.log_records.last() assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_ACK, action_source) # Unacknowledge alert group - alert_group.un_acknowledge_by_user(user, action_source=action_source) + alert_group.un_acknowledge_by_user_or_backsync(user, action_source=action_source) log_record = alert_group.log_records.last() assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_ACK, action_source) # Resolve alert group - alert_group.resolve_by_user(user, action_source=action_source) + alert_group.resolve_by_user_or_backsync(user, action_source=action_source) log_record = alert_group.log_records.last() assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_RESOLVED, action_source) # Unresolve alert group - alert_group.un_resolve_by_user(user, action_source=action_source) + alert_group.un_resolve_by_user_or_backsync(user, action_source=action_source) log_record = alert_group.log_records.last() assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_RESOLVED, action_source) @@ -594,16 +594,16 @@ def test_filter_active_alert_groups( # alert groups with active escalation alert_group_active = make_alert_group(alert_receive_channel) alert_group_active_silenced = make_alert_group(alert_receive_channel) - alert_group_active_silenced.silence_by_user(user, silence_delay=1800) # silence by period + alert_group_active_silenced.silence_by_user_or_backsync(user, silence_delay=1800) # silence by period # alert groups with inactive escalation alert_group_1 = make_alert_group(alert_receive_channel) - alert_group_1.acknowledge_by_user(user) + alert_group_1.acknowledge_by_user_or_backsync(user) alert_group_2 = make_alert_group(alert_receive_channel) - alert_group_2.resolve_by_user(user) + alert_group_2.resolve_by_user_or_backsync(user) alert_group_3 = make_alert_group(alert_receive_channel) alert_group_3.attach_by_user(user, alert_group_active) alert_group_4 = make_alert_group(alert_receive_channel) - alert_group_4.silence_by_user(user, silence_delay=None) # silence forever + alert_group_4.silence_by_user_or_backsync(user, silence_delay=None) # silence forever active_alert_groups = AlertGroup.objects.filter_active() assert active_alert_groups.count() == 2 @@ -688,3 +688,44 @@ def test_integration_config_on_alert_group_created(make_organization, make_alert assert alert.group.alerts.count() == 2 mock_on_alert_group_created.assert_called_once_with(alert.group) + + +@patch.object(AlertGroup, "start_escalation_if_needed") +@pytest.mark.django_db +@pytest.mark.parametrize( + "new_state,log_type,to_firing_log_type", + [ + (AlertGroupState.ACKNOWLEDGED, AlertGroupLogRecord.TYPE_ACK, AlertGroupLogRecord.TYPE_UN_ACK), + (AlertGroupState.RESOLVED, AlertGroupLogRecord.TYPE_RESOLVED, AlertGroupLogRecord.TYPE_UN_RESOLVED), + (AlertGroupState.SILENCED, AlertGroupLogRecord.TYPE_SILENCE, AlertGroupLogRecord.TYPE_UN_SILENCE), + ], +) +def test_update_state_by_backsync( + mock_start_escalation_if_needed, + new_state, + log_type, + to_firing_log_type, + make_organization, + make_alert_receive_channel, + make_alert_group, +): + organization = make_organization() + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + expected_log_data = (ActionSource.BACKSYNC, None, {"source_integration_name": alert_receive_channel.verbal_name}) + assert alert_group.state == AlertGroupState.FIRING + # set to new_state + alert_group.update_state_by_backsync(new_state) + alert_group.refresh_from_db() + assert alert_group.state == new_state + last_log = alert_group.log_records.last() + assert (last_log.action_source, last_log.author, last_log.step_specific_info) == expected_log_data + assert last_log.type == log_type + # set back to firing + alert_group.update_state_by_backsync(AlertGroupState.FIRING) + alert_group.refresh_from_db() + assert alert_group.state == AlertGroupState.FIRING + last_log = alert_group.log_records.last() + assert (last_log.action_source, last_log.author, last_log.step_specific_info) == expected_log_data + assert last_log.type == to_firing_log_type + mock_start_escalation_if_needed.assert_called_once() diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 1d4c2239..a25f897b 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -2149,8 +2149,8 @@ def test_timeline_api_action( alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter) make_alert(alert_group=alert_group, raw_request_data=alert_raw_request_data) - alert_group.acknowledge_by_user(user, action_source=ActionSource.WEB) - alert_group.resolve_by_user(user, action_source=ActionSource.API) + alert_group.acknowledge_by_user_or_backsync(user, action_source=ActionSource.WEB) + alert_group.resolve_by_user_or_backsync(user, action_source=ActionSource.API) client = APIClient() url = reverse("api-internal:alertgroup-detail", kwargs={"pk": alert_group.public_primary_key}) diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 0a71884f..cfcc8f61 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -469,7 +469,7 @@ class AlertGroupView( raise BadRequest(detail="Can't acknowledge maintenance alert group") if alert_group.root_alert_group is not None: raise BadRequest(detail="Can't acknowledge an attached alert group") - alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.WEB) + alert_group.acknowledge_by_user_or_backsync(self.request.user, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data) @@ -492,7 +492,7 @@ class AlertGroupView( if alert_group.resolved: raise BadRequest(detail="Can't unacknowledge a resolved alert group") - alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.WEB) + alert_group.un_acknowledge_by_user_or_backsync(self.request.user, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data) @@ -544,7 +544,7 @@ class AlertGroupView( }, status=status.HTTP_400_BAD_REQUEST, ) - alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB) + alert_group.resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data) @extend_schema(responses=AlertGroupSerializer) @@ -563,7 +563,7 @@ class AlertGroupView( if not alert_group.resolved: raise BadRequest(detail="The alert group is not resolved") - alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.WEB) + alert_group.un_resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data) @extend_schema( @@ -628,7 +628,7 @@ class AlertGroupView( if alert_group.root_alert_group is not None: raise BadRequest(detail="Can't silence an attached alert group") - alert_group.silence_by_user(request.user, silence_delay=delay, action_source=ActionSource.WEB) + alert_group.silence_by_user_or_backsync(request.user, silence_delay=delay, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": request}).data) @extend_schema( @@ -672,7 +672,7 @@ class AlertGroupView( if alert_group.root_alert_group is not None: raise BadRequest(detail="Can't unsilence an attached alert group") - alert_group.un_silence_by_user(request.user, action_source=ActionSource.WEB) + alert_group.un_silence_by_user_or_backsync(request.user, action_source=ActionSource.WEB) return Response(AlertGroupSerializer(alert_group, context={"request": request}).data) diff --git a/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py b/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py index fa05949b..e8699efd 100644 --- a/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py +++ b/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py @@ -122,22 +122,22 @@ def test_update_metric_alert_groups_total_cache_on_action( mock_cache_set_called_args = mock_cache_set.call_args_list arg_idx = get_called_arg_index_and_compare_results(expected_result_firing) - alert_group.acknowledge_by_user(user) + alert_group.acknowledge_by_user_or_backsync(user) arg_idx = get_called_arg_index_and_compare_results(expected_result_acked) - alert_group.un_acknowledge_by_user(user) + alert_group.un_acknowledge_by_user_or_backsync(user) arg_idx = get_called_arg_index_and_compare_results(expected_result_firing) - alert_group.resolve_by_user(user) + alert_group.resolve_by_user_or_backsync(user) arg_idx = get_called_arg_index_and_compare_results(expected_result_resolved) - alert_group.un_resolve_by_user(user) + alert_group.un_resolve_by_user_or_backsync(user) arg_idx = get_called_arg_index_and_compare_results(expected_result_firing) - alert_group.silence_by_user(user, silence_delay=None) + alert_group.silence_by_user_or_backsync(user, silence_delay=None) arg_idx = get_called_arg_index_and_compare_results(expected_result_silenced) - alert_group.un_silence_by_user(user) + alert_group.un_silence_by_user_or_backsync(user) get_called_arg_index_and_compare_results(expected_result_firing) @@ -212,30 +212,30 @@ def test_update_metric_alert_groups_response_time_cache_on_action( # alert_groups_response_time cache shouldn't be updated on create alert group assert_cache_was_not_changed_by_response_time_metric() - alert_group_1.acknowledge_by_user(user) + alert_group_1.acknowledge_by_user_or_backsync(user) arg_idx = get_called_arg_index_and_compare_results() # assert that only the first action counts - alert_group_1.un_acknowledge_by_user(user) + alert_group_1.un_acknowledge_by_user_or_backsync(user) assert_cache_was_not_changed_by_response_time_metric() - alert_group_1.resolve_by_user(user) + alert_group_1.resolve_by_user_or_backsync(user) assert_cache_was_not_changed_by_response_time_metric() - alert_group_1.un_resolve_by_user(user) + alert_group_1.un_resolve_by_user_or_backsync(user) assert_cache_was_not_changed_by_response_time_metric() - alert_group_1.silence_by_user(user, silence_delay=None) + alert_group_1.silence_by_user_or_backsync(user, silence_delay=None) assert_cache_was_not_changed_by_response_time_metric() - alert_group_1.un_silence_by_user(user) + alert_group_1.un_silence_by_user_or_backsync(user) assert_cache_was_not_changed_by_response_time_metric() # check that response_time cache updates on other actions with other alert groups - alert_group_2.resolve_by_user(user) + alert_group_2.resolve_by_user_or_backsync(user) arg_idx = get_called_arg_index_and_compare_results() - alert_group_3.silence_by_user(user, silence_delay=None) + alert_group_3.silence_by_user_or_backsync(user, silence_delay=None) get_called_arg_index_and_compare_results() diff --git a/engine/apps/public_api/views/incidents.py b/engine/apps/public_api/views/incidents.py index a015604f..76f25dc8 100644 --- a/engine/apps/public_api/views/incidents.py +++ b/engine/apps/public_api/views/incidents.py @@ -135,7 +135,7 @@ class IncidentView( if alert_group.is_maintenance_incident: raise BadRequest(detail="Can't acknowledge a maintenance alert group") - alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.API) + alert_group.acknowledge_by_user_or_backsync(self.request.user, action_source=ActionSource.API) return Response(status=status.HTTP_200_OK) @action(methods=["post"], detail=True) @@ -154,7 +154,7 @@ class IncidentView( if alert_group.is_maintenance_incident: raise BadRequest(detail="Can't unacknowledge a maintenance alert group") - alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.API) + alert_group.un_acknowledge_by_user_or_backsync(self.request.user, action_source=ActionSource.API) return Response(status=status.HTTP_200_OK) @action(methods=["post"], detail=True) @@ -170,7 +170,7 @@ class IncidentView( if alert_group.is_maintenance_incident: alert_group.stop_maintenance(self.request.user) else: - alert_group.resolve_by_user(self.request.user, action_source=ActionSource.API) + alert_group.resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.API) return Response(status=status.HTTP_200_OK) @@ -187,5 +187,5 @@ class IncidentView( if alert_group.is_maintenance_incident: raise BadRequest(detail="Can't unresolve a maintenance alert group") - alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.API) + alert_group.un_resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.API) return Response(status=status.HTTP_200_OK) diff --git a/engine/apps/slack/scenarios/distribute_alerts.py b/engine/apps/slack/scenarios/distribute_alerts.py index 873410f9..39917d22 100644 --- a/engine/apps/slack/scenarios/distribute_alerts.py +++ b/engine/apps/slack/scenarios/distribute_alerts.py @@ -291,7 +291,7 @@ class SilenceGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): # Deprecated handler kept for backward compatibility (so older Slack messages can still be processed) silence_delay = int(value) - alert_group.silence_by_user(self.user, silence_delay, action_source=ActionSource.SLACK) + alert_group.silence_by_user_or_backsync(self.user, silence_delay, action_source=ActionSource.SLACK) def process_signal(self, log_record: AlertGroupLogRecord) -> None: self.alert_group_slack_service.update_alert_group_slack_message(log_record.alert_group) @@ -311,7 +311,7 @@ class UnSilenceGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): self.open_unauthorized_warning(payload) return - alert_group.un_silence_by_user(self.user, action_source=ActionSource.SLACK) + alert_group.un_silence_by_user_or_backsync(self.user, action_source=ActionSource.SLACK) def process_signal(self, log_record: AlertGroupLogRecord) -> None: self.alert_group_slack_service.update_alert_group_slack_message(log_record.alert_group) @@ -659,7 +659,7 @@ class ResolveGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): ) return - alert_group.resolve_by_user(self.user, action_source=ActionSource.SLACK) + alert_group.resolve_by_user_or_backsync(self.user, action_source=ActionSource.SLACK) def process_signal(self, log_record: AlertGroupLogRecord) -> None: alert_group = log_record.alert_group @@ -683,7 +683,7 @@ class UnResolveGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): self.open_unauthorized_warning(payload) return - alert_group.un_resolve_by_user(self.user, action_source=ActionSource.SLACK) + alert_group.un_resolve_by_user_or_backsync(self.user, action_source=ActionSource.SLACK) def process_signal(self, log_record: AlertGroupLogRecord) -> None: self.alert_group_slack_service.update_alert_group_slack_message(log_record.alert_group) @@ -703,7 +703,7 @@ class AcknowledgeGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): self.open_unauthorized_warning(payload) return - alert_group.acknowledge_by_user(self.user, action_source=ActionSource.SLACK) + alert_group.acknowledge_by_user_or_backsync(self.user, action_source=ActionSource.SLACK) def process_signal(self, log_record: AlertGroupLogRecord) -> None: self.alert_group_slack_service.update_alert_group_slack_message(log_record.alert_group) @@ -723,7 +723,7 @@ class UnAcknowledgeGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep) self.open_unauthorized_warning(payload) return - alert_group.un_acknowledge_by_user(self.user, action_source=ActionSource.SLACK) + alert_group.un_acknowledge_by_user_or_backsync(self.user, action_source=ActionSource.SLACK) def process_signal(self, log_record: AlertGroupLogRecord) -> None: from apps.alerts.models import AlertGroupLogRecord diff --git a/engine/apps/telegram/tests/test_keyboard_renderer.py b/engine/apps/telegram/tests/test_keyboard_renderer.py index 449b844b..eaac18a8 100644 --- a/engine/apps/telegram/tests/test_keyboard_renderer.py +++ b/engine/apps/telegram/tests/test_keyboard_renderer.py @@ -92,7 +92,7 @@ def test_actions_keyboard_acknowledged( alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) - alert_group.acknowledge_by_user(user) + alert_group.acknowledge_by_user_or_backsync(user) renderer = TelegramKeyboardRenderer(alert_group=alert_group) keyboard = renderer.render_actions_keyboard() @@ -127,7 +127,7 @@ def test_actions_keyboard_resolved( alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) - alert_group.resolve_by_user(user) + alert_group.resolve_by_user_or_backsync(user) renderer = TelegramKeyboardRenderer(alert_group=alert_group) keyboard = renderer.render_actions_keyboard() @@ -155,7 +155,7 @@ def test_actions_keyboard_silenced( alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) - alert_group.silence_by_user(user, silence_delay=None) + alert_group.silence_by_user_or_backsync(user, silence_delay=None) renderer = TelegramKeyboardRenderer(alert_group=alert_group) keyboard = renderer.render_actions_keyboard() diff --git a/engine/apps/telegram/tests/test_message_renderer.py b/engine/apps/telegram/tests/test_message_renderer.py index b2ef56b0..5d021ce0 100644 --- a/engine/apps/telegram/tests/test_message_renderer.py +++ b/engine/apps/telegram/tests/test_message_renderer.py @@ -150,7 +150,7 @@ def test_personal_message( alert_group = make_alert_group(alert_receive_channel, channel_filter=default_channel_filter) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.tests["payload"]) - alert_group.acknowledge_by_user(user) + alert_group.acknowledge_by_user_or_backsync(user) renderer = TelegramMessageRenderer(alert_group=alert_group) text = renderer.render_personal_message() diff --git a/engine/apps/telegram/updates/update_handlers/button_press.py b/engine/apps/telegram/updates/update_handlers/button_press.py index 3df55e93..9eef131a 100644 --- a/engine/apps/telegram/updates/update_handlers/button_press.py +++ b/engine/apps/telegram/updates/update_handlers/button_press.py @@ -110,15 +110,15 @@ class ButtonPressHandler(UpdateHandler): @staticmethod def _map_action_context_to_fn(action_context: ActionContext) -> Tuple[Callable, dict]: action_to_fn = { - Action.RESOLVE: "resolve_by_user", - Action.UNRESOLVE: "un_resolve_by_user", - Action.ACKNOWLEDGE: "acknowledge_by_user", - Action.UNACKNOWLEDGE: "un_acknowledge_by_user", + Action.RESOLVE: "resolve_by_user_or_backsync", + Action.UNRESOLVE: "un_resolve_by_user_or_backsync", + Action.ACKNOWLEDGE: "acknowledge_by_user_or_backsync", + Action.UNACKNOWLEDGE: "un_acknowledge_by_user_or_backsync", Action.SILENCE: { - "fn_name": "silence_by_user", + "fn_name": "silence_by_user_or_backsync", "kwargs": {"silence_delay": int(action_context.action_data) if action_context.action_data else None}, }, - Action.UNSILENCE: "un_silence_by_user", + Action.UNSILENCE: "un_silence_by_user_or_backsync", } fn_info = action_to_fn[action_context.action] diff --git a/engine/apps/twilioapp/gather.py b/engine/apps/twilioapp/gather.py index ad06eba2..8b4f3036 100644 --- a/engine/apps/twilioapp/gather.py +++ b/engine/apps/twilioapp/gather.py @@ -74,11 +74,11 @@ def process_digit(call_sid, digit): f"twilio_phone_call_sid={call_sid} digit={digit} alert_group_id={alert_group.id} user_id={user.id}" ) if digit == "1": - alert_group.acknowledge_by_user(user, action_source=ActionSource.PHONE) + alert_group.acknowledge_by_user_or_backsync(user, action_source=ActionSource.PHONE) elif digit == "2": - alert_group.resolve_by_user(user, action_source=ActionSource.PHONE) + alert_group.resolve_by_user_or_backsync(user, action_source=ActionSource.PHONE) elif digit == "3": - alert_group.silence_by_user(user, silence_delay=1800, action_source=ActionSource.PHONE) + alert_group.silence_by_user_or_backsync(user, silence_delay=1800, action_source=ActionSource.PHONE) def get_gather_url(): diff --git a/engine/apps/zvonok/status_callback.py b/engine/apps/zvonok/status_callback.py index 253125d8..3a265cb5 100644 --- a/engine/apps/zvonok/status_callback.py +++ b/engine/apps/zvonok/status_callback.py @@ -81,4 +81,4 @@ def update_zvonok_call_status(call_id: str, call_status: str, user_choice: Optio f"alert_group_id={alert_group.id} user_id={user.id}" ) - alert_group.acknowledge_by_user(user, action_source=ActionSource.PHONE) + alert_group.acknowledge_by_user_or_backsync(user, action_source=ActionSource.PHONE)