summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2017-09-01 09:19:03 +0000
committerGerrit Code Review <review@openstack.org>2017-09-01 09:19:03 +0000
commitfb535756d7fbdf8115fb1fff9bcc2c1443ebbc33 (patch)
treeecb394a937af789b7c8095a33a6af31e06b01b2e
parent67bcb4318fe680fe40b310aa5a45d5d80a4a39c2 (diff)
parent347606ac540bca81d29fdaaf014730aa64004068 (diff)
Merge "Added a field 'Grafana Url' in the email"
-rw-r--r--monasca_notification/plugins/email_notifier.py49
-rw-r--r--notification.yaml3
-rw-r--r--tests/test_email_notification.py120
3 files changed, 153 insertions, 19 deletions
diff --git a/monasca_notification/plugins/email_notifier.py b/monasca_notification/plugins/email_notifier.py
index 28be0d6..f99e872 100644
--- a/monasca_notification/plugins/email_notifier.py
+++ b/monasca_notification/plugins/email_notifier.py
@@ -16,17 +16,21 @@
16import email.header 16import email.header
17import email.mime.text 17import email.mime.text
18import email.utils 18import email.utils
19import six
19import smtplib 20import smtplib
20import time 21import time
21 22
23
22from monasca_notification.plugins import abstract_notifier 24from monasca_notification.plugins import abstract_notifier
23 25
24EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message} 26EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message}
25 27
26Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC 28Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
27alarm_id: {alarm_id} 29alarm_id: {alarm_id}
30
28Lifecycle state: {lifecycle_state} 31Lifecycle state: {lifecycle_state}
29Link: {link} 32Link: {link}
33Link to Grafana: {grafana_url}
30 34
31With dimensions: 35With dimensions:
32{metric_dimensions}''' 36{metric_dimensions}'''
@@ -35,8 +39,10 @@ EMAIL_MULTIPLE_HOST_BASE = u'''On host "{hostname}" {message}
35 39
36Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC 40Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
37alarm_id: {alarm_id} 41alarm_id: {alarm_id}
42
38Lifecycle state: {lifecycle_state} 43Lifecycle state: {lifecycle_state}
39Link: {link} 44Link: {link}
45Link to Grafana: {grafana_url}
40 46
41With dimensions: 47With dimensions:
42{metric_dimensions}''' 48{metric_dimensions}'''
@@ -45,8 +51,10 @@ EMAIL_NO_HOST_BASE = u'''On multiple hosts {message}
45 51
46Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC 52Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
47Alarm_id: {alarm_id} 53Alarm_id: {alarm_id}
54
48Lifecycle state: {lifecycle_state} 55Lifecycle state: {lifecycle_state}
49Link: {link} 56Link: {link}
57Link to Grafana: {grafana_url}
50 58
51With dimensions 59With dimensions
52{metric_dimensions}''' 60{metric_dimensions}'''
@@ -152,6 +160,12 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
152 be treated as type #2. 160 be treated as type #2.
153 """ 161 """
154 timestamp = time.asctime(time.gmtime(notification.alarm_timestamp)) 162 timestamp = time.asctime(time.gmtime(notification.alarm_timestamp))
163
164 alarm_seconds = notification.alarm_timestamp
165 alarm_ms = int(round(alarm_seconds * 1000))
166
167 graf_url = self._get_link_url(notification.metrics[0], alarm_ms)
168
155 dimensions = _format_dimensions(notification) 169 dimensions = _format_dimensions(notification)
156 170
157 if len(hostname) == 1: # Type 1 171 if len(hostname) == 1: # Type 1
@@ -166,6 +180,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
166 alarm_id=notification.alarm_id, 180 alarm_id=notification.alarm_id,
167 metric_dimensions=dimensions, 181 metric_dimensions=dimensions,
168 link=notification.link, 182 link=notification.link,
183 grafana_url=graf_url,
169 lifecycle_state=notification.lifecycle_state 184 lifecycle_state=notification.lifecycle_state
170 ) 185 )
171 subject = u'{} {} "{}" for Host: {} Target: {}'.format( 186 subject = u'{} {} "{}" for Host: {} Target: {}'.format(
@@ -183,6 +198,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
183 alarm_id=notification.alarm_id, 198 alarm_id=notification.alarm_id,
184 metric_dimensions=dimensions, 199 metric_dimensions=dimensions,
185 link=notification.link, 200 link=notification.link,
201 grafana_url=graf_url,
186 lifecycle_state=notification.lifecycle_state 202 lifecycle_state=notification.lifecycle_state
187 ) 203 )
188 subject = u'{} {} "{}" for Host: {}'.format( 204 subject = u'{} {} "{}" for Host: {}'.format(
@@ -197,6 +213,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
197 alarm_id=notification.alarm_id, 213 alarm_id=notification.alarm_id,
198 metric_dimensions=dimensions, 214 metric_dimensions=dimensions,
199 link=notification.link, 215 link=notification.link,
216 grafana_url=graf_url,
200 lifecycle_state=notification.lifecycle_state 217 lifecycle_state=notification.lifecycle_state
201 ) 218 )
202 subject = u'{} {} "{}" '.format(notification.state, 219 subject = u'{} {} "{}" '.format(notification.state,
@@ -211,6 +228,38 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
211 228
212 return msg 229 return msg
213 230
231 def _get_link_url(self, metric, timestamp_ms):
232 """Returns the url to Grafana including a query with the
233 respective metric info (name, dimensions, timestamp)
234 :param metric: the metric for which to display the graph in Grafana
235 :param timestamp_ms: timestamp of the alarm for the metric in milliseconds
236 :return: the url to the graph for the given metric or None if no Grafana host
237 has been defined.
238 """
239
240 grafana_url = self._config.get('grafana_url', None)
241 if grafana_url is None:
242 return None
243
244 url = ''
245 metric_query = ''
246
247 metric_query = "?metric=%s" % metric['name']
248
249 dimensions = metric['dimensions']
250 for key, value in six.iteritems(dimensions):
251 metric_query += "&dim_%s=%s" % (key, value)
252
253 # Show the graph within a range of ten minutes before and after the alarm occurred.
254 offset = 600000
255 from_ms = timestamp_ms - offset
256 to_ms = timestamp_ms + offset
257 time_query = "&from=%s&to=%s" % (from_ms, to_ms)
258
259 url = grafana_url + '/dashboard/script/drilldown.js'
260
261 return url + metric_query + time_query
262
214 263
215def _format_dimensions(notification): 264def _format_dimensions(notification):
216 dimension_sets = [] 265 dimension_sets = []
diff --git a/notification.yaml b/notification.yaml
index 1b75e88..10c6a6a 100644
--- a/notification.yaml
+++ b/notification.yaml
@@ -45,6 +45,7 @@ notification_types:
45 password: 45 password:
46 timeout: 60 46 timeout: 60
47 from_addr: monasca-notification@none.invalid 47 from_addr: monasca-notification@none.invalid
48 grafana_url: 'http://127.0.0.1:3000'
48 49
49 webhook: 50 webhook:
50 timeout: 5 51 timeout: 5
@@ -125,4 +126,4 @@ logging: # Used in logging.dictConfig
125 level: DEBUG 126 level: DEBUG
126statsd: 127statsd:
127 host: 'localhost' 128 host: 'localhost'
128 port: 8125 129 port: 8125 \ No newline at end of file
diff --git a/tests/test_email_notification.py b/tests/test_email_notification.py
index cde5fd7..baef179 100644
--- a/tests/test_email_notification.py
+++ b/tests/test_email_notification.py
@@ -24,6 +24,14 @@ import unittest
24 24
25import six 25import six
26 26
27import datetime
28
29if six.PY2:
30 import urlparse
31else:
32 from urllib import parse
33 from urllib.parse import urlparse
34
27from monasca_notification.notification import Notification 35from monasca_notification.notification import Notification
28from monasca_notification.plugins import email_notifier 36from monasca_notification.plugins import email_notifier
29 37
@@ -114,7 +122,8 @@ class TestEmail(unittest.TestCase):
114 'user': None, 122 'user': None,
115 'password': None, 123 'password': None,
116 'timeout': 60, 124 'timeout': 60,
117 'from_addr': 'hpcs.mon@hp.com'} 125 'from_addr': 'hpcs.mon@hp.com',
126 'grafana_url': 'http://127.0.0.1:3000'}
118 127
119 def tearDown(self): 128 def tearDown(self):
120 pass 129 pass
@@ -134,7 +143,6 @@ class TestEmail(unittest.TestCase):
134 mock_log.error = self.trap.append 143 mock_log.error = self.trap.append
135 144
136 email = email_notifier.EmailNotifier(mock_log) 145 email = email_notifier.EmailNotifier(mock_log)
137
138 email.config(self.email_config) 146 email.config(self.email_config)
139 147
140 alarm_dict = alarm(metric) 148 alarm_dict = alarm(metric)
@@ -148,7 +156,9 @@ class TestEmail(unittest.TestCase):
148 """ 156 """
149 157
150 metrics = [] 158 metrics = []
151 metric_data = {'dimensions': {'hostname': u'foo1' + UNICODE_CHAR, u'service' + UNICODE_CHAR: 'bar1'}} 159 metric_data = {'name': 'cpu.percent',
160 'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
161 u'service' + UNICODE_CHAR: 'bar1'}}
152 metrics.append(metric_data) 162 metrics.append(metric_data)
153 163
154 self.notify(self._smtpStub, metrics) 164 self.notify(self._smtpStub, metrics)
@@ -176,9 +186,9 @@ class TestEmail(unittest.TestCase):
176 """ 186 """
177 187
178 metrics = [] 188 metrics = []
179 metric_data = {'dimensions': {'hostname': u'foo1' + UNICODE_CHAR, 189 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
180 u'service' + UNICODE_CHAR: 'bar1', 190 u'service' + UNICODE_CHAR: 'bar1',
181 u'target_host': u'some_where'}} 191 u'target_host': u'some_where'}}
182 metrics.append(metric_data) 192 metrics.append(metric_data)
183 193
184 self.notify(self._smtpStub, metrics) 194 self.notify(self._smtpStub, metrics)
@@ -204,9 +214,9 @@ class TestEmail(unittest.TestCase):
204 """ 214 """
205 215
206 metrics = [] 216 metrics = []
207 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 217 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
208 metrics.append(metric_data) 218 metrics.append(metric_data)
209 metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}} 219 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
210 metrics.append(metric_data) 220 metrics.append(metric_data)
211 221
212 self.notify(self._smtpStub, metrics) 222 self.notify(self._smtpStub, metrics)
@@ -232,9 +242,9 @@ class TestEmail(unittest.TestCase):
232 """ 242 """
233 243
234 metrics = [] 244 metrics = []
235 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 245 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
236 metrics.append(metric_data) 246 metrics.append(metric_data)
237 metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}} 247 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
238 metrics.append(metric_data) 248 metrics.append(metric_data)
239 249
240 mock_log = mock.MagicMock() 250 mock_log = mock.MagicMock()
@@ -276,9 +286,9 @@ class TestEmail(unittest.TestCase):
276 """ 286 """
277 287
278 metrics = [] 288 metrics = []
279 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 289 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
280 metrics.append(metric_data) 290 metrics.append(metric_data)
281 metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}} 291 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
282 metrics.append(metric_data) 292 metrics.append(metric_data)
283 293
284 mock_log = mock.MagicMock() 294 mock_log = mock.MagicMock()
@@ -324,9 +334,9 @@ class TestEmail(unittest.TestCase):
324 """ 334 """
325 335
326 metrics = [] 336 metrics = []
327 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 337 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
328 metrics.append(metric_data) 338 metrics.append(metric_data)
329 metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}} 339 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
330 metrics.append(metric_data) 340 metrics.append(metric_data)
331 341
332 mock_log = mock.MagicMock() 342 mock_log = mock.MagicMock()
@@ -366,9 +376,9 @@ class TestEmail(unittest.TestCase):
366 """ 376 """
367 377
368 metrics = [] 378 metrics = []
369 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 379 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
370 metrics.append(metric_data) 380 metrics.append(metric_data)
371 metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}} 381 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
372 metrics.append(metric_data) 382 metrics.append(metric_data)
373 383
374 mock_log = mock.MagicMock() 384 mock_log = mock.MagicMock()
@@ -406,9 +416,9 @@ class TestEmail(unittest.TestCase):
406 """ 416 """
407 417
408 metrics = [] 418 metrics = []
409 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 419 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
410 metrics.append(metric_data) 420 metrics.append(metric_data)
411 metric_data = {'dimensions': {'hostname': 'foo2', 'service': 'bar2'}} 421 metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo2', 'service': 'bar2'}}
412 metrics.append(metric_data) 422 metrics.append(metric_data)
413 423
414 mock_log = mock.MagicMock() 424 mock_log = mock.MagicMock()
@@ -438,3 +448,77 @@ class TestEmail(unittest.TestCase):
438 448
439 self.assertNotIn("SMTP server disconnected. Will reconnect and retry message.", self.trap) 449 self.assertNotIn("SMTP server disconnected. Will reconnect and retry message.", self.trap)
440 self.assertIn("Error sending Email Notification", self.trap) 450 self.assertIn("Error sending Email Notification", self.trap)
451
452 @mock.patch('monasca_notification.plugins.email_notifier.smtplib')
453 def test_get_link_url(self, mock_smtp):
454 # Given one metric with name and dimensions
455 metrics = []
456 metric = {'name': 'cpu.percent',
457 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
458 metrics.append(metric)
459
460 mock_log = mock.MagicMock()
461 mock_log.warn = self.trap.append
462 mock_log.error = self.trap.append
463 mock_log.debug = self.trap.append
464 mock_log.info = self.trap.append
465 mock_log.exception = self.trap.append
466
467 mock_smtp.SMTP.return_value = mock_smtp
468 mock_smtp.sendmail.side_effect = smtplib.SMTPException
469
470 mock_smtp.SMTPServerDisconnected = smtplib.SMTPServerDisconnected
471 mock_smtp.SMTPException = smtplib.SMTPException
472
473 email = email_notifier.EmailNotifier(mock_log)
474 email.config(self.email_config)
475
476 # Create alarm timestamp and timestamp for 'from' and 'to' dates in milliseconds.
477 alarm_date = datetime.datetime(2017, 6, 7, 18, 0)
478 alarm_ms, expected_from_ms, expected_to_ms = self.create_time_data(alarm_date)
479
480 # When retrieving the link to Grafana for the first metric and given timestamp
481 result_url = email._get_link_url(metrics[0], alarm_ms)
482 self.assertIsNotNone(result_url)
483
484 # Then the following link to Grafana (including the metric info and timestamp) is expected.
485 expected_url = "http://127.0.0.1:3000/dashboard/script/drilldown.js" \
486 "?metric=cpu.percent&dim_hostname=foo1&dim_service=bar1" \
487 "&from=%s&to=%s" % (expected_from_ms, expected_to_ms)
488 self._assert_equal_urls(expected_url, result_url)
489
490 def create_time_data(self, alarm_date):
491 epoch = datetime.datetime.utcfromtimestamp(0)
492 alarm_ms = int(round((alarm_date - epoch).total_seconds() * 1000))
493
494 # From and to dates are 10 minutes before and after the alarm occurred.
495 from_date = alarm_date - datetime.timedelta(minutes=10)
496 to_date = alarm_date + datetime.timedelta(minutes=10)
497
498 expected_from_ms = int(round((from_date - epoch).total_seconds() * 1000))
499 expected_to_ms = int(round((to_date - epoch).total_seconds() * 1000))
500
501 return alarm_ms, expected_from_ms, expected_to_ms
502
503 def _assert_equal_urls(self, expected_url, result_url):
504 if six.PY2:
505 expected_parsed = urlparse.urlparse(expected_url)
506 result_parsed = urlparse.urlparse(result_url)
507 else:
508 expected_parsed = urlparse(expected_url)
509 result_parsed = urlparse(result_url)
510
511 self.assertEqual(expected_parsed.netloc, result_parsed.netloc)
512 self.assertEqual(expected_parsed.path, result_parsed.path)
513
514 if six.PY2:
515 expected_parsed_query = urlparse.parse_qs(expected_parsed.query)
516 result_parsed_query = urlparse.parse_qs(result_parsed.query)
517 else:
518 expected_parsed_query = parse.parse_qs(expected_parsed.query)
519 result_parsed_query = parse.parse_qs(result_parsed.query)
520
521 self.assertEqual(len(expected_parsed_query), len(result_parsed_query))
522
523 for key in six.iterkeys(result_parsed_query):
524 self.assertEqual(expected_parsed_query[key], result_parsed_query[key])