django-analytics-middleware 0.1.0__py3-none-any.whl
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-audit-analytics-middleware/__init__.py +1 -0
- django-audit-analytics-middleware/apps.py +105 -0
- django-audit-analytics-middleware/middleware.py +58 -0
- django_analytics_middleware-0.1.0.dist-info/METADATA +144 -0
- django_analytics_middleware-0.1.0.dist-info/RECORD +8 -0
- django_analytics_middleware-0.1.0.dist-info/WHEEL +5 -0
- django_analytics_middleware-0.1.0.dist-info/licenses/LICENSE +21 -0
- django_analytics_middleware-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnalyticsMiddlewareConfig(AppConfig):
|
|
10
|
+
"""Django app configuration for the analytics middleware"""
|
|
11
|
+
|
|
12
|
+
name = "django_analytics_middleware"
|
|
13
|
+
verbose_name = "Django Analytics Middleware"
|
|
14
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
15
|
+
|
|
16
|
+
def ready(self):
|
|
17
|
+
"""
|
|
18
|
+
Called once when Django starts
|
|
19
|
+
"""
|
|
20
|
+
if self._is_running_management_command():
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# Validation Configuration
|
|
24
|
+
self._validate_configuration()
|
|
25
|
+
self._setup_package_logging()
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
logger.info("Analytics middleware initiated successfully")
|
|
29
|
+
|
|
30
|
+
def _is_running_management_command(self):
|
|
31
|
+
"""Check if Django is running a command"""
|
|
32
|
+
return len(sys.argv) > 1 and sys.argv[1] in [
|
|
33
|
+
"migrate",
|
|
34
|
+
"makemigrations",
|
|
35
|
+
"collectstatic",
|
|
36
|
+
"flush",
|
|
37
|
+
"loaddata",
|
|
38
|
+
"dumpdata",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def _validate_configuration(self):
|
|
42
|
+
"""Validate user settings at startup"""
|
|
43
|
+
log_path = getattr(settings, "ANALYTICS_LOG_PATH", None)
|
|
44
|
+
|
|
45
|
+
if not log_path:
|
|
46
|
+
print(
|
|
47
|
+
"Analytics middleware: You must set ANALYTICS_LOG_PATH in your settings.py"
|
|
48
|
+
)
|
|
49
|
+
print(
|
|
50
|
+
"Example: ANALYTICS_LOG_PATH = os.path.join(BASEDIR, 'logs', 'analytics.log')"
|
|
51
|
+
)
|
|
52
|
+
print("Or in .env ANALYTICS_LOG_PATH=<LOG PATH>")
|
|
53
|
+
raise ImproperlyConfigured(
|
|
54
|
+
"Analytics middleware: You must set ANALYTICS_LOG_PATH in your settings.py\n"
|
|
55
|
+
"Example: ANALYTICS_LOG_PATH = os.path.join(BASEDIR, 'logs', 'analytics.log')\n"
|
|
56
|
+
"Or in .env ANALYTICS_LOG_PATH=<LOG PATH>"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Test directory actually exists and create
|
|
60
|
+
log_dir = os.path.dirname(log_path)
|
|
61
|
+
if log_dir and not os.path.exists(log_dir):
|
|
62
|
+
try:
|
|
63
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
64
|
+
print(f"Created log directory: {log_dir}")
|
|
65
|
+
|
|
66
|
+
except PermissionError:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"Analytics middleware: Cannot create directory '{log_dir}'."
|
|
69
|
+
f"Either fix permissions or change ANALYTICS_LOG_PATH within settings.py."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Tries to write to log to test permissions
|
|
73
|
+
try:
|
|
74
|
+
with open(log_path, "a") as test_file:
|
|
75
|
+
test_file.write("")
|
|
76
|
+
except PermissionError:
|
|
77
|
+
raise ImproperlyConfigured(
|
|
78
|
+
"Can't write to log file. Please ensure you have the correct permissions on your log folder."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Retrieves noise paths list from settings.py
|
|
82
|
+
noise_paths = getattr(settings, "ANALYTICS_NOISE_PATHS", None)
|
|
83
|
+
if noise_paths is None:
|
|
84
|
+
print("No noise path field discovered within settings.py.")
|
|
85
|
+
print("Using default noise paths: /health, /admin, /favicon.ico")
|
|
86
|
+
noise_paths = ["/health", "/admin", "/favicon.ico"] # Set default
|
|
87
|
+
|
|
88
|
+
def _setup_package_logging(self):
|
|
89
|
+
"""Configure logging"""
|
|
90
|
+
|
|
91
|
+
log_level_name = getattr(settings, "ANALYTICS_LOG_LEVEL", "WARNING")
|
|
92
|
+
log_level = getattr(logging, log_level_name.upper(), logging.WARNING)
|
|
93
|
+
|
|
94
|
+
# Configure package logger
|
|
95
|
+
logger = logging.getLogger(__name__)
|
|
96
|
+
logger.setLevel(log_level)
|
|
97
|
+
|
|
98
|
+
# Dont add handlers if already configured
|
|
99
|
+
if not logger.handlers:
|
|
100
|
+
handler = logging.StreamHandler()
|
|
101
|
+
formatter = logging.Formatter(
|
|
102
|
+
"[%(asctime)s] %(levelname)s: %(message)s", datefmt="%d-%m-%Y %H:%M:%S"
|
|
103
|
+
)
|
|
104
|
+
handler.setFormatter(formatter)
|
|
105
|
+
logger.addHandler(handler)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from django.utils import timezone
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnalyticsMiddleware:
|
|
7
|
+
def __init__(self, get_response):
|
|
8
|
+
self.get_response = get_response
|
|
9
|
+
self.log_path = getattr(settings, "ANALYTICS_LOG_PATH", None)
|
|
10
|
+
self.noise_paths = getattr(
|
|
11
|
+
settings, "ANALYTICS_NOISE_PATHS", ["/health", "/admin", "/favicon.ico"]
|
|
12
|
+
)
|
|
13
|
+
self.disabled = not self.log_path
|
|
14
|
+
|
|
15
|
+
def __call__(self, request):
|
|
16
|
+
if self.disabled:
|
|
17
|
+
return self.get_response(request)
|
|
18
|
+
|
|
19
|
+
if any(request.path.startswith(path) for path in self.noise_paths):
|
|
20
|
+
return self.get_response(request)
|
|
21
|
+
|
|
22
|
+
start = timezone.now()
|
|
23
|
+
response = self.get_response(request)
|
|
24
|
+
duration = timezone.now() - start
|
|
25
|
+
|
|
26
|
+
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
27
|
+
ip = (
|
|
28
|
+
x_forwarded_for.split(",")[0].strip()
|
|
29
|
+
if x_forwarded_for
|
|
30
|
+
else request.META.get("REMOTE_ADDR")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
log_entry = {
|
|
34
|
+
"path": request.path,
|
|
35
|
+
"method": request.method,
|
|
36
|
+
"status": response.status_code,
|
|
37
|
+
"user_uuid": str(request.user.uuid)
|
|
38
|
+
if request.user.is_authenticated
|
|
39
|
+
else "unauthorized user",
|
|
40
|
+
"duration_ms": int(round(duration.total_seconds() * 1000)),
|
|
41
|
+
"ip": ip,
|
|
42
|
+
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
|
43
|
+
"referrer": request.META.get("HTTP_REFERER", ""),
|
|
44
|
+
"timestamp": timezone.now().isoformat(),
|
|
45
|
+
"month": timezone.now().strftime("%b"),
|
|
46
|
+
"week": timezone.now().strftime("%V"),
|
|
47
|
+
"day": timezone.now().strftime("%a"),
|
|
48
|
+
"hour": timezone.now().strftime("%H"),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
with open(self.log_path, "a") as f:
|
|
53
|
+
f.write(json.dumps(log_entry) + "\n")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"[Analytics Middleware Error]: {str(e)}")
|
|
57
|
+
|
|
58
|
+
return response
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-analytics-middleware
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django middleware for request analytics logging
|
|
5
|
+
Author-email: Aaron Browne <github@futurumlabs.net>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Knowledgex187/django-audit-analytics-middleware.git
|
|
8
|
+
Project-URL: Source, https://github.com/Knowledgex187/django-audit-analytics-middleware.git
|
|
9
|
+
Project-URL: Issues, https://github.com/Knowledgex187/django-audit-analytics-middleware.git/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: Django>=2.2
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# django-audit-analytics-middleware
|
|
22
|
+
|
|
23
|
+
**One middleware. Audit logs for compliance. Analytics for product.**
|
|
24
|
+
|
|
25
|
+
## Status: 🏗️ Building in public
|
|
26
|
+
|
|
27
|
+
This package is under active development. I'm posting every step on LinkedIn.
|
|
28
|
+
|
|
29
|
+
## What it will do (already working locally)
|
|
30
|
+
|
|
31
|
+
- [x] Log every request (path, method, status, user, IP, user agent)
|
|
32
|
+
- [x] Filter noise paths (/admin, /health, /static)
|
|
33
|
+
- [x] Duration tracking in milliseconds
|
|
34
|
+
- [x] IP extraction behind proxies (X-Forwarded-For)
|
|
35
|
+
- [ ] Configurable log file path
|
|
36
|
+
- [ ] Django admin view for logs
|
|
37
|
+
- [ ] Export to CSV/JSON command
|
|
38
|
+
- [ ] Tests (coming this week)
|
|
39
|
+
- [ ] PyPI release
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- 📊 Logs all HTTP requests with timing data
|
|
44
|
+
- 👤 Captures authenticated user UUIDs
|
|
45
|
+
- 🌐 Extracts real IP addresses (handles proxies)
|
|
46
|
+
- 🚫 Skips configurable noise paths (health checks, admin, etc.)
|
|
47
|
+
- 📝 Writes JSON logs for easy parsing
|
|
48
|
+
- ⚡ Minimal performance impact
|
|
49
|
+
- 🔒 Never breaks your application if logging fails
|
|
50
|
+
|
|
51
|
+
## Want to help?
|
|
52
|
+
|
|
53
|
+
- Open an issue with your wishlist
|
|
54
|
+
- Star the repo to follow progress
|
|
55
|
+
- DM me on LinkedIn
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install django-analytics-middleware
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Add to INSTALLED_APPS
|
|
64
|
+
INSTALLED_APPS = [
|
|
65
|
+
...
|
|
66
|
+
'django_analytics_middleware',
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Add to MIDDLEWARE
|
|
70
|
+
MIDDLEWARE = [
|
|
71
|
+
...
|
|
72
|
+
'django_analytics_middleware.middleware.AnalyticsMiddleware',
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# Configure log path
|
|
76
|
+
ANALYTICS_LOG_PATH = os.path.join(BASE_DIR, 'logs', 'analytics.log')
|
|
77
|
+
|
|
78
|
+
# For .env in settings.py
|
|
79
|
+
ANALYTICS_LOG_PATH = ANALYTICS_LOG_PATH=<env_handler>(<LOG PATH>)
|
|
80
|
+
|
|
81
|
+
Configuration
|
|
82
|
+
Required Settings
|
|
83
|
+
Setting Description Example
|
|
84
|
+
ANALYTICS_LOG_PATH Where to write log files os.path.join(BASE_DIR, 'logs', 'analytics.log')
|
|
85
|
+
Optional Settings
|
|
86
|
+
Setting Default Description
|
|
87
|
+
ANALYTICS_NOISE_PATHS ['/health', '/admin', '/favicon.ico'] Paths to skip logging
|
|
88
|
+
ANALYTICS_LOG_LEVEL 'WARNING' Logging level for the package
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Example settings.py
|
|
92
|
+
|
|
93
|
+
# Required
|
|
94
|
+
ANALYTICS_LOG_PATH = os.path.join(BASE_DIR, 'logs', 'analytics.log')
|
|
95
|
+
|
|
96
|
+
# Optional - Custom noise paths
|
|
97
|
+
ANALYTICS_NOISE_PATHS = [
|
|
98
|
+
'/health',
|
|
99
|
+
'/metrics',
|
|
100
|
+
'/admin',
|
|
101
|
+
'/favicon.ico',
|
|
102
|
+
'/robots.txt'
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Optional - Set log level (DEBUG, INFO, WARNING, ERROR)
|
|
106
|
+
ANALYTICS_LOG_LEVEL = 'INFO'
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Log Format
|
|
110
|
+
{
|
|
111
|
+
"path": "/api/users/",
|
|
112
|
+
"method": "GET",
|
|
113
|
+
"status": 200,
|
|
114
|
+
"user_uuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
115
|
+
"duration_ms": 45,
|
|
116
|
+
"ip": "192.168.1.100",
|
|
117
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
118
|
+
"referrer": "https://example.com/",
|
|
119
|
+
"timestamp": "2024-01-15T10:30:45.123456+00:00",
|
|
120
|
+
"month": "Jan",
|
|
121
|
+
"week": "03",
|
|
122
|
+
"day": "Mon",
|
|
123
|
+
"hour": "10"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Log Field Descriptions
|
|
127
|
+
Field Type Description
|
|
128
|
+
path string Request URL path
|
|
129
|
+
method string HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
130
|
+
status integer HTTP response status code
|
|
131
|
+
user_uuid string Authenticated user's UUID or "unauthorized user"
|
|
132
|
+
duration_ms integer Request processing time in milliseconds
|
|
133
|
+
ip string Client IP address (handles proxy forwarding)
|
|
134
|
+
user_agent string Browser/device user agent string
|
|
135
|
+
referrer string Referring URL (where user came from)
|
|
136
|
+
timestamp string ISO 8601 timestamp with timezone
|
|
137
|
+
month string Three-letter month abbreviation (Jan, Feb, Mar)
|
|
138
|
+
week string ISO week number (01-53)
|
|
139
|
+
day string Three-letter day abbreviation (Mon, Tue, Wed)
|
|
140
|
+
hour string Hour in 24-hour format (00-23)
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT – because audit logs shouldn't be paywalled.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
django-audit-analytics-middleware/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
django-audit-analytics-middleware/apps.py,sha256=SltiFWtPir85UqAUrDMJGgPZL7fc8yMzePtL-v8iPwg,3889
|
|
3
|
+
django-audit-analytics-middleware/middleware.py,sha256=USE8indBk8ss_ia3S_a72R2ciA2_W_-2BEtqS-v8e6o,1996
|
|
4
|
+
django_analytics_middleware-0.1.0.dist-info/licenses/LICENSE,sha256=F5eAheQ2ng9GuuSo5_mJJVKvAbyN41fyb2Vj1Fsa45I,1072
|
|
5
|
+
django_analytics_middleware-0.1.0.dist-info/METADATA,sha256=227twJVnodlpFHkKI3iDAXN0jhhusfIA1axHUWrqVU4,4396
|
|
6
|
+
django_analytics_middleware-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
django_analytics_middleware-0.1.0.dist-info/top_level.txt,sha256=kEMpjYS0hzHWI0-ToyrDdS-2q9GiMGVa7z7rAjavGk0,34
|
|
8
|
+
django_analytics_middleware-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Futurumlabs Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django-audit-analytics-middleware
|