setu-trafficmonitor 2.0.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.
Files changed (42) hide show
  1. setu_trafficmonitor-2.0.0.dist-info/LICENSE +21 -0
  2. setu_trafficmonitor-2.0.0.dist-info/METADATA +401 -0
  3. setu_trafficmonitor-2.0.0.dist-info/RECORD +42 -0
  4. setu_trafficmonitor-2.0.0.dist-info/WHEEL +5 -0
  5. setu_trafficmonitor-2.0.0.dist-info/top_level.txt +1 -0
  6. trafficmonitor/__init__.py +11 -0
  7. trafficmonitor/admin.py +217 -0
  8. trafficmonitor/analytics/__init__.py +0 -0
  9. trafficmonitor/analytics/enhanced_queries.py +286 -0
  10. trafficmonitor/analytics/serializers.py +238 -0
  11. trafficmonitor/analytics/tests.py +757 -0
  12. trafficmonitor/analytics/urls.py +18 -0
  13. trafficmonitor/analytics/views.py +694 -0
  14. trafficmonitor/apps.py +7 -0
  15. trafficmonitor/circuit_breaker.py +63 -0
  16. trafficmonitor/conf.py +154 -0
  17. trafficmonitor/dashboard_security.py +111 -0
  18. trafficmonitor/db_utils.py +37 -0
  19. trafficmonitor/exceptions.py +93 -0
  20. trafficmonitor/health.py +66 -0
  21. trafficmonitor/load_test.py +423 -0
  22. trafficmonitor/load_test_api.py +307 -0
  23. trafficmonitor/management/__init__.py +1 -0
  24. trafficmonitor/management/commands/__init__.py +1 -0
  25. trafficmonitor/management/commands/cleanup_request_logs.py +77 -0
  26. trafficmonitor/middleware.py +383 -0
  27. trafficmonitor/migrations/0001_initial.py +93 -0
  28. trafficmonitor/migrations/__init__.py +0 -0
  29. trafficmonitor/models.py +206 -0
  30. trafficmonitor/monitoring.py +104 -0
  31. trafficmonitor/permissions.py +64 -0
  32. trafficmonitor/security.py +180 -0
  33. trafficmonitor/settings_production.py +105 -0
  34. trafficmonitor/static/analytics/css/dashboard.css +99 -0
  35. trafficmonitor/static/analytics/js/dashboard-production.js +339 -0
  36. trafficmonitor/static/analytics/js/dashboard-v2.js +697 -0
  37. trafficmonitor/static/analytics/js/dashboard.js +693 -0
  38. trafficmonitor/tasks.py +137 -0
  39. trafficmonitor/templates/analytics/dashboard.html +500 -0
  40. trafficmonitor/tests.py +246 -0
  41. trafficmonitor/views.py +3 -0
  42. trafficmonitor/websocket_consumers.py +128 -0
@@ -0,0 +1,104 @@
1
+ """
2
+ Monitoring and observability utilities
3
+ """
4
+ import time
5
+ import logging
6
+ from typing import Dict, Any, Optional
7
+ from contextlib import contextmanager
8
+ from django.core.cache import cache
9
+ from django.conf import settings
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class MetricsCollector:
15
+ """Collects application metrics"""
16
+
17
+ def __init__(self):
18
+ self.metrics = {}
19
+
20
+ def increment(self, metric_name: str, value: int = 1, tags: Optional[Dict[str, str]] = None):
21
+ """Increment a counter metric"""
22
+ key = f"trafficmonitor.{metric_name}"
23
+ current = cache.get(key, 0)
24
+ cache.set(key, current + value, timeout=3600)
25
+
26
+ # Log structured metric
27
+ logger.info("metric.increment", extra={
28
+ "metric_name": metric_name,
29
+ "value": value,
30
+ "tags": tags or {},
31
+ "timestamp": time.time()
32
+ })
33
+
34
+ def gauge(self, metric_name: str, value: float, tags: Optional[Dict[str, str]] = None):
35
+ """Set a gauge metric"""
36
+ key = f"trafficmonitor.gauge.{metric_name}"
37
+ cache.set(key, value, timeout=3600)
38
+
39
+ logger.info("metric.gauge", extra={
40
+ "metric_name": metric_name,
41
+ "value": value,
42
+ "tags": tags or {},
43
+ "timestamp": time.time()
44
+ })
45
+
46
+ def timing(self, metric_name: str, duration_ms: float, tags: Optional[Dict[str, str]] = None):
47
+ """Record timing metric"""
48
+ logger.info("metric.timing", extra={
49
+ "metric_name": metric_name,
50
+ "duration_ms": duration_ms,
51
+ "tags": tags or {},
52
+ "timestamp": time.time()
53
+ })
54
+
55
+
56
+ class StructuredLogger:
57
+ """Structured logging for enterprise applications"""
58
+
59
+ def __init__(self, name: str):
60
+ self.logger = logging.getLogger(name)
61
+
62
+ def log_request(self, request, response, duration_ms: float, **kwargs):
63
+ """Log request with structured format"""
64
+ self.logger.info("http.request", extra={
65
+ "method": request.method,
66
+ "path": request.path,
67
+ "status_code": response.status_code,
68
+ "duration_ms": duration_ms,
69
+ "user_agent": request.META.get('HTTP_USER_AGENT', ''),
70
+ "ip_address": self._get_client_ip(request),
71
+ **kwargs
72
+ })
73
+
74
+ def log_error(self, error: Exception, context: Dict[str, Any]):
75
+ """Log error with context"""
76
+ self.logger.error("application.error", extra={
77
+ "error_type": type(error).__name__,
78
+ "error_message": str(error),
79
+ "context": context,
80
+ "timestamp": time.time()
81
+ }, exc_info=True)
82
+
83
+ def _get_client_ip(self, request) -> str:
84
+ """Get client IP address"""
85
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
86
+ if x_forwarded_for:
87
+ return x_forwarded_for.split(',')[0].strip()
88
+ return request.META.get('REMOTE_ADDR', '')
89
+
90
+
91
+ @contextmanager
92
+ def performance_timer(metric_name: str, tags: Optional[Dict[str, str]] = None):
93
+ """Context manager for timing operations"""
94
+ start_time = time.time()
95
+ try:
96
+ yield
97
+ finally:
98
+ duration_ms = (time.time() - start_time) * 1000
99
+ metrics.timing(metric_name, duration_ms, tags)
100
+
101
+
102
+ # Global instances
103
+ metrics = MetricsCollector()
104
+ structured_logger = StructuredLogger('trafficmonitor')
@@ -0,0 +1,64 @@
1
+ """
2
+ Role-based permission classes for TrafficMonitor analytics.
3
+ Enterprise-grade access control using header-based authentication.
4
+ """
5
+ from rest_framework import permissions
6
+ from trafficmonitor.conf import TrafficMonitorConfig
7
+
8
+
9
+ class HasTrafficMonitorAccess(permissions.BasePermission):
10
+ """
11
+ Permission class for analytics access.
12
+ Allows access if:
13
+ 1. User is authenticated via Django (staff or superuser)
14
+ 2. No role header exists (open access)
15
+ 3. Role header exists and is authorized
16
+ """
17
+ message = "You do not have permission to access traffic monitoring analytics."
18
+
19
+ def has_permission(self, request, view):
20
+ # Allow Django authenticated staff/superusers
21
+ if request.user and request.user.is_authenticated:
22
+ if request.user.is_staff or request.user.is_superuser:
23
+ return True
24
+
25
+ # Check role-based access via headers
26
+ user_info = TrafficMonitorConfig.get_user_info_from_request(request)
27
+ user_role = user_info.get('role')
28
+
29
+ # If no role header, allow access
30
+ if not user_role:
31
+ return True
32
+
33
+ # If role header exists, check if user role is in allowed roles
34
+ return TrafficMonitorConfig.is_user_authorized(user_role)
35
+
36
+
37
+
38
+ class HasTrafficMonitorAdminAccess(permissions.BasePermission):
39
+ """
40
+ Permission class for admin-only analytics access.
41
+ Allows access if:
42
+ 1. User is Django superuser
43
+ 2. No role header exists (open access)
44
+ 3. Role header exists and has admin role
45
+ """
46
+ message = "You need admin privileges to access this resource."
47
+
48
+ def has_permission(self, request, view):
49
+ # Allow Django superusers
50
+ if request.user and request.user.is_authenticated:
51
+ if request.user.is_superuser:
52
+ return True
53
+
54
+ # Check role-based access via headers
55
+ user_info = TrafficMonitorConfig.get_user_info_from_request(request)
56
+ user_role = user_info.get('role')
57
+
58
+ # If no role header, allow access
59
+ if not user_role:
60
+ return True
61
+
62
+ # If role header exists, check if user has admin role
63
+ return TrafficMonitorConfig.is_admin(user_role)
64
+
@@ -0,0 +1,180 @@
1
+ """
2
+ Security utilities for PII protection and data sanitization
3
+ """
4
+ import re
5
+ import json
6
+ import datetime
7
+ import decimal
8
+ from uuid import UUID
9
+ from typing import Dict, Any, Optional
10
+ from django.conf import settings
11
+
12
+
13
+ class DataSanitizer:
14
+ """Sanitizes sensitive data from requests/responses"""
15
+
16
+ # PII patterns
17
+ EMAIL_PATTERN = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
18
+ PHONE_PATTERN = re.compile(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b')
19
+ SSN_PATTERN = re.compile(r'\b\d{3}-\d{2}-\d{4}\b')
20
+ CREDIT_CARD_PATTERN = re.compile(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b')
21
+
22
+ SENSITIVE_KEYS = {
23
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'key', 'auth',
24
+ 'authorization', 'credential', 'api_key', 'access_token', 'refresh_token',
25
+ 'ssn', 'social_security', 'credit_card', 'card_number', 'cvv', 'pin'
26
+ }
27
+
28
+ @staticmethod
29
+ def sanitize_for_json(obj):
30
+ """
31
+ Recursively sanitize any Python object so it becomes JSON serializable.
32
+ Handles datetime, Decimal, UUID, Django objects, and any non-serializable type.
33
+ """
34
+ if obj is None:
35
+ return None
36
+
37
+ # Ellipsis
38
+ if obj is Ellipsis:
39
+ return None
40
+
41
+ # Primitives
42
+ if isinstance(obj, (str, int, float, bool)):
43
+ # Truncate very long strings
44
+ if isinstance(obj, str) and len(obj) > 1000:
45
+ return obj[:1000] + '...'
46
+ return obj
47
+
48
+ # Decimal
49
+ if isinstance(obj, decimal.Decimal):
50
+ return float(obj)
51
+
52
+ # UUID
53
+ if isinstance(obj, UUID):
54
+ return str(obj)
55
+
56
+ # Date/Time
57
+ if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
58
+ try:
59
+ from django.utils.timezone import is_aware
60
+ # Make datetime timezone-naive ISO if aware
61
+ if isinstance(obj, datetime.datetime) and is_aware(obj):
62
+ obj = obj.astimezone(datetime.timezone.utc).replace(tzinfo=None)
63
+ except ImportError:
64
+ pass
65
+ return obj.isoformat()
66
+
67
+ # Lists / Tuples
68
+ if isinstance(obj, (list, tuple)):
69
+ return [DataSanitizer.sanitize_for_json(item) for item in obj]
70
+
71
+ # Sets
72
+ if isinstance(obj, set):
73
+ return [DataSanitizer.sanitize_for_json(item) for item in obj]
74
+
75
+ # Dict
76
+ if isinstance(obj, dict):
77
+ return {
78
+ DataSanitizer.sanitize_for_json(k): DataSanitizer.sanitize_for_json(v)
79
+ for k, v in obj.items()
80
+ }
81
+
82
+ # Django model instance — return pk if exists
83
+ if hasattr(obj, "pk"):
84
+ return str(obj.pk)
85
+
86
+ # Fallback: string representation
87
+ return str(obj)
88
+
89
+ @classmethod
90
+ def sanitize_headers(cls, headers: Dict[str, Any]) -> Dict[str, Any]:
91
+ """Remove sensitive headers and ensure JSON serializable values"""
92
+ if not isinstance(headers, dict):
93
+ return {}
94
+
95
+ sanitized = {}
96
+ for key, value in headers.items():
97
+ # Skip WSGI internal objects completely (not serializable)
98
+ if isinstance(key, str) and key.startswith('wsgi.'):
99
+ continue
100
+
101
+ # Skip non-string keys
102
+ if not isinstance(key, str):
103
+ key = str(key)
104
+
105
+ # Redact sensitive keys
106
+ if any(sensitive in key.lower() for sensitive in cls.SENSITIVE_KEYS):
107
+ sanitized[key] = "[REDACTED]"
108
+ else:
109
+ # Use sanitize_for_json to handle any type of value
110
+ sanitized[key] = cls.sanitize_for_json(value)
111
+
112
+ return sanitized
113
+
114
+ @classmethod
115
+ def sanitize_body(cls, body: str, max_length: int = 10000) -> str:
116
+ """Sanitize request/response body"""
117
+ if not body:
118
+ return body
119
+
120
+ # Truncate if too long
121
+ if len(body) > max_length:
122
+ body = body[:max_length] + "...[TRUNCATED]"
123
+
124
+ # Remove PII patterns
125
+ body = cls.EMAIL_PATTERN.sub('[EMAIL_REDACTED]', body)
126
+ body = cls.PHONE_PATTERN.sub('[PHONE_REDACTED]', body)
127
+ body = cls.SSN_PATTERN.sub('[SSN_REDACTED]', body)
128
+ body = cls.CREDIT_CARD_PATTERN.sub('[CARD_REDACTED]', body)
129
+
130
+ # Try to parse as JSON and sanitize keys
131
+ try:
132
+ data = json.loads(body)
133
+ if isinstance(data, dict):
134
+ data = cls._sanitize_dict(data)
135
+ return json.dumps(data)
136
+ except (json.JSONDecodeError, TypeError):
137
+ pass
138
+
139
+ return body
140
+
141
+ @classmethod
142
+ def _sanitize_dict(cls, data: Dict[str, Any]) -> Dict[str, Any]:
143
+ """Recursively sanitize dictionary keys"""
144
+ sanitized = {}
145
+ for key, value in data.items():
146
+ if any(sensitive in key.lower() for sensitive in cls.SENSITIVE_KEYS):
147
+ sanitized[key] = "[REDACTED]"
148
+ elif isinstance(value, dict):
149
+ sanitized[key] = cls._sanitize_dict(value)
150
+ elif isinstance(value, list):
151
+ sanitized[key] = [cls._sanitize_dict(item) if isinstance(item, dict) else item for item in value]
152
+ else:
153
+ sanitized[key] = value
154
+ return sanitized
155
+
156
+
157
+ class SecurityValidator:
158
+ """Validates security requirements"""
159
+
160
+ @staticmethod
161
+ def validate_user_access(user_id: str, user_role: str, required_roles: list) -> bool:
162
+ """Validate user access with proper logging"""
163
+ if not user_id or not user_role:
164
+ return False
165
+
166
+ # Add audit logging here
167
+ return user_role.lower() in [r.lower() for r in required_roles]
168
+
169
+ @staticmethod
170
+ def get_client_ip(request) -> Optional[str]:
171
+ """Get real client IP considering proxies"""
172
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
173
+ if x_forwarded_for:
174
+ return x_forwarded_for.split(',')[0].strip()
175
+
176
+ x_real_ip = request.META.get('HTTP_X_REAL_IP')
177
+ if x_real_ip:
178
+ return x_real_ip
179
+
180
+ return request.META.get('REMOTE_ADDR')
@@ -0,0 +1,105 @@
1
+ """
2
+ Production settings template for TrafficMonitor
3
+ Copy these settings to your Django settings.py and adjust as needed.
4
+ """
5
+
6
+ # TrafficMonitor Production Configuration
7
+ TRAFFICMONITOR_ASYNC_LOGGING = True
8
+ TRAFFICMONITOR_ENABLE_SAMPLING = True
9
+ TRAFFICMONITOR_SAMPLE_RATE = 0.1 # Log 10% of requests in production
10
+
11
+ # Circuit Breaker
12
+ TRAFFICMONITOR_CIRCUIT_BREAKER_ENABLED = True
13
+ TRAFFICMONITOR_CB_FAILURE_THRESHOLD = 5
14
+
15
+ # Security
16
+ TRAFFICMONITOR_ENABLE_PII_SANITIZATION = True
17
+ TRAFFICMONITOR_MAX_BODY_LENGTH = 5000 # Smaller in production
18
+
19
+ # Performance
20
+ TRAFFICMONITOR_BULK_BATCH_SIZE = 5000
21
+ TRAFFICMONITOR_ENABLE_METRICS = True
22
+
23
+ # Data Retention
24
+ TRAFFICMONITOR_RETENTION_DAYS = 30 # Shorter retention in production
25
+
26
+ # Authentication (adjust based on your auth system)
27
+ TRAFFICMONITOR_USER_ID_HEADER = 'X-User-ID'
28
+ TRAFFICMONITOR_USER_ROLE_HEADER = 'X-User-Role'
29
+ TRAFFICMONITOR_VIEWER_ROLES = ['analyst', 'viewer', 'developer']
30
+ TRAFFICMONITOR_ADMIN_ROLES = ['admin', 'superuser']
31
+
32
+ # Excluded paths (add your specific paths)
33
+ TRAFFICMONITOR_EXCLUDED_PATHS = [
34
+ '/admin/jsi18n/',
35
+ '/static/',
36
+ '/media/',
37
+ '/__debug__/',
38
+ '/favicon.ico',
39
+ '/health/',
40
+ '/healthz/',
41
+ '/metrics/',
42
+ '/prometheus/',
43
+ # Add your application-specific paths
44
+ '/api/health/',
45
+ '/api/metrics/',
46
+ ]
47
+
48
+ # Logging Configuration
49
+ LOGGING = {
50
+ 'version': 1,
51
+ 'disable_existing_loggers': False,
52
+ 'formatters': {
53
+ 'structured': {
54
+ 'format': '%(asctime)s %(name)s %(levelname)s %(message)s',
55
+ },
56
+ },
57
+ 'handlers': {
58
+ 'trafficmonitor': {
59
+ 'level': 'INFO',
60
+ 'class': 'logging.StreamHandler',
61
+ 'formatter': 'structured',
62
+ },
63
+ },
64
+ 'loggers': {
65
+ 'trafficmonitor': {
66
+ 'handlers': ['trafficmonitor'],
67
+ 'level': 'INFO',
68
+ 'propagate': False,
69
+ },
70
+ },
71
+ }
72
+
73
+ # Database Configuration (example for PostgreSQL)
74
+ DATABASES = {
75
+ 'default': {
76
+ 'ENGINE': 'django.db.backends.postgresql',
77
+ 'NAME': 'your_db_name',
78
+ 'USER': 'your_db_user',
79
+ 'PASSWORD': 'your_db_password',
80
+ 'HOST': 'your_db_host',
81
+ 'PORT': '5432',
82
+ 'OPTIONS': {
83
+ 'MAX_CONNS': 20, # Connection pooling
84
+ },
85
+ }
86
+ }
87
+
88
+ # Cache Configuration (Redis recommended)
89
+ CACHES = {
90
+ 'default': {
91
+ 'BACKEND': 'django_redis.cache.RedisCache',
92
+ 'LOCATION': 'redis://127.0.0.1:6379/1',
93
+ 'OPTIONS': {
94
+ 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
95
+ 'CONNECTION_POOL_KWARGS': {'max_connections': 50},
96
+ }
97
+ }
98
+ }
99
+
100
+ # Celery Configuration (for async logging)
101
+ CELERY_BROKER_URL = 'redis://localhost:6379/0'
102
+ CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
103
+ CELERY_TASK_SERIALIZER = 'json'
104
+ CELERY_ACCEPT_CONTENT = ['json']
105
+ CELERY_RESULT_SERIALIZER = 'json'
@@ -0,0 +1,99 @@
1
+ /* Production-ready dashboard styles */
2
+ :root {
3
+ --primary-color: #3b82f6;
4
+ --success-color: #10b981;
5
+ --warning-color: #f59e0b;
6
+ --danger-color: #ef4444;
7
+ --info-color: #8b5cf6;
8
+ --gray-color: #6b7280;
9
+ }
10
+
11
+ /* Performance optimizations */
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .card {
17
+ transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
18
+ will-change: transform;
19
+ }
20
+
21
+ .card:hover {
22
+ transform: translateY(-2px);
23
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
24
+ }
25
+
26
+ /* Chart containers with proper aspect ratios */
27
+ .chart-container {
28
+ position: relative;
29
+ height: 300px;
30
+ margin-bottom: 1rem;
31
+ contain: layout style paint;
32
+ }
33
+
34
+ .chart-container-large {
35
+ position: relative;
36
+ height: 400px;
37
+ margin-bottom: 1rem;
38
+ contain: layout style paint;
39
+ }
40
+
41
+ /* Responsive design */
42
+ @media (max-width: 768px) {
43
+ .chart-container,
44
+ .chart-container-large {
45
+ height: 250px;
46
+ }
47
+ }
48
+
49
+ /* Loading states */
50
+ .loading-skeleton {
51
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
52
+ background-size: 200% 100%;
53
+ animation: loading 1.5s infinite;
54
+ }
55
+
56
+ @keyframes loading {
57
+ 0% { background-position: 200% 0; }
58
+ 100% { background-position: -200% 0; }
59
+ }
60
+
61
+ /* Error states */
62
+ .error-state {
63
+ color: var(--danger-color);
64
+ text-align: center;
65
+ padding: 2rem;
66
+ }
67
+
68
+ /* Accessibility improvements */
69
+ .sr-only {
70
+ position: absolute;
71
+ width: 1px;
72
+ height: 1px;
73
+ padding: 0;
74
+ margin: -1px;
75
+ overflow: hidden;
76
+ clip: rect(0, 0, 0, 0);
77
+ white-space: nowrap;
78
+ border: 0;
79
+ }
80
+
81
+ /* Focus states for keyboard navigation */
82
+ button:focus,
83
+ select:focus,
84
+ input:focus {
85
+ outline: 2px solid var(--primary-color);
86
+ outline-offset: 2px;
87
+ }
88
+
89
+ /* Print styles */
90
+ @media print {
91
+ .no-print {
92
+ display: none !important;
93
+ }
94
+
95
+ .chart-container,
96
+ .chart-container-large {
97
+ break-inside: avoid;
98
+ }
99
+ }