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,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)
@@ -0,0 +1,3 @@
1
+ from django.shortcuts import render
2
+
3
+ # Create your views here.
@@ -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
+ }