-
Notifications
You must be signed in to change notification settings - Fork 261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Google Chat webhook sink #1245
Merged
Merged
Google Chat webhook sink #1245
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use tabulate to convert the table to string?
see
to_table_string
inblocks.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per
tabulate
documentation, this would result in:What I wanted to achieve in this case was more like a bullet list, to make this as similar to Slack/mail formatting as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional note: tables wider than 2 columns are rendered using
to_table_string