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.
Files changed (31) hide show
  1. django_activity_audit-1.3.0.dev20/.claude/settings.local.json +7 -0
  2. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/PKG-INFO +87 -29
  3. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/README.md +84 -28
  4. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/__init__.py +3 -0
  5. django_activity_audit-1.3.0.dev20/activity_audit/apps.py +17 -0
  6. django_activity_audit-1.3.0.dev20/activity_audit/config.py +82 -0
  7. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/formatters.py +4 -2
  8. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/handlers.py +14 -8
  9. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/middleware.py +32 -44
  10. django_activity_audit-1.3.0.dev20/activity_audit/shared_processors.py +148 -0
  11. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/signals.py +20 -17
  12. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/utils.py +1 -1
  13. django_activity_audit-1.3.0.dev20/docs/hipaa-audit-gaps.md +133 -0
  14. django_activity_audit-1.3.0.dev20/docs/structlog-integration.md +419 -0
  15. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/pyproject.toml +3 -1
  16. django_activity_audit-1.3.0.dev18/activity_audit/apps.py +0 -22
  17. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/.gitignore +0 -0
  18. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/.pre-commit-config.yaml +0 -0
  19. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/LICENSE +0 -0
  20. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/MANIFEST.in +0 -0
  21. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/README.rst +0 -0
  22. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/constants.py +0 -0
  23. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/logger_levels.py +0 -0
  24. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/protocols.py +0 -0
  25. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/settings.py +0 -0
  26. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/activity_audit/unregistered.py +0 -0
  27. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/docs/django-activity-audit.md +0 -0
  28. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/docs/improvements.md +0 -0
  29. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/docs/user-activity-feed-plan.md +0 -0
  30. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/hatch +0 -0
  31. {django_activity_audit-1.3.0.dev18 → django_activity_audit-1.3.0.dev20}/pytest.ini +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(.venv/bin/python -m pip install \"structlog>=24.0\")"
5
+ ]
6
+ }
7
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-activity-audit
3
- Version: 1.3.0.dev18
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
- "json": get_json_formatter(),
87
- "verbose": get_console_formatter(),
102
+ "structlog": get_stdlib_formatter(),
103
+ "default": get_plain_formatter(),
88
104
  },
89
105
  "handlers": {
90
- "console": {
91
- "level": "DEBUG",
92
- "class": "logging.StreamHandler",
93
- "formatter": "verbose",
94
- },
95
- "file": get_json_handler(level="DEBUG", formatter="json"),
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
- "audit.request": {
102
- "handlers": ["api_file"],
103
- "level": "API",
104
- "propagate": False,
105
- },
106
- "audit.model": {
107
- "handlers": ["audit_file"],
108
- "level": "AUDIT",
109
- "propagate": False,
110
- },
111
- "django": {
112
- "handlers": ["console", "file"],
113
- "level": "INFO",
114
- "propagate": False,
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
- "json": get_json_formatter(),
47
- "verbose": get_console_formatter(),
60
+ "structlog": get_stdlib_formatter(),
61
+ "default": get_plain_formatter(),
48
62
  },
49
63
  "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(),
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
- "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
- }
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"
@@ -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)
@@ -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) or get_request_id() or "",
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,
@@ -3,8 +3,14 @@ import queue
3
3
 
4
4
  from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
5
5
 
6
- from .formatters import APIFormatter, AppFormatter, AuditFormatter, LoginFormatter
7
- from .middleware import get_request_id
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
- # Capture request_id in the calling thread before the record is queued,
114
- # since thread-locals are not accessible from the QueueListener thread.
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 = get_request_id() or ""
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
- # Capture request_id in the calling thread before the record is queued,
174
- # since thread-locals are not accessible from the QueueListener thread.
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 = get_request_id() or ""
182
+ record.request_id = ctx.get_contextvars().get("request_id", "")
177
183
  return record
178
184
 
179
185
  def close(self):
@@ -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 asgiref.local import Local
9
- from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
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
- logger = logging.getLogger("audit.request")
21
+ _log = get_logger("audit.request")
17
22
 
18
- _thread_locals = Local()
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 getattr(_thread_locals, "request", None)
34
+ return _request_var.get()
30
35
 
31
36
 
32
37
  def set_current_request(request):
33
- _thread_locals.request = request
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
- try:
53
- _thread_locals.request.user = user
54
- except AttributeError:
55
- request = MockRequest(user=user)
56
- _thread_locals.request = request
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
- with contextlib.suppress(AttributeError):
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
- set_request_id(request_id)
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
- log_data["user_id"] = user_id
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
- logger.api("Audit Internal Request", extra=log_data)
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
- set_request_id(request_id)
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 = await sync_to_async(
254
- get_user_details, thread_sensitive=True
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
- logger.api("Audit Internal Request", extra=log_data)
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