mirror of
https://github.com/getredash/redash.git
synced 2025-12-23 20:01:59 -05:00
Add webex as destination (#5574)
* Add webex as destination * import from destinations explicitly * make format * remove unattributed image * make webex bot token required * don't use magic string * add metadata kwarg * simplify link creation * simplify alert description * simplify alert subject * split attachments template into method * DRY message posting * use api endpoint method * add missing param to post_message * static method * static method attachments template * log exception if send fails * simplify destination handling * transparent image / right size webex logo * remove unused organization param * rename api endpoint and make it a property * add test --------- Co-authored-by: Justin Clift <justin@postgresql.org> Co-authored-by: Guido Petri <18634426+guidopetri@users.noreply.github.com>
This commit is contained in:
BIN
client/app/assets/images/destinations/webex.png
Normal file
BIN
client/app/assets/images/destinations/webex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
138
redash/destinations/webex.py
Normal file
138
redash/destinations/webex.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
import requests
|
||||
|
||||
from redash.destinations import BaseDestination, register
|
||||
from redash.models import Alert
|
||||
|
||||
|
||||
class Webex(BaseDestination):
|
||||
@classmethod
|
||||
def configuration_schema(cls):
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"webex_bot_token": {"type": "string", "title": "Webex Bot Token"},
|
||||
"to_person_emails": {
|
||||
"type": "string",
|
||||
"title": "People (comma-separated)",
|
||||
},
|
||||
"to_room_ids": {
|
||||
"type": "string",
|
||||
"title": "Rooms (comma-separated)",
|
||||
},
|
||||
},
|
||||
"secret": ["webex_bot_token"],
|
||||
"required": ["webex_bot_token"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def icon(cls):
|
||||
return "fa-webex"
|
||||
|
||||
@property
|
||||
def api_base_url(self):
|
||||
return "https://webexapis.com/v1/messages"
|
||||
|
||||
@staticmethod
|
||||
def formatted_attachments_template(subject, description, query_link, alert_link):
|
||||
return [
|
||||
{
|
||||
"contentType": "application/vnd.microsoft.card.adaptive",
|
||||
"content": {
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.0",
|
||||
"body": [
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"width": 4,
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": {subject},
|
||||
"weight": "bolder",
|
||||
"size": "medium",
|
||||
"wrap": True,
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": {description},
|
||||
"isSubtle": True,
|
||||
"wrap": True,
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": f"Click [here]({query_link}) to check your query!",
|
||||
"wrap": True,
|
||||
"isSubtle": True,
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": f"Click [here]({alert_link}) to check your alert!",
|
||||
"wrap": True,
|
||||
"isSubtle": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
def notify(self, alert, query, user, new_state, app, host, metadata, options):
|
||||
# Documentation: https://developer.webex.com/docs/api/guides/cards
|
||||
|
||||
query_link = f"{host}/queries/{query.id}"
|
||||
alert_link = f"{host}/alerts/{alert.id}"
|
||||
|
||||
if new_state == Alert.TRIGGERED_STATE:
|
||||
subject = alert.custom_subject or f"{alert.name} just triggered"
|
||||
else:
|
||||
subject = f"{alert.name} went back to normal"
|
||||
|
||||
attachments = self.formatted_attachments_template(
|
||||
subject=subject, description=alert.custom_body, query_link=query_link, alert_link=alert_link
|
||||
)
|
||||
|
||||
template_payload = {"markdown": subject + "\n" + alert.custom_body, "attachments": attachments}
|
||||
|
||||
headers = {"Authorization": f"Bearer {options['webex_bot_token']}"}
|
||||
|
||||
api_destinations = {
|
||||
"toPersonEmail": options.get("to_person_emails"),
|
||||
"roomId": options.get("to_room_ids"),
|
||||
}
|
||||
|
||||
for payload_tag, destinations in api_destinations.items():
|
||||
if destinations is None:
|
||||
continue
|
||||
|
||||
# destinations is guaranteed to be a comma-separated string
|
||||
for destination_id in destinations.split(","):
|
||||
payload = deepcopy(template_payload)
|
||||
payload[payload_tag] = destination_id
|
||||
self.post_message(payload, headers)
|
||||
|
||||
def post_message(self, payload, headers):
|
||||
try:
|
||||
resp = requests.post(
|
||||
self.api_base_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=5.0,
|
||||
)
|
||||
logging.warning(resp.text)
|
||||
if resp.status_code != 200:
|
||||
logging.error("Webex send ERROR. status_code => {status}".format(status=resp.status_code))
|
||||
except Exception as e:
|
||||
logging.exception(f"Webex send ERROR: {e}")
|
||||
|
||||
|
||||
register(Webex)
|
||||
@@ -364,6 +364,7 @@ default_destinations = [
|
||||
"redash.destinations.hangoutschat",
|
||||
"redash.destinations.microsoft_teams_webhook",
|
||||
"redash.destinations.asana",
|
||||
"redash.destinations.webex",
|
||||
]
|
||||
|
||||
enabled_destinations = array_from_string(os.environ.get("REDASH_ENABLED_DESTINATIONS", ",".join(default_destinations)))
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest import mock
|
||||
|
||||
from redash.destinations.asana import Asana
|
||||
from redash.destinations.discord import Discord
|
||||
from redash.destinations.webex import Webex
|
||||
from redash.models import Alert, NotificationDestination
|
||||
from tests import BaseTestCase
|
||||
|
||||
@@ -196,3 +197,53 @@ def test_asana_notify_calls_requests_post():
|
||||
)
|
||||
|
||||
assert mock_response.status_code == 204
|
||||
|
||||
|
||||
def test_webex_notify_calls_requests_post():
|
||||
alert = mock.Mock(spec_set=["id", "name", "custom_subject", "custom_body", "render_template"])
|
||||
alert.id = 1
|
||||
alert.name = "Test Alert"
|
||||
alert.custom_subject = "Test custom subject"
|
||||
alert.custom_body = "Test custom body"
|
||||
|
||||
alert.render_template = mock.Mock(return_value={"Rendered": "template"})
|
||||
query = mock.Mock()
|
||||
query.id = 1
|
||||
|
||||
user = mock.Mock()
|
||||
app = mock.Mock()
|
||||
host = "https://localhost:5000"
|
||||
options = {"webex_bot_token": "abcd", "to_room_ids": "1234"}
|
||||
metadata = {"Scheduled": False}
|
||||
|
||||
new_state = Alert.TRIGGERED_STATE
|
||||
destination = Webex(options)
|
||||
|
||||
with mock.patch("redash.destinations.webex.requests.post") as mock_post:
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 204
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
destination.notify(alert, query, user, new_state, app, host, metadata, options)
|
||||
|
||||
query_link = f"{host}/queries/{query.id}"
|
||||
alert_link = f"{host}/alerts/{alert.id}"
|
||||
|
||||
formatted_attachments = Webex.formatted_attachments_template(
|
||||
alert.custom_subject, alert.custom_body, query_link, alert_link
|
||||
)
|
||||
|
||||
expected_payload = {
|
||||
"markdown": alert.custom_subject + "\n" + alert.custom_body,
|
||||
"attachments": formatted_attachments,
|
||||
"roomId": "1234",
|
||||
}
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
destination.api_base_url,
|
||||
json=expected_payload,
|
||||
headers={"Authorization": "Bearer abcd"},
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
assert mock_response.status_code == 204
|
||||
|
||||
Reference in New Issue
Block a user