Skip to content

Commit 09bdfa9

Browse files
authored
fix: change housekeeping delete threshold to seconds (alerta#1508)
1 parent 7fee108 commit 09bdfa9

File tree

12 files changed

+134
-52
lines changed

12 files changed

+134
-52
lines changed

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ repos:
2323
args: ['--autofix']
2424
- id: name-tests-test
2525
args: ['--django']
26+
exclude: ^tests/helpers/
2627
- id: requirements-txt-fixer
2728
- id: trailing-whitespace
2829
- repo: https://gitlab.com/pycqa/flake8

alerta/database/backends/mongodb/base.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1584,17 +1584,17 @@ def update_timer(self, timer):
15841584
# HOUSEKEEPING
15851585

15861586
def get_expired(self, expired_threshold, info_threshold):
1587-
# delete 'closed' or 'expired' alerts older than "expired_threshold" hours
1588-
# and 'informational' alerts older than "info_threshold" hours
1587+
# delete 'closed' or 'expired' alerts older than "expired_threshold" seconds
1588+
# and 'informational' alerts older than "info_threshold" seconds
15891589

15901590
if expired_threshold:
1591-
expired_hours_ago = datetime.utcnow() - timedelta(hours=expired_threshold)
1591+
expired_seconds_ago = datetime.utcnow() - timedelta(seconds=expired_threshold)
15921592
self.get_db().alerts.delete_many(
1593-
{'status': {'$in': ['closed', 'expired']}, 'lastReceiveTime': {'$lt': expired_hours_ago}})
1593+
{'status': {'$in': ['closed', 'expired']}, 'lastReceiveTime': {'$lt': expired_seconds_ago}})
15941594

15951595
if info_threshold:
1596-
info_hours_ago = datetime.utcnow() - timedelta(hours=info_threshold)
1597-
self.get_db().alerts.delete_many({'severity': 'informational', 'lastReceiveTime': {'$lt': info_hours_ago}})
1596+
info_seconds_ago = datetime.utcnow() - timedelta(seconds=info_threshold)
1597+
self.get_db().alerts.delete_many({'severity': 'informational', 'lastReceiveTime': {'$lt': info_seconds_ago}})
15981598

15991599
# get list of alerts to be newly expired
16001600
pipeline = [

alerta/database/backends/postgres/base.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1500,22 +1500,22 @@ def update_timer(self, timer):
15001500
# HOUSEKEEPING
15011501

15021502
def get_expired(self, expired_threshold, info_threshold):
1503-
# delete 'closed' or 'expired' alerts older than "expired_threshold" hours
1504-
# and 'informational' alerts older than "info_threshold" hours
1503+
# delete 'closed' or 'expired' alerts older than "expired_threshold" seconds
1504+
# and 'informational' alerts older than "info_threshold" seconds
15051505

15061506
if expired_threshold:
15071507
delete = """
15081508
DELETE FROM alerts
15091509
WHERE (status IN ('closed', 'expired')
1510-
AND last_receive_time < (NOW() at time zone 'utc' - INTERVAL '%(expired_threshold)s hours'))
1510+
AND last_receive_time < (NOW() at time zone 'utc' - INTERVAL '%(expired_threshold)s seconds'))
15111511
"""
15121512
self._deleteall(delete, {'expired_threshold': expired_threshold})
15131513

15141514
if info_threshold:
15151515
delete = """
15161516
DELETE FROM alerts
15171517
WHERE (severity='informational'
1518-
AND last_receive_time < (NOW() at time zone 'utc' - INTERVAL '%(info_threshold)s hours'))
1518+
AND last_receive_time < (NOW() at time zone 'utc' - INTERVAL '%(info_threshold)s seconds'))
15191519
"""
15201520
self._deleteall(delete, {'info_threshold': info_threshold})
15211521

alerta/management/views.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,18 @@ def health_check():
145145
@cross_origin()
146146
@permission(Scope.admin_management)
147147
def housekeeping():
148-
expired_threshold = request.args.get('expired', default=current_app.config['DEFAULT_EXPIRED_DELETE_HRS'], type=int)
149-
info_threshold = request.args.get('info', default=current_app.config['DEFAULT_INFO_DELETE_HRS'], type=int)
148+
expired_threshold_hrs = request.args.get('expired', type=int)
149+
info_threshold_hrs = request.args.get('info', type=int)
150+
151+
if expired_threshold_hrs:
152+
expired_threshold = expired_threshold_hrs * 60 * 60 # convert hours to seconds
153+
else:
154+
expired_threshold = current_app.config['DELETE_EXPIRED_AFTER'] # seconds
155+
156+
if info_threshold_hrs:
157+
info_threshold = info_threshold_hrs * 60 * 60 # convert hours to seconds
158+
else:
159+
info_threshold = current_app.config['DELETE_INFO_AFTER'] # seconds
150160

151161
has_expired, shelve_timeout, ack_timeout = Alert.housekeeping(expired_threshold, info_threshold)
152162

alerta/models/alert.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ def delete_note(self, note_id):
589589
return Note.delete_by_id(note_id)
590590

591591
@staticmethod
592-
def housekeeping(expired_threshold: int = 2, info_threshold: int = 12) -> Tuple[List['Alert'], List['Alert'], List['Alert']]:
592+
def housekeeping(expired_threshold: int, info_threshold: int) -> Tuple[List['Alert'], List['Alert'], List['Alert']]:
593593
return (
594594
[Alert.from_db(alert) for alert in db.get_expired(expired_threshold, info_threshold)],
595595
[Alert.from_db(alert) for alert in db.get_unshelve()],

alerta/settings.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@
188188
SHELVE_TIMEOUT = 7200 # auto-unshelve alerts after x seconds (0 seconds = do not auto-unshelve)
189189

190190
# Housekeeping settings
191-
DEFAULT_EXPIRED_DELETE_HRS = 2 # hours (0 hours = do not delete)
192-
DEFAULT_INFO_DELETE_HRS = 12 # hours (0 hours = do not delete)
191+
DELETE_EXPIRED_AFTER = 2 * 60 * 60 # seconds (0 = do not delete)
192+
DELETE_INFO_AFTER = 12 * 60 * 60 # seconds (0 = do not delete)
193193

194194
# Send verification emails to new BasicAuth users
195195
EMAIL_VERIFICATION = False

alerta/utils/config.py

+16
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ def get_user_config():
7575

7676
config['GOOGLE_TRACKING_ID'] = get_config('GOOGLE_TRACKING_ID', default=None, type=str, config=config)
7777

78+
# housekeeping
79+
delete_expired_hrs = (
80+
os.environ.get('DEFAULT_EXPIRED_DELETE_HRS', None)
81+
or os.environ.get('HK_EXPIRED_DELETE_HRS', None)
82+
)
83+
delete_expired = delete_expired_hrs * 60 * 60 if delete_expired_hrs else None
84+
config['DELETE_EXPIRED_AFTER'] = get_config('DELETE_EXPIRED_AFTER', default=delete_expired, type=int, config=config)
85+
86+
delete_info_hrs = (
87+
os.environ.get('DEFAULT_INFO_DELETE_HRS', None)
88+
or os.environ.get('HK_INFO_DELETE_HRS', None)
89+
)
90+
delete_info = delete_info_hrs * 60 * 60 if delete_info_hrs else None
91+
config['DELETE_INFO_AFTER'] = get_config('DELETE_INFO_AFTER', default=delete_info, type=int, config=config)
92+
93+
# plugins
7894
config['PLUGINS'] = get_config('PLUGINS', default=[], type=list, config=config)
7995

8096
# blackout plugin

setup.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ universal = 1
33

44
[pycodestyle]
55
max-line-length = 120
6+
7+
[tool:pytest]
8+
norecursedirs=tests/helpers

tests/helpers/__init__.py

Whitespace-only changes.

tests/helpers/utils.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import contextlib
2+
import os
3+
4+
5+
@contextlib.contextmanager
6+
def mod_env(*remove, **update):
7+
"""
8+
See https://stackoverflow.com/questions/2059482#34333710
9+
10+
Temporarily updates the ``os.environ`` dictionary in-place.
11+
12+
The ``os.environ`` dictionary is updated in-place so that the modification
13+
is sure to work in all situations.
14+
15+
:param remove: Environment variables to remove.
16+
:param update: Dictionary of environment variables and values to add/update.
17+
"""
18+
env = os.environ
19+
update = update or {}
20+
remove = remove or []
21+
22+
# List of environment variables being updated or removed.
23+
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
24+
# Environment variables and values to restore on exit.
25+
update_after = {k: env[k] for k in stomped}
26+
# Environment variables and values to remove on exit.
27+
remove_after = frozenset(k for k in update if k not in env)
28+
29+
try:
30+
env.update(update)
31+
[env.pop(k, None) for k in remove]
32+
yield
33+
finally:
34+
env.update(update_after)
35+
[env.pop(k) for k in remove_after]

tests/test_config.py

+1-35
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,8 @@
1-
import contextlib
21
import json
3-
import os
42
import unittest
53

64
from alerta.app import create_app, db
7-
8-
9-
@contextlib.contextmanager
10-
def mod_env(*remove, **update):
11-
"""
12-
See https://stackoverflow.com/questions/2059482#34333710
13-
14-
Temporarily updates the ``os.environ`` dictionary in-place.
15-
16-
The ``os.environ`` dictionary is updated in-place so that the modification
17-
is sure to work in all situations.
18-
19-
:param remove: Environment variables to remove.
20-
:param update: Dictionary of environment variables and values to add/update.
21-
"""
22-
env = os.environ
23-
update = update or {}
24-
remove = remove or []
25-
26-
# List of environment variables being updated or removed.
27-
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
28-
# Environment variables and values to restore on exit.
29-
update_after = {k: env[k] for k in stomped}
30-
# Environment variables and values to remove on exit.
31-
remove_after = frozenset(k for k in update if k not in env)
32-
33-
try:
34-
env.update(update)
35-
[env.pop(k, None) for k in remove]
36-
yield
37-
finally:
38-
env.update(update_after)
39-
[env.pop(k) for k in remove_after]
5+
from tests.helpers.utils import mod_env
406

417

428
class ConfigTestCase(unittest.TestCase):

tests/test_management.py

+53-2
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,29 @@
44
from uuid import uuid4
55

66
from alerta.app import create_app, db
7+
from tests.helpers.utils import mod_env
78

89

910
class ManagementTestCase(unittest.TestCase):
1011

1112
def setUp(self):
1213

14+
self.maxDiff = None
15+
1316
test_config = {
1417
'DEBUG': False,
1518
'TESTING': True,
1619
'AUTH_REQUIRED': False,
1720
# 'ACK_TIMEOUT': 2,
1821
# 'SHELVE_TIMEOUT': 3
1922
}
20-
self.app = create_app(test_config)
21-
self.client = self.app.test_client()
23+
24+
with mod_env(
25+
DELETE_EXPIRED_AFTER='2',
26+
DELETE_INFO_AFTER='3'
27+
):
28+
self.app = create_app(test_config)
29+
self.client = self.app.test_client()
2230

2331
self.headers = {
2432
'Content-type': 'application/json'
@@ -79,6 +87,16 @@ def random_resource():
7987
'correlate': ['node_down', 'node_marginal', 'node_up']
8088
}
8189

90+
self.info_alert = {
91+
'event': 'node_init',
92+
'resource': random_resource(),
93+
'environment': 'Production',
94+
'service': ['Network'],
95+
'severity': 'informational',
96+
'correlate': ['node_down', 'node_marginal', 'node_up'],
97+
'timeout': 3
98+
}
99+
82100
def tearDown(self):
83101
db.destroy()
84102

@@ -153,8 +171,14 @@ def test_housekeeping(self):
153171
# create an alert that should be unaffected
154172
response = self.client.post('/alert', data=json.dumps(self.ok_alert), headers=self.headers)
155173
self.assertEqual(response.status_code, 201)
174+
175+
# create an info alert that should be deleted
176+
response = self.client.post('/alert', data=json.dumps(self.info_alert), headers=self.headers)
177+
self.assertEqual(response.status_code, 201)
156178
data = json.loads(response.data.decode('utf-8'))
157179

180+
info_id = data['id']
181+
158182
# create an alert and ack it then shelve it
159183
response = self.client.post('/alert', data=json.dumps(self.acked_and_shelved_alert), headers=self.headers)
160184
self.assertEqual(response.status_code, 201)
@@ -184,6 +208,7 @@ def test_housekeeping(self):
184208

185209
time.sleep(5)
186210

211+
# run housekeeping (1st time)
187212
response = self.client.get('/management/housekeeping', headers=self.headers)
188213
self.assertEqual(response.status_code, 200)
189214
data = json.loads(response.data.decode('utf-8'))
@@ -232,3 +257,29 @@ def test_housekeeping(self):
232257
self.assertEqual(data['alert']['history'][2]['timeout'], 3)
233258
self.assertEqual(data['alert']['history'][3]['status'], 'ack')
234259
self.assertEqual(data['alert']['history'][3]['timeout'], 4)
260+
261+
response = self.client.get('/alert/' + info_id)
262+
self.assertEqual(response.status_code, 404)
263+
264+
time.sleep(5)
265+
266+
# run housekeeping (2nd time)
267+
response = self.client.get('/management/housekeeping', headers=self.headers)
268+
self.assertEqual(response.status_code, 200)
269+
data = json.loads(response.data.decode('utf-8'))
270+
self.assertEqual(data['count'], 1)
271+
self.assertListEqual(data['expired'], [])
272+
self.assertListEqual(data['unshelve'], [])
273+
self.assertListEqual(data['unack'], [acked_and_shelved_id])
274+
275+
response = self.client.get('/alert/' + expired_id)
276+
self.assertEqual(response.status_code, 404)
277+
278+
# run housekeeping (3rd time)
279+
response = self.client.get('/management/housekeeping', headers=self.headers)
280+
self.assertEqual(response.status_code, 200)
281+
data = json.loads(response.data.decode('utf-8'))
282+
self.assertEqual(data['count'], 0)
283+
self.assertListEqual(data['expired'], [])
284+
self.assertListEqual(data['unshelve'], [])
285+
self.assertListEqual(data['unack'], [])

0 commit comments

Comments
 (0)