Skip to content

Commit aad3835

Browse files
authored
[ROB-1286] Updated holmes slack format (#1826)
* deprecating holmes_button * fixing text * updated holmes docs * adding holmes to slack * fixing links * refactoring holmes * added check for holmes_slackbot enabled - pre-test * fixes * fixing indentation * fixing tests
1 parent 9dd49dd commit aad3835

9 files changed

Lines changed: 102 additions & 43 deletions

File tree

docs/configuration/holmesgpt/index.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,38 @@ Reading the Robusta UI Token from a secret in HolmesGPT
311311
312312
Run a :ref:`Helm Upgrade <Simple Upgrade>` to apply the configuration.
313313

314+
Enable Holmes in Slack in the Platform
315+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
316+
317+
1. **Go to** https://platform.robusta.dev/
318+
319+
2. **Navigate to:**
320+
**Settings** → **AI Assistant**
321+
322+
.. image:: /images/Enabling_AI_in_slack.png
323+
:width: 1000px
324+
325+
3. **Enable Holmes** using the toggle.
326+
327+
4. **Click** **Connect Slack Workspace** to authorize Holmes in your Slack workspace.
328+
329+
5. **Use Holmes in Slack**
330+
331+
In any Slack channel or thread, tag Holmes using `@holmes` like::
332+
333+
@holmes can you look into this
334+
335+
Or ask natural language questions about a specific cluster. Examples::
336+
337+
.. code-block:: bash
338+
@holmes what apps are crashing in my `prod-cluster`
339+
@holmes show me the CPU usage for the frontend deployment in `staging-cluster`
340+
@holmes why is my alert firing on `eu-prod-atc-eks`?
341+
@holmes investigate high memory usage in `dev-cluster`
342+
343+
Holmes will respond in the thread with insights and troubleshooting steps based on the specified cluster.
344+
345+
314346
Test Holmes Integration
315347
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
316348
284 KB
Loading

src/robusta/core/sinks/robusta/robusta_sink.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@
4242
from robusta.integrations.receiver import ActionRequestReceiver
4343
from robusta.runner.web_api import WebApi
4444
from robusta.utils.stack_tracer import StackTracer
45+
from robusta.core.model.env_vars import ROBUSTA_API_ENDPOINT
46+
from cachetools import TTLCache
4547

48+
RUNNER_GET_HOLMES_SLACKBOT_INFO = f"{ROBUSTA_API_ENDPOINT}/api/holmes/integrations/slack/runner"
49+
HOLMES_SLACKBOT_CACHE_TTL = int(os.getenv("HOLMES_SLACKBOT_CACHE_TTL", 15 * 60))
50+
51+
# Define the cache with a single slot and the configured TTL
52+
_holmes_slackbot_cache = TTLCache(maxsize=1, ttl=HOLMES_SLACKBOT_CACHE_TTL)
4653

4754
class RobustaSink(SinkBase, EventHandler):
4855
services_publish_lock = threading.Lock()
@@ -703,3 +710,26 @@ def __update_job(self, new_job: Job, operation: K8sOperationType):
703710
self.__safe_delete_job(job_key)
704711
self.__discovery_metrics.on_jobs_updated(1)
705712
return
713+
714+
def is_holmes_slackbot_connected(self) -> bool:
715+
if 'status' in _holmes_slackbot_cache:
716+
return _holmes_slackbot_cache['status']
717+
session_token = self.dal.get_session_token()
718+
try:
719+
message_json = {
720+
"session_token": session_token,
721+
"account_id": self.account_id,
722+
}
723+
response = requests.post(
724+
RUNNER_GET_HOLMES_SLACKBOT_INFO,
725+
json=message_json,
726+
headers={"Content-Type": "application/json"}
727+
)
728+
response.raise_for_status()
729+
is_connected = bool(response.json().get("integrations"))
730+
_holmes_slackbot_cache['status'] = is_connected
731+
return is_connected
732+
except Exception as e:
733+
logging.warning(f"Failed to get holmes slackbot info {e}")
734+
_holmes_slackbot_cache['status'] = False
735+
return False

src/robusta/core/sinks/slack/slack_sink.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __init__(self, sink_config: SlackSinkConfigWrapper, registry, is_preview=Fal
1616
self.slack_channel = slack_sink_params.slack_channel
1717
self.api_key = slack_sink_params.api_key
1818
self.slack_sender = slack_module.SlackSender(
19-
self.api_key, self.account_id, self.cluster_name, self.signing_key, self.slack_channel, is_preview
19+
self.api_key, self.account_id, self.cluster_name, self.signing_key, self.slack_channel, registry, is_preview
2020
)
2121
self.registry.subscribe("replace_callback_with_string", self)
2222

src/robusta/integrations/slack/sender.py

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from robusta.core.sinks.sink_base import KeyT
4949
from robusta.core.sinks.slack.slack_sink_params import SlackSinkParams
5050
from robusta.core.sinks.slack.preview.slack_sink_preview_params import SlackSinkPreviewParams
51-
5251
from robusta.core.sinks.transformer import Transformer
5352

5453
ACTION_TRIGGER_PLAYBOOK = "trigger_playbook"
@@ -62,7 +61,7 @@ class SlackSender:
6261
verified_api_tokens: Set[str] = set()
6362
channel_name_to_id = {}
6463

65-
def __init__(self, slack_token: str, account_id: str, cluster_name: str, signing_key: str, slack_channel: str, is_preview: bool = False):
64+
def __init__(self, slack_token: str, account_id: str, cluster_name: str, signing_key: str, slack_channel: str, registry, is_preview: bool = False):
6665
"""
6766
Connect to Slack and verify that the Slack token is valid.
6867
Return True on success, False on failure
@@ -80,6 +79,7 @@ def __init__(self, slack_token: str, account_id: str, cluster_name: str, signing
8079
timeout=SLACK_REQUEST_TIMEOUT,
8180
retry_handlers=all_builtin_retry_handlers(),
8281
)
82+
self.registry = registry
8383
self.signing_key = signing_key
8484
self.account_id = account_id
8585
self.cluster_name = cluster_name
@@ -394,33 +394,6 @@ def __limit_labels_size(self, labels: dict, max_size: int = 1000) -> dict:
394394

395395
return limited_labels
396396

397-
def __create_holmes_callback(self, finding: Finding) -> CallbackBlock:
398-
resource = ResourceInfo(
399-
name=finding.subject.name if finding.subject.name else "",
400-
namespace=finding.subject.namespace,
401-
kind=finding.subject.subject_type.value if finding.subject.subject_type.value else "",
402-
node=finding.subject.node,
403-
container=finding.subject.container,
404-
)
405-
406-
context: Dict[str, Any] = {
407-
"robusta_issue_id": str(finding.id),
408-
"issue_type": finding.aggregation_key,
409-
"source": finding.source.name,
410-
"labels": self.__limit_labels_size(labels=finding.subject.labels),
411-
}
412-
413-
return CallbackBlock(
414-
{
415-
"Ask HolmesGPT": CallbackChoice(
416-
action=ask_holmes,
417-
action_params=AIInvestigateParams(
418-
resource=resource, investigation_type="issue", ask="Why is this alert firing?", context=context
419-
),
420-
)
421-
}
422-
)
423-
424397
@staticmethod
425398
def extract_mentions(title) -> (str, str):
426399
mentions = MENTION_PATTERN.findall(title)
@@ -667,6 +640,16 @@ def send_holmes_analysis(
667640
except Exception:
668641
logging.exception(f"error sending message to slack. {title}")
669642

643+
def get_holmes_block(self, platform_enabled: bool, slackbot_enabled) -> Optional[MarkdownBlock]:
644+
if not platform_enabled and not slackbot_enabled:
645+
return MarkdownBlock("_Ask AI questions about this alert, by connecting <https://platform.robusta.dev/create-account|Robusta SaaS> and tagging @holmes._")
646+
elif platform_enabled and not slackbot_enabled:
647+
return MarkdownBlock("_Ask AI questions about this alert, by adding @holmes to your <https://docs.robusta.dev/master/configuration/holmesgpt/index.html#enable-holmes-in-slack-in-the-platform|Slack>._")
648+
elif platform_enabled and slackbot_enabled:
649+
return MarkdownBlock("_Ask AI questions about this alert, by tagging @holmes in a threaded reply_")
650+
return None
651+
652+
670653
def send_finding_to_slack(
671654
self,
672655
finding: Finding,
@@ -691,6 +674,15 @@ def send_finding_to_slack(
691674
thread_ts=thread_ts
692675
)
693676

677+
def __is_holmes_slackbot_enabled(self) -> bool:
678+
robusta_sinks = self.registry.get_sinks().get_robusta_sinks() if self.registry else None
679+
if not robusta_sinks:
680+
logging.debug("No robusta sinks found, holmes not connected to slackbot")
681+
return False
682+
683+
robusta_sink = robusta_sinks[0]
684+
return robusta_sink.is_holmes_slackbot_connected()
685+
694686
def __send_finding_to_slack(
695687
self,
696688
finding: Finding,
@@ -725,9 +717,6 @@ def __send_finding_to_slack(
725717
)
726718
blocks.append(links_block)
727719

728-
if HOLMES_ENABLED and HOLMES_ASK_SLACK_BUTTON_ENABLED:
729-
blocks.append(self.__create_holmes_callback(finding))
730-
731720
blocks.append(MarkdownBlock(text=f"*Source:* `{self.cluster_name}`"))
732721
if finding.description:
733722
if finding.source == FindingSource.PROMETHEUS:
@@ -753,6 +742,12 @@ def __send_finding_to_slack(
753742

754743
blocks.append(DividerBlock())
755744

745+
is_holmes_slackbot_enabled = self.__is_holmes_slackbot_enabled()
746+
holmes_block = self.get_holmes_block(platform_enabled, is_holmes_slackbot_enabled)
747+
if holmes_block:
748+
blocks.append(holmes_block)
749+
750+
756751
if len(attachment_blocks):
757752
attachment_blocks.append(DividerBlock())
758753

tests/manual_tests/test_slack_integration_manual.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ def main():
442442
cluster_name="test-cluster",
443443
signing_key="test-signing-key",
444444
slack_channel=SLACK_CHANNEL,
445+
registry=None,
445446
is_preview=False
446447
)
447448

@@ -452,6 +453,7 @@ def main():
452453
cluster_name="test-cluster",
453454
signing_key="test-signing-key",
454455
slack_channel=SLACK_CHANNEL,
456+
registry=None,
455457
is_preview=True
456458
)
457459

tests/test_blocks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
def test_send_to_slack(slack_channel: SlackChannel):
3333
slack_sender = SlackSender(
34-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
34+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
3535
)
3636
msg = "Test123"
3737
finding = Finding(title=msg, aggregation_key=msg)
@@ -127,7 +127,7 @@ def test_callback(event: ExecutionBaseEvent):
127127

128128
def test_all_block_types(slack_channel: SlackChannel):
129129
slack_sender = SlackSender(
130-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
130+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name,registry=None
131131
)
132132
slack_params = SlackSinkParams(name="test_slack", slack_channel=slack_channel.channel_name, api_key="")
133133
finding = create_finding_with_all_blocks()

tests/test_slack.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
def test_send_to_slack(slack_channel: SlackChannel):
1919
slack_sender = SlackSender(
20-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
20+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
2121
)
2222
msg = "Test123"
2323
finding = Finding(title=msg, aggregation_key=msg)
@@ -29,7 +29,7 @@ def test_send_to_slack(slack_channel: SlackChannel):
2929

3030
def test_long_slack_messages(slack_channel: SlackChannel):
3131
slack_sender = SlackSender(
32-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
32+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
3333
)
3434
finding = Finding(title="A" * 151, aggregation_key="A" * 151)
3535
finding.add_enrichment([MarkdownBlock("H" * 3001)])
@@ -39,7 +39,7 @@ def test_long_slack_messages(slack_channel: SlackChannel):
3939

4040
def test_long_table_columns(slack_channel: SlackChannel):
4141
slack_sender = SlackSender(
42-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
42+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
4343
)
4444
finding = Finding(title="Testing table blocks", aggregation_key="TestingTableBlocks")
4545
finding.add_enrichment(
@@ -59,7 +59,7 @@ def test_long_table_columns(slack_channel: SlackChannel):
5959

6060
def test_send_file_spooled_tempfile_fails(slack_channel: SlackChannel):
6161
slack_sender = SlackSender(
62-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
62+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
6363
)
6464

6565
# Test with a text file
@@ -82,7 +82,7 @@ def test_send_file_spooled_tempfile_fails(slack_channel: SlackChannel):
8282

8383
def test_send_file_named_tempfile_fails(slack_channel: SlackChannel):
8484
slack_sender = SlackSender(
85-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
85+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
8686
)
8787

8888
finding = Finding(title=TEST_FINDING_TITLE, aggregation_key="TestTextFileUpload")
@@ -102,7 +102,7 @@ def test_send_file_named_tempfile_fails(slack_channel: SlackChannel):
102102

103103
def test_temporary_file_creation_failure(slack_channel: SlackChannel):
104104
slack_sender = SlackSender(
105-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name
105+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None
106106
)
107107

108108
# Test with a text file

tests/test_slack_preview.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def extract_text_from_blocks(message):
3131

3232
def test_slack_preview_default_template(slack_channel: SlackChannel):
3333
slack_sender = SlackSender(
34-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, is_preview=True
34+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None, is_preview=True
3535
)
3636

3737
# Create a subject for the finding
@@ -83,7 +83,7 @@ def test_slack_preview_default_template(slack_channel: SlackChannel):
8383

8484
def test_slack_preview_custom_template(slack_channel: SlackChannel):
8585
slack_sender = SlackSender(
86-
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, is_preview=True
86+
CONFIG.PYTEST_IN_CLUSTER_SLACK_TOKEN, TEST_ACCOUNT, TEST_CLUSTER, TEST_KEY, slack_channel.channel_name, registry=None, is_preview=True
8787
)
8888

8989
subject = FindingSubject(

0 commit comments

Comments
 (0)