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
import random
import threading
import uuid
from django.conf import settings
from django.db.models.signals import post_delete, post_save
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 .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'):
return
@ -27,14 +32,7 @@ def record_object_change(user, request_id, instance, **kwargs):
else:
action = OBJECTCHANGE_ACTION_DELETE
instance.log_change(user, request_id, 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()
_thread_locals.changed_objects.append((instance, action))
class ChangeLoggingMiddleware(object):
@ -44,22 +42,30 @@ class ChangeLoggingMiddleware(object):
def __call__(self, request):
def get_user(request):
return request.user
# Initialize the list of changed objects
_thread_locals.changed_objects = []
# DRF employs a separate authentication mechanism outside Django's normal request/response cycle, so calling
# request.user in middleware will always return AnonymousUser for API requests. To work around this, we point
# to a lazy object that doesn't resolve the user until after DRF's authentication has been called. For more
# detail, see https://stackoverflow.com/questions/26240832/
user = SimpleLazyObject(lambda: get_user(request))
# Assign a random unique ID to the request. This will be used to associate multiple object changes made during
# the same request.
request.id = uuid.uuid4()
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
# record_object_change() to include the user associated with the current request.
_record_object_change = curry(record_object_change, user, request_id)
# Process the request
response = self.get_response(request)
post_save.connect(_record_object_change, dispatch_uid='record_object_saved')
post_delete.connect(_record_object_change, dispatch_uid='record_object_deleted')
# Record object changes
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