middleware.py 4.6 KB

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