-
-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5097 from kobotoolbox/delete-access-logs-config
Delete expired access logs
- Loading branch information
Showing
6 changed files
with
159 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from datetime import timedelta | ||
|
||
from constance import config | ||
from django.conf import settings | ||
from django.utils import timezone | ||
from more_itertools import chunked | ||
|
||
from kobo.apps.audit_log.models import ( | ||
AccessLog, | ||
AuditLog, | ||
) | ||
from kobo.celery import celery_app | ||
from kpi.utils.log import logging | ||
|
||
|
||
@celery_app.task() | ||
def batch_delete_audit_logs_by_id(ids): | ||
logs = AuditLog.objects.filter(id__in=ids) | ||
count, _ = logs.delete() | ||
logging.info(f'Deleted {count} audit logs from database') | ||
|
||
|
||
@celery_app.task() | ||
def spawn_access_log_cleaning_tasks(): | ||
""" | ||
Enqueue tasks to delete access logs older than ACCESS_LOG_LIFESPAN days old. | ||
ACCESS_LOG_LIFESPAN is configured via constance. | ||
Ids are batched into multiple tasks. | ||
""" | ||
|
||
expiration_date = timezone.now() - timedelta( | ||
days=config.ACCESS_LOG_LIFESPAN | ||
) | ||
|
||
expired_logs = ( | ||
AccessLog.objects.filter(date_created__lt=expiration_date) | ||
.values_list('id', flat=True) | ||
.iterator() | ||
) | ||
for id_batch in chunked( | ||
expired_logs, settings.ACCESS_LOG_DELETION_BATCH_SIZE | ||
): | ||
# queue up a new task for each batch of expired ids | ||
batch_delete_audit_logs_by_id.delay(ids=id_batch) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
from datetime import timedelta | ||
from unittest.mock import patch | ||
|
||
from constance.test import override_config | ||
from django.test import override_settings | ||
from django.utils import timezone | ||
|
||
from kobo.apps.audit_log.models import AccessLog | ||
from kobo.apps.audit_log.tasks import ( | ||
batch_delete_audit_logs_by_id, | ||
spawn_access_log_cleaning_tasks, | ||
) | ||
from kobo.apps.kobo_auth.shortcuts import User | ||
from kpi.tests.base_test_case import BaseTestCase | ||
|
||
|
||
@override_config(ACCESS_LOG_LIFESPAN=1) | ||
class AuditLogTasksTestCase(BaseTestCase): | ||
|
||
fixtures = ['test_data'] | ||
|
||
def test_spawn_deletion_task_identifies_expired_logs(self): | ||
""" | ||
Test the spawning task correctly identifies which logs to delete. | ||
Separated for easier debugging of the spawning vs deleting steps | ||
""" | ||
user = User.objects.get(username='someuser') | ||
old_log = AccessLog.objects.create( | ||
user=user, | ||
date_created=timezone.now() - timedelta(days=1, hours=1), | ||
) | ||
older_log = AccessLog.objects.create( | ||
user=user, | ||
date_created=timezone.now() - timedelta(days=2) | ||
) | ||
new_log = AccessLog.objects.create(user=user) | ||
|
||
with patch( | ||
'kobo.apps.audit_log.tasks.batch_delete_audit_logs_by_id.delay' | ||
) as patched_spawned_task: | ||
spawn_access_log_cleaning_tasks() | ||
|
||
# get the list of ids passed for any call to the actual deletion task | ||
id_lists = [kwargs['ids'] for _, _, kwargs in patched_spawned_task.mock_calls] | ||
# flatten the list | ||
all_deleted_ids = [log_id for id_list in id_lists for log_id in id_list] | ||
self.assertIn(old_log.id, all_deleted_ids) | ||
self.assertIn(older_log.id, all_deleted_ids) | ||
self.assertNotIn(new_log.id, all_deleted_ids) | ||
|
||
@override_settings(ACCESS_LOG_DELETION_BATCH_SIZE=2) | ||
def test_spawn_task_batches_ids(self): | ||
three_days_ago = timezone.now() - timedelta(days=3) | ||
user = User.objects.get(username='someuser') | ||
old_log_1 = AccessLog.objects.create( | ||
user=user, date_created=three_days_ago | ||
) | ||
old_log_2 = AccessLog.objects.create( | ||
user=user, date_created=three_days_ago | ||
) | ||
old_log_3 = AccessLog.objects.create( | ||
user=user, date_created=three_days_ago | ||
) | ||
|
||
with patch( | ||
'kobo.apps.audit_log.tasks.batch_delete_audit_logs_by_id.delay' | ||
) as patched_spawned_task: | ||
spawn_access_log_cleaning_tasks() | ||
|
||
# Should be 2 batches | ||
self.assertEqual(patched_spawned_task.call_count, 2) | ||
# make sure all batches were <= ACCESS_LOG_DELETION_BATCH_SIZE | ||
all_deleted_ids = [] | ||
for task_call in patched_spawned_task.mock_calls: | ||
_, _, kwargs = task_call | ||
id_list = kwargs['ids'] | ||
self.assertLessEqual(len(id_list), 2) | ||
all_deleted_ids.extend(id_list) | ||
|
||
# make sure we queued everything for deletion | ||
self.assertIn(old_log_1.id, all_deleted_ids) | ||
self.assertIn(old_log_2.id, all_deleted_ids) | ||
self.assertIn(old_log_3.id, all_deleted_ids) | ||
|
||
def test_batch_delete_audit_logs_by_id(self): | ||
user = User.objects.get(username='someuser') | ||
log_1 = AccessLog.objects.create(user=user) | ||
log_2 = AccessLog.objects.create(user=user) | ||
log_3 = AccessLog.objects.create(user=user) | ||
self.assertEqual(AccessLog.objects.count(), 3) | ||
|
||
batch_delete_audit_logs_by_id(ids=[log_1.id, log_2.id]) | ||
# only log_3 should remain | ||
self.assertEqual(AccessLog.objects.count(), 1) | ||
self.assertEqual(AccessLog.objects.first().id, log_3.id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters