django-activity-audit 1.3.0.dev18__tar.gz → 1.3.0.dev20__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.dev20/.claude/settings.local.json +7 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/PKG-INFO +87 -29
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/README.md +84 -28
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/__init__.py +3 -0
- django_activity_audit-1.3.0.dev20/activity_audit/apps.py +17 -0
- django_activity_audit-1.3.0.dev20/activity_audit/config.py +82 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/formatters.py +4 -2
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/handlers.py +14 -8
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/middleware.py +32 -44
- django_activity_audit-1.3.0.dev20/activity_audit/shared_processors.py +148 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/signals.py +20 -17
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/utils.py +1 -1
- django_activity_audit-1.3.0.dev20/docs/hipaa-audit-gaps.md +133 -0
- django_activity_audit-1.3.0.dev20/docs/structlog-integration.md +419 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/pyproject.toml +3 -1
- django_activity_audit-1.3.0.dev18/activity_audit/apps.py +0 -22
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/.gitignore +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/.pre-commit-config.yaml +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/LICENSE +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/MANIFEST.in +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/README.rst +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/constants.py +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/logger_levels.py +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/protocols.py +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/settings.py +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/unregistered.py +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/docs/django-activity-audit.md +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/docs/improvements.md +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/docs/user-activity-feed-plan.md +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/hatch +0 -0
- {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/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.dev20
|
|
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,6 +26,8 @@ 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'
|
|
@@ -59,6 +61,8 @@ A Django package that extends the default logging mechanism to track CRUD operat
|
|
|
59
61
|
pip install django-activity-audit
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
This also installs [`structlog`](https://www.structlog.org/) and [`orjson`](https://github.com/ijl/orjson) as required dependencies.
|
|
65
|
+
|
|
62
66
|
2. Add 'activity_audit' to your INSTALLED_APPS in settings.py:
|
|
63
67
|
```python
|
|
64
68
|
INSTALLED_APPS = [
|
|
@@ -76,47 +80,101 @@ MIDDLEWARE = [
|
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
4. Configure logging in settings.py:
|
|
83
|
+
|
|
84
|
+
Import the formatter helpers from `activity_audit.config`:
|
|
85
|
+
|
|
79
86
|
```python
|
|
80
|
-
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
|
|
81
97
|
|
|
82
98
|
LOGGING = {
|
|
83
99
|
"version": 1,
|
|
84
100
|
"disable_existing_loggers": False,
|
|
85
101
|
"formatters": {
|
|
86
|
-
"
|
|
87
|
-
"
|
|
102
|
+
"structlog": get_stdlib_formatter(),
|
|
103
|
+
"default": get_plain_formatter(),
|
|
88
104
|
},
|
|
89
105
|
"handlers": {
|
|
90
|
-
"console":
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
96
|
-
"api_file": get_api_file_handler(),
|
|
97
|
-
"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
|
|
98
112
|
},
|
|
99
|
-
"root": {"level": "DEBUG", "handlers": ["console", "file"]},
|
|
100
113
|
"loggers": {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
},
|
|
117
135
|
}
|
|
118
136
|
```
|
|
119
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
|
+
|
|
120
178
|
5. Configure the service name in `settings.py` (optional, defaults to `"default"`):
|
|
121
179
|
```python
|
|
122
180
|
AUDIT_SERVICE_NAME = "my_service"
|
|
@@ -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"
|
{django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/__init__.py
RENAMED
|
@@ -16,6 +16,8 @@ from activity_audit.utils import (
|
|
|
16
16
|
get_login_handler,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
+
from .config import get_logger
|
|
20
|
+
|
|
19
21
|
from . import logger_levels
|
|
20
22
|
|
|
21
23
|
__all__ = [
|
|
@@ -32,4 +34,5 @@ __all__ = [
|
|
|
32
34
|
"get_async_api_handler",
|
|
33
35
|
"get_async_audit_handler",
|
|
34
36
|
"get_async_login_handler",
|
|
37
|
+
"get_logger",
|
|
35
38
|
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuditLoggingConfig(AppConfig):
|
|
5
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
6
|
+
name = "activity_audit"
|
|
7
|
+
verbose_name = "Django Activity Audit"
|
|
8
|
+
|
|
9
|
+
def ready(self):
|
|
10
|
+
from . import (
|
|
11
|
+
logger_levels,
|
|
12
|
+
signals, # noqa
|
|
13
|
+
unregistered, # noqa
|
|
14
|
+
)
|
|
15
|
+
from .config import configure
|
|
16
|
+
|
|
17
|
+
configure()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
|
|
3
|
+
from activity_audit.shared_processors import (
|
|
4
|
+
_json_default,
|
|
5
|
+
_orjson_dumps,
|
|
6
|
+
shared_processors,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuditBoundLogger(structlog.stdlib.BoundLogger):
|
|
11
|
+
"""Extends structlog BoundLogger with AUDIT, API, and LOGIN log levels."""
|
|
12
|
+
|
|
13
|
+
def audit(self, event=None, *args, **kw):
|
|
14
|
+
return self._proxy_to_logger("audit", event, *args, **kw)
|
|
15
|
+
|
|
16
|
+
def api(self, event=None, *args, **kw):
|
|
17
|
+
return self._proxy_to_logger("api", event, *args, **kw)
|
|
18
|
+
|
|
19
|
+
def login(self, event=None, *args, **kw):
|
|
20
|
+
return self._proxy_to_logger("login", event, *args, **kw)
|
|
21
|
+
|
|
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
|
+
|
|
39
|
+
def get_stdlib_formatter() -> dict:
|
|
40
|
+
"""
|
|
41
|
+
LOGGING formatter dict that routes stdlib loggers through the structlog
|
|
42
|
+
processor chain. Use for any logger that isn't a native structlog logger
|
|
43
|
+
(e.g. celery, django, uvicorn).
|
|
44
|
+
"""
|
|
45
|
+
return {
|
|
46
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
47
|
+
"processors": [
|
|
48
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
49
|
+
structlog.processors.JSONRenderer(
|
|
50
|
+
serializer=_orjson_dumps, default=_json_default
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
"foreign_pre_chain": shared_processors,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def configure() -> None:
|
|
58
|
+
"""
|
|
59
|
+
Configure structlog for console-only JSON output.
|
|
60
|
+
|
|
61
|
+
Produces the same JSON structure as the existing AuditFormatter /
|
|
62
|
+
APIFormatter / LoginFormatter — same field names, same timestamp format,
|
|
63
|
+
same uppercase level names — but outputs to stdout via StreamHandler.
|
|
64
|
+
No file handlers are used.
|
|
65
|
+
|
|
66
|
+
merge_contextvars is wired in so Phase 2 (replacing thread-locals with
|
|
67
|
+
structlog contextvars in middleware) only requires middleware changes;
|
|
68
|
+
this function and signals.py do not need to change again.
|
|
69
|
+
"""
|
|
70
|
+
structlog.configure(
|
|
71
|
+
processors=shared_processors
|
|
72
|
+
+ [
|
|
73
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
74
|
+
],
|
|
75
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
76
|
+
wrapper_class=AuditBoundLogger,
|
|
77
|
+
cache_logger_on_first_use=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_logger(name: str) -> AuditBoundLogger:
|
|
82
|
+
return structlog.get_logger(name)
|
{django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/formatters.py
RENAMED
|
@@ -4,8 +4,9 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
|
+
import structlog.contextvars as ctx
|
|
8
|
+
|
|
7
9
|
from .constants import LogType
|
|
8
|
-
from .middleware import get_request_id
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _json_default(obj):
|
|
@@ -55,7 +56,8 @@ class AppFormatter(logging.Formatter):
|
|
|
55
56
|
"path": record.pathname,
|
|
56
57
|
"module": record.module,
|
|
57
58
|
"function": record.funcName,
|
|
58
|
-
"request_id": getattr(record, "request_id", None)
|
|
59
|
+
"request_id": getattr(record, "request_id", None)
|
|
60
|
+
or ctx.get_contextvars().get("request_id", ""),
|
|
59
61
|
"message": record.getMessage(),
|
|
60
62
|
"exception": "",
|
|
61
63
|
"log_type": self.log_type,
|
{django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/handlers.py
RENAMED
|
@@ -3,8 +3,14 @@ import queue
|
|
|
3
3
|
|
|
4
4
|
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import structlog.contextvars as ctx
|
|
7
|
+
|
|
8
|
+
from .formatters import (
|
|
9
|
+
APIFormatter,
|
|
10
|
+
AppFormatter,
|
|
11
|
+
AuditFormatter,
|
|
12
|
+
LoginFormatter,
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
class BaseAuditHandler(RotatingFileHandler):
|
|
@@ -110,10 +116,10 @@ class AsyncBaseAuditHandler(QueueHandler):
|
|
|
110
116
|
self._listener.start()
|
|
111
117
|
|
|
112
118
|
def prepare(self, record):
|
|
113
|
-
#
|
|
114
|
-
#
|
|
119
|
+
# Snapshot request_id from contextvars before the record is queued —
|
|
120
|
+
# the QueueListener thread runs in a different context.
|
|
115
121
|
record = super().prepare(record)
|
|
116
|
-
record.request_id =
|
|
122
|
+
record.request_id = ctx.get_contextvars().get("request_id", "")
|
|
117
123
|
return record
|
|
118
124
|
|
|
119
125
|
def close(self):
|
|
@@ -170,10 +176,10 @@ class AsyncJsonHandler(QueueHandler):
|
|
|
170
176
|
self._listener.start()
|
|
171
177
|
|
|
172
178
|
def prepare(self, record):
|
|
173
|
-
#
|
|
174
|
-
#
|
|
179
|
+
# Snapshot request_id from contextvars before the record is queued —
|
|
180
|
+
# the QueueListener thread runs in a different context.
|
|
175
181
|
record = super().prepare(record)
|
|
176
|
-
record.request_id =
|
|
182
|
+
record.request_id = ctx.get_contextvars().get("request_id", "")
|
|
177
183
|
return record
|
|
178
184
|
|
|
179
185
|
def close(self):
|
{django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/middleware.py
RENAMED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
import contextlib
|
|
2
1
|
import json
|
|
3
|
-
import logging
|
|
4
2
|
import re
|
|
5
3
|
import time
|
|
6
4
|
import uuid
|
|
7
5
|
|
|
8
|
-
from
|
|
9
|
-
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
|
|
8
|
+
import structlog.contextvars as ctx
|
|
9
|
+
|
|
10
|
+
from asgiref.sync import (
|
|
11
|
+
iscoroutinefunction,
|
|
12
|
+
markcoroutinefunction,
|
|
13
|
+
)
|
|
10
14
|
from django.http import HttpResponse
|
|
11
15
|
from django.utils.deprecation import MiddlewareMixin
|
|
12
16
|
|
|
17
|
+
from .config import get_logger
|
|
13
18
|
from .constants import REQUEST_TYPES
|
|
14
19
|
from .settings import REGISTERED_URLS, SERVICE_NAME, UNREGISTERED_URLS
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
_log = get_logger("audit.request")
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
_request_var: ContextVar = ContextVar("current_request", default=None)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
class MockRequest:
|
|
@@ -26,19 +31,11 @@ class MockRequest:
|
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
def get_current_request():
|
|
29
|
-
return
|
|
34
|
+
return _request_var.get()
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
def set_current_request(request):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_request_id():
|
|
37
|
-
return getattr(_thread_locals, "request_id", None)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def set_request_id(request_id):
|
|
41
|
-
_thread_locals.request_id = request_id
|
|
38
|
+
_request_var.set(request)
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
def get_current_user():
|
|
@@ -49,11 +46,11 @@ def get_current_user():
|
|
|
49
46
|
|
|
50
47
|
|
|
51
48
|
def set_current_user(user):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
request = _request_var.get()
|
|
50
|
+
if request is not None:
|
|
51
|
+
request.user = user
|
|
52
|
+
else:
|
|
53
|
+
_request_var.set(MockRequest(user=user))
|
|
57
54
|
|
|
58
55
|
|
|
59
56
|
def get_user_details():
|
|
@@ -75,10 +72,7 @@ def get_user_details():
|
|
|
75
72
|
|
|
76
73
|
|
|
77
74
|
def clear_request():
|
|
78
|
-
|
|
79
|
-
del _thread_locals.request
|
|
80
|
-
with contextlib.suppress(AttributeError):
|
|
81
|
-
del _thread_locals.request_id
|
|
75
|
+
_request_var.set(None)
|
|
82
76
|
|
|
83
77
|
|
|
84
78
|
def should_log_url(url):
|
|
@@ -135,7 +129,8 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
135
129
|
return self.__acall__(request)
|
|
136
130
|
set_current_request(request)
|
|
137
131
|
request_id = str(uuid.uuid4())
|
|
138
|
-
|
|
132
|
+
ctx.clear_contextvars()
|
|
133
|
+
ctx.bind_contextvars(request_id=request_id)
|
|
139
134
|
|
|
140
135
|
if not should_log_url(request.path):
|
|
141
136
|
return self.get_response(request)
|
|
@@ -144,9 +139,6 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
144
139
|
"service_name": SERVICE_NAME,
|
|
145
140
|
"request_type": REQUEST_TYPES[0],
|
|
146
141
|
"protocol": None,
|
|
147
|
-
"request_id": "",
|
|
148
|
-
"user_id": "",
|
|
149
|
-
"user_info": {},
|
|
150
142
|
"request_repr": {},
|
|
151
143
|
"response_repr": {},
|
|
152
144
|
"error_message": None,
|
|
@@ -175,8 +167,7 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
175
167
|
|
|
176
168
|
# Capture user details AFTER authentication has happened
|
|
177
169
|
user_id, user_info = get_user_details()
|
|
178
|
-
|
|
179
|
-
log_data["user_info"] = user_info
|
|
170
|
+
ctx.bind_contextvars(user_id=user_id, user_info=user_info)
|
|
180
171
|
|
|
181
172
|
# TODO: Find way to add status code to response_data
|
|
182
173
|
|
|
@@ -198,20 +189,22 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
198
189
|
|
|
199
190
|
log_data["execution_time"] = end_time - start_time
|
|
200
191
|
log_data["protocol"] = "https" if request.is_secure() else "http"
|
|
201
|
-
log_data["request_id"] = request_id
|
|
202
192
|
log_data["request_repr"] = request_data
|
|
203
193
|
log_data["response_repr"] = response_data
|
|
204
194
|
|
|
205
|
-
|
|
195
|
+
bound = _log.bind(**log_data)
|
|
196
|
+
bound.api("Audit Internal Request")
|
|
206
197
|
|
|
207
198
|
clear_request()
|
|
199
|
+
ctx.clear_contextvars()
|
|
208
200
|
|
|
209
201
|
return response
|
|
210
202
|
|
|
211
203
|
async def __acall__(self, request):
|
|
212
204
|
set_current_request(request)
|
|
213
205
|
request_id = str(uuid.uuid4())
|
|
214
|
-
|
|
206
|
+
ctx.clear_contextvars()
|
|
207
|
+
ctx.bind_contextvars(request_id=request_id)
|
|
215
208
|
|
|
216
209
|
if not should_log_url(request.path):
|
|
217
210
|
return await self.get_response(request)
|
|
@@ -220,9 +213,6 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
220
213
|
"service_name": SERVICE_NAME,
|
|
221
214
|
"request_type": REQUEST_TYPES[0],
|
|
222
215
|
"protocol": None,
|
|
223
|
-
"request_id": "",
|
|
224
|
-
"user_id": "",
|
|
225
|
-
"user_info": {},
|
|
226
216
|
"request_repr": {},
|
|
227
217
|
"response_repr": {},
|
|
228
218
|
"error_message": None,
|
|
@@ -250,11 +240,8 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
250
240
|
end_time = time.time()
|
|
251
241
|
|
|
252
242
|
# Capture user details AFTER authentication has happened
|
|
253
|
-
user_id, user_info =
|
|
254
|
-
|
|
255
|
-
)()
|
|
256
|
-
log_data["user_id"] = user_id
|
|
257
|
-
log_data["user_info"] = user_info
|
|
243
|
+
user_id, user_info = get_user_details()
|
|
244
|
+
ctx.bind_contextvars(user_id=user_id, user_info=user_info)
|
|
258
245
|
|
|
259
246
|
# TODO: Find way to add status code to response_data
|
|
260
247
|
|
|
@@ -276,12 +263,13 @@ class AuditLoggingMiddleware(MiddlewareMixin):
|
|
|
276
263
|
|
|
277
264
|
log_data["execution_time"] = end_time - start_time
|
|
278
265
|
log_data["protocol"] = "https" if request.is_secure() else "http"
|
|
279
|
-
log_data["request_id"] = request_id
|
|
280
266
|
log_data["request_repr"] = request_data
|
|
281
267
|
log_data["response_repr"] = response_data
|
|
282
268
|
|
|
283
|
-
|
|
269
|
+
bound = _log.bind(**log_data)
|
|
270
|
+
bound.api("Audit Internal Request")
|
|
284
271
|
|
|
285
272
|
clear_request()
|
|
273
|
+
ctx.clear_contextvars()
|
|
286
274
|
|
|
287
275
|
return response
|