django-activity-audit 1.3.0.dev19__tar.gz → 1.3.0.dev21__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.dev19 → django_activity_audit-1.3.0.dev21}/PKG-INFO +90 -32
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/README.md +87 -28
- django_activity_audit-1.3.0.dev21/README.rst +341 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/config.py +16 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/formatters.py +5 -1
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/middleware.py +26 -34
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/shared_processors.py +20 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/utils.py +1 -1
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/pyproject.toml +3 -3
- django_activity_audit-1.3.0.dev19/README.rst +0 -263
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/.claude/settings.local.json +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/.gitignore +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/.pre-commit-config.yaml +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/LICENSE +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/MANIFEST.in +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/__init__.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/apps.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/constants.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/handlers.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/logger_levels.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/protocols.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/settings.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/signals.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/unregistered.py +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/django-activity-audit.md +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/hipaa-audit-gaps.md +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/improvements.md +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/structlog-integration.md +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/user-activity-feed-plan.md +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/hatch +0 -0
- {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/pytest.ini +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.dev21
|
|
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
|
|
@@ -26,19 +26,18 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
27
|
Requires-Python: >=3.8
|
|
28
28
|
Requires-Dist: django<6.0,>=4.2
|
|
29
|
+
Requires-Dist: orjson>=3.0
|
|
30
|
+
Requires-Dist: structlog>=24.0
|
|
29
31
|
Provides-Extra: dev
|
|
30
32
|
Requires-Dist: pre-commit>=3.5; (python_version >= '3.9') and extra == 'dev'
|
|
31
33
|
Requires-Dist: pre-commit~=3.5; (python_version < '3.9') and extra == 'dev'
|
|
32
34
|
Requires-Dist: ruff>=0.1.11; extra == 'dev'
|
|
33
|
-
Provides-Extra: structlog
|
|
34
|
-
Requires-Dist: structlog>=24.0; extra == 'structlog'
|
|
35
35
|
Provides-Extra: test
|
|
36
36
|
Requires-Dist: djangorestframework>=3.14.0; extra == 'test'
|
|
37
37
|
Requires-Dist: factory-boy>=3.2.0; extra == 'test'
|
|
38
38
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
|
|
39
39
|
Requires-Dist: pytest-django>=4.0.0; extra == 'test'
|
|
40
40
|
Requires-Dist: pytest>=6.0.0; extra == 'test'
|
|
41
|
-
Requires-Dist: structlog>=24.0; extra == 'test'
|
|
42
41
|
Description-Content-Type: text/markdown
|
|
43
42
|
|
|
44
43
|
# Django Activity Audit
|
|
@@ -62,6 +61,8 @@ A Django package that extends the default logging mechanism to track CRUD operat
|
|
|
62
61
|
pip install django-activity-audit
|
|
63
62
|
```
|
|
64
63
|
|
|
64
|
+
This also installs [`structlog`](https://www.structlog.org/) and [`orjson`](https://github.com/ijl/orjson) as required dependencies.
|
|
65
|
+
|
|
65
66
|
2. Add 'activity_audit' to your INSTALLED_APPS in settings.py:
|
|
66
67
|
```python
|
|
67
68
|
INSTALLED_APPS = [
|
|
@@ -79,47 +80,101 @@ MIDDLEWARE = [
|
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
4. Configure logging in settings.py:
|
|
83
|
+
|
|
84
|
+
Import the formatter helpers from `activity_audit.config`:
|
|
85
|
+
|
|
82
86
|
```python
|
|
83
|
-
from activity_audit import
|
|
87
|
+
from activity_audit.config import get_plain_formatter, get_stdlib_formatter
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `get_stdlib_formatter()` — structlog JSON renderer. Use in staging/production where logs are ingested by a pipeline (Vector, CloudWatch, etc.).
|
|
91
|
+
- `get_plain_formatter()` — structlog plain-text renderer. Use locally for human-readable console output.
|
|
92
|
+
|
|
93
|
+
**Local development** (plain text output):
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from activity_audit.config import get_plain_formatter, get_stdlib_formatter
|
|
84
97
|
|
|
85
98
|
LOGGING = {
|
|
86
99
|
"version": 1,
|
|
87
100
|
"disable_existing_loggers": False,
|
|
88
101
|
"formatters": {
|
|
89
|
-
"
|
|
90
|
-
"
|
|
102
|
+
"structlog": get_stdlib_formatter(),
|
|
103
|
+
"default": get_plain_formatter(),
|
|
91
104
|
},
|
|
92
105
|
"handlers": {
|
|
93
|
-
"console":
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
"api_file": get_api_file_handler(),
|
|
100
|
-
"audit_file": get_audit_handler(),
|
|
106
|
+
"console": {"class": "logging.StreamHandler", "formatter": "default"},
|
|
107
|
+
"console_struct": {"class": "logging.StreamHandler", "formatter": "structlog"},
|
|
108
|
+
},
|
|
109
|
+
"root": {
|
|
110
|
+
"level": "INFO",
|
|
111
|
+
"handlers": ["console"], # plain text fallback for all loggers
|
|
101
112
|
},
|
|
102
|
-
"root": {"level": "DEBUG", "handlers": ["console", "file"]},
|
|
103
113
|
"loggers": {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
# Structlog owns these — explicit handler, no propagation to avoid double output
|
|
115
|
+
"audit.model": {"handlers": ["console_struct"], "propagate": False},
|
|
116
|
+
"audit.request": {"handlers": ["console_struct"], "propagate": False},
|
|
117
|
+
"audit.login": {"handlers": ["console_struct"], "propagate": False},
|
|
118
|
+
|
|
119
|
+
# Celery — structlog formats log_type correctly
|
|
120
|
+
"celery": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
121
|
+
"celery.task": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
122
|
+
"celery.beat": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
123
|
+
|
|
124
|
+
# Third-party noise control — WARNING only, routed to root
|
|
125
|
+
"django.db.backends": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
126
|
+
"boto3": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
127
|
+
"botocore": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
128
|
+
|
|
129
|
+
# Framework loggers
|
|
130
|
+
"django": {"level": "INFO", "handlers": [], "propagate": True},
|
|
131
|
+
"uvicorn": {"level": "INFO", "handlers": [], "propagate": True},
|
|
132
|
+
"uvicorn.error": {"level": "INFO", "handlers": [], "propagate": True},
|
|
133
|
+
"uvicorn.access":{"level": "INFO", "handlers": [], "propagate": True},
|
|
134
|
+
},
|
|
120
135
|
}
|
|
121
136
|
```
|
|
122
137
|
|
|
138
|
+
**Staging / production** (structured JSON output): identical structure, but the `root` handler also uses `console_struct` (or keep `console` for mixed output — both handlers use the same `StreamHandler` class):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
"root": {
|
|
142
|
+
"level": "INFO",
|
|
143
|
+
"handlers": ["console"],
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### When to add a logger entry
|
|
150
|
+
|
|
151
|
+
Add an explicit logger entry when you need **any** of the following:
|
|
152
|
+
|
|
153
|
+
| Situation | What to set |
|
|
154
|
+
|-----------|-------------|
|
|
155
|
+
| Route to structured JSON (`console_struct`) | `handlers: ["console_struct"], propagate: False` |
|
|
156
|
+
| Suppress a noisy third-party library | `level: "WARNING", handlers: [], propagate: True` |
|
|
157
|
+
| Prevent double output for a structlog-owned logger | `handlers: ["console_struct"], propagate: False` |
|
|
158
|
+
| Change the log level for a specific namespace | Set `level` explicitly |
|
|
159
|
+
|
|
160
|
+
**Do not** add a logger entry if the default behaviour is acceptable — a logger with no entry propagates to `root` and is emitted in plain text at INFO level. That is the correct behaviour for most application loggers.
|
|
161
|
+
|
|
162
|
+
### Silencing audit loggers (route to root instead of structlog)
|
|
163
|
+
|
|
164
|
+
By default `audit.model`, `audit.request`, and `audit.login` are pointed at `console_struct` with `propagate: False` so only the structlog-formatted JSON line is emitted.
|
|
165
|
+
|
|
166
|
+
To stop structlog from handling them and fall back to the plain-text root logger instead, set `handlers: []` and `propagate: True`:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
"loggers": {
|
|
170
|
+
"audit.model": {"handlers": [], "propagate": True},
|
|
171
|
+
"audit.request": {"handlers": [], "propagate": True},
|
|
172
|
+
"audit.login": {"handlers": [], "propagate": True},
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
This routes all three through the `root` logger (`console` handler, `default` / plain-text formatter). Use this when you want to completely disable structured audit output — for example, in a minimal local environment or during debugging.
|
|
177
|
+
|
|
123
178
|
5. Configure the service name in `settings.py` (optional, defaults to `"default"`):
|
|
124
179
|
```python
|
|
125
180
|
AUDIT_SERVICE_NAME = "my_service"
|
|
@@ -170,6 +225,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
|
|
|
170
225
|
"level": "AUDIT",
|
|
171
226
|
"name": "audit.model",
|
|
172
227
|
"message": "CREATE event by User (id: 6f77b814-f9c1-4cab-a737-6677734bc303)",
|
|
228
|
+
"request_id": "f3c9a1b2-0001-4abc-beef-deadbeef0001",
|
|
173
229
|
"model": "User",
|
|
174
230
|
"event_type": "CREATE",
|
|
175
231
|
"instance_id": "6f77b814-f9c1-4cab-a737-6677734bc303",
|
|
@@ -200,6 +256,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
|
|
|
200
256
|
"level": "API",
|
|
201
257
|
"name": "audit.request",
|
|
202
258
|
"message": "Audit Internal Request",
|
|
259
|
+
"request_id": "f3c9a1b2-0001-4abc-beef-deadbeef0001",
|
|
203
260
|
"service_name": "my_service",
|
|
204
261
|
"request_type": "internal",
|
|
205
262
|
"protocol": "http",
|
|
@@ -246,6 +303,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
|
|
|
246
303
|
"level": "API",
|
|
247
304
|
"name": "audit.request",
|
|
248
305
|
"message": "Audit External Service",
|
|
306
|
+
"request_id": "f3c9a1b2-0001-4abc-beef-deadbeef0001",
|
|
249
307
|
"service_name": "apollo",
|
|
250
308
|
"request_type": "external",
|
|
251
309
|
"protocol": "http",
|
|
@@ -19,6 +19,8 @@ A Django package that extends the default logging mechanism to track CRUD operat
|
|
|
19
19
|
pip install django-activity-audit
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
This also installs [`structlog`](https://www.structlog.org/) and [`orjson`](https://github.com/ijl/orjson) as required dependencies.
|
|
23
|
+
|
|
22
24
|
2. Add 'activity_audit' to your INSTALLED_APPS in settings.py:
|
|
23
25
|
```python
|
|
24
26
|
INSTALLED_APPS = [
|
|
@@ -36,47 +38,101 @@ MIDDLEWARE = [
|
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
4. Configure logging in settings.py:
|
|
41
|
+
|
|
42
|
+
Import the formatter helpers from `activity_audit.config`:
|
|
43
|
+
|
|
39
44
|
```python
|
|
40
|
-
from activity_audit import
|
|
45
|
+
from activity_audit.config import get_plain_formatter, get_stdlib_formatter
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `get_stdlib_formatter()` — structlog JSON renderer. Use in staging/production where logs are ingested by a pipeline (Vector, CloudWatch, etc.).
|
|
49
|
+
- `get_plain_formatter()` — structlog plain-text renderer. Use locally for human-readable console output.
|
|
50
|
+
|
|
51
|
+
**Local development** (plain text output):
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from activity_audit.config import get_plain_formatter, get_stdlib_formatter
|
|
41
55
|
|
|
42
56
|
LOGGING = {
|
|
43
57
|
"version": 1,
|
|
44
58
|
"disable_existing_loggers": False,
|
|
45
59
|
"formatters": {
|
|
46
|
-
"
|
|
47
|
-
"
|
|
60
|
+
"structlog": get_stdlib_formatter(),
|
|
61
|
+
"default": get_plain_formatter(),
|
|
48
62
|
},
|
|
49
63
|
"handlers": {
|
|
50
|
-
"console":
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"api_file": get_api_file_handler(),
|
|
57
|
-
"audit_file": get_audit_handler(),
|
|
64
|
+
"console": {"class": "logging.StreamHandler", "formatter": "default"},
|
|
65
|
+
"console_struct": {"class": "logging.StreamHandler", "formatter": "structlog"},
|
|
66
|
+
},
|
|
67
|
+
"root": {
|
|
68
|
+
"level": "INFO",
|
|
69
|
+
"handlers": ["console"], # plain text fallback for all loggers
|
|
58
70
|
},
|
|
59
|
-
"root": {"level": "DEBUG", "handlers": ["console", "file"]},
|
|
60
71
|
"loggers": {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
# Structlog owns these — explicit handler, no propagation to avoid double output
|
|
73
|
+
"audit.model": {"handlers": ["console_struct"], "propagate": False},
|
|
74
|
+
"audit.request": {"handlers": ["console_struct"], "propagate": False},
|
|
75
|
+
"audit.login": {"handlers": ["console_struct"], "propagate": False},
|
|
76
|
+
|
|
77
|
+
# Celery — structlog formats log_type correctly
|
|
78
|
+
"celery": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
79
|
+
"celery.task": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
80
|
+
"celery.beat": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
81
|
+
|
|
82
|
+
# Third-party noise control — WARNING only, routed to root
|
|
83
|
+
"django.db.backends": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
84
|
+
"boto3": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
85
|
+
"botocore": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
86
|
+
|
|
87
|
+
# Framework loggers
|
|
88
|
+
"django": {"level": "INFO", "handlers": [], "propagate": True},
|
|
89
|
+
"uvicorn": {"level": "INFO", "handlers": [], "propagate": True},
|
|
90
|
+
"uvicorn.error": {"level": "INFO", "handlers": [], "propagate": True},
|
|
91
|
+
"uvicorn.access":{"level": "INFO", "handlers": [], "propagate": True},
|
|
92
|
+
},
|
|
77
93
|
}
|
|
78
94
|
```
|
|
79
95
|
|
|
96
|
+
**Staging / production** (structured JSON output): identical structure, but the `root` handler also uses `console_struct` (or keep `console` for mixed output — both handlers use the same `StreamHandler` class):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
"root": {
|
|
100
|
+
"level": "INFO",
|
|
101
|
+
"handlers": ["console"],
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### When to add a logger entry
|
|
108
|
+
|
|
109
|
+
Add an explicit logger entry when you need **any** of the following:
|
|
110
|
+
|
|
111
|
+
| Situation | What to set |
|
|
112
|
+
|-----------|-------------|
|
|
113
|
+
| Route to structured JSON (`console_struct`) | `handlers: ["console_struct"], propagate: False` |
|
|
114
|
+
| Suppress a noisy third-party library | `level: "WARNING", handlers: [], propagate: True` |
|
|
115
|
+
| Prevent double output for a structlog-owned logger | `handlers: ["console_struct"], propagate: False` |
|
|
116
|
+
| Change the log level for a specific namespace | Set `level` explicitly |
|
|
117
|
+
|
|
118
|
+
**Do not** add a logger entry if the default behaviour is acceptable — a logger with no entry propagates to `root` and is emitted in plain text at INFO level. That is the correct behaviour for most application loggers.
|
|
119
|
+
|
|
120
|
+
### Silencing audit loggers (route to root instead of structlog)
|
|
121
|
+
|
|
122
|
+
By default `audit.model`, `audit.request`, and `audit.login` are pointed at `console_struct` with `propagate: False` so only the structlog-formatted JSON line is emitted.
|
|
123
|
+
|
|
124
|
+
To stop structlog from handling them and fall back to the plain-text root logger instead, set `handlers: []` and `propagate: True`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
"loggers": {
|
|
128
|
+
"audit.model": {"handlers": [], "propagate": True},
|
|
129
|
+
"audit.request": {"handlers": [], "propagate": True},
|
|
130
|
+
"audit.login": {"handlers": [], "propagate": True},
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This routes all three through the `root` logger (`console` handler, `default` / plain-text formatter). Use this when you want to completely disable structured audit output — for example, in a minimal local environment or during debugging.
|
|
135
|
+
|
|
80
136
|
5. Configure the service name in `settings.py` (optional, defaults to `"default"`):
|
|
81
137
|
```python
|
|
82
138
|
AUDIT_SERVICE_NAME = "my_service"
|
|
@@ -127,6 +183,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
|
|
|
127
183
|
"level": "AUDIT",
|
|
128
184
|
"name": "audit.model",
|
|
129
185
|
"message": "CREATE event by User (id: 6f77b814-f9c1-4cab-a737-6677734bc303)",
|
|
186
|
+
"request_id": "f3c9a1b2-0001-4abc-beef-deadbeef0001",
|
|
130
187
|
"model": "User",
|
|
131
188
|
"event_type": "CREATE",
|
|
132
189
|
"instance_id": "6f77b814-f9c1-4cab-a737-6677734bc303",
|
|
@@ -157,6 +214,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
|
|
|
157
214
|
"level": "API",
|
|
158
215
|
"name": "audit.request",
|
|
159
216
|
"message": "Audit Internal Request",
|
|
217
|
+
"request_id": "f3c9a1b2-0001-4abc-beef-deadbeef0001",
|
|
160
218
|
"service_name": "my_service",
|
|
161
219
|
"request_type": "internal",
|
|
162
220
|
"protocol": "http",
|
|
@@ -203,6 +261,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
|
|
|
203
261
|
"level": "API",
|
|
204
262
|
"name": "audit.request",
|
|
205
263
|
"message": "Audit External Service",
|
|
264
|
+
"request_id": "f3c9a1b2-0001-4abc-beef-deadbeef0001",
|
|
206
265
|
"service_name": "apollo",
|
|
207
266
|
"request_type": "external",
|
|
208
267
|
"protocol": "http",
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
Django Activity Audit
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
A Django package that extends the default logging mechanism to track CRUD operations and container logs.
|
|
5
|
+
|
|
6
|
+
Features
|
|
7
|
+
--------
|
|
8
|
+
|
|
9
|
+
- Automatic logging of CRUD operations (Create, Read, Update, Delete)
|
|
10
|
+
- Tracks both HTTP requests and model changes
|
|
11
|
+
- Custom log levels Audit(21) and API(22) for CRUD and Request-Response auditing.
|
|
12
|
+
- Structured JSON logs for audit trails
|
|
13
|
+
- Human-readable container logs
|
|
14
|
+
- Separate log files for audit and container logs
|
|
15
|
+
- Console and file output options
|
|
16
|
+
|
|
17
|
+
Installation
|
|
18
|
+
------------
|
|
19
|
+
|
|
20
|
+
1. Install the package:
|
|
21
|
+
|
|
22
|
+
.. code-block:: bash
|
|
23
|
+
|
|
24
|
+
pip install django-activity-audit
|
|
25
|
+
|
|
26
|
+
This also installs `structlog <https://www.structlog.org/>`_ and `orjson <https://github.com/ijl/orjson>`_ as required dependencies.
|
|
27
|
+
|
|
28
|
+
2. Add ``activity_audit`` to your ``INSTALLED_APPS`` in ``settings.py``:
|
|
29
|
+
|
|
30
|
+
.. code-block:: python
|
|
31
|
+
|
|
32
|
+
INSTALLED_APPS = [
|
|
33
|
+
...
|
|
34
|
+
'activity_audit',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
3. Add the middleware to your ``MIDDLEWARE`` in ``settings.py``:
|
|
38
|
+
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
MIDDLEWARE = [
|
|
42
|
+
...
|
|
43
|
+
'activity_audit.middleware.AuditLoggingMiddleware',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
4. Configure logging in ``settings.py``.
|
|
47
|
+
|
|
48
|
+
Import the formatter helpers from ``activity_audit.config``:
|
|
49
|
+
|
|
50
|
+
.. code-block:: python
|
|
51
|
+
|
|
52
|
+
from activity_audit.config import get_plain_formatter, get_stdlib_formatter
|
|
53
|
+
|
|
54
|
+
- ``get_stdlib_formatter()`` — structlog JSON renderer. Use in staging/production where logs are ingested by a pipeline (Vector, CloudWatch, etc.).
|
|
55
|
+
- ``get_plain_formatter()`` — structlog plain-text renderer. Use locally for human-readable console output.
|
|
56
|
+
|
|
57
|
+
**Local development** (plain text output):
|
|
58
|
+
|
|
59
|
+
.. code-block:: python
|
|
60
|
+
|
|
61
|
+
from activity_audit.config import get_plain_formatter, get_stdlib_formatter
|
|
62
|
+
|
|
63
|
+
LOGGING = {
|
|
64
|
+
"version": 1,
|
|
65
|
+
"disable_existing_loggers": False,
|
|
66
|
+
"formatters": {
|
|
67
|
+
"structlog": get_stdlib_formatter(),
|
|
68
|
+
"default": get_plain_formatter(),
|
|
69
|
+
},
|
|
70
|
+
"handlers": {
|
|
71
|
+
"console": {"class": "logging.StreamHandler", "formatter": "default"},
|
|
72
|
+
"console_struct": {"class": "logging.StreamHandler", "formatter": "structlog"},
|
|
73
|
+
},
|
|
74
|
+
"root": {
|
|
75
|
+
"level": "INFO",
|
|
76
|
+
"handlers": ["console"], # plain text fallback for all loggers
|
|
77
|
+
},
|
|
78
|
+
"loggers": {
|
|
79
|
+
# Structlog owns these — explicit handler, no propagation to avoid double output
|
|
80
|
+
"audit.model": {"handlers": ["console_struct"], "propagate": False},
|
|
81
|
+
"audit.request": {"handlers": ["console_struct"], "propagate": False},
|
|
82
|
+
"audit.login": {"handlers": ["console_struct"], "propagate": False},
|
|
83
|
+
|
|
84
|
+
# Celery — structlog formats log_type correctly
|
|
85
|
+
"celery": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
86
|
+
"celery.task": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
87
|
+
"celery.beat": {"level": "INFO", "handlers": ["console_struct"], "propagate": False},
|
|
88
|
+
|
|
89
|
+
# Third-party noise control — WARNING only, routed to root
|
|
90
|
+
"django.db.backends": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
91
|
+
"boto3": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
92
|
+
"botocore": {"level": "WARNING", "handlers": [], "propagate": True},
|
|
93
|
+
|
|
94
|
+
# Framework loggers
|
|
95
|
+
"django": {"level": "INFO", "handlers": [], "propagate": True},
|
|
96
|
+
"uvicorn": {"level": "INFO", "handlers": [], "propagate": True},
|
|
97
|
+
"uvicorn.error": {"level": "INFO", "handlers": [], "propagate": True},
|
|
98
|
+
"uvicorn.access":{"level": "INFO", "handlers": [], "propagate": True},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
**Staging / production** (structured JSON output): identical structure, but the ``root`` handler also uses ``console_struct`` (or keep ``console`` for mixed output — both handlers use the same ``StreamHandler`` class):
|
|
103
|
+
|
|
104
|
+
.. code-block:: python
|
|
105
|
+
|
|
106
|
+
"root": {
|
|
107
|
+
"level": "INFO",
|
|
108
|
+
"handlers": ["console"],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
When to add a logger entry
|
|
114
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
115
|
+
|
|
116
|
+
Add an explicit logger entry when you need **any** of the following:
|
|
117
|
+
|
|
118
|
+
.. list-table::
|
|
119
|
+
:header-rows: 1
|
|
120
|
+
:widths: 60 40
|
|
121
|
+
|
|
122
|
+
* - Situation
|
|
123
|
+
- What to set
|
|
124
|
+
* - Route to structured JSON (``console_struct``)
|
|
125
|
+
- ``handlers: ["console_struct"], propagate: False``
|
|
126
|
+
* - Suppress a noisy third-party library
|
|
127
|
+
- ``level: "WARNING", handlers: [], propagate: True``
|
|
128
|
+
* - Prevent double output for a structlog-owned logger
|
|
129
|
+
- ``handlers: ["console_struct"], propagate: False``
|
|
130
|
+
* - Change the log level for a specific namespace
|
|
131
|
+
- Set ``level`` explicitly
|
|
132
|
+
|
|
133
|
+
**Do not** add a logger entry if the default behaviour is acceptable — a logger with no entry propagates to ``root`` and is emitted in plain text at INFO level. That is the correct behaviour for most application loggers.
|
|
134
|
+
|
|
135
|
+
Silencing audit loggers (route to root instead of structlog)
|
|
136
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
137
|
+
|
|
138
|
+
By default ``audit.model``, ``audit.request``, and ``audit.login`` are pointed at ``console_struct`` with ``propagate: False`` so only the structlog-formatted JSON line is emitted.
|
|
139
|
+
|
|
140
|
+
To stop structlog from handling them and fall back to the plain-text root logger instead, set ``handlers: []`` and ``propagate: True``:
|
|
141
|
+
|
|
142
|
+
.. code-block:: python
|
|
143
|
+
|
|
144
|
+
"loggers": {
|
|
145
|
+
"audit.model": {"handlers": [], "propagate": True},
|
|
146
|
+
"audit.request": {"handlers": [], "propagate": True},
|
|
147
|
+
"audit.login": {"handlers": [], "propagate": True},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
This routes all three through the ``root`` logger (``console`` handler, ``default`` / plain-text formatter). Use this when you want to completely disable structured audit output — for example, in a minimal local environment or during debugging.
|
|
151
|
+
|
|
152
|
+
5. Configure the service name in ``settings.py`` (optional, defaults to ``"default"``):
|
|
153
|
+
|
|
154
|
+
.. code-block:: python
|
|
155
|
+
|
|
156
|
+
AUDIT_SERVICE_NAME = "my_service"
|
|
157
|
+
|
|
158
|
+
6. For external services logging, extend ``HTTPClient`` or ``SFTPClient``:
|
|
159
|
+
|
|
160
|
+
.. code-block:: python
|
|
161
|
+
|
|
162
|
+
class ExternalService(HTTPClient):
|
|
163
|
+
def __init__(self):
|
|
164
|
+
super().__init__("service_name")
|
|
165
|
+
|
|
166
|
+
def connect(self):
|
|
167
|
+
url = "https://www.sample.com"
|
|
168
|
+
response = self.get(url) # sample log structure below
|
|
169
|
+
|
|
170
|
+
8. Create ``audit_logs`` folder in project directory
|
|
171
|
+
|
|
172
|
+
Log Types
|
|
173
|
+
---------
|
|
174
|
+
|
|
175
|
+
Container Logs
|
|
176
|
+
~~~~~~~~~~~~~~
|
|
177
|
+
|
|
178
|
+
Console Log Format::
|
|
179
|
+
|
|
180
|
+
'%(levelname)s %(asctime)s %(pathname)s %(module)s %(funcName)s %(message)s'
|
|
181
|
+
-----------------------------------------------------------------------------
|
|
182
|
+
INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_with_contacts_and_diseases Patient 'd6c9a056-0b57-453a-8c0f-44319004b761 - Patient3' created.
|
|
183
|
+
|
|
184
|
+
APP Log
|
|
185
|
+
~~~~~~~
|
|
186
|
+
|
|
187
|
+
.. code-block:: json
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
"timestamp": "2025-05-15 13:38:02.141",
|
|
191
|
+
"level": "DEBUG",
|
|
192
|
+
"name": "botocore.auth",
|
|
193
|
+
"path": "/opt/venv/lib/python3.11/site-packages/botocore/auth.py",
|
|
194
|
+
"module": "auth",
|
|
195
|
+
"function": "add_auth",
|
|
196
|
+
"message": "Calculating signature using v4 auth.",
|
|
197
|
+
"exception": ""
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
CRUD Log
|
|
201
|
+
~~~~~~~~
|
|
202
|
+
|
|
203
|
+
.. code-block:: json
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
"timestamp": "2025-08-16 17:06:32.403",
|
|
207
|
+
"level": "AUDIT",
|
|
208
|
+
"name": "audit.model",
|
|
209
|
+
"message": "CREATE event by User (id: 6f77b814-f9c1-4cab-a737-6677734bc303)",
|
|
210
|
+
"model": "User",
|
|
211
|
+
"event_type": "CREATE",
|
|
212
|
+
"instance_id": "6f77b814-f9c1-4cab-a737-6677734bc303",
|
|
213
|
+
"instance_repr": {
|
|
214
|
+
"name": "Test Model",
|
|
215
|
+
"is_active": true,
|
|
216
|
+
"created_at": "2025-08-29T08:18:54Z",
|
|
217
|
+
"updated_at": "2025-08-29T08:18:54Z"
|
|
218
|
+
},
|
|
219
|
+
"user_id": "cae8ffb4-ba52-409c-9a6f-e10362bfaf97",
|
|
220
|
+
"user_info": {
|
|
221
|
+
"title": "mr",
|
|
222
|
+
"email": "example@source.com",
|
|
223
|
+
"first_name": "mohamlal",
|
|
224
|
+
"middle_name": "v",
|
|
225
|
+
"last_name": "nair",
|
|
226
|
+
"sex": "m"
|
|
227
|
+
},
|
|
228
|
+
"extra": {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Request-Response Log
|
|
232
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
233
|
+
|
|
234
|
+
Incoming Log Format:
|
|
235
|
+
|
|
236
|
+
.. code-block:: json
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
"timestamp": "2025-05-19 15:25:27.836",
|
|
240
|
+
"level": "API",
|
|
241
|
+
"name": "audit.request",
|
|
242
|
+
"message": "Audit Internal Request",
|
|
243
|
+
"service_name": "my_service",
|
|
244
|
+
"request_type": "internal",
|
|
245
|
+
"protocol": "http",
|
|
246
|
+
"user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
|
|
247
|
+
"user_info": {
|
|
248
|
+
"title": "mr",
|
|
249
|
+
"email": "example@email.com",
|
|
250
|
+
"first_name": "mohanlal",
|
|
251
|
+
"middle_name": "",
|
|
252
|
+
"last_name": "nair",
|
|
253
|
+
"sex": "male",
|
|
254
|
+
"date_of_birth": "21/30/1939"
|
|
255
|
+
},
|
|
256
|
+
"request_repr": {
|
|
257
|
+
"method": "GET",
|
|
258
|
+
"path": "/api/v1/health/",
|
|
259
|
+
"query_params": {},
|
|
260
|
+
"headers": {
|
|
261
|
+
"Content-Type": "application/json"
|
|
262
|
+
},
|
|
263
|
+
"body": {
|
|
264
|
+
"title": "hello"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"response_repr": {
|
|
268
|
+
"headers": {
|
|
269
|
+
"Content-Type": "application/json"
|
|
270
|
+
},
|
|
271
|
+
"body": {
|
|
272
|
+
"status": "ok"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"error_message": null,
|
|
276
|
+
"execution_time": 5.376734018325806
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
External Log Format:
|
|
280
|
+
|
|
281
|
+
.. code-block:: json
|
|
282
|
+
|
|
283
|
+
{
|
|
284
|
+
"timestamp": "2025-05-19 15:25:27.717",
|
|
285
|
+
"level": "API",
|
|
286
|
+
"name": "audit.request",
|
|
287
|
+
"message": "Audit External Service",
|
|
288
|
+
"service_name": "apollo",
|
|
289
|
+
"request_type": "external",
|
|
290
|
+
"protocol": "http",
|
|
291
|
+
"user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
|
|
292
|
+
"user_info": {
|
|
293
|
+
"title": "mr",
|
|
294
|
+
"email": "example@email.com",
|
|
295
|
+
"first_name": "mohanlal",
|
|
296
|
+
"middle_name": "",
|
|
297
|
+
"last_name": "nair",
|
|
298
|
+
"sex": "male",
|
|
299
|
+
"date_of_birth": "21/30/1939"
|
|
300
|
+
},
|
|
301
|
+
"request_repr": {
|
|
302
|
+
"endpoint": "example.com",
|
|
303
|
+
"method": "GET",
|
|
304
|
+
"headers": {},
|
|
305
|
+
"body": {}
|
|
306
|
+
},
|
|
307
|
+
"response_repr": {
|
|
308
|
+
"status_code": 200,
|
|
309
|
+
"body": {
|
|
310
|
+
"title": "title",
|
|
311
|
+
"expiresIn": 3600,
|
|
312
|
+
"error": "",
|
|
313
|
+
"errorDescription": ""
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
"error_message": "",
|
|
317
|
+
"execution_time": 5.16809344291687
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Notes
|
|
321
|
+
-----
|
|
322
|
+
|
|
323
|
+
- Compatible with **Django 4.2+** and **Python 3.8+**.
|
|
324
|
+
- Designed for easy integration with observability stacks using Vector, ClickHouse, and Grafana.
|
|
325
|
+
- Capture Django CRUD operations automatically
|
|
326
|
+
- Write structured JSON logs
|
|
327
|
+
- Ready for production-grade logging pipelines
|
|
328
|
+
- Simple pip install, reusable across projects
|
|
329
|
+
- Zero additional database overhead!
|
|
330
|
+
|
|
331
|
+
Related Tools
|
|
332
|
+
-------------
|
|
333
|
+
|
|
334
|
+
- `Vector.dev <https://vector.dev/>`_
|
|
335
|
+
- `ClickHouse <https://clickhouse.com/>`_
|
|
336
|
+
- `Grafana <https://grafana.com/>`_
|
|
337
|
+
|
|
338
|
+
License
|
|
339
|
+
-------
|
|
340
|
+
|
|
341
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/config.py
RENAMED
|
@@ -20,6 +20,22 @@ class AuditBoundLogger(structlog.stdlib.BoundLogger):
|
|
|
20
20
|
return self._proxy_to_logger("login", event, *args, **kw)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def get_plain_formatter() -> dict:
|
|
24
|
+
"""
|
|
25
|
+
LOGGING formatter dict that routes stdlib loggers through the structlog
|
|
26
|
+
processor chain but renders as plain text instead of JSON. Use for local
|
|
27
|
+
development when human-readable output is preferred over structured JSON.
|
|
28
|
+
"""
|
|
29
|
+
return {
|
|
30
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
31
|
+
"processors": [
|
|
32
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
33
|
+
structlog.dev.ConsoleRenderer(colors=False),
|
|
34
|
+
],
|
|
35
|
+
"foreign_pre_chain": shared_processors,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
23
39
|
def get_stdlib_formatter() -> dict:
|
|
24
40
|
"""
|
|
25
41
|
LOGGING formatter dict that routes stdlib loggers through the structlog
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/formatters.py
RENAMED
|
@@ -63,10 +63,13 @@ class AppFormatter(logging.Formatter):
|
|
|
63
63
|
"log_type": self.log_type,
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
# Add exception info if present for ERROR
|
|
67
66
|
if record.exc_info:
|
|
68
67
|
log_data["exception"] = "{}".format(self.formatException(record.exc_info))
|
|
69
68
|
|
|
69
|
+
extra = getattr(record, "extra", "")
|
|
70
|
+
if extra:
|
|
71
|
+
log_data["extra"] = extra
|
|
72
|
+
|
|
70
73
|
return json.dumps(log_data, default=_json_default)
|
|
71
74
|
|
|
72
75
|
|
|
@@ -106,6 +109,7 @@ class APIFormatter(logging.Formatter):
|
|
|
106
109
|
"response_repr",
|
|
107
110
|
"error_message",
|
|
108
111
|
"execution_time",
|
|
112
|
+
"extra",
|
|
109
113
|
]
|
|
110
114
|
|
|
111
115
|
for field in audit_fields:
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/middleware.py
RENAMED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import contextlib
|
|
2
1
|
import json
|
|
3
2
|
import re
|
|
4
3
|
import time
|
|
5
4
|
import uuid
|
|
6
5
|
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
|
|
7
8
|
import structlog.contextvars as ctx
|
|
8
9
|
|
|
9
|
-
from asgiref.local import Local
|
|
10
10
|
from asgiref.sync import (
|
|
11
11
|
iscoroutinefunction,
|
|
12
12
|
markcoroutinefunction,
|
|
13
|
-
sync_to_async,
|
|
14
13
|
)
|
|
15
14
|
from django.http import HttpResponse
|
|
16
15
|
from django.utils.deprecation import MiddlewareMixin
|
|
@@ -21,7 +20,7 @@ from .settings import REGISTERED_URLS, SERVICE_NAME, UNREGISTERED_URLS
|
|
|
21
20
|
|
|
22
21
|
_log = get_logger("audit.request")
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
_request_var: ContextVar = ContextVar("current_request", default=None)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class MockRequest:
|
|
@@ -32,11 +31,11 @@ class MockRequest:
|
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
def get_current_request():
|
|
35
|
-
return
|
|
34
|
+
return _request_var.get()
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def set_current_request(request):
|
|
39
|
-
|
|
38
|
+
_request_var.set(request)
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def get_current_user():
|
|
@@ -47,11 +46,11 @@ def get_current_user():
|
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
def set_current_user(user):
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
request = _request_var.get()
|
|
50
|
+
if request is not None:
|
|
51
|
+
request.user = user
|
|
52
|
+
else:
|
|
53
|
+
_request_var.set(MockRequest(user=user))
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def get_user_details():
|
|
@@ -73,8 +72,7 @@ def get_user_details():
|
|
|
73
72
|
|
|
74
73
|
|
|
75
74
|
def clear_request():
|
|
76
|
-
|
|
77
|
-
del _thread_locals.request
|
|
75
|
+
_request_var.set(None)
|
|
78
76
|
|
|
79
77
|
|
|
80
78
|
def should_log_url(url):
|
|
@@ -120,6 +118,18 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
120
118
|
}
|
|
121
119
|
"""
|
|
122
120
|
|
|
121
|
+
def _init_log_data(self):
|
|
122
|
+
return {
|
|
123
|
+
"service_name": SERVICE_NAME,
|
|
124
|
+
"request_type": REQUEST_TYPES[0],
|
|
125
|
+
"protocol": None,
|
|
126
|
+
"request_repr": {},
|
|
127
|
+
"response_repr": {},
|
|
128
|
+
"error_message": None,
|
|
129
|
+
"execution_time": 0,
|
|
130
|
+
"extra": {},
|
|
131
|
+
}
|
|
132
|
+
|
|
123
133
|
def __init__(self, get_response):
|
|
124
134
|
self.get_response = get_response
|
|
125
135
|
|
|
@@ -137,15 +147,7 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
137
147
|
if not should_log_url(request.path):
|
|
138
148
|
return self.get_response(request)
|
|
139
149
|
|
|
140
|
-
log_data =
|
|
141
|
-
"service_name": SERVICE_NAME,
|
|
142
|
-
"request_type": REQUEST_TYPES[0],
|
|
143
|
-
"protocol": None,
|
|
144
|
-
"request_repr": {},
|
|
145
|
-
"response_repr": {},
|
|
146
|
-
"error_message": None,
|
|
147
|
-
"execution_time": 0,
|
|
148
|
-
}
|
|
150
|
+
log_data = self._init_log_data()
|
|
149
151
|
start_time = time.time()
|
|
150
152
|
|
|
151
153
|
# Log request
|
|
@@ -211,15 +213,7 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
211
213
|
if not should_log_url(request.path):
|
|
212
214
|
return await self.get_response(request)
|
|
213
215
|
|
|
214
|
-
log_data =
|
|
215
|
-
"service_name": SERVICE_NAME,
|
|
216
|
-
"request_type": REQUEST_TYPES[0],
|
|
217
|
-
"protocol": None,
|
|
218
|
-
"request_repr": {},
|
|
219
|
-
"response_repr": {},
|
|
220
|
-
"error_message": None,
|
|
221
|
-
"execution_time": 0,
|
|
222
|
-
}
|
|
216
|
+
log_data = self._init_log_data()
|
|
223
217
|
start_time = time.time()
|
|
224
218
|
|
|
225
219
|
# Log request
|
|
@@ -242,9 +236,7 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
242
236
|
end_time = time.time()
|
|
243
237
|
|
|
244
238
|
# Capture user details AFTER authentication has happened
|
|
245
|
-
user_id, user_info =
|
|
246
|
-
get_user_details, thread_sensitive=True
|
|
247
|
-
)()
|
|
239
|
+
user_id, user_info = get_user_details()
|
|
248
240
|
ctx.bind_contextvars(user_id=user_id, user_info=user_info)
|
|
249
241
|
|
|
250
242
|
# TODO: Find way to add status code to response_data
|
|
@@ -96,6 +96,26 @@ _STANDARD_KEYS = frozenset(
|
|
|
96
96
|
"request_id",
|
|
97
97
|
"_record",
|
|
98
98
|
"_from_structlog",
|
|
99
|
+
# audit.model fields
|
|
100
|
+
"model",
|
|
101
|
+
"event_type",
|
|
102
|
+
"instance_id",
|
|
103
|
+
"instance_repr",
|
|
104
|
+
"user_id",
|
|
105
|
+
"user_info",
|
|
106
|
+
"extra",
|
|
107
|
+
# audit.request fields
|
|
108
|
+
"service_name",
|
|
109
|
+
"request_type",
|
|
110
|
+
"protocol",
|
|
111
|
+
"request_repr",
|
|
112
|
+
"response_repr",
|
|
113
|
+
"error_message",
|
|
114
|
+
"execution_time",
|
|
115
|
+
# audit.login fields
|
|
116
|
+
"event",
|
|
117
|
+
"success",
|
|
118
|
+
"error",
|
|
99
119
|
}
|
|
100
120
|
)
|
|
101
121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "django-activity-audit"
|
|
3
|
-
version = "1.3.0.
|
|
3
|
+
version = "1.3.0.dev21"
|
|
4
4
|
description = "A Django package for easy CRUD operation logging and container logs"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Shreeshan", email = "shreeshan256@gmail.com" }
|
|
@@ -31,6 +31,8 @@ classifiers = [
|
|
|
31
31
|
requires-python = ">=3.8"
|
|
32
32
|
dependencies = [
|
|
33
33
|
"django>=4.2,<6.0",
|
|
34
|
+
"structlog>=24.0",
|
|
35
|
+
"orjson>=3.0",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.urls]
|
|
@@ -50,9 +52,7 @@ test = [
|
|
|
50
52
|
"pytest-django>=4.0.0",
|
|
51
53
|
"pytest-cov>=4.0.0",
|
|
52
54
|
"djangorestframework>=3.14.0",
|
|
53
|
-
"structlog>=24.0",
|
|
54
55
|
]
|
|
55
|
-
structlog = ["structlog>=24.0"]
|
|
56
56
|
|
|
57
57
|
[build-system]
|
|
58
58
|
requires = ["hatchling"]
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
Django Activity Audit
|
|
2
|
-
=====================
|
|
3
|
-
|
|
4
|
-
A Django package that extends the default logging mechanism to track CRUD operations and container logs.
|
|
5
|
-
|
|
6
|
-
Features
|
|
7
|
-
--------
|
|
8
|
-
|
|
9
|
-
- Automatic logging of CRUD operations (Create, Read, Update, Delete)
|
|
10
|
-
- Tracks both HTTP requests and model changes
|
|
11
|
-
- Custom log levels Audit(21) and API(22) for CRUD and Request-Response auditing.
|
|
12
|
-
- Structured JSON logs for audit trails
|
|
13
|
-
- Human-readable container logs
|
|
14
|
-
- Separate log files for audit and container logs
|
|
15
|
-
- Console and file output options
|
|
16
|
-
|
|
17
|
-
Installation
|
|
18
|
-
------------
|
|
19
|
-
|
|
20
|
-
1. Install the package::
|
|
21
|
-
|
|
22
|
-
pip install django-activity-audit
|
|
23
|
-
|
|
24
|
-
2. Add ``activity_audit`` to your ``INSTALLED_APPS`` in ``settings.py``::
|
|
25
|
-
|
|
26
|
-
INSTALLED_APPS = [
|
|
27
|
-
...
|
|
28
|
-
'activity_audit',
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
3. Add the middleware to your ``MIDDLEWARE`` in ``settings.py``::
|
|
32
|
-
|
|
33
|
-
MIDDLEWARE = [
|
|
34
|
-
...
|
|
35
|
-
'activity_audit.middleware.AuditLoggingMiddleware',
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
4. Configure logging in ``settings.py``::
|
|
39
|
-
|
|
40
|
-
from activity_audit import *
|
|
41
|
-
|
|
42
|
-
LOGGING = {
|
|
43
|
-
"version": 1,
|
|
44
|
-
"disable_existing_loggers": False,
|
|
45
|
-
"formatters": {
|
|
46
|
-
"json": get_json_formatter(),
|
|
47
|
-
"verbose": get_console_formatter(),
|
|
48
|
-
},
|
|
49
|
-
"handlers": {
|
|
50
|
-
"console": {
|
|
51
|
-
"level": "DEBUG",
|
|
52
|
-
"class": "logging.StreamHandler",
|
|
53
|
-
"formatter": "verbose",
|
|
54
|
-
},
|
|
55
|
-
"file": get_json_handler(level="DEBUG", formatter="json"),
|
|
56
|
-
"api_file": get_api_file_handler(),
|
|
57
|
-
"audit_file": get_audit_handler(),
|
|
58
|
-
},
|
|
59
|
-
"root": {"level": "DEBUG", "handlers": ["console", "file"]},
|
|
60
|
-
"loggers": {
|
|
61
|
-
"audit.request": {
|
|
62
|
-
"handlers": ["api_file"],
|
|
63
|
-
"level": "API",
|
|
64
|
-
"propagate": False,
|
|
65
|
-
},
|
|
66
|
-
"audit.model": {
|
|
67
|
-
"handlers": ["audit_file"],
|
|
68
|
-
"level": "AUDIT",
|
|
69
|
-
"propagate": False,
|
|
70
|
-
},
|
|
71
|
-
"django": {
|
|
72
|
-
"handlers": ["console", "file"],
|
|
73
|
-
"level": "INFO",
|
|
74
|
-
"propagate": False,
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
5. Configure the service name in ``settings.py`` (optional, defaults to ``"default"``)::
|
|
80
|
-
|
|
81
|
-
AUDIT_SERVICE_NAME = "my_service"
|
|
82
|
-
|
|
83
|
-
6. For external services logging, extend ``HTTPClient`` or ``SFTPClient``::
|
|
84
|
-
|
|
85
|
-
class ExternalService(HTTPClient):
|
|
86
|
-
def __init__(self):
|
|
87
|
-
super().__init__("service_name")
|
|
88
|
-
|
|
89
|
-
def connect(self):
|
|
90
|
-
url = "https://www.sample.com"
|
|
91
|
-
response = self.get(url) # sample log structure below
|
|
92
|
-
|
|
93
|
-
7. Create ``audit_logs`` folder in project directory
|
|
94
|
-
|
|
95
|
-
Log Types
|
|
96
|
-
---------
|
|
97
|
-
|
|
98
|
-
Container Logs
|
|
99
|
-
--------------
|
|
100
|
-
|
|
101
|
-
Console Log Format::
|
|
102
|
-
|
|
103
|
-
'%(levelname)s %(asctime)s %(pathname)s %(module)s %(funcName)s %(message)s'
|
|
104
|
-
-----------------------------------------------------------------------------
|
|
105
|
-
INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_with_contacts_and_diseases Patient 'd6c9a056-0b57-453a-8c0f-44319004b761 - Patient3' created.
|
|
106
|
-
|
|
107
|
-
APP Log
|
|
108
|
-
-------
|
|
109
|
-
|
|
110
|
-
::
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
"timestamp": "2025-05-15 13:38:02.141",
|
|
114
|
-
"level": "DEBUG",
|
|
115
|
-
"name": "botocore.auth",
|
|
116
|
-
"path": "/opt/venv/lib/python3.11/site-packages/botocore/auth.py",
|
|
117
|
-
"module": "auth",
|
|
118
|
-
"function": "add_auth",
|
|
119
|
-
"message": "Calculating signature using v4 auth.",
|
|
120
|
-
"exception": ""
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
CRUD Log
|
|
124
|
-
--------
|
|
125
|
-
|
|
126
|
-
::
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
"timestamp": "2025-08-16 17:06:32.403",
|
|
130
|
-
"level": "AUDIT",
|
|
131
|
-
"name": "audit.model",
|
|
132
|
-
"message": "CREATE event by User (id: 6f77b814-f9c1-4cab-a737-6677734bc303)",
|
|
133
|
-
"model": "User",
|
|
134
|
-
"event_type": "CREATE",
|
|
135
|
-
"instance_id": "6f77b814-f9c1-4cab-a737-6677734bc303",
|
|
136
|
-
"instance_repr" : {
|
|
137
|
-
"name": "Test Model",
|
|
138
|
-
"is_active": true,
|
|
139
|
-
"created_at": "2025-08-29T08:18:54Z",
|
|
140
|
-
"updated_at": "2025-08-29T08:18:54Z"
|
|
141
|
-
},
|
|
142
|
-
"user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
|
|
143
|
-
"user_info": {
|
|
144
|
-
"title": "mr",
|
|
145
|
-
"email": "example@email.com",
|
|
146
|
-
"first_name": "mohanlal",
|
|
147
|
-
"middle_name": "",
|
|
148
|
-
"last_name": "nair",
|
|
149
|
-
"sex": "male",
|
|
150
|
-
"date_of_birth": "21/30/1939"
|
|
151
|
-
},
|
|
152
|
-
"extra": {}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
Request-Response Log
|
|
156
|
-
--------------------
|
|
157
|
-
|
|
158
|
-
Incoming Log Format::
|
|
159
|
-
|
|
160
|
-
{
|
|
161
|
-
"timestamp": "2025-05-19 15:25:27.836",
|
|
162
|
-
"level": "API",
|
|
163
|
-
"name": "audit.request",
|
|
164
|
-
"message": "Audit Internal Request",
|
|
165
|
-
"service_name": "my_service",
|
|
166
|
-
"request_type": "internal",
|
|
167
|
-
"protocol": "http",
|
|
168
|
-
"user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
|
|
169
|
-
"user_info": {
|
|
170
|
-
"title": "mr",
|
|
171
|
-
"email": "example@email.com",
|
|
172
|
-
"first_name": "mohanlal",
|
|
173
|
-
"middle_name": "",
|
|
174
|
-
"last_name": "nair",
|
|
175
|
-
"sex": "male",
|
|
176
|
-
"date_of_birth": "21/30/1939"
|
|
177
|
-
},
|
|
178
|
-
"request_repr": {
|
|
179
|
-
"method": "GET",
|
|
180
|
-
"path": "/api/v1/health/",
|
|
181
|
-
"query_params": {},
|
|
182
|
-
"headers": {
|
|
183
|
-
"Content-Type": "application/json",
|
|
184
|
-
},
|
|
185
|
-
"user": null,
|
|
186
|
-
"body": {
|
|
187
|
-
"title": "hello"
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
"response_repr": {
|
|
191
|
-
"status_code": 200,
|
|
192
|
-
"headers": {
|
|
193
|
-
"Content-Type": "application/json",
|
|
194
|
-
},
|
|
195
|
-
"body": {
|
|
196
|
-
"status": "ok"
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
"error_message": null,
|
|
200
|
-
"execution_time": 5.376734018325806
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
External Log format::
|
|
204
|
-
|
|
205
|
-
{
|
|
206
|
-
"timestamp": "2025-05-19 15:25:27.717",
|
|
207
|
-
"level": "API",
|
|
208
|
-
"name": "audit.request",
|
|
209
|
-
"message": "Audit External Service",
|
|
210
|
-
"service_name": "apollo",
|
|
211
|
-
"request_type": "external",
|
|
212
|
-
"protocol": "http",
|
|
213
|
-
"user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
|
|
214
|
-
"user_info": {
|
|
215
|
-
"title": "mr",
|
|
216
|
-
"email": "example@email.com",
|
|
217
|
-
"first_name": "mohanlal",
|
|
218
|
-
"middle_name": "",
|
|
219
|
-
"last_name": "nair",
|
|
220
|
-
"sex": "male",
|
|
221
|
-
"date_of_birth": "21/30/1939"
|
|
222
|
-
},
|
|
223
|
-
"request_repr": {
|
|
224
|
-
"endpoint": "example.com",
|
|
225
|
-
"method": "GET",
|
|
226
|
-
"headers": {},
|
|
227
|
-
"body": {}
|
|
228
|
-
},
|
|
229
|
-
"response_repr": {
|
|
230
|
-
"status_code": 200,
|
|
231
|
-
"body": {
|
|
232
|
-
"title": "title",
|
|
233
|
-
"expiresIn": 3600,
|
|
234
|
-
"error": "",
|
|
235
|
-
"errorDescription": ""
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
"error_message": "",
|
|
239
|
-
"execution_time": 5.16809344291687
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
Notes
|
|
243
|
-
-----
|
|
244
|
-
|
|
245
|
-
- Compatible with **Django 3.2+** and **Python 3.7+**.
|
|
246
|
-
- Designed for easy integration with observability stacks using Vector, ClickHouse, and Grafana.
|
|
247
|
-
- Capture Django CRUD operations automatically
|
|
248
|
-
- Write structured JSON logs
|
|
249
|
-
- Ready for production-grade logging pipelines
|
|
250
|
-
- Simple pip install, reusable across projects
|
|
251
|
-
- Zero additional database overhead!
|
|
252
|
-
|
|
253
|
-
Related Tools
|
|
254
|
-
-------------
|
|
255
|
-
|
|
256
|
-
- `Vector.dev <https://vector.dev/>`_
|
|
257
|
-
- `ClickHouse <https://clickhouse.com/>`_
|
|
258
|
-
- `Grafana <https://grafana.com/>`_
|
|
259
|
-
|
|
260
|
-
License
|
|
261
|
-
-------
|
|
262
|
-
|
|
263
|
-
This project is licensed under the MIT License - see the LICENSE file for details.
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/.claude/settings.local.json
RENAMED
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/.pre-commit-config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/__init__.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/apps.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/constants.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/protocols.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/settings.py
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/activity_audit/signals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/hipaa-audit-gaps.md
RENAMED
|
File without changes
|
{django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev21}/docs/improvements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|