Templates for Slack notifications

This change adds an optional, user configurable template which
may be used to format the text contained in Slack notifications.

Story: 2001308
Task: 5859
Change-Id: Id936c3dc8b4f3e2430de20c8b69d0e703b1cf9ef
This commit is contained in:
Doug Szumski 2017-11-14 11:37:09 +00:00
parent ac837fa3cc
commit 39a906b8fb
4 changed files with 134 additions and 2 deletions

View File

@ -131,6 +131,94 @@ StatsD server launched by monasca-agent. Default host and port points to
- ConfigDBTime
- SendNotificationTime
Plugins
-------
The following notification plugins are available:
- Email
- HipChat
- Jira
- Pagerduty
- Slack
- Webhook
The plugins can be configured via the Monasca Notification config file. In
general you will need to follow these steps to enable a plugin:
- Make sure that the plugin is enabled in the config file
- Make sure that the plugin is configured in the config file
- Restart the Monasca Notification service
Slack plugin
~~~~~~~~~~~~
To use the Slack plugin you must first configure an incoming `webhook`_
for the Slack channel you wish to post notifications to. The notification can
then be created as follows:
::
monasca notification-create slack_notification slack https://hooks.slack.com/services/MY/SECRET/WEBHOOK/URL
Note that whilst it is also possible to use a token instead of a webhook,
this approach is now `deprecated`_.
By default the Slack notification will dump all available information into
the alert. For example, a notification may be posted to Slack which looks
like this:
::
{
"metrics":[
{
"dimensions":{
"hostname":"operator"
},
"id":null,
"name":"cpu.user_perc"
}
],
"alarm_id":"20a54a65-44b8-4ac9-a398-1f2d888827d2",
"state":"ALARM",
"alarm_timestamp":1556703552,
"tenant_id":"62f7a7a314904aa3ab137d569d6b4fde",
"old_state":"OK",
"alarm_description":"Dummy alarm",
"message":"Thresholds were exceeded for the sub-alarms: count(cpu.user_perc, deterministic) >= 1.0 with the values: [1.0]",
"alarm_definition_id":"78ce7b53-f7e6-4b51-88d0-cb741e7dc906",
"alarm_name":"dummy_alarm"
}
The format of the above message can be customised with a Jinja template. All fields
from the raw Slack message are available in the template. For example, you may
configure the plugin as follows:
::
[notification_types]
enabled = slack
[slack_notifier]
message_template = /etc/monasca/slack_template.j2
timeout = 10
ca_certs = /etc/ssl/certs/ca-bundle.crt
insecure = False
With the following contents of `/etc/monasca/slack_template.j2`:
::
{{ alarm_name }} has triggered on {% for item in metrics %}host {{ item.dimensions.hostname }}{% if not loop.last %}, {% endif %}{% endfor %}.
With this configuration, the raw Slack message above would be transformed
into:
::
dummy_alarm has triggered on host(s): operator.
Future Considerations
=====================
@ -146,3 +234,7 @@ Future Considerations
NotificationEngine instance using webhooks to a local http server. Is
that fast enough?
- Are we putting too much load on Kafka at ~200 commits per second?
.. _webhook: https://api.slack.com/incoming-webhooks
.. _deprecated: https://api.slack.com/custom-integrations/legacy-tokens

View File

@ -14,11 +14,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import requests
from six.moves import urllib
import ujson as json
from debtcollector import removals
import jinja2
from oslo_config import cfg
from monasca_notification.plugins import abstract_notifier
@ -86,10 +88,19 @@ class SlackNotifier(abstract_notifier.AbstractNotifier):
'metrics': notification.metrics}
slack_request = {}
if CONF.slack_notifier.message_template:
slack_request['text'] = self._render_message_template(body)
else:
slack_request['text'] = json.dumps(body, indent=3)
return slack_request
def _render_message_template(self, params):
path, name = os.path.split(CONF.slack_notifier.message_template)
loader = jinja2.FileSystemLoader(path)
env = jinja2.Environment(loader=loader, autoescape=True)
return env.get_template(name).render(params)
def _check_response(self, result):
if 'application/json' in result.headers.get('Content-Type'):
response = result.json()
@ -192,7 +203,8 @@ slack_notifier_opts = [
cfg.IntOpt(name='timeout', default=5, min=1),
cfg.BoolOpt(name='insecure', default=True),
cfg.StrOpt(name='ca_certs', default=None),
cfg.StrOpt(name='proxy', default=None)
cfg.StrOpt(name='proxy', default=None),
cfg.StrOpt(name='message_template', default=None)
]

View File

@ -0,0 +1 @@
{{ alarm_name }} has triggered on {% for item in metrics %}host {{ metrics[0].dimensions.hostname }}, service {{ item.dimensions.service }}{% if not loop.last %}, {% endif %}{% endfor %}.

View File

@ -110,6 +110,15 @@ class TestSlack(base.PluginTestCase):
def _validate_post_args(self, post_args, data_format):
self.assertEqual(slack_text(),
json.loads(post_args.get(data_format).get('text')))
self._validate_post_args_base(post_args)
def _validate_templated_post_args(self, post_args, data_format,
expected_slack_text):
self.assertEqual(expected_slack_text,
post_args.get(data_format).get('text'))
self._validate_post_args_base(post_args)
def _validate_post_args_base(self, post_args):
self.assertEqual({'https': 'http://yourid:password@proxyserver:8080'},
post_args.get('proxies'))
self.assertEqual(50, post_args.get('timeout'))
@ -128,6 +137,24 @@ class TestSlack(base.PluginTestCase):
self._validate_post_args(mock_method.call_args_list[0][1], 'json')
self.assertEqual([], slack_notifier.SlackNotifier._raw_data_url_caches)
def test_templated_slack_webhook_success(self):
"""A templated message is successfully sent as json
"""
self.conf_override(
message_template='tests/resources/test_slackformat.template',
group='slack_notifier'
)
response_list = [RequestsResponse(200, 'ok',
{'Content-Type': 'application/text'})]
mock_method, result = self._notify(response_list)
self.assertTrue(result)
mock_method.assert_called_once()
expected_text = 'test Alarm has triggered on host foo1, service bar1.'
self._validate_templated_post_args(mock_method.call_args_list[0][1],
'json',
expected_text)
self.assertEqual([], slack_notifier.SlackNotifier._raw_data_url_caches)
def test_slack_webhook_fail(self):
"""data is sent twice as json and raw data, and slack returns failure for
both requests