diff --git a/alerta/database/backends/postgres/base.py b/alerta/database/backends/postgres/base.py index 2ad1d2b9b..b3270a332 100644 --- a/alerta/database/backends/postgres/base.py +++ b/alerta/database/backends/postgres/base.py @@ -266,7 +266,7 @@ def dedup_alert(self, alert, history): SET status=%(status)s, service=%(service)s, value=%(value)s, text=%(text)s, timeout=%(timeout)s, raw_data=%(raw_data)s, last_receive_id=%(last_receive_id)s, last_receive_time=%(last_receive_time)s, - tags=%(tags)s, attributes=attributes || %(attributes)s, + tags=%(tags)s, attributes=%(attributes)s, origin=%(origin)s, duplicate_count=duplicate_count + 1, {update_time}, history=(%(history)s || history)[1:{limit}] WHERE environment=%(environment)s AND resource=%(resource)s @@ -289,8 +289,8 @@ def correlate_alert(self, alert, history): text=%(text)s, create_time=%(create_time)s, timeout=%(timeout)s, raw_data=%(raw_data)s, duplicate_count=%(duplicate_count)s, previous_severity=%(previous_severity)s, receive_time=%(receive_time)s, last_receive_id=%(last_receive_id)s, - last_receive_time=%(last_receive_time)s, tags=%(tags)s, - attributes=attributes || %(attributes)s, {update_time}, history=(%(history)s || history)[1:{limit}] + last_receive_time=%(last_receive_time)s, tags=%(tags)s, origin=%(origin)s, + attributes=%(attributes)s, {update_time}, history=(%(history)s || history)[1:{limit}], customer=%(customer)s WHERE environment=%(environment)s AND resource=%(resource)s AND (event=%(event)s AND severity!=%(severity)s) @@ -317,9 +317,9 @@ def correlate_multiple_alerts(self, alerts): %(environment{i})s, %(resource{i})s, %(event{i})s, %(severity{i})s, %(status{i})s,%(service{i})s, %(value{i})s, %(text{i})s, %(timeout{i})s, %(raw_data{i})s, %(last_receive_id{i})s, (%(last_receive_time{i})s)::timestamp without time zone, - %(tags{i})s, (%(attributes{i})s)::jsonb, (%(update_time{i})s)::timestamp without time zone, + %(tags{i})s::text[], (%(attributes{i})s)::jsonb, (%(update_time{i})s)::timestamp without time zone, (%(history{i})s), %(customer{i})s, (%(create_time{i})s)::timestamp without time zone, %(previous_severity{i})s, - (%(receive_time{i})s)::timestamp without time zone, %(duplicate_count{i})s + (%(receive_time{i})s)::timestamp without time zone, %(duplicate_count{i})s, %(origin{i})s ) """ ) @@ -327,11 +327,11 @@ def correlate_multiple_alerts(self, alerts): update = f""" UPDATE alerts SET event=al.event, severity=al.severity, status=al.status, service=al.service, value=al.value, text=al.text, - create_time=al.create_time, timeout=CASE WHEN al.status != ALL (ARRAY['ack', 'shelved']) THEN al.timeout ELSE alerts.timeout END, raw_data=al.raw_data, previous_severity=al.previous_severity, + create_time=al.create_time, timeout=al.timeout, raw_data=al.raw_data, previous_severity=al.previous_severity, receive_time=al.receive_time, last_receive_id=al.last_receive_id, last_receive_time=al.last_receive_time, - tags=ARRAY(SELECT DISTINCT UNNEST(alerts.tags || al.tags)), attributes=alerts.attributes || al.attributes, + tags=al.tags, attributes=al.attributes, duplicate_count=al.duplicate_count, update_time=COALESCE(al.update_time, alerts.update_time), - history=CASE WHEN al.history IS NULL THEN alerts.history ELSE (al.history || alerts.history)[1:{current_app.config['HISTORY_LIMIT']}] END + history=CASE WHEN al.history IS NULL THEN alerts.history ELSE (al.history || alerts.history)[1:{current_app.config['HISTORY_LIMIT']}] END, origin=al.origin FROM (VALUES {",".join(alerts_values)} ) as al( @@ -339,7 +339,7 @@ def correlate_multiple_alerts(self, alerts): status, service, value, text, timeout, raw_data, last_receive_id, last_receive_time, tags, attributes, update_time, history, customer, create_time, previous_severity, - receive_time, duplicate_count + receive_time, duplicate_count, origin ) WHERE alerts.environment=al.environment AND alerts.resource=al.resource @@ -362,8 +362,8 @@ def dedup_multiple_alerts(self, alerts): %(environment{i})s, %(resource{i})s, %(event{i})s, %(severity{i})s, %(status{i})s,%(service{i})s,%(value{i})s, %(text{i})s, %(raw_data{i})s, %(last_receive_id{i})s, (%(last_receive_time{i})s)::timestamp without time zone, - %(tags{i})s, (%(attributes{i})s)::jsonb, (%(update_time{i})s)::timestamp without time zone, - (%(history{i})s)::history, %(customer{i})s + %(tags{i})s::text[], (%(attributes{i})s)::jsonb, (%(update_time{i})s)::timestamp without time zone, + (%(history{i})s)::history, %(customer{i})s, %(origin{i})s ) """ ) @@ -373,7 +373,7 @@ def dedup_multiple_alerts(self, alerts): SET status=al.status, service=al.service, value=al.value, text=al.text, raw_data=al.raw_data, last_receive_id=al.last_receive_id, last_receive_time=al.last_receive_time, - tags=ARRAY(SELECT DISTINCT UNNEST(alerts.tags || al.tags)), attributes=alerts.attributes || al.attributes, + tags=al.tags, attributes=al.attributes, origin=al.origin, duplicate_count=alerts.duplicate_count + 1, update_time=COALESCE(al.update_time, alerts.update_time), history=CASE WHEN al.history IS NULL THEN alerts.history ELSE (al.history || alerts.history)[1:{current_app.config['HISTORY_LIMIT']}] END FROM (VALUES @@ -382,7 +382,7 @@ def dedup_multiple_alerts(self, alerts): environment, resource, event, severity, status, service, value, text, raw_data, last_receive_id, last_receive_time, - tags, attributes, update_time, history, customer + tags, attributes, update_time, history, customer, origin ) WHERE alerts.environment=al.environment AND alerts.resource=al.resource diff --git a/tests/test_alerts.py b/tests/test_alerts.py index 0d3f2d5aa..fd828c1e9 100644 --- a/tests/test_alerts.py +++ b/tests/test_alerts.py @@ -204,7 +204,12 @@ def test_bulk_alert(self): response = self.client.post('/alerts', data=json.dumps([{**self.major_alert, 'resource': 'major_alert'}, {**self.major_alert, 'resource': 'major_alert_2'}]), headers=self.headers) self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) - first, second = data['alerts'] + first_res = self.client.get(f'/alert/{first_id}', headers=self.headers) + self.assertEqual(first_res.status_code, 200) + second_res = self.client.get(f'/alert/{second_id}', headers=self.headers) + self.assertEqual(second_res.status_code, 200) + first = json.loads(first_res.data.decode('utf-8'))['alert'] + second = json.loads(second_res.data.decode('utf-8'))['alert'] self.assertIn(first_id, first['id']) self.assertEqual(first['service'], ['Network', 'Shared']) @@ -224,7 +229,12 @@ def test_bulk_alert(self): response = self.client.post('/alerts', data=json.dumps([{**self.critical_alert, 'resource': 'major_alert'}, {**self.critical_alert, 'resource': 'major_alert_2'}]), headers=self.headers) self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) - first, second = data['alerts'] + first_res = self.client.get(f'/alert/{first_id}', headers=self.headers) + self.assertEqual(first_res.status_code, 200) + second_res = self.client.get(f'/alert/{second_id}', headers=self.headers) + self.assertEqual(second_res.status_code, 200) + first = json.loads(first_res.data.decode('utf-8'))['alert'] + second = json.loads(second_res.data.decode('utf-8'))['alert'] self.assertIn(first_id, first['id']) self.assertEqual(first['status'], 'open') self.assertEqual(first['service'], ['Network']) @@ -241,11 +251,16 @@ def test_bulk_alert(self): self.assertEqual(second['previousSeverity'], self.major_alert['severity']) self.assertEqual(second['updateTime'], second_update_time) - # # de-duplicate + # de-duplicate response = self.client.post('/alerts', data=json.dumps([{**self.critical_alert, 'resource': 'major_alert'}, {**self.critical_alert, 'resource': 'major_alert_2'}]), headers=self.headers) self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) - first, second = data['alerts'] + first_res = self.client.get(f'/alert/{first_id}', headers=self.headers) + self.assertEqual(first_res.status_code, 200) + second_res = self.client.get(f'/alert/{second_id}', headers=self.headers) + self.assertEqual(second_res.status_code, 200) + first = json.loads(first_res.data.decode('utf-8'))['alert'] + second = json.loads(second_res.data.decode('utf-8'))['alert'] self.assertIn(first_id, first['id']) self.assertNotIn(second_id, first['id']) @@ -263,6 +278,85 @@ def test_bulk_alert(self): self.assertEqual(second['duplicateCount'], 1) self.assertEqual(second['updateTime'], second_update_time) + # de-duplicate + correlate + response = self.client.post('/alerts', data=json.dumps([{**self.critical_alert, 'resource': 'major_alert'}, {**self.critical_alert, 'resource': 'major_alert_2', 'severity': 'major'}]), headers=self.headers) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + first_res = self.client.get(f'/alert/{first_id}', headers=self.headers) + self.assertEqual(first_res.status_code, 200) + second_res = self.client.get(f'/alert/{second_id}', headers=self.headers) + self.assertEqual(second_res.status_code, 200) + first = json.loads(first_res.data.decode('utf-8'))['alert'] + second = json.loads(second_res.data.decode('utf-8'))['alert'] + + self.assertIn(first_id, first['id']) + self.assertNotIn(second_id, first['id']) + self.assertEqual(first['service'], ['Network']) + self.assertEqual(first['severity'], 'critical') + self.assertEqual(first['status'], 'open') + self.assertEqual(first['tags'], []) + self.assertEqual(first['duplicateCount'], 2) + self.assertEqual(first['updateTime'], first_update_time) + self.assertEqual(sorted(first['attributes']), sorted({'ip': '10.0.0.1'})) + + self.assertIn(second_id, second['id']) + self.assertNotIn(first_id, second['id']) + self.assertEqual(second['service'], ['Network']) + self.assertEqual(second['severity'], 'major') + self.assertEqual(second['previousSeverity'],'critical') + self.assertEqual(second['status'], 'open') + self.assertEqual(second['tags'], []) + self.assertEqual(second['duplicateCount'], 0) + self.assertEqual(second['timeout'], 0) + self.assertEqual(second['updateTime'], second_update_time) + self.assertEqual(sorted(second['attributes']), sorted({'ip': '10.0.0.1'})) + + # update dedup + corr + response = self.client.post( + '/alerts', + data=json.dumps([ + {**self.critical_alert, 'resource': 'major_alert', 'service': ['Shared'], 'tags': ['a', 'b'], 'attributes': {'foo': 'abc def', 'bar': 1234, 'baz': False}, 'timeout': 1000}, + {**self.critical_alert, 'resource': 'major_alert_2', 'service': ['Shared'], 'tags': ['a', 'b'], 'attributes': {'foo': 'abc def', 'bar': 1234, 'baz': False}, 'timeout': 1100} + ]), + headers=self.headers + ) + self.assertEqual(response.status_code, 201) + data = json.loads(response.data.decode('utf-8')) + first_res = self.client.get(f'/alert/{first_id}', headers=self.headers) + self.assertEqual(first_res.status_code, 200) + second_res = self.client.get(f'/alert/{second_id}', headers=self.headers) + self.assertEqual(second_res.status_code, 200) + first = json.loads(first_res.data.decode('utf-8'))['alert'] + second = json.loads(second_res.data.decode('utf-8'))['alert'] + + self.assertIn(first_id, first['id']) + self.assertNotIn(second_id, first['id']) + self.assertEqual(first['service'], ['Shared']) + self.assertEqual(sorted(first['tags']), sorted(['a', 'b'])) + self.assertEqual(first['severity'], 'critical') + self.assertEqual(first['status'], 'open') + self.assertEqual(first['duplicateCount'], 3) + # timeout is not updated, only acknowlegde and shelve action is allowed to update timeout + self.assertEqual(first['timeout'], 0) + self.assertEqual(first['updateTime'], first_update_time) + self.assertIn(first['updateTime'], first_update_time) + self.assertEqual(sorted(first['attributes']), sorted( + {'foo': 'abc def', 'bar': 1234, 'baz': False, 'ip': '10.0.0.1'})) + + self.assertIn(second_id, second['id']) + self.assertNotIn(first_id, second['id']) + self.assertEqual(second['service'], ['Shared']) + self.assertEqual(sorted(second['tags']), sorted(['a', 'b'])) + self.assertEqual(second['severity'], 'critical') + self.assertEqual(second['previousSeverity'],'major') + self.assertEqual(second['status'], 'open') + self.assertEqual(second['duplicateCount'], 0) + # timeout is not updated, only acknowlegde and shelve action is allowed to update timeout + self.assertEqual(second['timeout'], 0) + self.assertEqual(second['updateTime'], second_update_time) + self.assertEqual(sorted(second['attributes']), sorted( + {'foo': 'abc def', 'bar': 1234, 'baz': False, 'ip': '10.0.0.1'})) + # # get alerts response = self.client.get('/alert/' + first_id) self.assertEqual(response.status_code, 200) @@ -656,12 +750,12 @@ def test_alert_attributes(self): self.assertEqual(sorted(data['alert']['attributes']), sorted( {'foo': 'abc def', 'baz': False, 'quux': ['q', 'u', 'u', 'x'], 'ip': '10.0.0.1'})) - # re-send duplicate alert with custom attributes ('quux' should not change) + # re-send duplicate alert with custom attributes ('quux' should change) response = self.client.post('/alert', data=json.dumps(self.fatal_alert), headers=self.headers) self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) self.assertEqual(sorted(data['alert']['attributes']), sorted( - {'foo': 'abc def', 'bar': 1234, 'baz': False, 'quux': ['q', 'u', 'u', 'x'], 'ip': '10.0.0.1'})) + {'foo': 'abc def', 'bar': 1234, 'baz': False, 'ip': '10.0.0.1'})) # update custom attribute again (only 'quux' should change) response = self.client.put('/alert/' + alert_id + '/attributes', @@ -673,14 +767,13 @@ def test_alert_attributes(self): self.assertEqual(sorted(data['alert']['attributes']), sorted( {'foo': 'abc def', 'bar': 1234, 'baz': False, 'quux': [1, 'u', 'u', 4], 'ip': '10.0.0.1'})) - # send correlated alert with custom attributes (nothing should change) - response = self.client.post('/alert', data=json.dumps(self.critical_alert), headers=self.headers) + # send correlated alert with custom attributes (attributes should be updated) + response = self.client.post('/alert', data=json.dumps({**self.fatal_alert, 'severity': 'major', 'attributes': {}}), headers=self.headers) self.assertEqual(response.status_code, 201) response = self.client.get('/alert/' + alert_id) self.assertEqual(response.status_code, 200) data = json.loads(response.data.decode('utf-8')) - self.assertEqual(sorted(data['alert']['attributes']), sorted( - {'foo': 'abc def', 'bar': 1234, 'baz': False, 'quux': [1, 'u', 'u', 4], 'ip': '10.0.0.1'})) + self.assertEqual(sorted(data['alert']['attributes']), sorted({'ip': '10.0.0.1'})) def test_history_limit(self): diff --git a/tests/test_builtins.py b/tests/test_builtins.py index 143d4da56..64b466b2f 100644 --- a/tests/test_builtins.py +++ b/tests/test_builtins.py @@ -146,13 +146,13 @@ def test_acked_by_plugin(self): data = json.loads(response.data.decode('utf-8')) self.assertEqual(data['alert']['attributes']['acked-by'], 'admin@alerta.io') - # clear alert + # clear alert (acked-by attribute is deleted) response = self.client.post('/alert', json=self.ok_alert, headers=self.headers) self.assertEqual(response.status_code, 201) data = json.loads(response.data.decode('utf-8')) self.assertEqual(data['alert']['severity'], 'ok') self.assertEqual(data['alert']['status'], 'closed') - self.assertEqual(data['alert']['attributes']['acked-by'], 'admin@alerta.io') + self.assertNotIn('acked-by', data['alert']['attributes']) # critical alert (unacked) response = self.client.post('/alert', json=self.critical_alert, headers=self.headers)