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

Added mapping from alert attributes to 1..n teams channel #358

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions plugins/msteams/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,47 @@ MS_TEAMS_WEBHOOK_URL = 'https://outlook.office.com/webhook/.../IncomingWebhook/
DASHBOARD_URL = 'http://try.alerta.io'
```

The `MS_TEAMS_ALERT_WEBHOOK_MAPPING` configuration variable can be used to form
a mapping based on attribute values from an alert to a specific teams webhook.
Also custom attributes are supported.
For example:
```python
MS_TEAMS_ALERT_WEBHOOK_MAPPING = [
{
"attributes" : {
"event": "NodeDown",
},
"ms_teams_webhook": 'https://outlook.office.com/webhook/.../IncomingWebhook/.../...'
},
{
"attributes" : {
"custom_attribute_1": "=~custom_value_1",
"custom_attribute_2": "custom_value_2",
"severity": "critical",
},
"ms_teams_webhook": 'https://outlook.office.com/webhook/.../IncomingWebhook/.../...'
}
]
```
### Regex support

If the value of an attribute is prefixed with __=~__ the expression will be evaluated as RegEx.

If all attributes of the alert are matched for multiple mappings,
the alert is sent to each matching channel.

### Default Channel

The `MS_TEAMS_WEBHOOK_URL` configuration variable can be used to set a default MS Teams webhook, wich will catch all alerts. It will not affect the mappings defined in `MS_TEAMS_ALERT_WEBHOOK_MAPPING`.


The `MS_TEAMS_SUMMARY_FMT` configuration variable is a Jinja2 template
string or filename to a template file and accepts any Jinja2 syntax.
The formatter has access to two variables in the template environment,
'alert' for all alert details and 'config' for access to the alerta
configuration.


If you have Jinja2 available you can try customizing the message like
this:

Expand Down
88 changes: 79 additions & 9 deletions plugins/msteams/alerta_msteams.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import requests
import pymsteams
import re

try:
from alerta.plugins import app # alerta >= 5.0
Expand All @@ -27,13 +28,16 @@
MS_TEAMS_DEFAULT_COLOR = '00AA5A'
MS_TEAMS_DEFAULT_TIMEOUT = 7 # pymsteams http_timeout

REGEX_PREFIX = "=~"

class SendConnectorCardMessage(PluginBase):

def __init__(self, name=None):
# override user-defined severities(colors)
self._colors = MS_TEAMS_DEFAULT_COLORS_MAP
self._colors.update(MS_TEAMS_COLORS_MAP)


super(SendConnectorCardMessage, self).__init__(name)

def _load_template(self, templateFmt):
Expand All @@ -60,6 +64,13 @@ def post_receive(self, alert, **kwargs):
MS_TEAMS_APIKEY = self.get_config('MS_TEAMS_APIKEY', default=None, type=str, **kwargs) # X-API-Key (needs webhook.write permission)
DASHBOARD_URL = self.get_config('DASHBOARD_URL', default='', type=str, **kwargs)

MS_TEAMS_ALERT_WEBHOOK_MAP = self.get_config("MS_TEAMS_ALERT_WEBHOOK_MAP", default=[], type=list, **kwargs)

ms_teams_webhooks = self._get_ms_teams_webhooks(MS_TEAMS_ALERT_WEBHOOK_MAP, alert)

if len(ms_teams_webhooks) <= 0 and not MS_TEAMS_WEBHOOK_URL:
return alert

if alert.repeat:
return

Expand Down Expand Up @@ -123,19 +134,78 @@ def post_receive(self, alert, **kwargs):
try:
if MS_TEAMS_PAYLOAD:
# Use requests.post to send raw json message card
LOG.debug("MS Teams sending(json payload) POST to %s", MS_TEAMS_WEBHOOK_URL)
r = requests.post(MS_TEAMS_WEBHOOK_URL, data=card_json, timeout=MS_TEAMS_DEFAULT_TIMEOUT)
LOG.debug('MS Teams response: %s / %s' % (r.status_code, r.text))
if MS_TEAMS_WEBHOOK_URL:
LOG.debug("MS Teams sending(json payload) POST to %s", MS_TEAMS_WEBHOOK_URL)
r = requests.post(MS_TEAMS_WEBHOOK_URL, data=card_json, timeout=MS_TEAMS_DEFAULT_TIMEOUT)
LOG.debug('MS Teams response: %s / %s' % (r.status_code, r.text))
if ms_teams_webhooks:
for webhook in ms_teams_webhooks:
LOG.debug("MS Teams sending(json payload) POST to %s", webhook)
r = requests.post(webhook, data=card_json, timeout=MS_TEAMS_DEFAULT_TIMEOUT)
LOG.debug('MS Teams response: %s / %s' % (r.status_code, r.text))
else:
# Use pymsteams to send card
msTeamsMessage = pymsteams.connectorcard(hookurl=MS_TEAMS_WEBHOOK_URL, http_timeout=MS_TEAMS_DEFAULT_TIMEOUT)
msTeamsMessage.title(summary)
msTeamsMessage.text(text)
msTeamsMessage.addLinkButton("Open in Alerta", url)
msTeamsMessage.color(color)
msTeamsMessage.send()
if MS_TEAMS_WEBHOOK_URL:
self._send_card(MS_TEAMS_WEBHOOK_URL, summary, text, url, color)
if ms_teams_webhooks:
for webhook in ms_teams_webhooks:
self._send_card(webhook, summary, text, url, color)
except Exception as e:
raise RuntimeError("MS Teams: ERROR - %s", e)

def _send_card(self, webhook_url, summary, text, url, color):
LOG.debug("MS Teams sending(json payload) POST to %s", webhook_url)
msTeamsMessage = pymsteams.connectorcard(hookurl=webhook_url, http_timeout=MS_TEAMS_DEFAULT_TIMEOUT)
msTeamsMessage.title(summary)
msTeamsMessage.text(text)
msTeamsMessage.addLinkButton("Open in Alerta", url)
msTeamsMessage.color(color)
msTeamsMessage.send()

def status_change(self, alert, status, text, **kwargs):
return

def _get_ms_teams_webhooks(self, webhook_mappings, alert):
webhooks = list()
for mapping in webhook_mappings:
LOG.debug(mapping['attributes'])
attributes_match = self._match_attributes(mapping, alert)
if attributes_match:
webhooks.append(mapping['ms_teams_webhook'])
return webhooks

def _match_attributes(self, mapping, alert):
for k, v in mapping['attributes'].items():
LOG.debug(f"{k} = {v}")
if not hasattr(alert, k) and k not in alert.attributes:
LOG.debug(f"Attribute {k} does not exist!")
return False
if v.startswith(REGEX_PREFIX):
match = self._match_attribute_regex(alert, k, v)
if not match:
return False
else:
match = self._match_attribute_str(alert, k, v)
if not match:
return False
return True

def _match_attribute_regex(self, alert, attr_name, regex_pattern):
# Check if attribute exists as default alerta variable, if not check if
# attribute is in custom defined attributes array of the alert
pattern = self.remove_prefix(regex_pattern, REGEX_PREFIX)
if hasattr(alert, attr_name):
return re.fullmatch(pattern, getattr(attr_name))
return re.fullmatch(pattern, alert.attributes.get(attr_name))

def _match_attribute_str(self,alert, attr_name, attr_value):
# Check if attribute exists as default alerta variable, if not check if
# attribute is in custom defined attributes array of the alert
if hasattr(alert, attr_name) and getattr(alert, attr_name) == attr_value:
return True
return alert.attributes.get(attr_name) == attr_value

def remove_prefix(self, text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return text