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,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Circuit breaker pattern for database resilience
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Callable, Any
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CircuitState(Enum):
|
|
14
|
+
CLOSED = "closed"
|
|
15
|
+
OPEN = "open"
|
|
16
|
+
HALF_OPEN = "half_open"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CircuitBreaker:
|
|
20
|
+
"""Circuit breaker for database operations"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, failure_threshold: int = 5, timeout: int = 60):
|
|
23
|
+
self.failure_threshold = failure_threshold
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
self.failure_count = 0
|
|
26
|
+
self.last_failure_time = None
|
|
27
|
+
self.state = CircuitState.CLOSED
|
|
28
|
+
self._lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
def call(self, func: Callable, *args, **kwargs) -> Any:
|
|
31
|
+
"""Execute function with circuit breaker protection"""
|
|
32
|
+
with self._lock:
|
|
33
|
+
if self.state == CircuitState.OPEN:
|
|
34
|
+
if time.time() - self.last_failure_time > self.timeout:
|
|
35
|
+
self.state = CircuitState.HALF_OPEN
|
|
36
|
+
else:
|
|
37
|
+
raise Exception("Circuit breaker is OPEN")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
result = func(*args, **kwargs)
|
|
41
|
+
self._on_success()
|
|
42
|
+
return result
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self._on_failure()
|
|
45
|
+
raise e
|
|
46
|
+
|
|
47
|
+
def _on_success(self):
|
|
48
|
+
"""Reset circuit breaker on successful operation"""
|
|
49
|
+
self.failure_count = 0
|
|
50
|
+
self.state = CircuitState.CLOSED
|
|
51
|
+
|
|
52
|
+
def _on_failure(self):
|
|
53
|
+
"""Handle failure in circuit breaker"""
|
|
54
|
+
self.failure_count += 1
|
|
55
|
+
self.last_failure_time = time.time()
|
|
56
|
+
|
|
57
|
+
if self.failure_count >= self.failure_threshold:
|
|
58
|
+
self.state = CircuitState.OPEN
|
|
59
|
+
logger.error(f"Circuit breaker opened after {self.failure_count} failures")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Global circuit breaker instance
|
|
63
|
+
db_circuit_breaker = CircuitBreaker()
|
trafficmonitor/conf.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TrafficMonitor Configuration
|
|
3
|
+
Enterprise-grade configuration management with environment support.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TrafficMonitorConfig:
|
|
11
|
+
"""
|
|
12
|
+
Centralized configuration for TrafficMonitor.
|
|
13
|
+
Supports environment variables and Django settings override.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Environment Detection
|
|
17
|
+
ENVIRONMENT = os.getenv('DJANGO_ENV', getattr(settings, 'ENVIRONMENT', 'development'))
|
|
18
|
+
IS_PRODUCTION = ENVIRONMENT.lower() in ['production', 'prod']
|
|
19
|
+
|
|
20
|
+
# Role-based Access Control
|
|
21
|
+
VIEWER_ROLES = getattr(settings, 'TRAFFICMONITOR_VIEWER_ROLES',
|
|
22
|
+
os.getenv('TRAFFICMONITOR_VIEWER_ROLES', 'analyst,viewer,developer').split(','))
|
|
23
|
+
ADMIN_ROLES = getattr(settings, 'TRAFFICMONITOR_ADMIN_ROLES',
|
|
24
|
+
os.getenv('TRAFFICMONITOR_ADMIN_ROLES', 'admin,superuser').split(','))
|
|
25
|
+
|
|
26
|
+
# Authentication Headers
|
|
27
|
+
USER_ID_HEADER = getattr(settings, 'TRAFFICMONITOR_USER_ID_HEADER',
|
|
28
|
+
os.getenv('TRAFFICMONITOR_USER_ID_HEADER', 'X-User-ID'))
|
|
29
|
+
USER_ROLE_HEADER = getattr(settings, 'TRAFFICMONITOR_USER_ROLE_HEADER',
|
|
30
|
+
os.getenv('TRAFFICMONITOR_USER_ROLE_HEADER', 'X-User-Role'))
|
|
31
|
+
|
|
32
|
+
# Data Retention
|
|
33
|
+
RETENTION_DAYS = int(getattr(settings, 'TRAFFICMONITOR_RETENTION_DAYS',
|
|
34
|
+
os.getenv('TRAFFICMONITOR_RETENTION_DAYS', '90')))
|
|
35
|
+
|
|
36
|
+
# Performance Settings
|
|
37
|
+
ASYNC_LOGGING = getattr(settings, 'TRAFFICMONITOR_ASYNC_LOGGING',
|
|
38
|
+
os.getenv('TRAFFICMONITOR_ASYNC_LOGGING', 'false').lower() == 'true')
|
|
39
|
+
|
|
40
|
+
ENABLE_SAMPLING = getattr(settings, 'TRAFFICMONITOR_ENABLE_SAMPLING',
|
|
41
|
+
os.getenv('TRAFFICMONITOR_ENABLE_SAMPLING', 'false').lower() == 'true')
|
|
42
|
+
|
|
43
|
+
SAMPLE_RATE = float(getattr(settings, 'TRAFFICMONITOR_SAMPLE_RATE',
|
|
44
|
+
os.getenv('TRAFFICMONITOR_SAMPLE_RATE', '1.0')))
|
|
45
|
+
|
|
46
|
+
# Database Settings
|
|
47
|
+
BULK_CREATE_BATCH_SIZE = int(getattr(settings, 'TRAFFICMONITOR_BULK_BATCH_SIZE',
|
|
48
|
+
os.getenv('TRAFFICMONITOR_BULK_BATCH_SIZE', '1000')))
|
|
49
|
+
|
|
50
|
+
# Circuit Breaker Settings
|
|
51
|
+
CIRCUIT_BREAKER_ENABLED = getattr(settings, 'TRAFFICMONITOR_CIRCUIT_BREAKER_ENABLED',
|
|
52
|
+
os.getenv('TRAFFICMONITOR_CIRCUIT_BREAKER_ENABLED', 'true').lower() == 'true')
|
|
53
|
+
|
|
54
|
+
CIRCUIT_BREAKER_FAILURE_THRESHOLD = int(getattr(settings, 'TRAFFICMONITOR_CB_FAILURE_THRESHOLD',
|
|
55
|
+
os.getenv('TRAFFICMONITOR_CB_FAILURE_THRESHOLD', '5')))
|
|
56
|
+
|
|
57
|
+
# Security Settings
|
|
58
|
+
ENABLE_PII_SANITIZATION = getattr(settings, 'TRAFFICMONITOR_ENABLE_PII_SANITIZATION',
|
|
59
|
+
os.getenv('TRAFFICMONITOR_ENABLE_PII_SANITIZATION', 'true').lower() == 'true')
|
|
60
|
+
|
|
61
|
+
MAX_BODY_LENGTH = int(getattr(settings, 'TRAFFICMONITOR_MAX_BODY_LENGTH',
|
|
62
|
+
os.getenv('TRAFFICMONITOR_MAX_BODY_LENGTH', '10000')))
|
|
63
|
+
|
|
64
|
+
# Excluded Paths (production optimized)
|
|
65
|
+
DEFAULT_EXCLUDED_PATHS = [
|
|
66
|
+
'/admin/jsi18n/',
|
|
67
|
+
'/static/',
|
|
68
|
+
'/media/',
|
|
69
|
+
'/__debug__/',
|
|
70
|
+
'/favicon.ico',
|
|
71
|
+
'/health/',
|
|
72
|
+
'/healthz/',
|
|
73
|
+
'/metrics/',
|
|
74
|
+
'/prometheus/',
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
EXCLUDED_PATHS = getattr(settings, 'TRAFFICMONITOR_EXCLUDED_PATHS', DEFAULT_EXCLUDED_PATHS)
|
|
78
|
+
|
|
79
|
+
# Monitoring & Alerting
|
|
80
|
+
ENABLE_METRICS = getattr(settings, 'TRAFFICMONITOR_ENABLE_METRICS',
|
|
81
|
+
os.getenv('TRAFFICMONITOR_ENABLE_METRICS', 'true').lower() == 'true')
|
|
82
|
+
|
|
83
|
+
ALERT_ERROR_THRESHOLD = int(getattr(settings, 'TRAFFICMONITOR_ALERT_ERROR_THRESHOLD',
|
|
84
|
+
os.getenv('TRAFFICMONITOR_ALERT_ERROR_THRESHOLD', '100')))
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def validate_configuration(cls) -> List[str]:
|
|
88
|
+
"""Validate configuration and return list of issues"""
|
|
89
|
+
issues = []
|
|
90
|
+
|
|
91
|
+
if cls.SAMPLE_RATE < 0 or cls.SAMPLE_RATE > 1:
|
|
92
|
+
issues.append("SAMPLE_RATE must be between 0 and 1")
|
|
93
|
+
|
|
94
|
+
if cls.RETENTION_DAYS < 1:
|
|
95
|
+
issues.append("RETENTION_DAYS must be positive")
|
|
96
|
+
|
|
97
|
+
if cls.BULK_CREATE_BATCH_SIZE < 1:
|
|
98
|
+
issues.append("BULK_CREATE_BATCH_SIZE must be positive")
|
|
99
|
+
|
|
100
|
+
return issues
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def get_production_settings(cls) -> Dict[str, Any]:
|
|
104
|
+
"""Get recommended production settings"""
|
|
105
|
+
return {
|
|
106
|
+
'ASYNC_LOGGING': True,
|
|
107
|
+
'ENABLE_SAMPLING': True,
|
|
108
|
+
'SAMPLE_RATE': 0.1, # 10% sampling in production
|
|
109
|
+
'CIRCUIT_BREAKER_ENABLED': True,
|
|
110
|
+
'ENABLE_PII_SANITIZATION': True,
|
|
111
|
+
'ENABLE_METRICS': True,
|
|
112
|
+
'BULK_CREATE_BATCH_SIZE': 5000,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def is_user_authorized(cls, user_role, required_roles=None):
|
|
117
|
+
"""
|
|
118
|
+
Check if user role has access.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
user_role: Role from header (X-User-Role)
|
|
122
|
+
required_roles: List of required roles (defaults to VIEWER_ROLES + ADMIN_ROLES)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Boolean indicating if user is authorized
|
|
126
|
+
"""
|
|
127
|
+
if not user_role:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
if required_roles is None:
|
|
131
|
+
required_roles = cls.VIEWER_ROLES + cls.ADMIN_ROLES
|
|
132
|
+
|
|
133
|
+
return user_role.lower() in [r.lower() for r in required_roles]
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def is_admin(cls, user_role):
|
|
137
|
+
"""Check if user has admin role"""
|
|
138
|
+
return cls.is_user_authorized(user_role, cls.ADMIN_ROLES)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def get_user_info_from_request(cls, request):
|
|
142
|
+
"""
|
|
143
|
+
Extract user ID and role from request headers.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
dict: {'user_id': str, 'role': str}
|
|
147
|
+
"""
|
|
148
|
+
user_id = request.META.get(f'HTTP_{cls.USER_ID_HEADER.replace("-", "_").upper()}')
|
|
149
|
+
user_role = request.META.get(f'HTTP_{cls.USER_ROLE_HEADER.replace("-", "_").upper()}')
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
'user_id': user_id,
|
|
153
|
+
'role': user_role
|
|
154
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard security middleware and utilities
|
|
3
|
+
"""
|
|
4
|
+
from django.http import HttpResponseForbidden, JsonResponse
|
|
5
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
6
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
7
|
+
from django.views.decorators.clickjacking import xframe_options_deny
|
|
8
|
+
from django.views.decorators.cache import never_cache
|
|
9
|
+
from functools import wraps
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DashboardSecurityMiddleware(MiddlewareMixin):
|
|
16
|
+
"""Security middleware specifically for dashboard endpoints"""
|
|
17
|
+
|
|
18
|
+
def process_request(self, request):
|
|
19
|
+
# Only apply to analytics endpoints
|
|
20
|
+
if not request.path.startswith('/analytics/') and not request.path.startswith('/api/analytics/'):
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
# Add security headers
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
def process_response(self, request, response):
|
|
27
|
+
# Only apply to analytics endpoints
|
|
28
|
+
if not request.path.startswith('/analytics/') and not request.path.startswith('/api/analytics/'):
|
|
29
|
+
return response
|
|
30
|
+
|
|
31
|
+
# Security headers for dashboard
|
|
32
|
+
response['X-Content-Type-Options'] = 'nosniff'
|
|
33
|
+
response['X-Frame-Options'] = 'DENY'
|
|
34
|
+
response['X-XSS-Protection'] = '1; mode=block'
|
|
35
|
+
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
|
36
|
+
|
|
37
|
+
# CSP for dashboard
|
|
38
|
+
csp = (
|
|
39
|
+
"default-src 'self'; "
|
|
40
|
+
"script-src 'self' 'unsafe-inline'; "
|
|
41
|
+
"style-src 'self' 'unsafe-inline'; "
|
|
42
|
+
"img-src 'self' data:; "
|
|
43
|
+
"font-src 'self'; "
|
|
44
|
+
"connect-src 'self'; "
|
|
45
|
+
"frame-ancestors 'none';"
|
|
46
|
+
)
|
|
47
|
+
response['Content-Security-Policy'] = csp
|
|
48
|
+
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def dashboard_auth_required(view_func):
|
|
53
|
+
"""
|
|
54
|
+
Decorator for dashboard views requiring authentication
|
|
55
|
+
"""
|
|
56
|
+
@wraps(view_func)
|
|
57
|
+
def wrapper(request, *args, **kwargs):
|
|
58
|
+
from trafficmonitor.conf import TrafficMonitorConfig
|
|
59
|
+
|
|
60
|
+
# Get user info from headers
|
|
61
|
+
user_info = TrafficMonitorConfig.get_user_info_from_request(request)
|
|
62
|
+
|
|
63
|
+
if not TrafficMonitorConfig.is_user_authorized(user_info.get('role')):
|
|
64
|
+
logger.warning(f"Unauthorized dashboard access attempt from {request.META.get('REMOTE_ADDR')}")
|
|
65
|
+
|
|
66
|
+
if request.path.startswith('/api/'):
|
|
67
|
+
return JsonResponse({'error': 'Unauthorized'}, status=401)
|
|
68
|
+
else:
|
|
69
|
+
return HttpResponseForbidden("Access denied. Required role not found in headers.")
|
|
70
|
+
|
|
71
|
+
# Add user info to request for logging
|
|
72
|
+
request.trafficmonitor_user = user_info
|
|
73
|
+
|
|
74
|
+
return view_func(request, *args, **kwargs)
|
|
75
|
+
|
|
76
|
+
return wrapper
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rate_limit_dashboard(max_requests=100, window_seconds=3600):
|
|
80
|
+
"""
|
|
81
|
+
Simple rate limiting for dashboard endpoints
|
|
82
|
+
"""
|
|
83
|
+
def decorator(view_func):
|
|
84
|
+
@wraps(view_func)
|
|
85
|
+
def wrapper(request, *args, **kwargs):
|
|
86
|
+
from django.core.cache import cache
|
|
87
|
+
|
|
88
|
+
# Get client identifier
|
|
89
|
+
client_ip = request.META.get('REMOTE_ADDR', 'unknown')
|
|
90
|
+
user_id = getattr(request, 'trafficmonitor_user', {}).get('user_id', 'anonymous')
|
|
91
|
+
|
|
92
|
+
cache_key = f"dashboard_rate_limit:{client_ip}:{user_id}"
|
|
93
|
+
|
|
94
|
+
# Get current request count
|
|
95
|
+
current_requests = cache.get(cache_key, 0)
|
|
96
|
+
|
|
97
|
+
if current_requests >= max_requests:
|
|
98
|
+
logger.warning(f"Rate limit exceeded for {client_ip}:{user_id}")
|
|
99
|
+
|
|
100
|
+
if request.path.startswith('/api/'):
|
|
101
|
+
return JsonResponse({'error': 'Rate limit exceeded'}, status=429)
|
|
102
|
+
else:
|
|
103
|
+
return HttpResponseForbidden("Rate limit exceeded. Please try again later.")
|
|
104
|
+
|
|
105
|
+
# Increment counter
|
|
106
|
+
cache.set(cache_key, current_requests + 1, timeout=window_seconds)
|
|
107
|
+
|
|
108
|
+
return view_func(request, *args, **kwargs)
|
|
109
|
+
|
|
110
|
+
return wrapper
|
|
111
|
+
return decorator
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database utilities for enterprise-grade performance
|
|
3
|
+
"""
|
|
4
|
+
from django.db import transaction
|
|
5
|
+
from django.core.cache import cache
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DatabaseManager:
|
|
13
|
+
"""Handles database operations with connection pooling and bulk operations"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
@transaction.atomic
|
|
17
|
+
def bulk_create_logs(log_data: List[Dict[str, Any]], batch_size: int = 1000):
|
|
18
|
+
"""Bulk create request logs with proper transaction handling"""
|
|
19
|
+
from trafficmonitor.models import RequestLog
|
|
20
|
+
|
|
21
|
+
logs = []
|
|
22
|
+
for data in log_data:
|
|
23
|
+
logs.append(RequestLog(**data))
|
|
24
|
+
|
|
25
|
+
# Use bulk_create with ignore_conflicts for high concurrency
|
|
26
|
+
RequestLog.objects.bulk_create(logs, batch_size=batch_size, ignore_conflicts=True)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def get_cached_analytics(cache_key: str, query_func, ttl: int = 300):
|
|
30
|
+
"""Cache analytics queries with proper invalidation"""
|
|
31
|
+
cached_result = cache.get(cache_key)
|
|
32
|
+
if cached_result is not None:
|
|
33
|
+
return cached_result
|
|
34
|
+
|
|
35
|
+
result = query_func()
|
|
36
|
+
cache.set(cache_key, result, ttl)
|
|
37
|
+
return result
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions and error handling for TrafficMonitor
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
import functools
|
|
6
|
+
from typing import Callable, Any, Type, Tuple
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TrafficMonitorException(Exception):
|
|
13
|
+
"""Base exception for TrafficMonitor"""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatabaseConnectionError(TrafficMonitorException):
|
|
18
|
+
"""Database connection related errors"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigurationError(TrafficMonitorException):
|
|
23
|
+
"""Configuration validation errors"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuthenticationError(TrafficMonitorException):
|
|
28
|
+
"""Authentication/authorization errors"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def retry_on_exception(
|
|
33
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
34
|
+
max_attempts: int = 3,
|
|
35
|
+
delay: float = 1.0,
|
|
36
|
+
backoff: float = 2.0
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Decorator for retrying functions on specific exceptions
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
exceptions: Tuple of exception types to retry on
|
|
43
|
+
max_attempts: Maximum number of retry attempts
|
|
44
|
+
delay: Initial delay between retries (seconds)
|
|
45
|
+
backoff: Multiplier for delay on each retry
|
|
46
|
+
"""
|
|
47
|
+
def decorator(func: Callable) -> Callable:
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
50
|
+
current_delay = delay
|
|
51
|
+
|
|
52
|
+
for attempt in range(max_attempts):
|
|
53
|
+
try:
|
|
54
|
+
return func(*args, **kwargs)
|
|
55
|
+
except exceptions as e:
|
|
56
|
+
if attempt == max_attempts - 1:
|
|
57
|
+
logger.error(f"Function {func.__name__} failed after {max_attempts} attempts: {e}")
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
logger.warning(f"Attempt {attempt + 1} failed for {func.__name__}: {e}. Retrying in {current_delay}s")
|
|
61
|
+
time.sleep(current_delay)
|
|
62
|
+
current_delay *= backoff
|
|
63
|
+
|
|
64
|
+
return None # Should never reach here
|
|
65
|
+
return wrapper
|
|
66
|
+
return decorator
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ErrorHandler:
|
|
70
|
+
"""Centralized error handling"""
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def handle_database_error(error: Exception, context: dict) -> None:
|
|
74
|
+
"""Handle database-related errors"""
|
|
75
|
+
logger.error("Database error occurred", extra={
|
|
76
|
+
"error_type": type(error).__name__,
|
|
77
|
+
"error_message": str(error),
|
|
78
|
+
"context": context
|
|
79
|
+
}, exc_info=True)
|
|
80
|
+
|
|
81
|
+
# Could integrate with alerting system here
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def handle_middleware_error(error: Exception, request) -> None:
|
|
85
|
+
"""Handle middleware errors gracefully"""
|
|
86
|
+
logger.error("Middleware error occurred", extra={
|
|
87
|
+
"error_type": type(error).__name__,
|
|
88
|
+
"error_message": str(error),
|
|
89
|
+
"path": getattr(request, 'path', 'unknown'),
|
|
90
|
+
"method": getattr(request, 'method', 'unknown')
|
|
91
|
+
}, exc_info=True)
|
|
92
|
+
|
|
93
|
+
# Don't let middleware errors break the request flow
|
trafficmonitor/health.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health checks for TrafficMonitor
|
|
3
|
+
"""
|
|
4
|
+
from django.db import connection
|
|
5
|
+
from django.core.cache import cache
|
|
6
|
+
from django.http import JsonResponse
|
|
7
|
+
from django.views import View
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HealthCheckView(View):
|
|
13
|
+
"""Health check endpoint for load balancers"""
|
|
14
|
+
|
|
15
|
+
def get(self, request):
|
|
16
|
+
"""Perform health checks"""
|
|
17
|
+
health_status = {
|
|
18
|
+
'status': 'healthy',
|
|
19
|
+
'timestamp': time.time(),
|
|
20
|
+
'checks': {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Database check
|
|
24
|
+
try:
|
|
25
|
+
with connection.cursor() as cursor:
|
|
26
|
+
cursor.execute("SELECT 1")
|
|
27
|
+
health_status['checks']['database'] = {'status': 'healthy'}
|
|
28
|
+
except Exception as e:
|
|
29
|
+
health_status['checks']['database'] = {'status': 'unhealthy', 'error': str(e)}
|
|
30
|
+
health_status['status'] = 'unhealthy'
|
|
31
|
+
|
|
32
|
+
# Cache check
|
|
33
|
+
try:
|
|
34
|
+
cache.set('health_check', 'ok', 30)
|
|
35
|
+
cache.get('health_check')
|
|
36
|
+
health_status['checks']['cache'] = {'status': 'healthy'}
|
|
37
|
+
except Exception as e:
|
|
38
|
+
health_status['checks']['cache'] = {'status': 'unhealthy', 'error': str(e)}
|
|
39
|
+
health_status['status'] = 'unhealthy'
|
|
40
|
+
|
|
41
|
+
# Configuration check
|
|
42
|
+
from trafficmonitor.conf import TrafficMonitorConfig
|
|
43
|
+
config_issues = TrafficMonitorConfig.validate_configuration()
|
|
44
|
+
if config_issues:
|
|
45
|
+
health_status['checks']['configuration'] = {'status': 'unhealthy', 'issues': config_issues}
|
|
46
|
+
health_status['status'] = 'unhealthy'
|
|
47
|
+
else:
|
|
48
|
+
health_status['checks']['configuration'] = {'status': 'healthy'}
|
|
49
|
+
|
|
50
|
+
status_code = 200 if health_status['status'] == 'healthy' else 503
|
|
51
|
+
return JsonResponse(health_status, status=status_code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ReadinessCheckView(View):
|
|
55
|
+
"""Readiness check for Kubernetes"""
|
|
56
|
+
|
|
57
|
+
def get(self, request):
|
|
58
|
+
"""Check if application is ready to serve traffic"""
|
|
59
|
+
from trafficmonitor.models import RequestLog
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Check if we can query the database
|
|
63
|
+
RequestLog.objects.first()
|
|
64
|
+
return JsonResponse({'status': 'ready'})
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return JsonResponse({'status': 'not_ready', 'error': str(e)}, status=503)
|