Added a field 'Grafana Url' in the email
Retrieve the grafana_url field from notification.yaml and append the query for metric info and timestamp. Unit tests for method get_link_url, adjust existing unit tests, python3 tests. Change-Id: Ie0e98f3df48eb68caad232e5b9293222d7c946c8 Story: 2001052 Task: 4652
This commit is contained in:
parent
46a36adde0
commit
347606ac54
|
@ -16,17 +16,21 @@
|
|||
import email.header
|
||||
import email.mime.text
|
||||
import email.utils
|
||||
import six
|
||||
import smtplib
|
||||
import time
|
||||
|
||||
|
||||
from monasca_notification.plugins import abstract_notifier
|
||||
|
||||
EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message}
|
||||
|
||||
Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
|
||||
alarm_id: {alarm_id}
|
||||
|
||||
Lifecycle state: {lifecycle_state}
|
||||
Link: {link}
|
||||
Link to Grafana: {grafana_url}
|
||||
|
||||
With dimensions:
|
||||
{metric_dimensions}'''
|
||||
|
@ -35,8 +39,10 @@ EMAIL_MULTIPLE_HOST_BASE = u'''On host "{hostname}" {message}
|
|||
|
||||
Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
|
||||
alarm_id: {alarm_id}
|
||||
|
||||
Lifecycle state: {lifecycle_state}
|
||||
Link: {link}
|
||||
Link to Grafana: {grafana_url}
|
||||
|
||||
With dimensions:
|
||||
{metric_dimensions}'''
|
||||
|
@ -45,8 +51,10 @@ EMAIL_NO_HOST_BASE = u'''On multiple hosts {message}
|
|||
|
||||
Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
|
||||
Alarm_id: {alarm_id}
|
||||
|
||||
Lifecycle state: {lifecycle_state}
|
||||
Link: {link}
|
||||
Link to Grafana: {grafana_url}
|
||||
|
||||
With dimensions
|
||||
{metric_dimensions}'''
|
||||
|
@ -152,6 +160,12 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
|
|||
be treated as type #2.
|
||||
"""
|
||||
timestamp = time.asctime(time.gmtime(notification.alarm_timestamp))
|
||||
|
||||
alarm_seconds = notification.alarm_timestamp
|
||||
alarm_ms = int(round(alarm_seconds * 1000))
|
||||
|
||||
graf_url = self._get_link_url(notification.metrics[0], alarm_ms)
|
||||
|
||||
dimensions = _format_dimensions(notification)
|
||||
|
||||
if len(hostname) == 1: # Type 1
|
||||
|
@ -166,6 +180,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
|
|||
alarm_id=notification.alarm_id,
|
||||
metric_dimensions=dimensions,
|
||||
link=notification.link,
|
||||
grafana_url=graf_url,
|
||||
lifecycle_state=notification.lifecycle_state
|
||||
)
|
||||
subject = u'{} {} "{}" for Host: {} Target: {}'.format(
|
||||
|
@ -183,6 +198,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
|
|||
alarm_id=notification.alarm_id,
|
||||
metric_dimensions=dimensions,
|
||||
link=notification.link,
|
||||
grafana_url=graf_url,
|
||||
lifecycle_state=notification.lifecycle_state
|
||||
)
|
||||
subject = u'{} {} "{}" for Host: {}'.format(
|
||||
|
@ -197,6 +213,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
|
|||
alarm_id=notification.alarm_id,
|
||||
metric_dimensions=dimensions,
|
||||
link=notification.link,
|
||||
grafana_url=graf_url,
|
||||
lifecycle_state=notification.lifecycle_state
|
||||
)
|
||||
subject = u'{} {} "{}" '.format(notification.state,
|
||||
|
@ -211,6 +228,38 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
|
|||
|
||||
return msg
|
||||
|
||||
def _get_link_url(self, metric, timestamp_ms):
|
||||
"""Returns the url to Grafana including a query with the
|
||||
respective metric info (name, dimensions, timestamp)
|
||||
:param metric: the metric for which to display the graph in Grafana
|
||||
:param timestamp_ms: timestamp of the alarm for the metric in milliseconds
|
||||
:return: the url to the graph for the given metric or None if no Grafana host
|
||||
has been defined.
|
||||
"""
|
||||
|
||||
grafana_url = self._config.get('grafana_url', None)
|
||||
if grafana_url is None:
|
||||
return None
|
||||
|
||||
url = ''
|
||||
metric_query = ''
|
||||
|
||||
metric_query = "?metric=%s" % metric['name']
|
||||
|
||||
dimensions = metric['dimensions']
|
||||
for key, value in six.iteritems(dimensions):
|
||||
metric_query += "&dim_%s=%s" % (key, value)
|
||||
|
||||
# Show the graph within a range of ten minutes before and after the alarm occurred.
|
||||
offset = 600000
|
||||
from_ms = timestamp_ms - offset
|
||||
to_ms = timestamp_ms + offset
|
||||
time_query = "&from=%s&to=%s" % (from_ms, to_ms)
|
||||
|
||||
url = grafana_url + '/dashboard/script/drilldown.js'
|
||||
|
||||
return url + metric_query + time_query
|
||||
|
||||
|
||||
def _format_dimensions(notification):
|
||||
dimension_sets = []
|
||||
|
|
|
@ -44,6 +44,7 @@ notification_types:
|
|||
password:
|
||||
timeout: 60
|
||||
from_addr: monasca-notification@none.invalid
|
||||
grafana_url: 'http://127.0.0.1:3000'
|
||||
|
||||
webhook:
|
||||
timeout: 5
|
||||
|
|
|
@ -24,6 +24,14 @@ import unittest
|
|||
|
||||
import six
|
||||
|
||||
import datetime
|
||||
|
||||
if six.PY2:
|
||||
import urlparse
|
||||
else:
|
||||
from urllib import parse
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from monasca_notification.notification import Notification
|
||||
from monasca_notification.plugins import email_notifier
|
||||
|
||||
|
@ -114,7 +122,8 @@ class TestEmail(unittest.TestCase):
|
|||
'user': None,
|
||||
'password': None,
|
||||
'timeout': 60,
|
||||
'from_addr': 'hpcs.mon@hp.com'}
|
||||
'from_addr': 'hpcs.mon@hp.com',
|
||||
'grafana_url': 'http://127.0.0.1:3000'}
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
@ -134,7 +143,6 @@ class TestEmail(unittest.TestCase):
|
|||
mock_log.error = self.trap.append
|
||||
|
||||
email = email_notifier.EmailNotifier(mock_log)
|
||||
|
||||
email.config(self.email_config)
|
||||
|
||||
alarm_dict = alarm(metric)
|
||||
|
@ -148,7 +156,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': u'foo1' + UNICODE_CHAR, u'service' + UNICODE_CHAR: 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent',
|
||||
'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
|
||||
u'service' + UNICODE_CHAR: 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
self.notify(self._smtpStub, metrics)
|
||||
|
@ -176,9 +186,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
|
||||
u'service' + UNICODE_CHAR: 'bar1',
|
||||
u'target_host': u'some_where'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
|
||||
u'service' + UNICODE_CHAR: 'bar1',
|
||||
u'target_host': u'some_where'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
self.notify(self._smtpStub, metrics)
|
||||
|
@ -204,9 +214,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
self.notify(self._smtpStub, metrics)
|
||||
|
@ -232,9 +242,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
mock_log = mock.MagicMock()
|
||||
|
@ -276,9 +286,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
mock_log = mock.MagicMock()
|
||||
|
@ -324,9 +334,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
mock_log = mock.MagicMock()
|
||||
|
@ -366,9 +376,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
mock_log = mock.MagicMock()
|
||||
|
@ -406,9 +416,9 @@ class TestEmail(unittest.TestCase):
|
|||
"""
|
||||
|
||||
metrics = []
|
||||
metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric_data)
|
||||
metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
|
||||
metrics.append(metric_data)
|
||||
|
||||
mock_log = mock.MagicMock()
|
||||
|
@ -438,3 +448,77 @@ class TestEmail(unittest.TestCase):
|
|||
|
||||
self.assertNotIn("SMTP server disconnected. Will reconnect and retry message.", self.trap)
|
||||
self.assertIn("Error sending Email Notification", self.trap)
|
||||
|
||||
@mock.patch('monasca_notification.plugins.email_notifier.smtplib')
|
||||
def test_get_link_url(self, mock_smtp):
|
||||
# Given one metric with name and dimensions
|
||||
metrics = []
|
||||
metric = {'name': 'cpu.percent',
|
||||
'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
|
||||
metrics.append(metric)
|
||||
|
||||
mock_log = mock.MagicMock()
|
||||
mock_log.warn = self.trap.append
|
||||
mock_log.error = self.trap.append
|
||||
mock_log.debug = self.trap.append
|
||||
mock_log.info = self.trap.append
|
||||
mock_log.exception = self.trap.append
|
||||
|
||||
mock_smtp.SMTP.return_value = mock_smtp
|
||||
mock_smtp.sendmail.side_effect = smtplib.SMTPException
|
||||
|
||||
mock_smtp.SMTPServerDisconnected = smtplib.SMTPServerDisconnected
|
||||
mock_smtp.SMTPException = smtplib.SMTPException
|
||||
|
||||
email = email_notifier.EmailNotifier(mock_log)
|
||||
email.config(self.email_config)
|
||||
|
||||
# Create alarm timestamp and timestamp for 'from' and 'to' dates in milliseconds.
|
||||
alarm_date = datetime.datetime(2017, 6, 7, 18, 0)
|
||||
alarm_ms, expected_from_ms, expected_to_ms = self.create_time_data(alarm_date)
|
||||
|
||||
# When retrieving the link to Grafana for the first metric and given timestamp
|
||||
result_url = email._get_link_url(metrics[0], alarm_ms)
|
||||
self.assertIsNotNone(result_url)
|
||||
|
||||
# Then the following link to Grafana (including the metric info and timestamp) is expected.
|
||||
expected_url = "http://127.0.0.1:3000/dashboard/script/drilldown.js" \
|
||||
"?metric=cpu.percent&dim_hostname=foo1&dim_service=bar1" \
|
||||
"&from=%s&to=%s" % (expected_from_ms, expected_to_ms)
|
||||
self._assert_equal_urls(expected_url, result_url)
|
||||
|
||||
def create_time_data(self, alarm_date):
|
||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||
alarm_ms = int(round((alarm_date - epoch).total_seconds() * 1000))
|
||||
|
||||
# From and to dates are 10 minutes before and after the alarm occurred.
|
||||
from_date = alarm_date - datetime.timedelta(minutes=10)
|
||||
to_date = alarm_date + datetime.timedelta(minutes=10)
|
||||
|
||||
expected_from_ms = int(round((from_date - epoch).total_seconds() * 1000))
|
||||
expected_to_ms = int(round((to_date - epoch).total_seconds() * 1000))
|
||||
|
||||
return alarm_ms, expected_from_ms, expected_to_ms
|
||||
|
||||
def _assert_equal_urls(self, expected_url, result_url):
|
||||
if six.PY2:
|
||||
expected_parsed = urlparse.urlparse(expected_url)
|
||||
result_parsed = urlparse.urlparse(result_url)
|
||||
else:
|
||||
expected_parsed = urlparse(expected_url)
|
||||
result_parsed = urlparse(result_url)
|
||||
|
||||
self.assertEqual(expected_parsed.netloc, result_parsed.netloc)
|
||||
self.assertEqual(expected_parsed.path, result_parsed.path)
|
||||
|
||||
if six.PY2:
|
||||
expected_parsed_query = urlparse.parse_qs(expected_parsed.query)
|
||||
result_parsed_query = urlparse.parse_qs(result_parsed.query)
|
||||
else:
|
||||
expected_parsed_query = parse.parse_qs(expected_parsed.query)
|
||||
result_parsed_query = parse.parse_qs(result_parsed.query)
|
||||
|
||||
self.assertEqual(len(expected_parsed_query), len(result_parsed_query))
|
||||
|
||||
for key in six.iterkeys(result_parsed_query):
|
||||
self.assertEqual(expected_parsed_query[key], result_parsed_query[key])
|
||||
|
|
Loading…
Reference in New Issue