oncall-engine/engine/apps/slack/scenarios/distribute_alerts.py
Joey Orlando fda05a6cc4
chore: remove deprecated slack_channel and heartbeat integration types (#5270)
# What this PR does

See [Slack
discussion](https://raintank-corp.slack.com/archives/C06K1MQ07GS/p1732110700877869)
for more context

## 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.
2024-11-20 11:17:04 -05:00

998 lines
42 KiB
Python

import json
import logging
import typing
from datetime import datetime
from django.core.cache import cache
from apps.alerts.constants import ActionSource
from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertSlackRenderer
from apps.alerts.models import Alert, AlertGroup, AlertGroupLogRecord, AlertReceiveChannel, Invitation
from apps.api.permissions import RBACPermission
from apps.slack.chatops_proxy_routing import make_private_metadata, make_value
from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME
from apps.slack.errors import (
SlackAPIChannelArchivedError,
SlackAPIChannelNotFoundError,
SlackAPIError,
SlackAPIInvalidAuthError,
SlackAPIMessageNotFoundError,
SlackAPIRatelimitError,
SlackAPIRestrictedActionError,
SlackAPITokenError,
)
from apps.slack.scenarios import scenario_step
from apps.slack.slack_formatter import SlackFormatter
from apps.slack.tasks import send_message_to_thread_if_bot_not_in_channel, update_incident_slack_message
from apps.slack.types import (
Block,
BlockActionType,
CompositionObjectOption,
EventPayload,
InteractiveMessageActionType,
ModalView,
PayloadType,
ScenarioRoute,
)
from apps.slack.utils import get_cache_key_update_incident_slack_message
from common.utils import clean_markup, is_string_with_visible_characters
from .step_mixins import AlertGroupActionsMixin
if typing.TYPE_CHECKING:
from apps.slack.models import SlackTeamIdentity, SlackUserIdentity
from apps.user_management.models import Organization
ATTACH_TO_ALERT_GROUPS_LIMIT = 20
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class AlertShootingStep(scenario_step.ScenarioStep):
def process_signal(self, alert: Alert) -> None:
# do not try to post alert group message to slack if its channel is rate limited
if alert.group.channel.is_rate_limited_in_slack:
logger.info("Skip posting or updating alert_group in Slack due to rate limit")
AlertGroup.objects.filter(
pk=alert.group.pk,
slack_message_sent=False,
).update(slack_message_sent=True, reason_to_skip_escalation=AlertGroup.RATE_LIMITED)
return
num_updated_rows = AlertGroup.objects.filter(pk=alert.group.pk, slack_message_sent=False).update(
slack_message_sent=True
)
if num_updated_rows == 1:
try:
channel_id = (
alert.group.channel_filter.slack_channel_id_or_org_default_id
if alert.group.channel_filter
# if channel filter is deleted mid escalation, use default Slack channel
else alert.group.channel.organization.default_slack_channel_slack_id
)
self._send_first_alert(alert, channel_id)
except (SlackAPIError, TimeoutError):
AlertGroup.objects.filter(pk=alert.group.pk).update(slack_message_sent=False)
raise
if alert.group.channel.maintenance_mode == AlertReceiveChannel.DEBUG_MAINTENANCE:
self._send_debug_mode_notice(alert.group, channel_id)
if alert.group.is_maintenance_incident:
# not sending log report message for maintenance incident
pass
else:
# check if alert group was posted to slack before posting message to thread
if not alert.group.skip_escalation_in_slack:
self._send_message_to_thread_if_bot_not_in_channel(alert.group, channel_id)
else:
# check if alert group was posted to slack before updating its message
if not alert.group.skip_escalation_in_slack:
update_task_id = update_incident_slack_message.apply_async(
(self.slack_team_identity.pk, alert.group.pk),
countdown=10,
)
cache.set(
get_cache_key_update_incident_slack_message(alert.group.pk),
update_task_id,
timeout=CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME,
)
else:
logger.info("Skip updating alert_group in Slack due to rate limit")
def _send_first_alert(self, alert: Alert, channel_id: str) -> None:
attachments = alert.group.render_slack_attachments()
blocks = alert.group.render_slack_blocks()
self._post_alert_group_to_slack(
slack_team_identity=self.slack_team_identity,
alert_group=alert.group,
alert=alert,
attachments=attachments,
channel_id=channel_id,
blocks=blocks,
)
def _post_alert_group_to_slack(
self,
slack_team_identity: "SlackTeamIdentity",
alert_group: AlertGroup,
alert: Alert,
attachments,
channel_id: str,
blocks: Block.AnyBlocks,
) -> None:
# channel_id can be None if general log channel for slack_team_identity is not set
if channel_id is None:
logger.info(f"Failed to post message to Slack for alert_group {alert_group.pk} because channel_id is None")
alert_group.reason_to_skip_escalation = AlertGroup.CHANNEL_NOT_SPECIFIED
alert_group.save(update_fields=["reason_to_skip_escalation"])
return
try:
result = self._slack_client.chat_postMessage(channel=channel_id, attachments=attachments, blocks=blocks)
alert_group.slack_messages.create(
slack_id=result["ts"],
organization=alert_group.channel.organization,
_slack_team_identity=slack_team_identity,
channel_id=channel_id,
)
alert.delivered = True
except SlackAPITokenError:
alert_group.reason_to_skip_escalation = AlertGroup.ACCOUNT_INACTIVE
alert_group.save(update_fields=["reason_to_skip_escalation"])
logger.info("Not delivering alert due to account_inactive.")
except SlackAPIInvalidAuthError:
alert_group.reason_to_skip_escalation = AlertGroup.INVALID_AUTH
alert_group.save(update_fields=["reason_to_skip_escalation"])
logger.info("Not delivering alert due to invalid_auth.")
except SlackAPIChannelArchivedError:
alert_group.reason_to_skip_escalation = AlertGroup.CHANNEL_ARCHIVED
alert_group.save(update_fields=["reason_to_skip_escalation"])
logger.info("Not delivering alert due to channel is archived.")
except SlackAPIRatelimitError as e:
# don't rate limit maintenance alert
if not alert_group.channel.is_maintenace_integration:
alert_group.reason_to_skip_escalation = AlertGroup.RATE_LIMITED
alert_group.save(update_fields=["reason_to_skip_escalation"])
alert_group.channel.start_send_rate_limit_message_task(e.retry_after)
logger.info("Not delivering alert due to slack rate limit.")
else:
raise e
except SlackAPIChannelNotFoundError:
alert_group.reason_to_skip_escalation = AlertGroup.CHANNEL_ARCHIVED
alert_group.save(update_fields=["reason_to_skip_escalation"])
logger.info("Not delivering alert due to channel is archived.")
except SlackAPIRestrictedActionError:
alert_group.reason_to_skip_escalation = AlertGroup.RESTRICTED_ACTION
alert_group.save(update_fields=["reason_to_skip_escalation"])
logger.info("Not delivering alert due to workspace restricted action.")
finally:
alert.save()
def _send_debug_mode_notice(self, alert_group: AlertGroup, channel_id: str) -> None:
blocks: Block.AnyBlocks = []
text = "Escalations are silenced due to Debug mode"
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": text}})
self._slack_client.chat_postMessage(
channel=channel_id,
text=text,
attachments=[],
thread_ts=alert_group.slack_message.slack_id,
mrkdwn=True,
blocks=blocks,
)
def _send_message_to_thread_if_bot_not_in_channel(self, alert_group: AlertGroup, channel_id: str) -> None:
send_message_to_thread_if_bot_not_in_channel.apply_async(
(alert_group.pk, self.slack_team_identity.pk, channel_id),
countdown=1, # delay for message so that the log report is published first
)
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
pass
class InviteOtherPersonToIncident(AlertGroupActionsMixin, scenario_step.ScenarioStep):
"""
THIS SCENARIO STEP IS DEPRECATED AND WILL BE REMOVED IN THE FUTURE.
Check out apps/slack/scenarios/manage_responders.py for the new version that uses direct paging.
"""
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
from apps.user_management.models import User
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
selected_user = None
try:
# user selection
selected_user_id = json.loads(payload["actions"][0]["selected_option"]["value"])["user_id"]
if selected_user_id is not None: # None if there are no users to select
selected_user = User.objects.get(pk=selected_user_id)
except (KeyError, json.JSONDecodeError):
# for old version with user slack_id selection
warning_text = "Oops! Something goes wrong, please try again"
self.open_warning_window(payload, warning_text)
if selected_user is not None:
Invitation.invite_user(selected_user, alert_group, self.user)
else:
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
self.alert_group_slack_service.update_alert_group_slack_message(log_record.alert_group)
class SilenceGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
value = payload["actions"][0]["selected_option"]["value"]
try:
silence_delay = json.loads(value)["delay"]
except TypeError:
# Deprecated handler kept for backward compatibility (so older Slack messages can still be processed)
silence_delay = int(value)
alert_group.silence_by_user_or_backsync(
self.user, silence_delay=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)
class UnSilenceGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
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)
class SelectAttachGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
blocks: Block.AnyBlocks = []
view: ModalView = {
"callback_id": AttachGroupStep.routing_uid(),
"blocks": blocks,
"type": "modal",
"title": {
"type": "plain_text",
"text": "Attach to Alert Group",
},
"private_metadata": make_private_metadata(
{
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
"alert_group_pk": alert_group.pk,
},
self.organization,
),
"close": {"type": "plain_text", "text": "Cancel", "emoji": True},
}
attached_incidents_exists = alert_group.dependent_alert_groups.exists()
if attached_incidents_exists:
attached_incidents = alert_group.dependent_alert_groups.all()
text = (
f"Oops! This Alert Group cannot be attached to another one because it already has "
f"attached Alert Group ({attached_incidents.count()}):\n"
)
for dependent_alert in attached_incidents:
if dependent_alert.slack_permalink:
dependent_alert_text = (
f"\n<{dependent_alert.slack_permalink}|{dependent_alert.long_verbose_name_without_formatting}>"
)
else:
dependent_alert_text = f"\n{dependent_alert.long_verbose_name}"
if len(dependent_alert_text + text) <= 2995: # max 3000 symbols
text += dependent_alert_text
else:
text += "\n..."
break
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text,
},
}
)
else:
blocks.extend(self.get_select_incidents_blocks(alert_group))
if blocks:
view["submit"] = {
"type": "plain_text",
"text": "Submit",
}
else:
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Oops! There are no Alert Groups available to attach.",
},
}
)
self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view)
def get_select_incidents_blocks(self, alert_group: AlertGroup) -> Block.AnyBlocks:
collected_options: typing.List[CompositionObjectOption] = []
blocks: Block.AnyBlocks = []
org = alert_group.channel.organization
alert_receive_channel_ids = AlertReceiveChannel.objects.filter(organization=org).values_list("id", flat=True)
alert_groups_queryset = (
AlertGroup.objects.prefetch_related(
"alerts",
"channel__organization",
)
.filter(channel_id__in=list(alert_receive_channel_ids), resolved=False, root_alert_group__isnull=True)
.exclude(pk=alert_group.pk)
.order_by("-pk")
)
sf = SlackFormatter(org)
for alert_group_to_attach in alert_groups_queryset[:ATTACH_TO_ALERT_GROUPS_LIMIT]:
# long_verbose_name_without_formatting was removed from here because it increases queries count due to
# alerts.first().
# alert_group_to_attach.alerts.exists() and alerts.all()[0] don't make additional queries to db due to
# prefetch_related.
first_alert = alert_group_to_attach.alerts.all()[0]
templated_alert = AlertSlackRenderer(first_alert).templated_alert
if is_string_with_visible_characters(templated_alert.title):
alert_name = templated_alert.title
alert_name = sf.format(alert_name)
alert_name = clean_markup(alert_name)
else:
alert_name = (
f"#{alert_group_to_attach.inside_organization_number} "
f"{DEFAULT_BACKUP_TITLE} via {alert_group_to_attach.channel.verbal_name}"
)
if len(alert_name) > 75:
alert_name = f"{alert_name[:72]}..."
collected_options.append(
{
"text": {"type": "plain_text", "text": f"{alert_name}", "emoji": True},
"value": make_value({root_ag_id_value_key: alert_group_to_attach.pk}, org),
}
)
if len(collected_options) > 0:
blocks.append(
{
"type": "input",
"block_id": self.routing_uid(),
"element": {
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": "Attach to...",
},
"action_id": AttachGroupStep.routing_uid(),
"options": collected_options[:ATTACH_TO_ALERT_GROUPS_LIMIT],
},
"label": {
"type": "plain_text",
"text": "Select Alert Group:",
"emoji": True,
},
}
)
return blocks
class AttachGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [] # Permissions are handled in SelectAttachGroupStep
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
alert_group = log_record.alert_group
if log_record.type == AlertGroupLogRecord.TYPE_ATTACHED and log_record.alert_group.is_maintenance_incident:
text = f"{log_record.rendered_log_line_action(for_slack=True)}"
self.alert_group_slack_service.publish_message_to_alert_group_thread(alert_group, text=text)
if log_record.type == AlertGroupLogRecord.TYPE_FAILED_ATTACHMENT:
ephemeral_text = log_record.rendered_log_line_action(for_slack=True)
slack_user_identity = log_record.author.slack_user_identity
if slack_user_identity:
self._slack_client.chat_postEphemeral(
user=slack_user_identity.slack_id,
channel=alert_group.slack_message.channel_id,
text="{}{}".format(ephemeral_text[:1].upper(), ephemeral_text[1:]),
unfurl_links=True,
)
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
# submit selection in modal window
if payload["type"] == PayloadType.VIEW_SUBMISSION:
alert_group_pk = json.loads(payload["view"]["private_metadata"])["alert_group_pk"]
alert_group = AlertGroup.objects.get(pk=alert_group_pk)
root_alert_group_pk = _get_root_alert_group_id_from_value(
payload["view"]["state"]["values"][SelectAttachGroupStep.routing_uid()][AttachGroupStep.routing_uid()][
"selected_option"
]["value"]
)
root_alert_group = AlertGroup.objects.get(pk=root_alert_group_pk)
# old version of attach selection by dropdown
else:
try:
root_alert_group_pk = int(
_get_root_alert_group_id_from_value(payload["actions"][0]["selected_options"][0]["value"])
)
except KeyError:
root_alert_group_pk = int(
_get_root_alert_group_id_from_value(payload["actions"][0]["selected_option"]["value"])
)
root_alert_group = AlertGroup.objects.get(pk=root_alert_group_pk)
alert_group = self.get_alert_group(slack_team_identity, payload)
alert_group.attach_by_user(self.user, root_alert_group, action_source=ActionSource.SLACK)
root_ag_id_value_key = "ag_id"
def _get_root_alert_group_id_from_value(value: str) -> str:
"""
Extract ag ID from value string.
It might be either JSON-encoded object or just a user ID.
Json encoded object introduced for Chatops-Proxy routing, plain string with user ID is legacy.
"""
try:
data = json.loads(value)
return data[root_ag_id_value_key]
except json.JSONDecodeError:
return value
class UnAttachGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
alert_group.un_attach_by_user(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)
class StopInvitationProcess(AlertGroupActionsMixin, scenario_step.ScenarioStep):
"""
THIS SCENARIO STEP IS DEPRECATED AND WILL BE REMOVED IN THE FUTURE.
Check out apps/slack/scenarios/manage_responders.py for the new version that uses direct paging.
"""
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
try:
value = json.loads(payload["actions"][0]["value"])
invitation_id = value["invitation_id"]
except KeyError:
# Deprecated handler kept for backward compatibility (so older Slack messages can still be processed)
invitation_id = payload["actions"][0]["name"].split("_")[1]
Invitation.stop_invitation(invitation_id, self.user)
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
self.alert_group_slack_service.update_alert_group_slack_message(log_record.invitation.alert_group)
class ResolveGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
ResolutionNoteModalStep = scenario_step.ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if alert_group.is_maintenance_incident:
alert_group.stop_maintenance(self.user)
else:
# TODO: refactor that check, it should be in alert core, not in slack.
if self.organization.is_resolution_note_required and not alert_group.has_resolution_notes:
resolution_note_data = {
"resolution_note_window_action": "edit",
"alert_group_pk": alert_group.pk,
"action_resolve": True,
}
ResolutionNoteModalStep(slack_team_identity, self.organization, self.user).process_scenario(
slack_user_identity, slack_team_identity, payload, data=resolution_note_data
)
return
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
# Do not rerender alert_groups which happened while maintenance.
# They have no slack messages, since they just attached to the maintenance incident.
if not alert_group.happened_while_maintenance:
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
class UnResolveGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
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)
class AcknowledgeGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
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)
class UnAcknowledgeGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
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
alert_group = log_record.alert_group
logger.debug(f"Started process_signal in UnAcknowledgeGroupStep for alert_group {alert_group.pk}")
if log_record.type == AlertGroupLogRecord.TYPE_AUTO_UN_ACK:
channel_id = alert_group.slack_message.channel_id
if log_record.author is not None:
user_verbal = log_record.author.get_username_with_slack_verbal(mention=True)
else:
user_verbal = "No one"
message_attachments = [
{
"callback_id": "alert",
"text": "",
"footer": "Escalation started again...",
},
]
text = (
f"{user_verbal} hasn't responded to an acknowledge timeout reminder."
f" Alert Group is unacknowledged automatically."
)
if alert_group.slack_message.ack_reminder_message_ts:
try:
self._slack_client.chat_update(
channel=channel_id,
ts=alert_group.slack_message.ack_reminder_message_ts,
text=text,
attachments=message_attachments,
)
except SlackAPIMessageNotFoundError:
# post to thread if ack reminder message was deleted in Slack
self.alert_group_slack_service.publish_message_to_alert_group_thread(
alert_group, attachments=message_attachments, text=text
)
except SlackAPITokenError:
pass
else:
self.alert_group_slack_service.publish_message_to_alert_group_thread(
alert_group, attachments=message_attachments, text=text
)
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
logger.debug(f"Finished process_signal in UnAcknowledgeGroupStep for alert_group {alert_group.pk}")
class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
slack_team_identity: "SlackTeamIdentity",
payload: "EventPayload",
predefined_org: typing.Optional["Organization"] = None,
) -> None:
from apps.alerts.models import AlertGroup
value = payload["actions"][0]["value"]
try:
alert_group_pk = json.loads(value)["alert_group_pk"]
except json.JSONDecodeError:
# Deprecated and kept for backward compatibility (so older Slack messages can still be processed)
alert_group_pk = value.split("_")[1]
alert_group = AlertGroup.objects.get(pk=alert_group_pk)
channel = payload["channel"]["id"]
message_ts = payload["message_ts"]
if alert_group.acknowledged:
if alert_group.acknowledged_by == AlertGroup.USER:
if self.user == alert_group.acknowledged_by_user:
user_verbal = alert_group.acknowledged_by_user.get_username_with_slack_verbal()
text = f"{user_verbal} confirmed that the Alert Group is still acknowledged."
self._slack_client.chat_update(channel=channel, ts=message_ts, text=text)
alert_group.acknowledged_by_confirmed = datetime.utcnow()
alert_group.save(update_fields=["acknowledged_by_confirmed"])
else:
self._slack_client.chat_postEphemeral(
channel=channel,
user=slack_user_identity.slack_id,
text="This Alert Group is acknowledged by another user. Acknowledge it yourself first.",
)
elif alert_group.acknowledged_by == AlertGroup.SOURCE:
user_verbal = self.user.get_username_with_slack_verbal()
text = f"{user_verbal} confirmed that the Alert Group is still acknowledged."
self._slack_client.chat_update(channel=channel, ts=message_ts, text=text)
alert_group.acknowledged_by_confirmed = datetime.utcnow()
alert_group.save(update_fields=["acknowledged_by_confirmed"])
else:
self._slack_client.chat_delete(channel=channel, ts=message_ts)
self._slack_client.chat_postEphemeral(
channel=channel,
user=slack_user_identity.slack_id,
text="This Alert Group is already unacknowledged.",
)
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
from apps.user_management.models import Organization
alert_group = log_record.alert_group
channel_id = alert_group.slack_message.channel_id
user_verbal = log_record.author.get_username_with_slack_verbal(mention=True)
text = f"{user_verbal}, please confirm that you're still working on this Alert Group."
if alert_group.channel.organization.unacknowledge_timeout != Organization.UNACKNOWLEDGE_TIMEOUT_NEVER:
attachments = [
{
"fallback": "Are you still working on this Alert Group?",
"text": text,
"callback_id": "alert",
"attachment_type": "default",
"footer": "This is a reminder that the Alert Group is still acknowledged"
" and not resolved. It will be unacknowledged automatically and escalation will"
" start again soon.",
"actions": [
{
"name": scenario_step.ScenarioStep.get_step(
"distribute_alerts", "AcknowledgeConfirmationStep"
).routing_uid(),
"text": "Confirm",
"type": "button",
"style": "primary",
"value": make_value({"alert_group_pk": alert_group.pk}, alert_group.channel.organization),
},
],
}
]
try:
response = self._slack_client.chat_postMessage(
channel=channel_id,
text=text,
attachments=attachments,
thread_ts=alert_group.slack_message.slack_id,
)
except (SlackAPITokenError, SlackAPIChannelArchivedError, SlackAPIChannelNotFoundError):
pass
else:
alert_group.slack_messages.create(
slack_id=response["ts"],
organization=alert_group.channel.organization,
_slack_team_identity=self.slack_team_identity,
channel_id=channel_id,
)
alert_group.slack_message.ack_reminder_message_ts = response["ts"]
alert_group.slack_message.save(update_fields=["ack_reminder_message_ts"])
else:
text = f"This is a reminder that the Alert Group is still acknowledged by {user_verbal}"
self.alert_group_slack_service.publish_message_to_alert_group_thread(alert_group, text=text)
class WipeGroupStep(scenario_step.ScenarioStep):
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
alert_group = log_record.alert_group
user_verbal = log_record.author.get_username_with_slack_verbal()
text = f"Wiped by {user_verbal}"
self.alert_group_slack_service.publish_message_to_alert_group_thread(alert_group, text=text)
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
class DeleteGroupStep(scenario_step.ScenarioStep):
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
alert_group = log_record.alert_group
# Remove "memo" emoji from resolution note messages
for message in alert_group.resolution_note_slack_messages.filter(added_to_resolution_note=True):
try:
self._slack_client.reactions_remove(channel=message.slack_channel_id, name="memo", timestamp=message.ts)
except SlackAPIRatelimitError:
# retries on ratelimit are handled in apps.alerts.tasks.delete_alert_group.delete_alert_group
raise
except SlackAPIError:
pass
message.delete()
# Remove resolution note messages posted by OnCall bot
for message in alert_group.resolution_note_slack_messages.filter(posted_by_bot=True):
try:
self._slack_client.chat_delete(channel=message.slack_channel_id, ts=message.ts)
except SlackAPIRatelimitError:
# retries on ratelimit are handled in apps.alerts.tasks.delete_alert_group.delete_alert_group
raise
except SlackAPIError:
pass
message.delete()
# Remove alert group Slack messages
for message in alert_group.slack_messages.all():
try:
self._slack_client.chat_delete(channel=message.channel_id, ts=message.slack_id)
except SlackAPIRatelimitError:
# retries on ratelimit are handled in apps.alerts.tasks.delete_alert_group.delete_alert_group
raise
except SlackAPIError:
pass
message.delete()
STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": ResolveGroupStep.routing_uid(),
"step": ResolveGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.BUTTON,
"block_action_id": ResolveGroupStep.routing_uid(),
"step": ResolveGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.BUTTON,
"block_action_id": UnResolveGroupStep.routing_uid(),
"step": UnResolveGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": AcknowledgeGroupStep.routing_uid(),
"step": AcknowledgeGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.BUTTON,
"block_action_id": AcknowledgeGroupStep.routing_uid(),
"step": AcknowledgeGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": AcknowledgeConfirmationStep.routing_uid(),
"step": AcknowledgeConfirmationStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": UnAcknowledgeGroupStep.routing_uid(),
"step": UnAcknowledgeGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.BUTTON,
"block_action_id": UnAcknowledgeGroupStep.routing_uid(),
"step": UnAcknowledgeGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.SELECT,
"action_name": SilenceGroupStep.routing_uid(),
"step": SilenceGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.STATIC_SELECT,
"block_action_id": SilenceGroupStep.routing_uid(),
"step": SilenceGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": UnSilenceGroupStep.routing_uid(),
"step": UnSilenceGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.BUTTON,
"block_action_id": UnSilenceGroupStep.routing_uid(),
"step": UnSilenceGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.BUTTON,
"block_action_id": SelectAttachGroupStep.routing_uid(),
"step": SelectAttachGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.SELECT,
"action_name": AttachGroupStep.routing_uid(),
"step": AttachGroupStep,
},
{
"payload_type": PayloadType.VIEW_SUBMISSION,
"view_callback_id": AttachGroupStep.routing_uid(),
"step": AttachGroupStep,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.STATIC_SELECT,
"block_action_id": AttachGroupStep.routing_uid(),
"step": AttachGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": UnAttachGroupStep.routing_uid(),
"step": UnAttachGroupStep,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.SELECT,
"action_name": InviteOtherPersonToIncident.routing_uid(),
"step": InviteOtherPersonToIncident,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.USERS_SELECT,
"block_action_id": InviteOtherPersonToIncident.routing_uid(),
"step": InviteOtherPersonToIncident,
},
{
"payload_type": PayloadType.BLOCK_ACTIONS,
"block_action_type": BlockActionType.STATIC_SELECT,
"block_action_id": InviteOtherPersonToIncident.routing_uid(),
"step": InviteOtherPersonToIncident,
},
{
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
"action_type": InteractiveMessageActionType.BUTTON,
"action_name": StopInvitationProcess.routing_uid(),
"step": StopInvitationProcess,
},
]