django-activity-audit 1.3.0.dev19__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 (30) hide show
  1. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/PKG-INFO +87 -32
  2. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/README.md +84 -28
  3. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/config.py +16 -0
  4. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/middleware.py +12 -16
  5. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/shared_processors.py +20 -0
  6. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/utils.py +1 -1
  7. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/pyproject.toml +3 -3
  8. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/.claude/settings.local.json +0 -0
  9. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/.gitignore +0 -0
  10. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/.pre-commit-config.yaml +0 -0
  11. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/LICENSE +0 -0
  12. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/MANIFEST.in +0 -0
  13. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/README.rst +0 -0
  14. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/__init__.py +0 -0
  15. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/apps.py +0 -0
  16. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/constants.py +0 -0
  17. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/formatters.py +0 -0
  18. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/handlers.py +0 -0
  19. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/logger_levels.py +0 -0
  20. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/protocols.py +0 -0
  21. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/settings.py +0 -0
  22. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/signals.py +0 -0
  23. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/activity_audit/unregistered.py +0 -0
  24. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/docs/django-activity-audit.md +0 -0
  25. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/docs/hipaa-audit-gaps.md +0 -0
  26. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/docs/improvements.md +0 -0
  27. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/docs/structlog-integration.md +0 -0
  28. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/docs/user-activity-feed-plan.md +0 -0
  29. {django_activity_audit-1.3.0.dev19 → django_activity_audit-1.3.0.dev20}/hatch +0 -0
  30. {django_activity_audit-1.3.0.dev19 → 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.dev19
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,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
- "json": get_json_formatter(),
90
- "verbose": get_console_formatter(),
102
+ "structlog": get_stdlib_formatter(),
103
+ "default": get_plain_formatter(),
91
104
  },
92
105
  "handlers": {
93
- "console": {
94
- "level": "DEBUG",
95
- "class": "logging.StreamHandler",
96
- "formatter": "verbose",
97
- },
98
- "file": get_json_handler(level="DEBUG", formatter="json"),
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
- "audit.request": {
105
- "handlers": ["api_file"],
106
- "level": "API",
107
- "propagate": False,
108
- },
109
- "audit.model": {
110
- "handlers": ["audit_file"],
111
- "level": "AUDIT",
112
- "propagate": False,
113
- },
114
- "django": {
115
- "handlers": ["console", "file"],
116
- "level": "INFO",
117
- "propagate": False,
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"
@@ -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"
@@ -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
@@ -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
- _thread_locals = Local()
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 getattr(_thread_locals, "request", None)
34
+ return _request_var.get()
36
35
 
37
36
 
38
37
  def set_current_request(request):
39
- _thread_locals.request = request
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
- try:
51
- _thread_locals.request.user = user
52
- except AttributeError:
53
- request = MockRequest(user=user)
54
- _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))
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
- with contextlib.suppress(AttributeError):
77
- del _thread_locals.request
75
+ _request_var.set(None)
78
76
 
79
77
 
80
78
  def should_log_url(url):
@@ -242,9 +240,7 @@ class AuditLoggingMiddleware(MiddlewareMixin):
242
240
  end_time = time.time()
243
241
 
244
242
  # Capture user details AFTER authentication has happened
245
- user_id, user_info = await sync_to_async(
246
- get_user_details, thread_sensitive=True
247
- )()
243
+ user_id, user_info = get_user_details()
248
244
  ctx.bind_contextvars(user_id=user_id, user_info=user_info)
249
245
 
250
246
  # 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
 
@@ -145,7 +145,7 @@ def push_usage_log(
145
145
  """
146
146
  import logging
147
147
 
148
- from .signals import get_user_details
148
+ from .middleware import get_user_details
149
149
 
150
150
  logger = logging.getLogger("audit.login")
151
151
  user_id, user_info = get_user_details()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-activity-audit"
3
- version = "1.3.0.dev19"
3
+ version = "1.3.0.dev20"
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"]