diff --git a/src/robusta/core/playbooks/internal/ai_integration.py b/src/robusta/core/playbooks/internal/ai_integration.py index ff16257a9..2bec199a2 100644 --- a/src/robusta/core/playbooks/internal/ai_integration.py +++ b/src/robusta/core/playbooks/internal/ai_integration.py @@ -1,5 +1,6 @@ import json import logging +from typing import Optional import requests @@ -8,14 +9,15 @@ HolmesChatParams, HolmesConversationParams, HolmesIssueChatParams, + HolmesWorkloadHealthChatParams, HolmesWorkloadHealthParams, ResourceInfo, - HolmesWorkloadHealthChatParams ) from robusta.core.model.events import ExecutionBaseEvent from robusta.core.playbooks.actions_registry import action from robusta.core.reporting import Finding, FindingSubject from robusta.core.reporting.base import EnrichmentType +from robusta.core.reporting.blocks import MarkdownBlock from robusta.core.reporting.consts import FindingSubjectType, FindingType from robusta.core.reporting.holmes import ( HolmesChatRequest, @@ -31,6 +33,7 @@ ) from robusta.core.schedule.model import FixedDelayRepeat from robusta.integrations.kubernetes.autogenerated.events import KubernetesAnyChangeEvent +from robusta.integrations.prometheus.models import PrometheusKubernetesAlert from robusta.integrations.prometheus.utils import HolmesDiscovery from robusta.utils.error_codes import ActionException, ErrorCodes @@ -43,7 +46,36 @@ def build_investigation_title(params: AIInvestigateParams) -> str: @action -def ask_holmes(event: ExecutionBaseEvent, params: AIInvestigateParams): +def auto_ask_holmes(event: ExecutionBaseEvent): + """ + Runs holmes investigation on an alert/event. + """ + subject: FindingSubject = event.get_subject() + resource = ResourceInfo( + name=subject.name, + namespace=subject.namespace, + kind=str(subject.subject_type.value), + node=subject.node, + container=subject.container, + ) + issue_type = event.alert_name if isinstance(event, PrometheusKubernetesAlert) else type(event).__name__ + logging.warning(issue_type) + source = event.get_source() + + context = { + "issue_type": issue_type, + "source": str(source.value), + "labels": subject.labels, + } + + action_params = AIInvestigateParams( + resource=resource, investigation_type="issue", ask="Why is this alert firing?", context=context + ) + ask_holmes(event, params=action_params, create_new_finding=False) + + +@action +def ask_holmes(event: ExecutionBaseEvent, params: AIInvestigateParams, create_new_finding: bool = True): holmes_url = HolmesDiscovery.find_holmes_url(params.holmes_url) if not holmes_url: raise ActionException(ErrorCodes.HOLMES_DISCOVERY_FAILED, "Robusta couldn't connect to the Holmes client.") @@ -60,20 +92,25 @@ def ask_holmes(event: ExecutionBaseEvent, params: AIInvestigateParams): context=params.context if params.context else {}, include_tool_calls=True, include_tool_call_results=True, - sections=params.sections + sections=params.sections, ) if params.stream: with requests.post(f"{holmes_url}/api/stream/investigate", data=holmes_req.json(), stream=True) as resp: - for line in resp.iter_content(chunk_size=None, decode_unicode=True): # Avoid streaming chunks from holmes. send them as they arrive. + for line in resp.iter_content( + chunk_size=None, decode_unicode=True + ): # Avoid streaming chunks from holmes. send them as they arrive. event.ws(data=line) return - else: result = requests.post(f"{holmes_url}/api/investigate", data=holmes_req.json()) result.raise_for_status() holmes_result = HolmesResult(**json.loads(result.text)) + + if not create_new_finding: + event.add_enrichment([MarkdownBlock(f"{holmes_result.analysis}")]) + title_suffix = ( f" on {params.resource.name}" if params.resource and params.resource.name and params.resource.name.lower() != "unresolved" @@ -178,7 +215,9 @@ def build_conversation_title(params: HolmesConversationParams) -> str: def add_labels_to_ask(params: HolmesConversationParams) -> str: - label_string = f"the alert has the following labels: {params.context.get('labels')}" if params.context.get("labels") else "" + label_string = ( + f"the alert has the following labels: {params.context.get('labels')}" if params.context.get("labels") else "" + ) ask = f"{params.ask}, {label_string}" if label_string else params.ask logging.debug(f"holmes ask query: {ask}") return ask @@ -377,11 +416,11 @@ def holmes_workload_chat(event: ExecutionBaseEvent, params: HolmesWorkloadHealth ask=params.ask, conversation_history=params.conversation_history, workload_health_result=params.workload_health_result, - resource=params.resource + resource=params.resource, ) result = requests.post(f"{holmes_url}/api/workload_health_chat", data=holmes_req.json()) result.raise_for_status() - + holmes_result = HolmesChatResult(**json.loads(result.text)) finding = Finding( diff --git a/src/robusta/core/sinks/webhook/webhook_sink.py b/src/robusta/core/sinks/webhook/webhook_sink.py index c56127492..cfaf25c08 100644 --- a/src/robusta/core/sinks/webhook/webhook_sink.py +++ b/src/robusta/core/sinks/webhook/webhook_sink.py @@ -5,6 +5,7 @@ import requests +from robusta.core.reporting import TableBlock, FileBlock from robusta.core.reporting import HeaderBlock, JsonBlock, KubernetesDiffBlock, ListBlock, MarkdownBlock from robusta.core.reporting.base import BaseBlock, Finding from robusta.core.sinks.sink_base import SinkBase @@ -25,6 +26,8 @@ def __init__(self, sink_config: WebhookSinkConfigWrapper, registry): ) self.size_limit = sink_config.webhook_sink.size_limit self.slack_webhook = sink_config.webhook_sink.slack_webhook + self.send_table_block = sink_config.webhook_sink.table_blocks + self.send_file_block = sink_config.webhook_sink.file_blocks def write_finding(self, finding: Finding, platform_enabled: bool): if self.format == "text": @@ -134,6 +137,10 @@ def __to_unformatted_text(cls, block: BaseBlock) -> List[str]: lines.append(cls.__to_clear_text(block.text)) elif isinstance(block, JsonBlock): lines.append(block.json_str) + elif isinstance(block, TableBlock) and cls.send_table_block: + lines.append(block.to_table_string()) + elif isinstance(block, FileBlock) and cls.send_file_block and block.is_text_file(): + lines.append(str(block.contents)) elif isinstance(block, KubernetesDiffBlock): for diff in block.diffs: lines.append(f"*{'.'.join(diff.path)}*: {diff.other_value} ==> {diff.value}") diff --git a/src/robusta/core/sinks/webhook/webhook_sink_params.py b/src/robusta/core/sinks/webhook/webhook_sink_params.py index 9462919ae..e12ef59ae 100644 --- a/src/robusta/core/sinks/webhook/webhook_sink_params.py +++ b/src/robusta/core/sinks/webhook/webhook_sink_params.py @@ -10,6 +10,8 @@ class WebhookSinkParams(SinkBaseParams): authorization: SecretStr = None format: str = "text" slack_webhook: bool = False + table_blocks: bool = False + file_blocks: bool = False @classmethod def _get_sink_type(cls):