1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Adapted change logging to queue changes in thread-local storage and record them at the end of the request

This commit is contained in:
Jeremy Stretch
2018-07-10 13:33:54 -04:00
parent 663bbd025e
commit df1f33992a

View File

@ -2,20 +2,25 @@ from __future__ import unicode_literals
from datetime import timedelta from datetime import timedelta
import random import random
import threading
import uuid import uuid
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.utils import timezone from django.utils import timezone
from django.utils.functional import curry, SimpleLazyObject
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
from .models import ObjectChange from .models import ObjectChange
def record_object_change(user, request_id, instance, **kwargs): _thread_locals = threading.local()
def mark_object_changed(instance, **kwargs):
""" """
Create an ObjectChange in response to an object being created or deleted. Mark an object as having been created, saved, or updated. At the end of the request, this change will be recorded.
We have to wait until the *end* of the request to the serialize the object, because related fields like tags and
custom fields have not yet been updated when the post_save signal is emitted.
""" """
if not hasattr(instance, 'log_change'): if not hasattr(instance, 'log_change'):
return return
@ -27,14 +32,7 @@ def record_object_change(user, request_id, instance, **kwargs):
else: else:
action = OBJECTCHANGE_ACTION_DELETE action = OBJECTCHANGE_ACTION_DELETE
instance.log_change(user, request_id, action) _thread_locals.changed_objects.append((instance, action))
# 1% chance of clearing out expired ObjectChanges
if settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
purged_count, _ = ObjectChange.objects.filter(
time__lt=cutoff
).delete()
class ChangeLoggingMiddleware(object): class ChangeLoggingMiddleware(object):
@ -44,22 +42,30 @@ class ChangeLoggingMiddleware(object):
def __call__(self, request): def __call__(self, request):
def get_user(request): # Initialize the list of changed objects
return request.user _thread_locals.changed_objects = []
# DRF employs a separate authentication mechanism outside Django's normal request/response cycle, so calling # Assign a random unique ID to the request. This will be used to associate multiple object changes made during
# request.user in middleware will always return AnonymousUser for API requests. To work around this, we point # the same request.
# to a lazy object that doesn't resolve the user until after DRF's authentication has been called. For more request.id = uuid.uuid4()
# detail, see https://stackoverflow.com/questions/26240832/
user = SimpleLazyObject(lambda: get_user(request))
request_id = uuid.uuid4() # Connect mark_object_changed to the post_save and post_delete receivers
post_save.connect(mark_object_changed, dispatch_uid='record_object_saved')
post_delete.connect(mark_object_changed, dispatch_uid='record_object_deleted')
# Django doesn't provide any request context with the post_save/post_delete signals, so we curry # Process the request
# record_object_change() to include the user associated with the current request. response = self.get_response(request)
_record_object_change = curry(record_object_change, user, request_id)
post_save.connect(_record_object_change, dispatch_uid='record_object_saved') # Record object changes
post_delete.connect(_record_object_change, dispatch_uid='record_object_deleted') for obj, action in _thread_locals.changed_objects:
if obj.pk:
obj.log_change(request.user, request.id, action)
return self.get_response(request) # Housekeeping: 1% chance of clearing out expired ObjectChanges
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
purged_count, _ = ObjectChange.objects.filter(
time__lt=cutoff
).delete()
return response