Browse Source

Merge "Added a field 'Grafana Url' in the email"

Jenkins 1 year ago
parent
commit
fb535756d7
3 changed files with 153 additions and 19 deletions
  1. 49
    0
      monasca_notification/plugins/email_notifier.py
  2. 2
    1
      notification.yaml
  3. 102
    18
      tests/test_email_notification.py

+ 49
- 0
monasca_notification/plugins/email_notifier.py View File

@@ -16,17 +16,21 @@
16 16
 import email.header
17 17
 import email.mime.text
18 18
 import email.utils
19
+import six
19 20
 import smtplib
20 21
 import time
21 22
 
23
+
22 24
 from monasca_notification.plugins import abstract_notifier
23 25
 
24 26
 EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message}
25 27
 
26 28
 Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
27 29
 alarm_id: {alarm_id}
30
+
28 31
 Lifecycle state: {lifecycle_state}
29 32
 Link: {link}
33
+Link to Grafana: {grafana_url}
30 34
 
31 35
 With dimensions:
32 36
 {metric_dimensions}'''
@@ -35,8 +39,10 @@ EMAIL_MULTIPLE_HOST_BASE = u'''On host "{hostname}" {message}
35 39
 
36 40
 Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
37 41
 alarm_id: {alarm_id}
42
+
38 43
 Lifecycle state: {lifecycle_state}
39 44
 Link: {link}
45
+Link to Grafana: {grafana_url}
40 46
 
41 47
 With dimensions:
42 48
 {metric_dimensions}'''
@@ -45,8 +51,10 @@ EMAIL_NO_HOST_BASE = u'''On multiple hosts {message}
45 51
 
46 52
 Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
47 53
 Alarm_id: {alarm_id}
54
+
48 55
 Lifecycle state: {lifecycle_state}
49 56
 Link: {link}
57
+Link to Grafana: {grafana_url}
50 58
 
51 59
 With dimensions
52 60
 {metric_dimensions}'''
@@ -152,6 +160,12 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
152 160
            be treated as type #2.
153 161
         """
154 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 169
         dimensions = _format_dimensions(notification)
156 170
 
157 171
         if len(hostname) == 1:  # Type 1
@@ -166,6 +180,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
166 180
                     alarm_id=notification.alarm_id,
167 181
                     metric_dimensions=dimensions,
168 182
                     link=notification.link,
183
+                    grafana_url=graf_url,
169 184
                     lifecycle_state=notification.lifecycle_state
170 185
                 )
171 186
                 subject = u'{} {} "{}" for Host: {} Target: {}'.format(
@@ -183,6 +198,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
183 198
                     alarm_id=notification.alarm_id,
184 199
                     metric_dimensions=dimensions,
185 200
                     link=notification.link,
201
+                    grafana_url=graf_url,
186 202
                     lifecycle_state=notification.lifecycle_state
187 203
                 )
188 204
                 subject = u'{} {} "{}" for Host: {}'.format(
@@ -197,6 +213,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
197 213
                 alarm_id=notification.alarm_id,
198 214
                 metric_dimensions=dimensions,
199 215
                 link=notification.link,
216
+                grafana_url=graf_url,
200 217
                 lifecycle_state=notification.lifecycle_state
201 218
             )
202 219
             subject = u'{} {} "{}" '.format(notification.state,
@@ -211,6 +228,38 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
211 228
 
212 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
 
215 264
 def _format_dimensions(notification):
216 265
     dimension_sets = []

+ 2
- 1
notification.yaml View File

@@ -45,6 +45,7 @@ notification_types:
45 45
         password:
46 46
         timeout: 60
47 47
         from_addr: monasca-notification@none.invalid
48
+        grafana_url: 'http://127.0.0.1:3000'
48 49
 
49 50
     webhook:
50 51
         timeout: 5
@@ -125,4 +126,4 @@ logging: # Used in logging.dictConfig
125 126
         level: DEBUG
126 127
 statsd:
127 128
     host: 'localhost'
128
-    port: 8125
129
+    port: 8125

+ 102
- 18
tests/test_email_notification.py View File

@@ -24,6 +24,14 @@ import unittest
24 24
 
25 25
 import six
26 26
 
27
+import datetime
28
+
29
+if six.PY2:
30
+    import urlparse
31
+else:
32
+    from urllib import parse
33
+    from urllib.parse import urlparse
34
+
27 35
 from monasca_notification.notification import Notification
28 36
 from monasca_notification.plugins import email_notifier
29 37
 
@@ -114,7 +122,8 @@ class TestEmail(unittest.TestCase):
114 122
                              'user': None,
115 123
                              'password': None,
116 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 128
     def tearDown(self):
120 129
         pass
@@ -134,7 +143,6 @@ class TestEmail(unittest.TestCase):
134 143
         mock_log.error = self.trap.append
135 144
 
136 145
         email = email_notifier.EmailNotifier(mock_log)
137
-
138 146
         email.config(self.email_config)
139 147
 
140 148
         alarm_dict = alarm(metric)
@@ -148,7 +156,9 @@ class TestEmail(unittest.TestCase):
148 156
         """
149 157
 
150 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 162
         metrics.append(metric_data)
153 163
 
154 164
         self.notify(self._smtpStub, metrics)
@@ -176,9 +186,9 @@ class TestEmail(unittest.TestCase):
176 186
         """
177 187
 
178 188
         metrics = []
179
-        metric_data = {'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
180
-                                      u'service' + UNICODE_CHAR: 'bar1',
181
-                                      u'target_host': u'some_where'}}
189
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': u'foo1' + UNICODE_CHAR,
190
+                                                             u'service' + UNICODE_CHAR: 'bar1',
191
+                                                             u'target_host': u'some_where'}}
182 192
         metrics.append(metric_data)
183 193
 
184 194
         self.notify(self._smtpStub, metrics)
@@ -204,9 +214,9 @@ class TestEmail(unittest.TestCase):
204 214
         """
205 215
 
206 216
         metrics = []
207
-        metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
217
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
208 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 220
         metrics.append(metric_data)
211 221
 
212 222
         self.notify(self._smtpStub, metrics)
@@ -232,9 +242,9 @@ class TestEmail(unittest.TestCase):
232 242
         """
233 243
 
234 244
         metrics = []
235
-        metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
245
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
236 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 248
         metrics.append(metric_data)
239 249
 
240 250
         mock_log = mock.MagicMock()
@@ -276,9 +286,9 @@ class TestEmail(unittest.TestCase):
276 286
         """
277 287
 
278 288
         metrics = []
279
-        metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
289
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
280 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 292
         metrics.append(metric_data)
283 293
 
284 294
         mock_log = mock.MagicMock()
@@ -324,9 +334,9 @@ class TestEmail(unittest.TestCase):
324 334
         """
325 335
 
326 336
         metrics = []
327
-        metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
337
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
328 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 340
         metrics.append(metric_data)
331 341
 
332 342
         mock_log = mock.MagicMock()
@@ -366,9 +376,9 @@ class TestEmail(unittest.TestCase):
366 376
         """
367 377
 
368 378
         metrics = []
369
-        metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
379
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
370 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 382
         metrics.append(metric_data)
373 383
 
374 384
         mock_log = mock.MagicMock()
@@ -406,9 +416,9 @@ class TestEmail(unittest.TestCase):
406 416
         """
407 417
 
408 418
         metrics = []
409
-        metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
419
+        metric_data = {'name': 'cpu.percent', 'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
410 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 422
         metrics.append(metric_data)
413 423
 
414 424
         mock_log = mock.MagicMock()
@@ -438,3 +448,77 @@ class TestEmail(unittest.TestCase):
438 448
 
439 449
         self.assertNotIn("SMTP server disconnected. Will reconnect and retry message.", self.trap)
440 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])

Loading…
Cancel
Save