middleware.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import random
  2. import threading
  3. import uuid
  4. from copy import deepcopy
  5. from datetime import timedelta
  6. from django.conf import settings
  7. from django.db.models.signals import pre_delete, post_save
  8. from django.utils import timezone
  9. from django_prometheus.models import model_deletes, model_inserts, model_updates
  10. from extras.utils import is_taggable
  11. from utilities.querysets import DummyQuerySet
  12. from .choices import ObjectChangeActionChoices
  13. from .models import ObjectChange
  14. from .signals import purge_changelog
  15. from .webhooks import enqueue_webhooks
  16. _thread_locals = threading.local()
  17. def handle_changed_object(sender, instance, **kwargs):
  18. """
  19. Fires when an object is created or updated.
  20. """
  21. # Queue the object for processing once the request completes
  22. action = ObjectChangeActionChoices.ACTION_CREATE if kwargs['created'] else ObjectChangeActionChoices.ACTION_UPDATE
  23. _thread_locals.changed_objects.append(
  24. (instance, action)
  25. )
  26. def handle_deleted_object(sender, instance, **kwargs):
  27. """
  28. Fires when an object is deleted.
  29. """
  30. # Cache custom fields prior to copying the instance
  31. if hasattr(instance, 'cache_custom_fields'):
  32. instance.cache_custom_fields()
  33. # Create a copy of the object being deleted
  34. copy = deepcopy(instance)
  35. # Preserve tags
  36. if is_taggable(instance):
  37. copy.tags = DummyQuerySet(instance.tags.all())
  38. # Queue the copy of the object for processing once the request completes
  39. _thread_locals.changed_objects.append(
  40. (copy, ObjectChangeActionChoices.ACTION_DELETE)
  41. )
  42. def purge_objectchange_cache(sender, **kwargs):
  43. """
  44. Delete any queued object changes waiting to be written.
  45. """
  46. _thread_locals.changed_objects = []
  47. class ObjectChangeMiddleware(object):
  48. """
  49. This middleware performs three functions in response to an object being created, updated, or deleted:
  50. 1. Create an ObjectChange to reflect the modification to the object in the changelog.
  51. 2. Enqueue any relevant webhooks.
  52. 3. Increment the metric counter for the event type.
  53. The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit
  54. differently for each. Objects being saved are cached into thread-local storage for action *after* the response has
  55. completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags)
  56. have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the
  57. object is recorded before it (and any related objects) are actually deleted from the database.
  58. """
  59. def __init__(self, get_response):
  60. self.get_response = get_response
  61. def __call__(self, request):
  62. # Initialize an empty list to cache objects being saved.
  63. _thread_locals.changed_objects = []
  64. # Assign a random unique ID to the request. This will be used to associate multiple object changes made during
  65. # the same request.
  66. request.id = uuid.uuid4()
  67. # Connect our receivers to the post_save and post_delete signals.
  68. post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
  69. pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
  70. # Provide a hook for purging the change cache
  71. purge_changelog.connect(purge_objectchange_cache)
  72. # Process the request
  73. response = self.get_response(request)
  74. # If the change cache is empty, there's nothing more we need to do.
  75. if not _thread_locals.changed_objects:
  76. return response
  77. # Create records for any cached objects that were changed.
  78. for instance, action in _thread_locals.changed_objects:
  79. # Refresh cached custom field values
  80. if action in [ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]:
  81. if hasattr(instance, 'cache_custom_fields'):
  82. instance.cache_custom_fields()
  83. # Record an ObjectChange if applicable
  84. if hasattr(instance, 'to_objectchange'):
  85. objectchange = instance.to_objectchange(action)
  86. objectchange.user = request.user
  87. objectchange.request_id = request.id
  88. objectchange.save()
  89. # Enqueue webhooks
  90. enqueue_webhooks(instance, request.user, request.id, action)
  91. # Increment metric counters
  92. if action == ObjectChangeActionChoices.ACTION_CREATE:
  93. model_inserts.labels(instance._meta.model_name).inc()
  94. elif action == ObjectChangeActionChoices.ACTION_UPDATE:
  95. model_updates.labels(instance._meta.model_name).inc()
  96. elif action == ObjectChangeActionChoices.ACTION_DELETE:
  97. model_deletes.labels(instance._meta.model_name).inc()
  98. # Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in
  99. # one or more changes being logged.
  100. if settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
  101. cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
  102. purged_count, _ = ObjectChange.objects.filter(
  103. time__lt=cutoff
  104. ).delete()
  105. return response