mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-10 08:01:17 +00:00
Mattermost Alert Backend
W.r.t. the user-contributed version, this: * removes commented-out code * removes channels (not supported at the UI level) * removes all other things Said differently: the mattermost Alert Service Backend is simply the Slack version with edits to make it mattermost-specific (and nothing else). (In a few places I've edited the slack backend to make comparing easier). Fix #277 See #253
This commit is contained in:
20
alerts/migrations/0004_alter_messagingserviceconfig_kind.py
Normal file
20
alerts/migrations/0004_alter_messagingserviceconfig_kind.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import alerts.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("alerts", "0003_messagingserviceconfig_last_failure_error_message_and_more"),
|
||||
]
|
||||
|
||||
# This is the "once and for all" migration since we depend on kinds_choices rather than a list now
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="messagingserviceconfig",
|
||||
name="kind",
|
||||
field=models.CharField(
|
||||
choices=alerts.models.kind_choices, default="slack", max_length=20
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -6,7 +6,8 @@ from .service_backends.mattermost import MattermostBackend
|
||||
|
||||
|
||||
def kind_choices():
|
||||
# no 18n needed for no
|
||||
# As a callable to avoid non-DB-affecting migrations for adding new kinds.
|
||||
# Messaging backends don't need translations since they are brand names.
|
||||
return [
|
||||
("slack", "Slack"),
|
||||
("mattermost", "Mattermost"),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import requests
|
||||
from string import Template
|
||||
from django.utils import timezone
|
||||
|
||||
from django import forms
|
||||
@@ -13,37 +12,10 @@ from bugsink.transaction import immediate_atomic
|
||||
from issues.models import Issue
|
||||
|
||||
|
||||
def default_format_title():
|
||||
return "$alert_reason issue"
|
||||
|
||||
|
||||
def default_format_text():
|
||||
return "[$issue_title]($issue_url)"
|
||||
|
||||
|
||||
class MattermostConfigForm(forms.Form):
|
||||
# NOTE: As of yet this code isn't plugged into the UI (because it requires dynamic loading of the config-specific
|
||||
# form)
|
||||
webhook_url = forms.URLField(required=True)
|
||||
channel = forms.CharField(
|
||||
required=False,
|
||||
help_text='Optional: Override channel (e.g., "town-square" or "@username" for DMs)',
|
||||
)
|
||||
|
||||
# format_title = forms.CharField(
|
||||
# required=False,
|
||||
# max_length=200,
|
||||
# help_text='Title template using $variable syntax (e.g., "$alert_reason issue"). '
|
||||
# "Available: $alert_reason, $project, $issue_url, $issue_title, $unmute_reason, "
|
||||
# "$release, $environment. "
|
||||
# "Leave empty for default.",
|
||||
# )
|
||||
# format_text = forms.CharField(
|
||||
# required=False,
|
||||
# widget=forms.Textarea(attrs={"rows": 3}),
|
||||
# help_text='Text template using $variable syntax (e.g., "$project\\n$issue_url"). '
|
||||
# "Available: $alert_reason, $project, $issue_url, $issue_title, $unmute_reason, "
|
||||
# "$release, $environment. "
|
||||
# "Leave empty for default.",
|
||||
# )
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
config = kwargs.pop("config", None)
|
||||
@@ -51,40 +23,16 @@ class MattermostConfigForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
if config:
|
||||
self.fields["webhook_url"].initial = config.get("webhook_url", "")
|
||||
self.fields["channel"].initial = config.get("channel", "")
|
||||
# self.fields["format_title"].initial = config.get(
|
||||
# "format_title", default_format_title()
|
||||
# )
|
||||
# self.fields["format_text"].initial = config.get(
|
||||
# "format_text", default_format_text()
|
||||
# )
|
||||
|
||||
def get_config(self):
|
||||
config = {
|
||||
return {
|
||||
"webhook_url": self.cleaned_data.get("webhook_url"),
|
||||
}
|
||||
if self.cleaned_data.get("channel"):
|
||||
config["channel"] = self.cleaned_data.get("channel")
|
||||
|
||||
# config["format_title"] = (
|
||||
# self.cleaned_data.get("format_title") or default_format_title()
|
||||
# )
|
||||
# config["format_text"] = (
|
||||
# self.cleaned_data.get("format_text") or default_format_text()
|
||||
# )
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _safe_markdown(text):
|
||||
# Mattermost uses similar markdown escaping as Slack
|
||||
return (
|
||||
text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("*", "\\*")
|
||||
.replace("_", "\\_")
|
||||
)
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("*", "\\*").replace("_", "\\_")
|
||||
|
||||
|
||||
def _store_failure_info(service_config_id, exception, response=None):
|
||||
@@ -102,9 +50,7 @@ def _store_failure_info(service_config_id, exception, response=None):
|
||||
# Handle requests-specific errors
|
||||
if response is not None:
|
||||
config.last_failure_status_code = response.status_code
|
||||
config.last_failure_response_text = response.text[
|
||||
:2000
|
||||
] # Limit response text size
|
||||
config.last_failure_response_text = response.text[:2000] # Limit response text size
|
||||
|
||||
# Check if response is JSON
|
||||
try:
|
||||
@@ -138,24 +84,27 @@ def _store_success_info(service_config_id):
|
||||
pass
|
||||
|
||||
|
||||
def _send_mattermost_message(
|
||||
webhook_url, service_config_id, title, text, color="#36a64f", channel=None
|
||||
):
|
||||
"""Send a message to Mattermost using attachments format"""
|
||||
data = {
|
||||
"text": text[:100], # Fallback text
|
||||
"attachments": [
|
||||
{
|
||||
"fallback": title,
|
||||
"color": color,
|
||||
"title": title,
|
||||
"text": text,
|
||||
}
|
||||
],
|
||||
}
|
||||
@shared_task
|
||||
def mattermost_backend_send_test_message(webhook_url, project_name, display_name, service_config_id):
|
||||
# See https://developers.mattermost.com/integrate/reference/message-attachments/
|
||||
|
||||
if channel:
|
||||
data["channel"] = channel
|
||||
data = {"text": "### Test message by Bugsink to test the webhook setup.",
|
||||
"attachments": [
|
||||
{
|
||||
"title": "TEST issue",
|
||||
"text": "Test message by Bugsink to test the webhook setup.",
|
||||
"fields": [
|
||||
{
|
||||
"title": "project",
|
||||
"value": _safe_markdown(project_name),
|
||||
},
|
||||
{
|
||||
"title": "message backend",
|
||||
"value": _safe_markdown(display_name),
|
||||
},
|
||||
]
|
||||
}
|
||||
]}
|
||||
|
||||
try:
|
||||
result = requests.post(
|
||||
@@ -169,66 +118,67 @@ def _send_mattermost_message(
|
||||
|
||||
_store_success_info(service_config_id)
|
||||
except requests.RequestException as e:
|
||||
response = getattr(e, "response", None)
|
||||
response = getattr(e, 'response', None)
|
||||
_store_failure_info(service_config_id, e, response)
|
||||
|
||||
except Exception as e:
|
||||
_store_failure_info(service_config_id, e)
|
||||
|
||||
|
||||
@shared_task
|
||||
def mattermost_backend_send_test_message(
|
||||
webhook_url, project_name, display_name, service_config_id, channel=None
|
||||
):
|
||||
title = "TEST issue"
|
||||
text = (
|
||||
f"Test message by Bugsink to test the webhook setup.\n\n"
|
||||
f"**project**: {_safe_markdown(project_name)}\n"
|
||||
f"**name**: {_safe_markdown(display_name)}"
|
||||
)
|
||||
|
||||
_send_mattermost_message(webhook_url, service_config_id, title, text, channel)
|
||||
|
||||
|
||||
@shared_task
|
||||
def mattermost_backend_send_alert(
|
||||
webhook_url,
|
||||
issue_id,
|
||||
state_description,
|
||||
alert_article,
|
||||
alert_reason,
|
||||
service_config_id,
|
||||
channel=None,
|
||||
unmute_reason=None,
|
||||
format_title=None,
|
||||
format_text=None,
|
||||
):
|
||||
webhook_url, issue_id, state_description, alert_article, alert_reason, service_config_id, unmute_reason=None):
|
||||
|
||||
issue = Issue.objects.get(id=issue_id)
|
||||
|
||||
issue_url = get_settings().BASE_URL + issue.get_absolute_url()
|
||||
link_text = _safe_markdown(truncatechars(issue.title(), 200))
|
||||
link = f"<{issue_url}|" + _safe_markdown(truncatechars(issue.title().replace("|", ""), 200)) + ">"
|
||||
|
||||
latest_event = issue.event_set.order_by("-digest_order").first()
|
||||
release = latest_event.release if latest_event else ""
|
||||
environment = latest_event.environment if latest_event else ""
|
||||
data = {"text": "### " + _safe_markdown(truncatechars(issue.title().replace("|", ""), 200)),
|
||||
"attachments": [
|
||||
{
|
||||
"title": f"{alert_reason} issue",
|
||||
"text": link,
|
||||
"fields": [],
|
||||
}
|
||||
]}
|
||||
|
||||
template_context = {
|
||||
"alert_reason": alert_reason,
|
||||
"project": _safe_markdown(issue.project.name),
|
||||
"issue_url": issue_url,
|
||||
"issue_title": link_text,
|
||||
"unmute_reason": unmute_reason or "",
|
||||
"release": _safe_markdown(release),
|
||||
"environment": _safe_markdown(environment),
|
||||
}
|
||||
if unmute_reason:
|
||||
data["attachments"][0]["text"] += "\n\n" + _safe_markdown(unmute_reason)
|
||||
|
||||
title = Template(format_title).safe_substitute(template_context)
|
||||
text = Template(format_text).safe_substitute(template_context)
|
||||
color = "#ff0000" if alert_reason == "NEW" else "#ff9900"
|
||||
# assumption: visavis email, project.name is of less importance, because in slack-like things you may (though not
|
||||
# always) do one-channel per project. more so for site_title (if you have multiple Bugsinks, you'll surely have
|
||||
# multiple slack channels)
|
||||
fields = [{
|
||||
"title": "Project",
|
||||
"value": _safe_markdown(issue.project.name),
|
||||
}]
|
||||
|
||||
_send_mattermost_message(
|
||||
webhook_url, service_config_id, title, text, color, channel
|
||||
)
|
||||
# left as a (possible) TODO, because the amount of refactoring (passing event to this function) is too big for now
|
||||
# if event.release:
|
||||
# fields.append({"title": "Release", "value": _safe_markdown(event.release)})
|
||||
# if event.environment:
|
||||
# fields.append("title": "Environment", "value": _safe_markdown(event.environment)})
|
||||
|
||||
data["attachments"][0]["fields"] += fields
|
||||
|
||||
try:
|
||||
result = requests.post(
|
||||
webhook_url,
|
||||
data=json.dumps(data),
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
result.raise_for_status()
|
||||
|
||||
_store_success_info(service_config_id)
|
||||
except requests.RequestException as e:
|
||||
response = getattr(e, 'response', None)
|
||||
_store_failure_info(service_config_id, e, response)
|
||||
|
||||
except Exception as e:
|
||||
_store_failure_info(service_config_id, e)
|
||||
|
||||
|
||||
class MattermostBackend:
|
||||
@@ -245,12 +195,9 @@ class MattermostBackend:
|
||||
self.service_config.project.name,
|
||||
self.service_config.display_name,
|
||||
self.service_config.id,
|
||||
channel=config.get("channel"),
|
||||
)
|
||||
|
||||
def send_alert(
|
||||
self, issue_id, state_description, alert_article, alert_reason, **kwargs
|
||||
):
|
||||
def send_alert(self, issue_id, state_description, alert_article, alert_reason, **kwargs):
|
||||
config = json.loads(self.service_config.config)
|
||||
mattermost_backend_send_alert.delay(
|
||||
config["webhook_url"],
|
||||
@@ -259,10 +206,5 @@ class MattermostBackend:
|
||||
alert_article,
|
||||
alert_reason,
|
||||
self.service_config.id,
|
||||
channel=config.get("channel"),
|
||||
format_title=default_format_title(),
|
||||
format_text=default_format_text(),
|
||||
# format_title=config.get("format_title", default_format_title()),
|
||||
# format_text=config.get("format_text", default_format_text()),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -151,6 +151,7 @@ def slack_backend_send_alert(
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
# TODO arguably issue.title() should get this location; "later" because I don't have a test env.
|
||||
"text": f"{alert_reason} issue",
|
||||
},
|
||||
},
|
||||
@@ -218,7 +219,6 @@ def slack_backend_send_alert(
|
||||
|
||||
|
||||
class SlackBackend:
|
||||
|
||||
def __init__(self, service_config):
|
||||
self.service_config = service_config
|
||||
|
||||
@@ -226,14 +226,22 @@ class SlackBackend:
|
||||
return SlackConfigForm
|
||||
|
||||
def send_test_message(self):
|
||||
config = json.loads(self.service_config.config)
|
||||
slack_backend_send_test_message.delay(
|
||||
json.loads(self.service_config.config)["webhook_url"],
|
||||
config["webhook_url"],
|
||||
self.service_config.project.name,
|
||||
self.service_config.display_name,
|
||||
self.service_config.id,
|
||||
)
|
||||
|
||||
def send_alert(self, issue_id, state_description, alert_article, alert_reason, **kwargs):
|
||||
config = json.loads(self.service_config.config)
|
||||
slack_backend_send_alert.delay(
|
||||
json.loads(self.service_config.config)["webhook_url"],
|
||||
issue_id, state_description, alert_article, alert_reason, self.service_config.id, **kwargs)
|
||||
config["webhook_url"],
|
||||
issue_id,
|
||||
state_description,
|
||||
alert_article,
|
||||
alert_reason,
|
||||
self.service_config.id,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user