-
Notifications
You must be signed in to change notification settings - Fork 261
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Robert Szefler
committed
Jan 15, 2024
1 parent
29e0bb2
commit c62973b
Showing
8 changed files
with
211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
Google Chat | ||
################# | ||
|
||
Robusta can report issues and events in your Kubernetes cluster by sending | ||
messages via the Google Chat. | ||
|
||
Setting up the Google Chat integration | ||
------------------------------------------------ | ||
|
||
All you need to set up Google Chat sink for Robusta is to enable a webhook | ||
for a certain Google Chat Space. This essentially means that an administrator | ||
of the chat has to extract a special URL for this Chat Space that enables | ||
the integration. You can find out more about webhook URLs in the Google | ||
documentation, for example <here|https://developers.google.com/chat/how-tos/webhooks> | ||
|
||
Configuring the Google Chat sink in Robusta | ||
------------------------------------------------ | ||
|
||
.. admonition:: Add this to your generated_values.yaml | ||
|
||
.. code-block:: yaml | ||
sinksConfig: | ||
- google_chat_sink: | ||
name: gchat_sink | ||
webhook_url: https://chat.googleapis.com/v1/spaces/space-id/messages?key=xyz&token=pqr | ||
Then do a :ref:`Helm Upgrade <Simple Upgrade>`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from robusta.core.reporting.base import Finding | ||
from robusta.core.sinks.sink_base import SinkBase | ||
from robusta.core.sinks.google_chat.google_chat_params import GoogleChatSinkConfigWrapper | ||
from robusta.integrations.google_chat.sender import GoogleChatSender | ||
|
||
|
||
class GoogleChatSink(SinkBase): | ||
def __init__(self, sink_config: GoogleChatSinkConfigWrapper, registry): | ||
sink_params = sink_config.get_params() | ||
super().__init__(sink_params, registry) | ||
self.sender = GoogleChatSender( | ||
sink_params, | ||
self.signing_key, | ||
self.account_id, | ||
self.cluster_name, | ||
) | ||
|
||
def write_finding(self, finding: Finding, platform_enabled: bool): | ||
self.sender.send_finding(finding, platform_enabled) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from pydantic import SecretStr | ||
|
||
from robusta.core.sinks.sink_base_params import SinkBaseParams | ||
from robusta.core.sinks.sink_config import SinkConfigBase | ||
|
||
|
||
class GoogleChatSinkParams(SinkBaseParams): | ||
webhook_url: SecretStr | ||
|
||
|
||
class GoogleChatSinkConfigWrapper(SinkConfigBase): | ||
google_chat_sink: GoogleChatSinkParams | ||
|
||
def get_params(self) -> GoogleChatSinkParams: | ||
return self.google_chat_sink |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import logging | ||
from typing import Dict, List | ||
|
||
from robusta.core.reporting.base import BaseBlock, Emojis, Finding, FindingStatus | ||
from robusta.core.reporting.blocks import ( | ||
LinksBlock, | ||
LinkProp, | ||
MarkdownBlock, FileBlock, HeaderBlock, TableBlock, ListBlock, | ||
) | ||
from robusta.core.reporting.consts import FindingSource | ||
from robusta.core.sinks.google_chat.google_chat_params import GoogleChatSinkParams | ||
|
||
import requests | ||
|
||
|
||
class GoogleChatSender: | ||
def __init__(self, params: GoogleChatSinkParams, signing_key, account_id, cluster_name): | ||
self.params = params | ||
self.signing_key = signing_key | ||
self.account_id = account_id | ||
self.cluster_name = cluster_name | ||
|
||
def send_finding(self, finding: Finding, platform_enabled: bool): | ||
blocks: List[BaseBlock] = [] | ||
|
||
status: FindingStatus = ( | ||
FindingStatus.RESOLVED if finding.title.startswith("[RESOLVED]") else FindingStatus.FIRING | ||
) | ||
blocks.append(self.__create_finding_header(finding, status)) | ||
|
||
if platform_enabled: | ||
blocks.append(self.__create_links(finding)) | ||
|
||
blocks.append(MarkdownBlock(text=f"*Source:* `{self.cluster_name}`\n\n")) | ||
if finding.description: | ||
if finding.source == FindingSource.PROMETHEUS: | ||
blocks.append(MarkdownBlock(f"{Emojis.Alert.value} *Alert:* {finding.description}")) | ||
elif finding.source == FindingSource.KUBERNETES_API_SERVER: | ||
blocks.append( | ||
MarkdownBlock(f"{Emojis.K8Notification.value} *K8s event detected:* {finding.description}") | ||
) | ||
else: | ||
blocks.append(MarkdownBlock(f"{Emojis.K8Notification.value} *Notification:* {finding.description}")) | ||
|
||
for enrichment in finding.enrichments: | ||
items = [ | ||
self.__block_to_markdown_text(block) | ||
for block in enrichment.blocks | ||
if not isinstance(block, FileBlock) | ||
] | ||
if items: | ||
blocks.extend(items) | ||
|
||
data = self.__blocks_to_data(blocks) | ||
self.__send_to_api(data) | ||
|
||
def __blocks_to_data(self, blocks: List[BaseBlock]) -> Dict: | ||
text = "" | ||
for block in blocks: | ||
appendix = self.__block_to_markdown_text(block) | ||
if appendix is not None: | ||
text += appendix | ||
# Eliminate double endlines that might have appeared due to e.g. the way we are | ||
# converting MarkdownBlocks into text. | ||
text = text.replace("\n\n", "\n") | ||
return {"text": text} | ||
|
||
def __block_to_markdown_text(self, block: BaseBlock) -> str: | ||
if isinstance(block, MarkdownBlock): | ||
return block.text + "\n" | ||
elif isinstance(block, FileBlock): | ||
# We ignore this case as we are not able to send attachments | ||
# using the Google Chat webhook API. | ||
return None | ||
elif isinstance(block, HeaderBlock): | ||
return MarkdownBlock(block.text).text | ||
elif isinstance(block, LinksBlock): | ||
return self.__format_links(block.links) + "\n\n" | ||
elif isinstance(block, TableBlock): | ||
if len(block.headers) == 2: | ||
# This is rendered as a bullet list to make it more consistent with the | ||
# way it's presented in | ||
return ( | ||
f"\n\n{block.table_name}\n" | ||
+ "\n".join(f" • {key} `{value}`" for key, value in block.rows) | ||
+ "\n\n" | ||
) | ||
else: | ||
return "```\n" + block.to_table_string(table_max_width=120) + "```" | ||
elif isinstance(block, ListBlock): | ||
if ''.join(block.items) == '': return None | ||
return ( | ||
"\n" | ||
+ "\n".join(f" • {self.__block_to_markdown_text(item)}" for item in block.items) | ||
+ "\n\n" | ||
) | ||
elif isinstance(block, str): | ||
return block if str else None | ||
else: | ||
if block is not None: # None means nothing to render and is acceptable here | ||
logging.warning(f"cannot convert block of type {type(block)} to Google Chat format block") | ||
return None | ||
|
||
def __format_links(self, links: List[LinkProp]): | ||
return "\n".join(f"<{link.url}|*{link.text}*>\n" for link in links) | ||
|
||
def __send_to_api(self, data: Dict): | ||
try: | ||
resp = requests.post(self.params.webhook_url.get_secret_value(), json=data) | ||
except Exception: | ||
logging.exception(f"Webhook request error\n headers: \n{resp.headers}") | ||
|
||
def __create_finding_header(self, finding: Finding, status: FindingStatus) -> MarkdownBlock: | ||
title = finding.title.removeprefix("[RESOLVED] ") | ||
sev = finding.severity | ||
status_name: str = "Prometheus Alert Firing" if status == FindingStatus.FIRING else "Prometheus resolved" | ||
status_str: str = f"{status.to_emoji()} `{status_name}`" | ||
return MarkdownBlock( | ||
f"{status_str} {sev.to_emoji()} `{sev.name.lower()}`\n" | ||
f"<{finding.get_investigate_uri(self.account_id, self.cluster_name)}|*{title}*>\n\n" | ||
) | ||
|
||
def __create_links(self, finding: Finding): | ||
links: List[LinkProp] = [] | ||
links.append( | ||
LinkProp( | ||
text="Investigate 🔎", | ||
url=finding.get_investigate_uri(self.account_id, self.cluster_name), | ||
) | ||
) | ||
|
||
if finding.add_silence_url: | ||
links.append( | ||
LinkProp( | ||
text="Configure Silences 🔕", | ||
url=finding.get_prometheus_silence_url(self.account_id, self.cluster_name), | ||
) | ||
) | ||
|
||
for video_link in finding.video_links: | ||
links.append(LinkProp(text=f"{video_link.name} 🎬", url=video_link.url)) | ||
|
||
return LinksBlock(links=links) |