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.
- setu_trafficmonitor-2.0.0.dist-info/LICENSE +21 -0
- setu_trafficmonitor-2.0.0.dist-info/METADATA +401 -0
- setu_trafficmonitor-2.0.0.dist-info/RECORD +42 -0
- setu_trafficmonitor-2.0.0.dist-info/WHEEL +5 -0
- setu_trafficmonitor-2.0.0.dist-info/top_level.txt +1 -0
- trafficmonitor/__init__.py +11 -0
- trafficmonitor/admin.py +217 -0
- trafficmonitor/analytics/__init__.py +0 -0
- trafficmonitor/analytics/enhanced_queries.py +286 -0
- trafficmonitor/analytics/serializers.py +238 -0
- trafficmonitor/analytics/tests.py +757 -0
- trafficmonitor/analytics/urls.py +18 -0
- trafficmonitor/analytics/views.py +694 -0
- trafficmonitor/apps.py +7 -0
- trafficmonitor/circuit_breaker.py +63 -0
- trafficmonitor/conf.py +154 -0
- trafficmonitor/dashboard_security.py +111 -0
- trafficmonitor/db_utils.py +37 -0
- trafficmonitor/exceptions.py +93 -0
- trafficmonitor/health.py +66 -0
- trafficmonitor/load_test.py +423 -0
- trafficmonitor/load_test_api.py +307 -0
- trafficmonitor/management/__init__.py +1 -0
- trafficmonitor/management/commands/__init__.py +1 -0
- trafficmonitor/management/commands/cleanup_request_logs.py +77 -0
- trafficmonitor/middleware.py +383 -0
- trafficmonitor/migrations/0001_initial.py +93 -0
- trafficmonitor/migrations/__init__.py +0 -0
- trafficmonitor/models.py +206 -0
- trafficmonitor/monitoring.py +104 -0
- trafficmonitor/permissions.py +64 -0
- trafficmonitor/security.py +180 -0
- trafficmonitor/settings_production.py +105 -0
- trafficmonitor/static/analytics/css/dashboard.css +99 -0
- trafficmonitor/static/analytics/js/dashboard-production.js +339 -0
- trafficmonitor/static/analytics/js/dashboard-v2.js +697 -0
- trafficmonitor/static/analytics/js/dashboard.js +693 -0
- trafficmonitor/tasks.py +137 -0
- trafficmonitor/templates/analytics/dashboard.html +500 -0
- trafficmonitor/tests.py +246 -0
- trafficmonitor/views.py +3 -0
- 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
|
+
}
|