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
trafficmonitor/tests.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
from django.test import TestCase, RequestFactory, override_settings
|
|
2
|
+
from django.contrib.auth import get_user_model
|
|
3
|
+
from django.http import HttpResponse
|
|
4
|
+
|
|
5
|
+
from trafficmonitor.models import RequestLog
|
|
6
|
+
from trafficmonitor.middleware import RequestLoggingMiddleware
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
User = get_user_model()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RequestLoggingMiddlewareTest(TestCase):
|
|
13
|
+
"""
|
|
14
|
+
Test cases for RequestLoggingMiddleware.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def setUp(self):
|
|
18
|
+
"""Set up test fixtures."""
|
|
19
|
+
self.factory = RequestFactory()
|
|
20
|
+
self.middleware = RequestLoggingMiddleware(get_response=lambda r: HttpResponse())
|
|
21
|
+
self.user = User.objects.create_user(
|
|
22
|
+
username='testuser',
|
|
23
|
+
email='test@example.com',
|
|
24
|
+
password='testpass123'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def test_middleware_logs_get_request(self):
|
|
28
|
+
"""Test that GET requests are logged."""
|
|
29
|
+
# Create a GET request
|
|
30
|
+
request = self.factory.get('/api/test/')
|
|
31
|
+
request.user = self.user
|
|
32
|
+
|
|
33
|
+
# Process request through middleware
|
|
34
|
+
self.middleware.process_request(request)
|
|
35
|
+
response = HttpResponse(status=200)
|
|
36
|
+
self.middleware.process_response(request, response)
|
|
37
|
+
|
|
38
|
+
# Verify log was created
|
|
39
|
+
self.assertEqual(RequestLog.objects.count(), 1)
|
|
40
|
+
log = RequestLog.objects.first()
|
|
41
|
+
self.assertEqual(log.method, 'GET')
|
|
42
|
+
self.assertEqual(log.path, '/api/test/')
|
|
43
|
+
self.assertEqual(log.status_code, 200)
|
|
44
|
+
self.assertEqual(log.user, self.user)
|
|
45
|
+
|
|
46
|
+
def test_middleware_logs_post_request_with_body(self):
|
|
47
|
+
"""Test that POST requests with body are logged."""
|
|
48
|
+
# Create a POST request with JSON body
|
|
49
|
+
request = self.factory.post(
|
|
50
|
+
'/api/test/',
|
|
51
|
+
data={'key': 'value'},
|
|
52
|
+
content_type='application/json'
|
|
53
|
+
)
|
|
54
|
+
request.user = self.user
|
|
55
|
+
|
|
56
|
+
# Process request through middleware
|
|
57
|
+
self.middleware.process_request(request)
|
|
58
|
+
response = HttpResponse(status=201)
|
|
59
|
+
self.middleware.process_response(request, response)
|
|
60
|
+
|
|
61
|
+
# Verify log was created with request body
|
|
62
|
+
self.assertEqual(RequestLog.objects.count(), 1)
|
|
63
|
+
log = RequestLog.objects.first()
|
|
64
|
+
self.assertEqual(log.method, 'POST')
|
|
65
|
+
self.assertEqual(log.status_code, 201)
|
|
66
|
+
self.assertIsNotNone(log.request_body)
|
|
67
|
+
|
|
68
|
+
def test_middleware_logs_anonymous_request(self):
|
|
69
|
+
"""Test that anonymous requests are logged."""
|
|
70
|
+
# Create request without user
|
|
71
|
+
request = self.factory.get('/api/public/')
|
|
72
|
+
request.user = None
|
|
73
|
+
|
|
74
|
+
# Process request through middleware
|
|
75
|
+
self.middleware.process_request(request)
|
|
76
|
+
response = HttpResponse(status=200)
|
|
77
|
+
self.middleware.process_response(request, response)
|
|
78
|
+
|
|
79
|
+
# Verify log was created without user
|
|
80
|
+
self.assertEqual(RequestLog.objects.count(), 1)
|
|
81
|
+
log = RequestLog.objects.first()
|
|
82
|
+
self.assertIsNone(log.user)
|
|
83
|
+
|
|
84
|
+
def test_middleware_excludes_static_paths(self):
|
|
85
|
+
"""Test that excluded paths are not logged."""
|
|
86
|
+
# Create requests for excluded paths
|
|
87
|
+
excluded_paths = [
|
|
88
|
+
'/static/css/style.css',
|
|
89
|
+
'/media/uploads/image.jpg',
|
|
90
|
+
'/admin/jsi18n/',
|
|
91
|
+
'/favicon.ico',
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for path in excluded_paths:
|
|
95
|
+
request = self.factory.get(path)
|
|
96
|
+
request.user = self.user
|
|
97
|
+
|
|
98
|
+
self.middleware.process_request(request)
|
|
99
|
+
response = HttpResponse(status=200)
|
|
100
|
+
self.middleware.process_response(request, response)
|
|
101
|
+
|
|
102
|
+
# Verify no logs were created
|
|
103
|
+
self.assertEqual(RequestLog.objects.count(), 0)
|
|
104
|
+
|
|
105
|
+
def test_middleware_logs_exception(self):
|
|
106
|
+
"""Test that exceptions are logged."""
|
|
107
|
+
# Create request
|
|
108
|
+
request = self.factory.get('/api/error/')
|
|
109
|
+
request.user = self.user
|
|
110
|
+
|
|
111
|
+
# Process request and simulate exception
|
|
112
|
+
self.middleware.process_request(request)
|
|
113
|
+
try:
|
|
114
|
+
raise ValueError("Test exception")
|
|
115
|
+
except ValueError as e:
|
|
116
|
+
self.middleware.process_exception(request, e)
|
|
117
|
+
|
|
118
|
+
# Verify log was created with exception
|
|
119
|
+
self.assertEqual(RequestLog.objects.count(), 1)
|
|
120
|
+
log = RequestLog.objects.first()
|
|
121
|
+
self.assertIsNotNone(log.exception)
|
|
122
|
+
self.assertIn('ValueError', log.exception)
|
|
123
|
+
self.assertIn('Test exception', log.exception)
|
|
124
|
+
self.assertEqual(log.status_code, 500)
|
|
125
|
+
|
|
126
|
+
def test_middleware_tracks_response_time(self):
|
|
127
|
+
"""Test that response time is tracked."""
|
|
128
|
+
# Create request
|
|
129
|
+
request = self.factory.get('/api/test/')
|
|
130
|
+
request.user = self.user
|
|
131
|
+
|
|
132
|
+
# Process request through middleware
|
|
133
|
+
self.middleware.process_request(request)
|
|
134
|
+
response = HttpResponse(status=200)
|
|
135
|
+
self.middleware.process_response(request, response)
|
|
136
|
+
|
|
137
|
+
# Verify response time was recorded
|
|
138
|
+
log = RequestLog.objects.first()
|
|
139
|
+
self.assertIsNotNone(log.response_time_ms)
|
|
140
|
+
self.assertGreaterEqual(log.response_time_ms, 0)
|
|
141
|
+
|
|
142
|
+
def test_middleware_extracts_ip_address(self):
|
|
143
|
+
"""Test that IP address is extracted correctly."""
|
|
144
|
+
# Create request with IP
|
|
145
|
+
request = self.factory.get('/api/test/')
|
|
146
|
+
request.user = self.user
|
|
147
|
+
request.META['REMOTE_ADDR'] = '192.168.1.100'
|
|
148
|
+
|
|
149
|
+
# Process request through middleware
|
|
150
|
+
self.middleware.process_request(request)
|
|
151
|
+
response = HttpResponse(status=200)
|
|
152
|
+
self.middleware.process_response(request, response)
|
|
153
|
+
|
|
154
|
+
# Verify IP was recorded
|
|
155
|
+
log = RequestLog.objects.first()
|
|
156
|
+
self.assertEqual(log.ip_address, '192.168.1.100')
|
|
157
|
+
|
|
158
|
+
def test_middleware_handles_x_forwarded_for(self):
|
|
159
|
+
"""Test that X-Forwarded-For header is handled correctly."""
|
|
160
|
+
# Create request with X-Forwarded-For
|
|
161
|
+
request = self.factory.get('/api/test/')
|
|
162
|
+
request.user = self.user
|
|
163
|
+
request.META['HTTP_X_FORWARDED_FOR'] = '203.0.113.1, 192.168.1.1'
|
|
164
|
+
request.META['REMOTE_ADDR'] = '192.168.1.100'
|
|
165
|
+
|
|
166
|
+
# Process request through middleware
|
|
167
|
+
self.middleware.process_request(request)
|
|
168
|
+
response = HttpResponse(status=200)
|
|
169
|
+
self.middleware.process_response(request, response)
|
|
170
|
+
|
|
171
|
+
# Verify first IP from X-Forwarded-For was used
|
|
172
|
+
log = RequestLog.objects.first()
|
|
173
|
+
self.assertEqual(log.ip_address, '203.0.113.1')
|
|
174
|
+
|
|
175
|
+
def test_request_log_str_method(self):
|
|
176
|
+
"""Test RequestLog string representation."""
|
|
177
|
+
# Create a log entry
|
|
178
|
+
request = self.factory.get('/api/test/')
|
|
179
|
+
request.user = self.user
|
|
180
|
+
|
|
181
|
+
self.middleware.process_request(request)
|
|
182
|
+
response = HttpResponse(status=200)
|
|
183
|
+
self.middleware.process_response(request, response)
|
|
184
|
+
|
|
185
|
+
# Verify string representation
|
|
186
|
+
log = RequestLog.objects.first()
|
|
187
|
+
str_repr = str(log)
|
|
188
|
+
self.assertIn('GET', str_repr)
|
|
189
|
+
self.assertIn('/api/test/', str_repr)
|
|
190
|
+
self.assertIn('200', str_repr)
|
|
191
|
+
self.assertIn('testuser', str_repr)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class RequestLogModelTest(TestCase):
|
|
195
|
+
"""
|
|
196
|
+
Test cases for RequestLog model.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def setUp(self):
|
|
200
|
+
"""Set up test fixtures."""
|
|
201
|
+
self.user = User.objects.create_user(
|
|
202
|
+
username='testuser',
|
|
203
|
+
email='test@example.com',
|
|
204
|
+
password='testpass123'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def test_create_request_log(self):
|
|
208
|
+
"""Test creating a RequestLog entry."""
|
|
209
|
+
log = RequestLog.objects.create(
|
|
210
|
+
method='GET',
|
|
211
|
+
path='/api/test/',
|
|
212
|
+
full_url='http://example.com/api/test/',
|
|
213
|
+
status_code=200,
|
|
214
|
+
user=self.user,
|
|
215
|
+
ip_address='192.168.1.100',
|
|
216
|
+
user_agent='Test Agent',
|
|
217
|
+
response_time_ms=50.5,
|
|
218
|
+
query_count=5,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
self.assertIsNotNone(log.id)
|
|
222
|
+
self.assertEqual(log.method, 'GET')
|
|
223
|
+
self.assertEqual(log.path, '/api/test/')
|
|
224
|
+
self.assertEqual(log.status_code, 200)
|
|
225
|
+
self.assertEqual(log.user, self.user)
|
|
226
|
+
self.assertEqual(log.ip_address, '192.168.1.100')
|
|
227
|
+
self.assertEqual(log.response_time_ms, 50.5)
|
|
228
|
+
self.assertEqual(log.query_count, 5)
|
|
229
|
+
|
|
230
|
+
def test_request_log_ordering(self):
|
|
231
|
+
"""Test that logs are ordered by timestamp descending."""
|
|
232
|
+
# Create multiple logs
|
|
233
|
+
for i in range(3):
|
|
234
|
+
RequestLog.objects.create(
|
|
235
|
+
method='GET',
|
|
236
|
+
path=f'/api/test/{i}/',
|
|
237
|
+
full_url=f'http://example.com/api/test/{i}/',
|
|
238
|
+
status_code=200,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Verify ordering
|
|
242
|
+
logs = list(RequestLog.objects.all())
|
|
243
|
+
self.assertEqual(len(logs), 3)
|
|
244
|
+
# Most recent should be first
|
|
245
|
+
self.assertGreater(logs[0].timestamp, logs[1].timestamp)
|
|
246
|
+
self.assertGreater(logs[1].timestamp, logs[2].timestamp)
|
trafficmonitor/views.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket consumers for real-time dashboard updates
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import asyncio
|
|
6
|
+
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
7
|
+
from channels.db import database_sync_to_async
|
|
8
|
+
from django.core.cache import cache
|
|
9
|
+
from trafficmonitor.conf import TrafficMonitorConfig
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DashboardConsumer(AsyncWebsocketConsumer):
|
|
16
|
+
"""WebSocket consumer for real-time dashboard updates"""
|
|
17
|
+
|
|
18
|
+
async def connect(self):
|
|
19
|
+
"""Handle WebSocket connection"""
|
|
20
|
+
# Authenticate user
|
|
21
|
+
user_info = await self.get_user_info()
|
|
22
|
+
|
|
23
|
+
if not TrafficMonitorConfig.is_user_authorized(user_info.get('role')):
|
|
24
|
+
await self.close(code=4001) # Unauthorized
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
# Join dashboard group
|
|
28
|
+
self.group_name = 'dashboard_updates'
|
|
29
|
+
await self.channel_layer.group_add(
|
|
30
|
+
self.group_name,
|
|
31
|
+
self.channel_name
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
await self.accept()
|
|
35
|
+
|
|
36
|
+
# Send initial data
|
|
37
|
+
await self.send_dashboard_update()
|
|
38
|
+
|
|
39
|
+
# Start periodic updates
|
|
40
|
+
asyncio.create_task(self.periodic_updates())
|
|
41
|
+
|
|
42
|
+
async def disconnect(self, close_code):
|
|
43
|
+
"""Handle WebSocket disconnection"""
|
|
44
|
+
if hasattr(self, 'group_name'):
|
|
45
|
+
await self.channel_layer.group_discard(
|
|
46
|
+
self.group_name,
|
|
47
|
+
self.channel_name
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def receive(self, text_data):
|
|
51
|
+
"""Handle messages from WebSocket"""
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(text_data)
|
|
54
|
+
message_type = data.get('type')
|
|
55
|
+
|
|
56
|
+
if message_type == 'request_update':
|
|
57
|
+
await self.send_dashboard_update()
|
|
58
|
+
elif message_type == 'subscribe_filters':
|
|
59
|
+
# Handle filter subscriptions
|
|
60
|
+
filters = data.get('filters', {})
|
|
61
|
+
await self.send_filtered_update(filters)
|
|
62
|
+
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
await self.send(text_data=json.dumps({
|
|
65
|
+
'type': 'error',
|
|
66
|
+
'message': 'Invalid JSON'
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
async def send_dashboard_update(self):
|
|
70
|
+
"""Send dashboard update to client"""
|
|
71
|
+
try:
|
|
72
|
+
# Get cached dashboard data
|
|
73
|
+
dashboard_data = cache.get('dashboard_realtime_data')
|
|
74
|
+
|
|
75
|
+
if not dashboard_data:
|
|
76
|
+
dashboard_data = await self.get_dashboard_data()
|
|
77
|
+
cache.set('dashboard_realtime_data', dashboard_data, timeout=30)
|
|
78
|
+
|
|
79
|
+
await self.send(text_data=json.dumps({
|
|
80
|
+
'type': 'dashboard_update',
|
|
81
|
+
'data': dashboard_data
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Error sending dashboard update: {e}")
|
|
86
|
+
await self.send(text_data=json.dumps({
|
|
87
|
+
'type': 'error',
|
|
88
|
+
'message': 'Failed to get dashboard data'
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
async def periodic_updates(self):
|
|
92
|
+
"""Send periodic updates every 30 seconds"""
|
|
93
|
+
while True:
|
|
94
|
+
try:
|
|
95
|
+
await asyncio.sleep(30)
|
|
96
|
+
await self.send_dashboard_update()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Error in periodic updates: {e}")
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
@database_sync_to_async
|
|
102
|
+
def get_user_info(self):
|
|
103
|
+
"""Get user info from headers"""
|
|
104
|
+
return TrafficMonitorConfig.get_user_info_from_request(self.scope)
|
|
105
|
+
|
|
106
|
+
@database_sync_to_async
|
|
107
|
+
def get_dashboard_data(self):
|
|
108
|
+
"""Get real-time dashboard data"""
|
|
109
|
+
from trafficmonitor.models import RequestLog
|
|
110
|
+
from django.utils import timezone
|
|
111
|
+
from datetime import timedelta
|
|
112
|
+
|
|
113
|
+
# Get last 5 minutes of data
|
|
114
|
+
now = timezone.now()
|
|
115
|
+
start_time = now - timedelta(minutes=5)
|
|
116
|
+
|
|
117
|
+
recent_logs = RequestLog.objects.filter(
|
|
118
|
+
timestamp__gte=start_time
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
'recent_requests': recent_logs.count(),
|
|
123
|
+
'recent_errors': recent_logs.filter(status_code__gte=400).count(),
|
|
124
|
+
'avg_response_time': recent_logs.aggregate(
|
|
125
|
+
avg=models.Avg('response_time_ms')
|
|
126
|
+
)['avg'] or 0,
|
|
127
|
+
'timestamp': now.isoformat(),
|
|
128
|
+
}
|