diff --git a/engine/apps/alerts/incident_appearance/renderers/slack_renderer.py b/engine/apps/alerts/incident_appearance/renderers/slack_renderer.py index 7f2eded8..c5b5570c 100644 --- a/engine/apps/alerts/incident_appearance/renderers/slack_renderer.py +++ b/engine/apps/alerts/incident_appearance/renderers/slack_renderer.py @@ -250,6 +250,10 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer): "action_id": ScenarioStep.get_step("declare_incident", "DeclareIncidentStep").routing_uid(), } + show_timeline_button = _make_button( + ":blue_book: Show Timeline", "OpenAlertGroupTimelineDialogStep", "alertgroup_timeline" + ) + buttons = [] if not alert_group.is_maintenance_incident: if not alert_group.resolved: @@ -282,6 +286,8 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer): if not alert_group.resolved: buttons.append(resolve_button) + buttons.append(show_timeline_button) + return [{"type": "actions", "elements": buttons}] def _get_invitation_attachment(self): diff --git a/engine/apps/slack/scenarios/alertgroup_timeline.py b/engine/apps/slack/scenarios/alertgroup_timeline.py new file mode 100644 index 00000000..87976b49 --- /dev/null +++ b/engine/apps/slack/scenarios/alertgroup_timeline.py @@ -0,0 +1,78 @@ +import typing + +from apps.api.permissions import RBACPermission +from apps.slack.chatops_proxy_routing import make_private_metadata +from apps.slack.scenarios import scenario_step +from apps.slack.scenarios.slack_renderer import AlertGroupLogSlackRenderer +from apps.slack.types import ( + Block, + BlockActionType, + EventPayload, + InteractiveMessageActionType, + ModalView, + PayloadType, + ScenarioRoute, +) + +from .step_mixins import AlertGroupActionsMixin + +if typing.TYPE_CHECKING: + from apps.slack.models import SlackTeamIdentity, SlackUserIdentity + + +class OpenAlertGroupTimelineDialogStep(AlertGroupActionsMixin, scenario_step.ScenarioStep): + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] + + def process_scenario( + self, + slack_user_identity: "SlackUserIdentity", + slack_team_identity: "SlackTeamIdentity", + payload: EventPayload, + ) -> None: + alert_group = self.get_alert_group(slack_team_identity, payload) + if not self.is_authorized(alert_group): + self.open_unauthorized_warning(payload) + return + + private_metadata = { + "organization_id": self.organization.pk, + "alert_group_pk": alert_group.pk, + "message_ts": payload.get("message_ts") or payload["container"]["message_ts"], + } + + alert_receive_channel = alert_group.channel + past_log_report = AlertGroupLogSlackRenderer.render_alert_group_past_log_report_text(alert_group) + future_log_report = AlertGroupLogSlackRenderer.render_alert_group_future_log_report_text(alert_group) + blocks: typing.List[Block.Section] = [] + if past_log_report: + blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": past_log_report}}) + if future_log_report: + blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": future_log_report}}) + + view: ModalView = { + "blocks": blocks, + "type": "modal", + "title": { + "type": "plain_text", + "text": "Alert group log", + }, + "private_metadata": make_private_metadata(private_metadata, alert_receive_channel.organization), + } + + self._slack_client.views_open(trigger_id=payload["trigger_id"], view=view) + + +STEPS_ROUTING: ScenarioRoute.RoutingSteps = [ + { + "payload_type": PayloadType.INTERACTIVE_MESSAGE, + "action_type": InteractiveMessageActionType.BUTTON, + "action_name": OpenAlertGroupTimelineDialogStep.routing_uid(), + "step": OpenAlertGroupTimelineDialogStep, + }, + { + "payload_type": PayloadType.BLOCK_ACTIONS, + "block_action_type": BlockActionType.BUTTON, + "block_action_id": OpenAlertGroupTimelineDialogStep.routing_uid(), + "step": OpenAlertGroupTimelineDialogStep, + }, +] diff --git a/engine/apps/slack/scenarios/slack_renderer.py b/engine/apps/slack/scenarios/slack_renderer.py index 982671a7..ee5de69c 100644 --- a/engine/apps/slack/scenarios/slack_renderer.py +++ b/engine/apps/slack/scenarios/slack_renderer.py @@ -10,16 +10,13 @@ if typing.TYPE_CHECKING: class AlertGroupLogSlackRenderer: @staticmethod - def render_incident_log_report_for_slack(alert_group: "AlertGroup"): + def render_alert_group_past_log_report_text(alert_group: "AlertGroup"): from apps.alerts.models import AlertGroupLogRecord from apps.base.models import UserNotificationPolicyLogRecord log_builder = IncidentLogBuilder(alert_group) all_log_records = log_builder.get_log_records_list() - attachments = [] - - # get rendered logs result = "" for log_record in all_log_records: # list of AlertGroupLogRecord and UserNotificationPolicyLogRecord logs if type(log_record) is AlertGroupLogRecord: @@ -27,14 +24,12 @@ class AlertGroupLogSlackRenderer: elif type(log_record) is UserNotificationPolicyLogRecord: result += f"{log_record.rendered_notification_log_line(for_slack=True)}\n" - attachments.append( - { - "text": result, - } - ) - result = "" + return result - # check if escalation or invitation active + @staticmethod + def render_alert_group_future_log_report_text(alert_group: "AlertGroup"): + log_builder = IncidentLogBuilder(alert_group) + result = "" if not (alert_group.resolved or alert_group.wiped_at or alert_group.root_alert_group): escalation_policies_plan = log_builder.get_incident_escalation_plan(for_slack=True) if escalation_policies_plan: @@ -43,11 +38,18 @@ class AlertGroupLogSlackRenderer: for time in sorted(escalation_policies_plan): for plan_line in escalation_policies_plan[time]: result += f"*{humanize.naturaldelta(time)}:* {plan_line}\n" + return result - if len(result) > 0: + @staticmethod + def render_incident_log_report_for_slack(alert_group: "AlertGroup"): + attachments = [] + past = AlertGroupLogSlackRenderer.render_alert_group_past_log_report_text(alert_group) + future = AlertGroupLogSlackRenderer.render_alert_group_future_log_report_text(alert_group) + text = past + future + if len(text) > 0: attachments.append( { - "text": result, + "text": text, } ) return attachments diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index 9578bbc4..7f23b70c 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -19,6 +19,7 @@ from apps.chatops_proxy.utils import uninstall_slack as uninstall_slack_from_cha from apps.slack.client import SlackClient from apps.slack.errors import SlackAPIError from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING +from apps.slack.scenarios.alertgroup_timeline import STEPS_ROUTING as ALERTGROUP_TIMELINE_ROUTING # Importing routes from scenarios from apps.slack.scenarios.declare_incident import STEPS_ROUTING as DECLARE_INCIDENT_ROUTING @@ -52,6 +53,7 @@ SCENARIOS_ROUTES.extend(SCHEDULES_ROUTING) SCENARIOS_ROUTES.extend(SHIFT_SWAP_REQUESTS_ROUTING) SCENARIOS_ROUTES.extend(SLACK_CHANNEL_INTEGRATION_ROUTING) SCENARIOS_ROUTES.extend(ALERTGROUP_APPEARANCE_ROUTING) +SCENARIOS_ROUTES.extend(ALERTGROUP_TIMELINE_ROUTING) SCENARIOS_ROUTES.extend(RESOLUTION_NOTE_ROUTING) SCENARIOS_ROUTES.extend(SLACK_USERGROUP_UPDATE_ROUTING) SCENARIOS_ROUTES.extend(CHANNEL_ROUTING)