Skip to content

Commit

Permalink
Feature/Add Discord Alert Destination (getredash#6106)
Browse files Browse the repository at this point in the history
* Add discord webhook

* Fix icon

* Boto3 dependency

* Add unit test for Discord webhook

* Add suggestions

* Apply suggestions from code review

Co-authored-by: Jun <junnplus@gmail.com>

* Misunderstood suggestion )

* Add suggestions

* Apply suggestions from code review

Co-authored-by: Jun <junnplus@gmail.com>

* Fix test

* Fix variables in strings

* Fix formatting using our pre-commit hook

---------

Co-authored-by: Jun <junnplus@gmail.com>
Co-authored-by: Justin Clift <justin@postgresql.org>
  • Loading branch information
3 people authored and harveyrendell committed Jan 8, 2025
1 parent 0ddd6f1 commit 6036d20
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 1 deletion.
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.
70 changes: 70 additions & 0 deletions redash/destinations/discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging

import requests

from redash.destinations import BaseDestination, register
from redash.models import Alert
from redash.utils import json_dumps

colors = {
# Colors are in a Decimal format as Discord requires them to be Decimals for embeds
Alert.OK_STATE: "2600544", # Green Decimal Code
Alert.TRIGGERED_STATE: "12597547", # Red Decimal Code
Alert.UNKNOWN_STATE: "16776960", # Yellow Decimal Code
}


class Discord(BaseDestination):
@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {"url": {"type": "string", "title": "Discord Webhook URL"}},
"secret": ["url"],
"required": ["url"],
}

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

def notify(self, alert, query, user, new_state, app, host, options):
# Documentation: https://birdie0.github.io/discord-webhooks-guide/discord_webhook.html
fields = [
{
"name": "Query",
"value": f"{host}/queries/{query.id}",
"inline": True,
},
{
"name": "Alert",
"value": f"{host}/alerts/{alert.id}",
"inline": True,
},
]
if alert.options.get("custom_body"):
fields.append({"name": "Description", "value": alert.options["custom_body"]})
if new_state == Alert.TRIGGERED_STATE:
if alert.options.get("custom_subject"):
text = alert.options["custom_subject"]
else:
text = f"{alert.name} just triggered"
else:
text = f"{alert.name} went back to normal"
color = colors.get(new_state)
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(f"Discord send ERROR. status_code => {resp.status_code}")
except Exception as e:
logging.exception("Discord send ERROR: %s", e)


register(Discord)
1 change: 1 addition & 0 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ def email_server_is_configured():
"redash.destinations.email",
"redash.destinations.slack",
"redash.destinations.webhook",
"redash.destinations.discord",
"redash.destinations.mattermost",
"redash.destinations.chatwork",
"redash.destinations.pagerduty",
Expand Down
57 changes: 56 additions & 1 deletion tests/handlers/test_destinations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from redash.models import NotificationDestination
import json
from unittest import mock

from redash.destinations.discord import Discord
from redash.models import Alert, NotificationDestination
from tests import BaseTestCase


Expand Down Expand Up @@ -86,3 +90,54 @@ 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 = Alert.TRIGGERED_STATE
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

0 comments on commit 6036d20

Please sign in to comment.