django-activity-audit 1.3.0.dev14__tar.gz → 1.3.0.dev16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/PKG-INFO +1 -1
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/formatters.py +2 -1
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/handlers.py +2 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/middleware.py +40 -28
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/signals.py +64 -95
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/pyproject.toml +1 -1
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/.gitignore +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/.pre-commit-config.yaml +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/LICENSE +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/MANIFEST.in +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/README.md +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/README.rst +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/__init__.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/apps.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/constants.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/logger_levels.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/protocols.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/settings.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/unregistered.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/utils.py +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/docs/django-activity-audit.md +0 -0
- /django_activity_audit-1.3.0.dev14/activity_audit/REVIEW.md → /django_activity_audit-1.3.0.dev16/docs/improvements.md +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/docs/user-activity-feed-plan.md +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/hatch +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/pytest.ini +0 -0
- {django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-activity-audit
|
|
3
|
-
Version: 1.3.0.
|
|
3
|
+
Version: 1.3.0.dev16
|
|
4
4
|
Summary: A Django package for easy CRUD operation logging and container logs
|
|
5
5
|
Project-URL: Homepage, https://github.com/shree256/django-activity-audit
|
|
6
6
|
Project-URL: Repository, https://github.com/shree256/django-activity-audit
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/formatters.py
RENAMED
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
7
|
from .constants import LogType
|
|
8
|
+
from .middleware import get_request_id
|
|
8
9
|
|
|
9
10
|
def _json_default(obj):
|
|
10
11
|
"""
|
|
@@ -49,7 +50,7 @@ class AppFormatter(logging.Formatter):
|
|
|
49
50
|
"path": record.pathname,
|
|
50
51
|
"module": record.module,
|
|
51
52
|
"function": record.funcName,
|
|
52
|
-
"request_id": getattr(record, "request_id",
|
|
53
|
+
"request_id": getattr(record, "request_id", None) or get_request_id() or "",
|
|
53
54
|
"message": record.getMessage(),
|
|
54
55
|
"exception": "",
|
|
55
56
|
"log_type": self.log_type,
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/handlers.py
RENAMED
|
@@ -170,6 +170,8 @@ class AsyncJsonHandler(QueueHandler):
|
|
|
170
170
|
self._listener.start()
|
|
171
171
|
|
|
172
172
|
def prepare(self, record):
|
|
173
|
+
# Capture request_id in the calling thread before the record is queued,
|
|
174
|
+
# since thread-locals are not accessible from the QueueListener thread.
|
|
173
175
|
record = super().prepare(record)
|
|
174
176
|
record.request_id = get_request_id() or ""
|
|
175
177
|
return record
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/middleware.py
RENAMED
|
@@ -126,18 +126,6 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
126
126
|
|
|
127
127
|
def __init__(self, get_response):
|
|
128
128
|
self.get_response = get_response
|
|
129
|
-
self.log_data = {
|
|
130
|
-
"service_name": SERVICE_NAME,
|
|
131
|
-
"request_type": REQUEST_TYPES[0],
|
|
132
|
-
"protocol": None,
|
|
133
|
-
"request_id": "",
|
|
134
|
-
"user_id": "",
|
|
135
|
-
"user_info": {},
|
|
136
|
-
"request_repr": {},
|
|
137
|
-
"response_repr": {},
|
|
138
|
-
"error_message": None,
|
|
139
|
-
"execution_time": 0,
|
|
140
|
-
}
|
|
141
129
|
|
|
142
130
|
if iscoroutinefunction(self.get_response):
|
|
143
131
|
markcoroutinefunction(self)
|
|
@@ -152,6 +140,18 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
152
140
|
if not should_log_url(request.path):
|
|
153
141
|
return self.get_response(request)
|
|
154
142
|
|
|
143
|
+
log_data = {
|
|
144
|
+
"service_name": SERVICE_NAME,
|
|
145
|
+
"request_type": REQUEST_TYPES[0],
|
|
146
|
+
"protocol": None,
|
|
147
|
+
"request_id": "",
|
|
148
|
+
"user_id": "",
|
|
149
|
+
"user_info": {},
|
|
150
|
+
"request_repr": {},
|
|
151
|
+
"response_repr": {},
|
|
152
|
+
"error_message": None,
|
|
153
|
+
"execution_time": 0,
|
|
154
|
+
}
|
|
155
155
|
start_time = time.time()
|
|
156
156
|
|
|
157
157
|
# Log request
|
|
@@ -175,8 +175,8 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
175
175
|
|
|
176
176
|
# Capture user details AFTER authentication has happened
|
|
177
177
|
user_id, user_info = get_user_details()
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
log_data["user_id"] = user_id
|
|
179
|
+
log_data["user_info"] = user_info
|
|
180
180
|
|
|
181
181
|
# TODO: Find way to add status code to response_data
|
|
182
182
|
|
|
@@ -196,13 +196,13 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
196
196
|
except UnicodeDecodeError:
|
|
197
197
|
response_data["body"] = "Binary content"
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
log_data["execution_time"] = end_time - start_time
|
|
200
|
+
log_data["protocol"] = "https" if request.is_secure() else "http"
|
|
201
|
+
log_data["request_id"] = request_id
|
|
202
|
+
log_data["request_repr"] = request_data
|
|
203
|
+
log_data["response_repr"] = response_data
|
|
204
204
|
|
|
205
|
-
logger.api("Audit Internal Request", extra=
|
|
205
|
+
logger.api("Audit Internal Request", extra=log_data)
|
|
206
206
|
|
|
207
207
|
clear_request()
|
|
208
208
|
|
|
@@ -216,6 +216,18 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
216
216
|
if not should_log_url(request.path):
|
|
217
217
|
return await self.get_response(request)
|
|
218
218
|
|
|
219
|
+
log_data = {
|
|
220
|
+
"service_name": SERVICE_NAME,
|
|
221
|
+
"request_type": REQUEST_TYPES[0],
|
|
222
|
+
"protocol": None,
|
|
223
|
+
"request_id": "",
|
|
224
|
+
"user_id": "",
|
|
225
|
+
"user_info": {},
|
|
226
|
+
"request_repr": {},
|
|
227
|
+
"response_repr": {},
|
|
228
|
+
"error_message": None,
|
|
229
|
+
"execution_time": 0,
|
|
230
|
+
}
|
|
219
231
|
start_time = time.time()
|
|
220
232
|
|
|
221
233
|
# Log request
|
|
@@ -241,8 +253,8 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
241
253
|
user_id, user_info = await sync_to_async(
|
|
242
254
|
get_user_details, thread_sensitive=True
|
|
243
255
|
)()
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
log_data["user_id"] = user_id
|
|
257
|
+
log_data["user_info"] = user_info
|
|
246
258
|
|
|
247
259
|
# TODO: Find way to add status code to response_data
|
|
248
260
|
|
|
@@ -262,13 +274,13 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
262
274
|
except UnicodeDecodeError:
|
|
263
275
|
response_data["body"] = "Binary content"
|
|
264
276
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
log_data["execution_time"] = end_time - start_time
|
|
278
|
+
log_data["protocol"] = "https" if request.is_secure() else "http"
|
|
279
|
+
log_data["request_id"] = request_id
|
|
280
|
+
log_data["request_repr"] = request_data
|
|
281
|
+
log_data["response_repr"] = response_data
|
|
270
282
|
|
|
271
|
-
logger.api("Audit Internal Request", extra=
|
|
283
|
+
logger.api("Audit Internal Request", extra=log_data)
|
|
272
284
|
|
|
273
285
|
clear_request()
|
|
274
286
|
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/signals.py
RENAMED
|
@@ -2,7 +2,7 @@ import inspect
|
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
4
|
from functools import wraps
|
|
5
|
-
from typing import Any, List
|
|
5
|
+
from typing import Any, List
|
|
6
6
|
|
|
7
7
|
from django.apps import apps
|
|
8
8
|
from django.db import models, transaction
|
|
@@ -54,32 +54,70 @@ def should_audit(instance_or_class):
|
|
|
54
54
|
return True
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
# Get the current frame
|
|
61
|
-
frame = inspect.currentframe()
|
|
62
|
-
# Go up 3 frames to get to the actual calling function
|
|
63
|
-
# (1 for get_calling_model, 1 for the signal handler, 1 for the bulk operation)
|
|
64
|
-
for _ in range(3):
|
|
65
|
-
frame = frame.f_back
|
|
66
|
-
if frame is None:
|
|
67
|
-
return None
|
|
68
|
-
|
|
69
|
-
# Get the calling function's name
|
|
70
|
-
calling_function = frame.f_code.co_name
|
|
71
|
-
# Get the module name
|
|
72
|
-
module_name = frame.f_globals.get("__name__", "")
|
|
73
|
-
|
|
74
|
-
# Check if this is a direct bulk operation call
|
|
75
|
-
if "bulk_create" in calling_function or "bulk_update" in calling_function:
|
|
76
|
-
return module_name.split(".")[-1]
|
|
77
|
-
except Exception:
|
|
78
|
-
pass
|
|
79
|
-
return None
|
|
57
|
+
_patched_models: set = set()
|
|
58
|
+
_queryset_patched: bool = False
|
|
80
59
|
|
|
81
60
|
|
|
82
|
-
|
|
61
|
+
def patch_queryset_bulk_methods() -> None:
|
|
62
|
+
"""Patch QuerySet.bulk_create and bulk_update exactly once."""
|
|
63
|
+
global _queryset_patched
|
|
64
|
+
if _queryset_patched:
|
|
65
|
+
return
|
|
66
|
+
_queryset_patched = True
|
|
67
|
+
|
|
68
|
+
original_bulk_create = models.QuerySet.bulk_create
|
|
69
|
+
original_bulk_update = models.QuerySet.bulk_update
|
|
70
|
+
|
|
71
|
+
@wraps(original_bulk_create)
|
|
72
|
+
def bulk_create_with_signals(
|
|
73
|
+
self, objs: List[models.Model], *args: Any, **kwargs: Any
|
|
74
|
+
) -> List[models.Model]:
|
|
75
|
+
if not objs:
|
|
76
|
+
return original_bulk_create(self, objs, *args, **kwargs)
|
|
77
|
+
|
|
78
|
+
created_objs = original_bulk_create(self, objs, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
model_class = self.model
|
|
81
|
+
if not should_audit(model_class):
|
|
82
|
+
return created_objs
|
|
83
|
+
|
|
84
|
+
first_obj = created_objs[0]
|
|
85
|
+
push_log(
|
|
86
|
+
f"{EVENT_TYPES[3]} event by {model_class.__name__} (id: {first_obj.pk})",
|
|
87
|
+
model_class.__name__,
|
|
88
|
+
EVENT_TYPES[3],
|
|
89
|
+
str(first_obj.pk),
|
|
90
|
+
instance_to_dict(first_obj),
|
|
91
|
+
{"total_count": len(created_objs)},
|
|
92
|
+
)
|
|
93
|
+
return created_objs
|
|
94
|
+
|
|
95
|
+
@wraps(original_bulk_update)
|
|
96
|
+
def bulk_update_with_signals(
|
|
97
|
+
self, objs: List[models.Model], fields: List[str], batch_size=None
|
|
98
|
+
) -> None:
|
|
99
|
+
if not objs:
|
|
100
|
+
return original_bulk_update(self, objs, fields, batch_size)
|
|
101
|
+
|
|
102
|
+
result = original_bulk_update(self, objs, fields, batch_size)
|
|
103
|
+
|
|
104
|
+
model_class = self.model
|
|
105
|
+
if not should_audit(model_class):
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
first_obj = objs[0]
|
|
109
|
+
push_log(
|
|
110
|
+
f"{EVENT_TYPES[4]} event by {model_class.__name__}",
|
|
111
|
+
model_class.__name__,
|
|
112
|
+
EVENT_TYPES[4],
|
|
113
|
+
str(first_obj.pk),
|
|
114
|
+
instance_to_dict(first_obj),
|
|
115
|
+
{"total_count": len(objs), "fields": fields},
|
|
116
|
+
)
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
models.QuerySet.bulk_create = bulk_create_with_signals
|
|
120
|
+
models.QuerySet.bulk_update = bulk_update_with_signals
|
|
83
121
|
|
|
84
122
|
|
|
85
123
|
def push_log(
|
|
@@ -126,8 +164,6 @@ def patch_model_event(model_class: type[models.Model]) -> None:
|
|
|
126
164
|
|
|
127
165
|
# Store the original methods
|
|
128
166
|
original_save = model_class.save
|
|
129
|
-
original_bulk_create = models.QuerySet.bulk_create
|
|
130
|
-
original_bulk_update = models.QuerySet.bulk_update
|
|
131
167
|
|
|
132
168
|
# SAVE ---------------------------------------------------------------------------
|
|
133
169
|
@wraps(original_save)
|
|
@@ -175,76 +211,8 @@ def patch_model_event(model_class: type[models.Model]) -> None:
|
|
|
175
211
|
instance_repr,
|
|
176
212
|
)
|
|
177
213
|
|
|
178
|
-
# BULK --------------------------------------------------------------------------
|
|
179
|
-
@wraps(original_bulk_create)
|
|
180
|
-
def bulk_create_with_signals(
|
|
181
|
-
self, objs: List[models.Model], *args: Any, **kwargs: Any
|
|
182
|
-
) -> List[models.Model]:
|
|
183
|
-
if not objs:
|
|
184
|
-
return original_bulk_create(self, objs, *args, **kwargs)
|
|
185
|
-
|
|
186
|
-
# Get the calling model
|
|
187
|
-
calling_model = get_calling_model()
|
|
188
|
-
if not calling_model:
|
|
189
|
-
return original_bulk_create(self, objs, *args, **kwargs)
|
|
190
|
-
|
|
191
|
-
# Call the original bulk_create method
|
|
192
|
-
created_objs = original_bulk_create(self, objs, *args, **kwargs)
|
|
193
|
-
|
|
194
|
-
# Log only if this is the calling model
|
|
195
|
-
if calling_model == model_class.__name__:
|
|
196
|
-
first_obj = created_objs[0]
|
|
197
|
-
instance_repr = instance_to_dict(first_obj)
|
|
198
|
-
|
|
199
|
-
push_log(
|
|
200
|
-
f"{EVENT_TYPES[3]} event by {model_class.__name__} (id: {first_obj.pk})",
|
|
201
|
-
model_class.__name__,
|
|
202
|
-
EVENT_TYPES[3],
|
|
203
|
-
str(first_obj.pk),
|
|
204
|
-
instance_repr,
|
|
205
|
-
{
|
|
206
|
-
"total_count": len(created_objs),
|
|
207
|
-
},
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
return created_objs
|
|
211
|
-
|
|
212
|
-
@wraps(original_bulk_update)
|
|
213
|
-
def bulk_update_with_signals(
|
|
214
|
-
self, objs: List[models.Model], fields: List[str], batch_size=None
|
|
215
|
-
) -> None:
|
|
216
|
-
if not objs:
|
|
217
|
-
return original_bulk_update(self, objs, fields, batch_size)
|
|
218
|
-
|
|
219
|
-
# Call the original bulk_update method
|
|
220
|
-
bulk_update = original_bulk_update(self, objs, fields, batch_size)
|
|
221
|
-
|
|
222
|
-
# Get the calling model
|
|
223
|
-
calling_model = get_calling_model()
|
|
224
|
-
if not calling_model:
|
|
225
|
-
return bulk_update
|
|
226
|
-
|
|
227
|
-
# Log only if this is the calling model
|
|
228
|
-
if calling_model == model_class.__name__:
|
|
229
|
-
first_obj = objs[0]
|
|
230
|
-
instance_repr = instance_to_dict(first_obj)
|
|
231
|
-
|
|
232
|
-
push_log(
|
|
233
|
-
f"{EVENT_TYPES[4]} event by {model_class.__name__}",
|
|
234
|
-
model_class.__name__,
|
|
235
|
-
EVENT_TYPES[4],
|
|
236
|
-
str(first_obj.pk),
|
|
237
|
-
instance_repr,
|
|
238
|
-
{
|
|
239
|
-
"total_count": len(objs),
|
|
240
|
-
"fields": fields,
|
|
241
|
-
},
|
|
242
|
-
)
|
|
243
|
-
|
|
244
214
|
# Replace the methods
|
|
245
215
|
model_class.save = save_with_signals
|
|
246
|
-
models.QuerySet.bulk_create = bulk_create_with_signals
|
|
247
|
-
models.QuerySet.bulk_update = bulk_update_with_signals
|
|
248
216
|
|
|
249
217
|
# DELETE -----------------------------------------------------------------------
|
|
250
218
|
@receiver(pre_delete, sender=model_class)
|
|
@@ -311,6 +279,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
|
|
|
311
279
|
|
|
312
280
|
def setup_model_signals() -> None:
|
|
313
281
|
"""Set up signals for all models in the project."""
|
|
282
|
+
patch_queryset_bulk_methods()
|
|
314
283
|
for app_config in apps.get_app_configs():
|
|
315
284
|
for model in app_config.get_models():
|
|
316
285
|
if not should_audit(model):
|
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/.pre-commit-config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/__init__.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/apps.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/protocols.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/settings.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev14 → django_activity_audit-1.3.0.dev16}/activity_audit/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|