From ce26d0f0fe3f9dd854332aeb7159e1bc8dac06ef Mon Sep 17 00:00:00 2001 From: avi robusta Date: Thu, 2 Jan 2025 14:29:39 +0200 Subject: [PATCH] working version --- .../robusta_playbooks/sink_enrichments.py | 20 ++++-- src/robusta/core/sinks/slack/slack_sink.py | 61 ++++++++++++++++++- src/robusta/integrations/receiver.py | 4 +- src/robusta/integrations/slack/sender.py | 32 +++++++++- 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/playbooks/robusta_playbooks/sink_enrichments.py b/playbooks/robusta_playbooks/sink_enrichments.py index 48e7b4bc0..9d20f66f4 100644 --- a/playbooks/robusta_playbooks/sink_enrichments.py +++ b/playbooks/robusta_playbooks/sink_enrichments.py @@ -20,7 +20,7 @@ class OpsGenieAckParams(ActionParams): alert_fingerprint: str slack_username: Optional[str] - slack_message: Optional[Dict[str, Any]] + slack_message: Optional[Any] @action @@ -32,8 +32,20 @@ def ack_opsgenie_alert() -> None: user=params.slack_username, note=f"This alert was ack-ed from a Robusta Slack message by {params.slack_username}" ) - logging.info(f"Acking {params.alert_fingerprint} {params.slack_username}") - logging.warning(params.slack_message) + + # slack action block + actions = params.slack_message.get("actions", []) + if len(actions) == 0: + return + block_id = actions[0].get("block_id") + + event.emit_event( + "replace_callback_with_string", + slack_message=params.slack_message, + block_id=block_id, + message_string=f"✅ *OpsGenie Ack by @{params.slack_username}*" + ) + ack_opsgenie_alert() @@ -81,4 +93,4 @@ def normalize_url_base(url_base: str) -> str: @action def opsgenie_link_enricher(alert: PrometheusKubernetesAlert, params: OpsgenieLinkParams): normalized_url_base = normalize_url_base(params.url_base) - alert.add_link(Link(url=f"https://{normalized_url_base}/alert/list?query=alias:{alert.alert.fingerprint}", name="OpsGenie Alert", type=LinkType.OPSGENIE)) \ No newline at end of file + alert.add_link(Link(url=f"https://{normalized_url_base}/alert/list?query=alias:{alert.alert.fingerprint}", name="OpsGenie Alert", type=LinkType.OPSGENIE)) diff --git a/src/robusta/core/sinks/slack/slack_sink.py b/src/robusta/core/sinks/slack/slack_sink.py index 96a664cbb..aeeb44f97 100644 --- a/src/robusta/core/sinks/slack/slack_sink.py +++ b/src/robusta/core/sinks/slack/slack_sink.py @@ -1,3 +1,5 @@ +import logging + from robusta.core.model.env_vars import ROBUSTA_UI_DOMAIN from robusta.core.reporting.base import Finding, FindingStatus from robusta.core.sinks.sink_base import NotificationGroup, NotificationSummary, SinkBase @@ -15,6 +17,13 @@ def __init__(self, sink_config: SlackSinkConfigWrapper, registry): self.slack_sender = slack_module.SlackSender( self.api_key, self.account_id, self.cluster_name, self.signing_key, self.slack_channel ) + self.registry.subscribe("replace_callback_with_string", self) + + def handle_event(self, event_name: str, **kwargs): + if event_name == "replace_callback_with_string": + self.__replace_callback_with_string(**kwargs) + else: + logging.warning("SlackSink subscriber called with unknown event") def write_finding(self, finding: Finding, platform_enabled: bool) -> None: if self.grouping_enabled: @@ -75,6 +84,56 @@ def handle_notification_grouping(self, finding: Finding, platform_enabled: bool) finding, self.params, platform_enabled, thread_ts=slack_thread_ts ) - def get_timeline_uri(self, account_id: str, cluster_name: str) -> str: return f"{ROBUSTA_UI_DOMAIN}/graphs?account_id={account_id}&cluster={cluster_name}" + + def __replace_callback_with_string(self, slack_message, block_id, message_string): + """ + Replace a specific block in a Slack message with a given string while preserving other blocks. + + Args: + json_message (dict): The JSON payload received from Slack. + block_id (str): The ID of the block to replace. + message_string (str): The text to replace the block content with. + """ + try: + # Extract required fields + channel_id = slack_message.get("channel", {}).get("id") + message_ts = slack_message.get("container", {}).get("message_ts") + blocks = slack_message.get("message", {}).get("blocks", []) + + # Validate required fields + if not channel_id or not message_ts or not blocks: + raise ValueError("Missing required fields: channel_id, message_ts, or blocks.") + + # Update the specific block + updated_blocks = [] + block_found = False + + for block in blocks: + if block.get("block_id") == block_id: + updated_blocks.append({ + "type": "section", + "block_id": block_id, + "text": { + "type": "mrkdwn", + "text": message_string + } + }) + block_found = True + else: + updated_blocks.append(block) + + if not block_found: + raise ValueError(f"No block found with block_id: {block_id}") + + # Call the shorter update function + return self.slack_sender.update_slack_message( + channel=channel_id, + ts=message_ts, + blocks=updated_blocks, + text=message_string + ) + + except Exception as e: + logging.exception(f"❌ Error updating Slack message: {e}") diff --git a/src/robusta/integrations/receiver.py b/src/robusta/integrations/receiver.py index edeb7a2b4..9e8e9796b 100644 --- a/src/robusta/integrations/receiver.py +++ b/src/robusta/integrations/receiver.py @@ -52,7 +52,7 @@ class ValidationResponse(BaseModel): class SlackExternalActionRequest(ExternalActionRequest): # Optional Slack Params slack_username: Optional[str] = None - slack_message: Optional[Dict[str, Any]] = None + slack_message: Optional[Any] = None class SlackActionRequest(BaseModel): @@ -211,7 +211,7 @@ def _parse_slack_message(message: Union[str, bytes, bytearray]) -> SlackActionsM slack_actions_message = SlackActionsMessage.parse_raw(message) # this is slack callback format for action in slack_actions_message.actions: action.value.slack_username = slack_actions_message.user.username - action.value.slack_message = slack_actions_message.message + action.value.slack_message = json.loads(message) return slack_actions_message def on_message(self, ws: websocket.WebSocketApp, message: str) -> None: diff --git a/src/robusta/integrations/slack/sender.py b/src/robusta/integrations/slack/sender.py index 94cde1bf3..4a666da5f 100644 --- a/src/robusta/integrations/slack/sender.py +++ b/src/robusta/integrations/slack/sender.py @@ -15,8 +15,8 @@ from robusta.core.model.base_params import AIInvestigateParams, ResourceInfo from robusta.core.model.env_vars import ( ADDITIONAL_CERTIFICATE, - SLACK_REQUEST_TIMEOUT, HOLMES_ENABLED, + SLACK_REQUEST_TIMEOUT, SLACK_TABLE_COLUMNS_LIMIT, ) from robusta.core.playbooks.internal.ai_integration import ask_holmes @@ -695,3 +695,33 @@ def send_or_update_summary_message( return resp["ts"] except Exception as e: logging.exception(f"error sending message to slack\n{e}\nchannel={channel}\n") + + def update_slack_message(self, channel: str, ts: str, blocks: list, text: str = ""): + """ + Update a Slack message with new blocks and optional text. + + Args: + channel (str): Slack channel ID. + ts (str): Timestamp of the message to update. + blocks (list): List of Slack Block Kit blocks for the updated message. + text (str, optional): Plain text summary for accessibility. Defaults to "". + """ + try: + # Ensure channel ID exists in the mapping + if channel not in self.channel_name_to_id.values(): + logging.error(f"Channel ID for {channel} could not be determined. Update aborted.") + return + + # Call Slack's chat_update method + resp = self.slack_client.chat_update( + channel=channel, + ts=ts, + text=text, + blocks=blocks + ) + logging.debug(f"Message updated successfully: {resp['ts']}") + return resp["ts"] + + except Exception as e: + logging.exception(f"Error updating Slack message: {e}") + return None