django-activity-audit 1.3.0.dev11__tar.gz → 1.3.0.dev13__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.dev11 → django_activity_audit-1.3.0.dev13}/PKG-INFO +1 -1
- django_activity_audit-1.3.0.dev13/activity_audit/REVIEW.md +297 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/__init__.py +6 -2
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/apps.py +3 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/constants.py +11 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/formatters.py +6 -9
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/handlers.py +16 -3
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/settings.py +0 -43
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/signals.py +1 -1
- django_activity_audit-1.3.0.dev13/activity_audit/unregistered.py +33 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/utils.py +13 -2
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/pyproject.toml +1 -1
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/.gitignore +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/.pre-commit-config.yaml +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/LICENSE +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/MANIFEST.in +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/README.md +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/README.rst +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/logger_levels.py +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/middleware.py +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/protocols.py +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/docs/django-activity-audit.md +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/docs/user-activity-feed-plan.md +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/hatch +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/pytest.ini +0 -0
- {django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/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.dev13
|
|
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
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# activity_audit — Code Review
|
|
2
|
+
|
|
3
|
+
> Reviewed: 2026-06-08
|
|
4
|
+
> Reviewer: Claude Code
|
|
5
|
+
> Scope: Full app audit following bug fixes for `AppRegistryNotReady` and `request_id` thread-local propagation
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Recent Fixes (Verified Correct)
|
|
10
|
+
|
|
11
|
+
| Fix | File | Status |
|
|
12
|
+
|-----|------|--------|
|
|
13
|
+
| `AppRegistryNotReady` — model imports deferred to `unregistered.py`, loaded in `apps.ready()` | `unregistered.py`, `apps.py`, `settings.py` | ✅ Correct |
|
|
14
|
+
| `request_id` not written to `app.log` — captured via `prepare()` in handler (producer thread) before record is queued to background `QueueListener` thread | `handlers.py` | ✅ Correct |
|
|
15
|
+
| `watchfiles` reload loop — log files moved to `/logs/` (outside `/app/`), `watchfiles` logger silenced at WARNING | `config/settings/local.py`, `local.yml` | ✅ Correct |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Critical Issues (Must Fix)
|
|
20
|
+
|
|
21
|
+
### 1. Cross-request PHI data leak — shared `self.log_data` in middleware
|
|
22
|
+
**File:** `middleware.py:127–140`
|
|
23
|
+
|
|
24
|
+
`self.log_data` is constructed once in `__init__` and stored on the middleware instance. Middleware instances are shared across all requests. Under concurrent ASGI (multiple coroutines on one instance), request A's `user_id`, `request_repr`, `response_repr` overwrite request B's in the same dict — PHI from one patient's session can be written into another patient's audit record.
|
|
25
|
+
|
|
26
|
+
**Fix:** Build `log_data` as a local dict inside `__call__` and `__acall__`, not on `self`.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
# Before (in __init__)
|
|
30
|
+
self.log_data = {"service_name": SERVICE_NAME, ...}
|
|
31
|
+
|
|
32
|
+
# After (top of __call__ and __acall__)
|
|
33
|
+
log_data = {"service_name": SERVICE_NAME, ...}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### 2. `QuerySet.bulk_create/bulk_update` monkey-patched N times — wrappers nest
|
|
39
|
+
**File:** `signals.py:129–130, 244–247`
|
|
40
|
+
|
|
41
|
+
Inside `patch_model_event`, the patching logic does:
|
|
42
|
+
```python
|
|
43
|
+
original_bulk_create = models.QuerySet.bulk_create # captured per call
|
|
44
|
+
...
|
|
45
|
+
models.QuerySet.bulk_create = bulk_create_with_signals # assigned globally
|
|
46
|
+
```
|
|
47
|
+
This runs once per audited model. The second model captures the already-patched
|
|
48
|
+
method as its "original", the third wraps the double-patched version, and so on.
|
|
49
|
+
After N models are patched, every `bulk_create` call traverses N nested wrappers.
|
|
50
|
+
|
|
51
|
+
**Fix:** Extract the `QuerySet` patches into a separate function called exactly once from `setup_model_signals()`, before the per-model loop.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### 3. `get_calling_model()` frame-walking is broken — bulk auditing is non-functional
|
|
56
|
+
**File:** `signals.py:57–79, 193–196, 225–228`
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
if calling_model == model_class.__name__:
|
|
60
|
+
```
|
|
61
|
+
`calling_model` is `module_name.split(".")[-1]` (e.g. `"views"`, `"tasks"`).
|
|
62
|
+
`model_class.__name__` is a class name (e.g. `"Patient"`, `"Appointment"`).
|
|
63
|
+
These will almost never match, so bulk audit logs are silently never written for virtually all models.
|
|
64
|
+
|
|
65
|
+
**Fix:** Rethink bulk auditing using Django signals (`post_bulk_create` is unavailable natively, but a custom `QuerySet` mixin on a project-level base queryset is more reliable than frame introspection).
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 4. `request.body` access can crash real requests
|
|
70
|
+
**File:** `middleware.py:165–170, 229–234`
|
|
71
|
+
|
|
72
|
+
Only `json.JSONDecodeError` is caught. The following are unhandled and will 500 the request:
|
|
73
|
+
- `RawPostDataException` — body stream already consumed by DRF/multipart parsers
|
|
74
|
+
- `RequestDataTooBig` — body exceeds `DATA_UPLOAD_MAX_MEMORY_SIZE`
|
|
75
|
+
- `UnicodeDecodeError` — binary upload body
|
|
76
|
+
|
|
77
|
+
Audit middleware must never break actual requests.
|
|
78
|
+
|
|
79
|
+
**Fix:**
|
|
80
|
+
```python
|
|
81
|
+
try:
|
|
82
|
+
body = json.loads(request.body)
|
|
83
|
+
request_data["body"] = body
|
|
84
|
+
except Exception:
|
|
85
|
+
pass # never let audit logging crash the request
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Warnings (Should Fix)
|
|
91
|
+
|
|
92
|
+
### 5. PHI/SSN written into `audit.log` via `instance_to_dict`
|
|
93
|
+
**File:** `signals.py:117–118`
|
|
94
|
+
|
|
95
|
+
`model_to_dict(instance, fields=[f.name for f in instance._meta.fields])` dumps all
|
|
96
|
+
concrete fields including encrypted `social_security_number` and other PHI directly
|
|
97
|
+
into the audit log file. This directly violates HIPAA rules documented in `CLAUDE.md`:
|
|
98
|
+
> "Never log PHI to console" / "SSN handling — only access via `get_ssn` endpoint"
|
|
99
|
+
|
|
100
|
+
**Fix:** Add a per-model field deny-list or use a model-level `AUDIT_EXCLUDE_FIELDS`
|
|
101
|
+
attribute. At minimum, exclude all `EncryptedField` instances.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 6. Regex patterns recompiled on every request in `should_log_url`
|
|
106
|
+
**File:** `middleware.py:84–100`
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
for unregistered_url in UNREGISTERED_URLS:
|
|
110
|
+
pattern = re.compile(unregistered_url) # compiled on every request
|
|
111
|
+
```
|
|
112
|
+
This is in the hot path for every HTTP request.
|
|
113
|
+
|
|
114
|
+
**Fix:** Compile once at module load:
|
|
115
|
+
```python
|
|
116
|
+
_UNREGISTERED_PATTERNS = [re.compile(p) for p in UNREGISTERED_URLS]
|
|
117
|
+
_REGISTERED_PATTERNS = [re.compile(p) for p in REGISTERED_URLS]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### 7. `clear_request()` not called on early return — thread-local leaks
|
|
123
|
+
**File:** `middleware.py:148–153`
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
set_current_request(request)
|
|
127
|
+
set_request_id(request_id)
|
|
128
|
+
|
|
129
|
+
if not should_log_url(request.path):
|
|
130
|
+
return self.get_response(request) # clear_request() never called
|
|
131
|
+
```
|
|
132
|
+
The thread-local `request` and `request_id` persist on the worker thread and
|
|
133
|
+
are seen by the next request handled by that thread.
|
|
134
|
+
|
|
135
|
+
**Fix:** Use `try/finally`:
|
|
136
|
+
```python
|
|
137
|
+
set_current_request(request)
|
|
138
|
+
set_request_id(request_id)
|
|
139
|
+
try:
|
|
140
|
+
if not should_log_url(request.path):
|
|
141
|
+
return self.get_response(request)
|
|
142
|
+
...
|
|
143
|
+
finally:
|
|
144
|
+
clear_request()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### 8. Bulk wrappers never call `should_audit` — unregistered models audited on bulk paths
|
|
150
|
+
**File:** `signals.py:179–242`
|
|
151
|
+
|
|
152
|
+
`bulk_create_with_signals` and `bulk_update_with_signals` have no `should_audit` guard,
|
|
153
|
+
so `Session`, `Permission`, `Migration` etc. are audited on bulk paths but excluded on
|
|
154
|
+
single-save paths. Inconsistent and wasteful.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### 9. `SFTPClient.upload` — unconditional assignment clobbers success and real errors
|
|
159
|
+
**File:** `protocols.py:209–237`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
if not error:
|
|
163
|
+
try:
|
|
164
|
+
...upload...
|
|
165
|
+
except Exception:
|
|
166
|
+
self.log_payload["error_message"] = str(e) # real error
|
|
167
|
+
|
|
168
|
+
# This runs unconditionally — overwrites both the success case and the real exception:
|
|
169
|
+
self.log_payload["error_message"] = f"Path validation failed. Error: {str(error)}"
|
|
170
|
+
```
|
|
171
|
+
On a fully successful upload, `error_message` is set to `"Path validation failed. Error: None"`.
|
|
172
|
+
|
|
173
|
+
**Fix:** Wrap the final assignment in an `else` block.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### 10. `HTTPClient.request` returns `None` on exception and calls `.json()` unconditionally
|
|
178
|
+
**File:** `protocols.py:97–118`
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
except Exception:
|
|
182
|
+
...
|
|
183
|
+
return response # response is None here
|
|
184
|
+
```
|
|
185
|
+
Callers expecting a `requests.Response` will raise `AttributeError`. On the success path,
|
|
186
|
+
`response.json()` is called regardless of `Content-Type`, which will raise for non-JSON
|
|
187
|
+
responses and discard the real response object.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### 11. `push_usage_log` imports `get_user_details` through `signals` — unwanted side effect
|
|
192
|
+
**File:** `utils.py:139`
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from .signals import get_user_details
|
|
196
|
+
```
|
|
197
|
+
Importing `signals` triggers `setup_model_signals()` at the bottom of that module as a
|
|
198
|
+
side effect. Import directly from the canonical source:
|
|
199
|
+
```python
|
|
200
|
+
from .middleware import get_user_details
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### 12. Sentry filter attached at settings-load time — before Sentry is initialised
|
|
206
|
+
**File:** `settings.py:24–27`
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
root_logger = logging.getLogger()
|
|
210
|
+
for handler in root_logger.handlers:
|
|
211
|
+
if handler.__class__.__name__.startswith("Sentry"):
|
|
212
|
+
handler.addFilter(AuditToSentryFilter())
|
|
213
|
+
```
|
|
214
|
+
At settings-load time Sentry SDK has not yet added its handler, so this loop finds nothing
|
|
215
|
+
and the filter is never attached. Move to `apps.ready()`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Suggestions (Consider)
|
|
220
|
+
|
|
221
|
+
### 13. `default_app_config` is dead
|
|
222
|
+
**File:** `__init__.py:1`
|
|
223
|
+
|
|
224
|
+
Deprecated in Django 3.2, removed in Django 4.1. It is a no-op. Remove it.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### 14. Timestamp formatting duplicated across all 4 formatters
|
|
229
|
+
**File:** `formatters.py:42–44` (and 3 other formatters)
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
datetime.datetime.fromtimestamp(record.created).strftime(self.timestamp_format)[:-3]
|
|
233
|
+
```
|
|
234
|
+
This block is copy-pasted in `JsonFormatter`, `APIFormatter`, `AuditFormatter`, and
|
|
235
|
+
`LoginFormatter`. Also uses server local time with no timezone. Extract a shared:
|
|
236
|
+
```python
|
|
237
|
+
class BaseAuditFormatter(logging.Formatter):
|
|
238
|
+
def _timestamp(self, record) -> str:
|
|
239
|
+
return datetime.datetime.fromtimestamp(
|
|
240
|
+
record.created, tz=datetime.timezone.utc
|
|
241
|
+
).strftime(self.timestamp_format)[:-3]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### 15. `apps.ready()` no-op attribute accesses are misleading
|
|
247
|
+
**File:** `apps.py:14–16`
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
logger_levels.AUDIT
|
|
251
|
+
logger_levels.API
|
|
252
|
+
logger_levels.LOGIN
|
|
253
|
+
```
|
|
254
|
+
These are bare attribute reads that do nothing — the level registration already
|
|
255
|
+
happened when `logger_levels` was imported. The import itself is the side effect.
|
|
256
|
+
Remove the three lines or replace with a comment.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### 16. `AuditToSentryFilter` mutates `record.levelname` — corrupts file logs
|
|
261
|
+
**File:** `settings.py:8–20`
|
|
262
|
+
|
|
263
|
+
Overwriting `record.levelname = "INFO"` means formatters downstream will log `"INFO"`
|
|
264
|
+
instead of `"AUDIT"` / `"API"` in file logs too (filters run before formatting).
|
|
265
|
+
If Sentry integration is needed, use a `before_send` hook in Sentry config instead.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### 17. Minor style issues
|
|
270
|
+
|
|
271
|
+
| Location | Issue |
|
|
272
|
+
|----------|-------|
|
|
273
|
+
| `middleware.py:64` | `id = str(user.id)` shadows the builtin `id` |
|
|
274
|
+
| `protocols.py:193` | `f"Connection not established"` — f-string with no placeholders |
|
|
275
|
+
| `signals.py:88` | `instance_repr: str` type hint but callers pass `dict` |
|
|
276
|
+
| `middleware.py:21–25` | `MockRequest.__init__` passes `*args, **kwargs` to `object.__init__` which rejects them |
|
|
277
|
+
| `formatters.py`, `signals.py` | Several lines exceed 80-char limit — run `make format` |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Priority Order
|
|
282
|
+
|
|
283
|
+
| Priority | Issue | File |
|
|
284
|
+
|----------|-------|------|
|
|
285
|
+
| 🔴 P0 | Cross-request PHI data leak (`self.log_data`) | `middleware.py` |
|
|
286
|
+
| 🔴 P0 | PHI/SSN in `audit.log` via `instance_to_dict` | `signals.py` |
|
|
287
|
+
| 🔴 P0 | `request.body` access can crash requests | `middleware.py` |
|
|
288
|
+
| 🟠 P1 | `QuerySet` patch nested N times | `signals.py` |
|
|
289
|
+
| 🟠 P1 | Bulk auditing effectively broken (`get_calling_model`) | `signals.py` |
|
|
290
|
+
| 🟠 P1 | `clear_request()` missing on early return | `middleware.py` |
|
|
291
|
+
| 🟡 P2 | Regex recompiled on every request | `middleware.py` |
|
|
292
|
+
| 🟡 P2 | Bulk wrappers skip `should_audit` | `signals.py` |
|
|
293
|
+
| 🟡 P2 | `SFTPClient.upload` error message clobbered | `protocols.py` |
|
|
294
|
+
| 🟡 P2 | `HTTPClient.request` returns `None` silently | `protocols.py` |
|
|
295
|
+
| 🟡 P2 | `push_usage_log` imports via `signals` | `utils.py` |
|
|
296
|
+
| 🟡 P2 | Sentry filter never attaches | `settings.py` |
|
|
297
|
+
| 🔵 P3 | Timestamp duplication, `default_app_config`, style nits | various |
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/__init__.py
RENAMED
|
@@ -8,7 +8,9 @@ from activity_audit.utils import (
|
|
|
8
8
|
get_async_login_handler,
|
|
9
9
|
get_audit_handler,
|
|
10
10
|
get_console_formatter,
|
|
11
|
-
|
|
11
|
+
get_app_formatter,
|
|
12
|
+
get_api_formatter,
|
|
13
|
+
get_audit_formatter,
|
|
12
14
|
get_json_handler,
|
|
13
15
|
get_login_handler,
|
|
14
16
|
)
|
|
@@ -17,7 +19,9 @@ from . import logger_levels
|
|
|
17
19
|
|
|
18
20
|
__all__ = [
|
|
19
21
|
"get_console_formatter",
|
|
20
|
-
"
|
|
22
|
+
"get_app_formatter",
|
|
23
|
+
"get_api_formatter",
|
|
24
|
+
"get_audit_formatter",
|
|
21
25
|
"get_json_handler",
|
|
22
26
|
"get_api_handler",
|
|
23
27
|
"get_audit_handler",
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/constants.py
RENAMED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LogType(str, enum.Enum):
|
|
5
|
+
APP = "app"
|
|
6
|
+
API = "api"
|
|
7
|
+
AUDIT = "audit"
|
|
8
|
+
LOGIN = "login"
|
|
9
|
+
|
|
10
|
+
|
|
1
11
|
CONSOLE_FORMAT = (
|
|
2
12
|
"%(levelname)s %(asctime)s %(pathname)s %(module)s %(funcName)s %(message)s"
|
|
3
13
|
)
|
|
@@ -6,3 +16,4 @@ REQUEST_TYPES = [
|
|
|
6
16
|
"internal",
|
|
7
17
|
"external",
|
|
8
18
|
]
|
|
19
|
+
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/formatters.py
RENAMED
|
@@ -4,8 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
7
|
+
from .constants import LogType
|
|
9
8
|
|
|
10
9
|
def _json_default(obj):
|
|
11
10
|
"""
|
|
@@ -31,7 +30,7 @@ def _json_default(obj):
|
|
|
31
30
|
return str(obj)
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
class
|
|
33
|
+
class AppFormatter(logging.Formatter):
|
|
35
34
|
def __init__(self, timestamp_format: str = "%Y-%m-%d %H:%M:%S.%f"):
|
|
36
35
|
super().__init__()
|
|
37
36
|
self.timestamp_format = timestamp_format
|
|
@@ -49,20 +48,16 @@ class JsonFormatter(logging.Formatter):
|
|
|
49
48
|
"path": record.pathname,
|
|
50
49
|
"module": record.module,
|
|
51
50
|
"function": record.funcName,
|
|
52
|
-
"request_id":
|
|
51
|
+
"request_id": getattr(record, "request_id", "") or "",
|
|
53
52
|
"message": record.getMessage(),
|
|
54
53
|
"exception": "",
|
|
55
|
-
|
|
54
|
+
"log_type": LogType.APP,
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
# Add exception info if present for ERROR
|
|
59
58
|
if record.exc_info:
|
|
60
59
|
log_data["exception"] = "{}".format(self.formatException(record.exc_info))
|
|
61
60
|
|
|
62
|
-
# Add extra fields if present
|
|
63
|
-
# if hasattr(record, "extra"):
|
|
64
|
-
# log_data.update(record.extra)
|
|
65
|
-
|
|
66
61
|
return json.dumps(log_data, default=_json_default)
|
|
67
62
|
|
|
68
63
|
|
|
@@ -82,6 +77,7 @@ class APIFormatter(logging.Formatter):
|
|
|
82
77
|
"level": record.levelname,
|
|
83
78
|
"name": record.name,
|
|
84
79
|
"message": record.getMessage(),
|
|
80
|
+
"log_type": LogType.API,
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
# Add all audit-specific fields if they exist
|
|
@@ -116,6 +112,7 @@ class AuditFormatter(logging.Formatter):
|
|
|
116
112
|
"level": record.levelname,
|
|
117
113
|
"name": record.name,
|
|
118
114
|
"message": record.getMessage(),
|
|
115
|
+
"log_type": LogType.AUDIT,
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
audit_fields = [
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/handlers.py
RENAMED
|
@@ -3,7 +3,8 @@ import queue
|
|
|
3
3
|
|
|
4
4
|
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
|
|
5
5
|
|
|
6
|
-
from .formatters import APIFormatter, AuditFormatter,
|
|
6
|
+
from .formatters import APIFormatter, AuditFormatter, AppFormatter, LoginFormatter
|
|
7
|
+
from .middleware import get_request_id
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class BaseAuditHandler(RotatingFileHandler):
|
|
@@ -108,6 +109,13 @@ class AsyncBaseAuditHandler(QueueHandler):
|
|
|
108
109
|
)
|
|
109
110
|
self._listener.start()
|
|
110
111
|
|
|
112
|
+
def prepare(self, record):
|
|
113
|
+
# Capture request_id in the calling thread before the record is queued,
|
|
114
|
+
# since thread-locals are not accessible from the QueueListener thread.
|
|
115
|
+
record = super().prepare(record)
|
|
116
|
+
record.request_id = get_request_id() or ""
|
|
117
|
+
return record
|
|
118
|
+
|
|
111
119
|
def close(self):
|
|
112
120
|
self._listener.stop()
|
|
113
121
|
super().close()
|
|
@@ -137,7 +145,7 @@ class AsyncLoginLogHandler(AsyncBaseAuditHandler):
|
|
|
137
145
|
class AsyncJsonHandler(QueueHandler):
|
|
138
146
|
"""
|
|
139
147
|
Non-blocking handler for general JSON logs. Wraps a RotatingFileHandler
|
|
140
|
-
with
|
|
148
|
+
with AppFormatter on the background thread.
|
|
141
149
|
"""
|
|
142
150
|
|
|
143
151
|
def __init__(
|
|
@@ -155,12 +163,17 @@ class AsyncJsonHandler(QueueHandler):
|
|
|
155
163
|
sync_handler = RotatingFileHandler(
|
|
156
164
|
filename, mode, maxBytes, backupCount, encoding, delay
|
|
157
165
|
)
|
|
158
|
-
sync_handler.setFormatter(
|
|
166
|
+
sync_handler.setFormatter(AppFormatter())
|
|
159
167
|
self._listener = QueueListener(
|
|
160
168
|
log_queue, sync_handler, respect_handler_level=True
|
|
161
169
|
)
|
|
162
170
|
self._listener.start()
|
|
163
171
|
|
|
172
|
+
def prepare(self, record):
|
|
173
|
+
record = super().prepare(record)
|
|
174
|
+
record.request_id = get_request_id() or ""
|
|
175
|
+
return record
|
|
176
|
+
|
|
164
177
|
def close(self):
|
|
165
178
|
self._listener.stop()
|
|
166
179
|
super().close()
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/settings.py
RENAMED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from django.apps import apps
|
|
4
3
|
from django.conf import settings
|
|
5
|
-
from django.contrib.auth.models import Permission
|
|
6
|
-
from django.contrib.contenttypes.models import ContentType
|
|
7
|
-
from django.contrib.sessions.models import Session
|
|
8
|
-
from django.db.migrations import Migration
|
|
9
|
-
from django.db.migrations.recorder import MigrationRecorder
|
|
10
4
|
|
|
11
5
|
|
|
12
6
|
# Handles AUDIT/API level as INFO for Sentry
|
|
@@ -32,43 +26,6 @@ for handler in root_logger.handlers:
|
|
|
32
26
|
if handler.__class__.__name__.startswith("Sentry"):
|
|
33
27
|
handler.addFilter(AuditToSentryFilter())
|
|
34
28
|
|
|
35
|
-
UNREGISTERED_CLASSES = [
|
|
36
|
-
Migration,
|
|
37
|
-
Session,
|
|
38
|
-
Permission,
|
|
39
|
-
ContentType,
|
|
40
|
-
MigrationRecorder.Migration,
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
# Remove silk models audit logging
|
|
44
|
-
SILK_INSTALLED = apps.is_installed("silk")
|
|
45
|
-
if SILK_INSTALLED:
|
|
46
|
-
from silk.models import (
|
|
47
|
-
BaseProfile,
|
|
48
|
-
Profile,
|
|
49
|
-
Request,
|
|
50
|
-
Response,
|
|
51
|
-
SQLQuery,
|
|
52
|
-
SQLQueryManager,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
UNREGISTERED_CLASSES.extend(
|
|
56
|
-
[
|
|
57
|
-
Request,
|
|
58
|
-
Response,
|
|
59
|
-
SQLQueryManager,
|
|
60
|
-
SQLQuery,
|
|
61
|
-
BaseProfile,
|
|
62
|
-
Profile,
|
|
63
|
-
]
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Import and unregister LogEntry class only if Django Admin app is installed
|
|
67
|
-
if apps.is_installed("django.contrib.admin"):
|
|
68
|
-
from django.contrib.admin.models import LogEntry
|
|
69
|
-
|
|
70
|
-
UNREGISTERED_CLASSES.extend([LogEntry])
|
|
71
|
-
|
|
72
29
|
# URL patterns to exclude from logging
|
|
73
30
|
UNREGISTERED_URLS = [r"^/admin/", r"^/static/", r"^/favicon.ico$"]
|
|
74
31
|
UNREGISTERED_URLS = getattr(
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/signals.py
RENAMED
|
@@ -16,7 +16,7 @@ from django.dispatch import receiver
|
|
|
16
16
|
from django.forms.models import model_to_dict
|
|
17
17
|
|
|
18
18
|
from activity_audit.middleware import get_request_id, get_user_details
|
|
19
|
-
from activity_audit.
|
|
19
|
+
from activity_audit.unregistered import UNREGISTERED_CLASSES
|
|
20
20
|
|
|
21
21
|
logger = logging.getLogger("audit.model")
|
|
22
22
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.contrib.auth.models import Permission
|
|
3
|
+
from django.contrib.contenttypes.models import ContentType
|
|
4
|
+
from django.contrib.sessions.models import Session
|
|
5
|
+
from django.db.migrations import Migration
|
|
6
|
+
from django.db.migrations.recorder import MigrationRecorder
|
|
7
|
+
|
|
8
|
+
UNREGISTERED_CLASSES = [
|
|
9
|
+
Migration,
|
|
10
|
+
Session,
|
|
11
|
+
Permission,
|
|
12
|
+
ContentType,
|
|
13
|
+
MigrationRecorder.Migration,
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
if apps.is_installed("silk"):
|
|
17
|
+
from silk.models import (
|
|
18
|
+
BaseProfile,
|
|
19
|
+
Profile,
|
|
20
|
+
Request,
|
|
21
|
+
Response,
|
|
22
|
+
SQLQuery,
|
|
23
|
+
SQLQueryManager,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
UNREGISTERED_CLASSES.extend(
|
|
27
|
+
[Request, Response, SQLQueryManager, SQLQuery, BaseProfile, Profile]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if apps.is_installed("django.contrib.admin"):
|
|
31
|
+
from django.contrib.admin.models import LogEntry
|
|
32
|
+
|
|
33
|
+
UNREGISTERED_CLASSES.extend([LogEntry])
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/utils.py
RENAMED
|
@@ -7,9 +7,19 @@ def get_console_formatter() -> dict:
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def get_app_formatter() -> dict:
|
|
11
11
|
return {
|
|
12
|
-
"()": "activity_audit.formatters.
|
|
12
|
+
"()": "activity_audit.formatters.AppFormatter",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def get_api_formatter() -> dict:
|
|
16
|
+
return {
|
|
17
|
+
"()": "activity_audit.formatters.APIFormatter"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def get_audit_formatter() -> dict:
|
|
21
|
+
return {
|
|
22
|
+
"()": "activity_audit.formatters.AuditFormatter"
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
|
|
@@ -30,6 +40,7 @@ def get_json_handler(
|
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
|
|
43
|
+
|
|
33
44
|
def get_api_handler(
|
|
34
45
|
filename: str = "audit_logs/api.log",
|
|
35
46
|
) -> dict:
|
|
File without changes
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/.pre-commit-config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/middleware.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev11 → django_activity_audit-1.3.0.dev13}/activity_audit/protocols.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|