Skip to content
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

Feature/Add Discord Alert Destination #6106

Merged
merged 15 commits into from
Jul 21, 2023
Merged
Binary file added client/app/assets/images/destinations/discord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions redash/destinations/discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import logging

import requests

junnplus marked this conversation as resolved.
Show resolved Hide resolved
junnplus marked this conversation as resolved.
Show resolved Hide resolved
from redash.destinations import BaseDestination, register
from redash.utils import json_dumps

RED_ALERT_COLOR = "12597547"
junnplus marked this conversation as resolved.
Show resolved Hide resolved

GREEN_ALERT_COLOR = "2600544"
junnplus marked this conversation as resolved.
Show resolved Hide resolved


class Discord(BaseDestination):
@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {
"url": {"type": "string", "title": "Discord Webhook URL"}
},
"secret": ["url"],
junnplus marked this conversation as resolved.
Show resolved Hide resolved
"required": ["url"],
}

@classmethod
def icon(cls):
return "fa-discord"

def notify(self, alert, query, user, app, host, options):
# Documentation: https://birdie0.github.io/discord-webhooks-guide/discord_webhook.html
fields = [
{
"name": "Query",
"value": "{host}/queries/{query_id}".format(
host=host, query_id=query.id
),
VitalyVakhteev marked this conversation as resolved.
Show resolved Hide resolved
"inline": True,
},
{
"name": "Alert",
"value": "{host}/alerts/{alert_id}".format(
host=host, alert_id=alert.id
),
VitalyVakhteev marked this conversation as resolved.
Show resolved Hide resolved
"inline": True,
},
]
if alert.options.get("custom_body"):
fields.append({"name": "Description", "value": alert.options["custom_body"]})
if alert.TRIGGERED_STATE == "triggered":
junnplus marked this conversation as resolved.
Show resolved Hide resolved
if alert.options.get("custom_subject"):
text = alert.options["custom_subject"]
else:
text = f"{alert.name} just triggered"
color = RED_ALERT_COLOR
junnplus marked this conversation as resolved.
Show resolved Hide resolved
else:
text = f"{alert.name} went back to normal"
color = GREEN_ALERT_COLOR
junnplus marked this conversation as resolved.
Show resolved Hide resolved

payload = {"content": text, "embeds": [{"color": color, "fields": fields}]}
headers = {"Content-Type": "application/json"}
try:
resp = requests.post(
options.get("url"),
data=json_dumps(payload),
headers=headers,
timeout=5.0,
)
if resp.status_code != 200 and resp.status_code != 204:
logging.error(
"webhook send ERROR. status_code => {status}".format(
VitalyVakhteev marked this conversation as resolved.
Show resolved Hide resolved
status=resp.status_code
)
)
except Exception as e:
logging.exception("webhook send ERROR: %s", e)
VitalyVakhteev marked this conversation as resolved.
Show resolved Hide resolved


register(Discord)
1 change: 1 addition & 0 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ def email_server_is_configured():
"redash.destinations.email",
"redash.destinations.slack",
"redash.destinations.webhook",
"redash.destinations.discord",
"redash.destinations.hipchat",
"redash.destinations.mattermost",
"redash.destinations.chatwork",
Expand Down
2 changes: 1 addition & 1 deletion requirements_all_ds.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pymongo[tls,srv]==4.3.3
vertica-python==1.1.1
td-client==1.0.0
pymssql==2.1.5
boto3>=1.10.0,<1.11.0
boto3>=1.14.0,<1.15.0
justinclift marked this conversation as resolved.
Show resolved Hide resolved
botocore>=1.13,<1.14.0
sasl>=0.1.3
thrift>=0.8.0
Expand Down
70 changes: 68 additions & 2 deletions tests/handlers/test_destinations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from tests import BaseTestCase
from mock import patch
import json
from unittest import mock

from redash.destinations.discord import Discord
from redash.models import NotificationDestination
from tests import BaseTestCase
from redash.destinations.slack import Slack


Expand Down Expand Up @@ -97,3 +99,67 @@ def test_post(self):
d = NotificationDestination.query.get(d.id)
self.assertEqual(d.name, data["name"])
self.assertEqual(d.options["url"], data["options"]["url"])


def test_discord_notify_calls_requests_post():
alert = mock.Mock(spec_set=["id", "name", "options", "render_template"])
alert.id = 1
alert.name = "Test Alert"
alert.options = {
"custom_subject": "Test custom subject",
"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 = {
"url": "https://discordapp.com/api/webhooks/test"
}

new_state = "triggered"
junnplus marked this conversation as resolved.
Show resolved Hide resolved
destination = Discord(options)

with mock.patch('redash.destinations.discord.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, options)

expected_payload = {
"content": "Test custom subject",
"embeds": [
{
"color": "12597547",
"fields": [
{
"name": "Query",
"value": f"{host}/queries/{query.id}",
"inline": True
},
{
"name": "Alert",
"value": f"{host}/alerts/{alert.id}",
"inline": True
},
{
"name": "Description",
"value": "Test custom body"
}
]
}
]
}

mock_post.assert_called_once_with(
"https://discordapp.com/api/webhooks/test",
data=json.dumps(expected_payload),
headers={"Content-Type": "application/json"},
timeout=5.0
)

assert mock_response.status_code == 204