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 - ConfigDBTime
- SendNotificationTime - 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 Future Considerations
===================== =====================
@ -146,3 +234,7 @@ Future Considerations
NotificationEngine instance using webhooks to a local http server. Is NotificationEngine instance using webhooks to a local http server. Is
that fast enough? that fast enough?
- Are we putting too much load on Kafka at ~200 commits per second? - 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
import requests import requests
from six.moves import urllib from six.moves import urllib
import ujson as json import ujson as json
from debtcollector import removals from debtcollector import removals
import jinja2
from oslo_config import cfg from oslo_config import cfg
from monasca_notification.plugins import abstract_notifier from monasca_notification.plugins import abstract_notifier
@ -86,10 +88,19 @@ class SlackNotifier(abstract_notifier.AbstractNotifier):
'metrics': notification.metrics} 'metrics': notification.metrics}
slack_request = {} slack_request = {}
slack_request['text'] = json.dumps(body, indent=3) 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 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): def _check_response(self, result):
if 'application/json' in result.headers.get('Content-Type'): if 'application/json' in result.headers.get('Content-Type'):
response = result.json() response = result.json()
@ -192,7 +203,8 @@ slack_notifier_opts = [
cfg.IntOpt(name='timeout', default=5, min=1), cfg.IntOpt(name='timeout', default=5, min=1),
cfg.BoolOpt(name='insecure', default=True), cfg.BoolOpt(name='insecure', default=True),
cfg.StrOpt(name='ca_certs', default=None), 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): def _validate_post_args(self, post_args, data_format):
self.assertEqual(slack_text(), self.assertEqual(slack_text(),
json.loads(post_args.get(data_format).get('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'}, self.assertEqual({'https': 'http://yourid:password@proxyserver:8080'},
post_args.get('proxies')) post_args.get('proxies'))
self.assertEqual(50, post_args.get('timeout')) 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._validate_post_args(mock_method.call_args_list[0][1], 'json')
self.assertEqual([], slack_notifier.SlackNotifier._raw_data_url_caches) 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): def test_slack_webhook_fail(self):
"""data is sent twice as json and raw data, and slack returns failure for """data is sent twice as json and raw data, and slack returns failure for
both requests both requests