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:
Georgia-Anna Farmaki 2017-05-31 13:16:56 +02:00
parent 46a36adde0
commit 347606ac54
3 changed files with 153 additions and 19 deletions

View File

@ -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 = []

View File

@ -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
@ -120,4 +121,4 @@ logging: # Used in logging.dictConfig
level: DEBUG
statsd:
host: 'localhost'
port: 8125
port: 8125

View File

@ -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])