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/admin.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.html import format_html
|
|
3
|
+
try:
|
|
4
|
+
from import_export.admin import ImportExportModelAdmin
|
|
5
|
+
BaseAdmin = ImportExportModelAdmin
|
|
6
|
+
except Exception: # import-export is optional
|
|
7
|
+
BaseAdmin = admin.ModelAdmin
|
|
8
|
+
|
|
9
|
+
from trafficmonitor.models import RequestLog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@admin.register(RequestLog)
|
|
13
|
+
class RequestLogAdmin(BaseAdmin):
|
|
14
|
+
"""
|
|
15
|
+
Admin interface for viewing and managing request logs.
|
|
16
|
+
"""
|
|
17
|
+
list_display = [
|
|
18
|
+
'timestamp',
|
|
19
|
+
'method_colored',
|
|
20
|
+
'path_truncated',
|
|
21
|
+
'status_code_colored',
|
|
22
|
+
'user_display',
|
|
23
|
+
'ip_address',
|
|
24
|
+
'response_time_display',
|
|
25
|
+
'query_count',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
list_filter = [
|
|
29
|
+
'method',
|
|
30
|
+
'status_code',
|
|
31
|
+
'timestamp',
|
|
32
|
+
'requested_user_id',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
search_fields = [
|
|
36
|
+
'path',
|
|
37
|
+
'full_url',
|
|
38
|
+
'ip_address',
|
|
39
|
+
'requested_user_id',
|
|
40
|
+
'exception',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
fields = [
|
|
44
|
+
'id',
|
|
45
|
+
'timestamp',
|
|
46
|
+
'method',
|
|
47
|
+
'path',
|
|
48
|
+
'full_url',
|
|
49
|
+
'status_code',
|
|
50
|
+
'requested_user_id',
|
|
51
|
+
'ip_address',
|
|
52
|
+
'user_agent',
|
|
53
|
+
'request_headers_formatted',
|
|
54
|
+
'request_body_formatted',
|
|
55
|
+
'response_body_formatted',
|
|
56
|
+
'response_time_ms',
|
|
57
|
+
'query_count',
|
|
58
|
+
'exception_formatted',
|
|
59
|
+
'content_length',
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
readonly_fields = [
|
|
63
|
+
'id',
|
|
64
|
+
'method',
|
|
65
|
+
'path',
|
|
66
|
+
'full_url',
|
|
67
|
+
'status_code',
|
|
68
|
+
'requested_user_id',
|
|
69
|
+
'ip_address',
|
|
70
|
+
'user_agent',
|
|
71
|
+
'request_headers_formatted',
|
|
72
|
+
'request_body_formatted',
|
|
73
|
+
'response_body_formatted',
|
|
74
|
+
'response_time_ms',
|
|
75
|
+
'query_count',
|
|
76
|
+
'exception_formatted',
|
|
77
|
+
'content_length',
|
|
78
|
+
'timestamp',
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
date_hierarchy = 'timestamp'
|
|
82
|
+
|
|
83
|
+
ordering = ['-timestamp']
|
|
84
|
+
|
|
85
|
+
# Disable add and edit permissions - this is a read-only log
|
|
86
|
+
def has_add_permission(self, request):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def has_change_permission(self, request, obj=None):
|
|
90
|
+
# Allow view permission but not edit
|
|
91
|
+
if obj is None:
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def has_delete_permission(self, request, obj=None):
|
|
96
|
+
# Allow deletion through admin
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def method_colored(self, obj):
|
|
100
|
+
"""Display HTTP method with color coding."""
|
|
101
|
+
colors = {
|
|
102
|
+
'GET': '#28a745', # Green
|
|
103
|
+
'POST': '#007bff', # Blue
|
|
104
|
+
'PUT': '#ffc107', # Yellow
|
|
105
|
+
'PATCH': '#fd7e14', # Orange
|
|
106
|
+
'DELETE': '#dc3545', # Red
|
|
107
|
+
}
|
|
108
|
+
color = colors.get(obj.method, '#6c757d')
|
|
109
|
+
return format_html(
|
|
110
|
+
'<span style="color: {}; font-weight: bold;">{}</span>',
|
|
111
|
+
color,
|
|
112
|
+
obj.method
|
|
113
|
+
)
|
|
114
|
+
method_colored.short_description = 'Method'
|
|
115
|
+
|
|
116
|
+
def status_code_colored(self, obj):
|
|
117
|
+
"""Display status code with color coding."""
|
|
118
|
+
if obj.status_code < 300:
|
|
119
|
+
color = '#28a745' # Green for success
|
|
120
|
+
elif obj.status_code < 400:
|
|
121
|
+
color = '#17a2b8' # Teal for redirects
|
|
122
|
+
elif obj.status_code < 500:
|
|
123
|
+
color = '#ffc107' # Yellow for client errors
|
|
124
|
+
else:
|
|
125
|
+
color = '#dc3545' # Red for server errors
|
|
126
|
+
|
|
127
|
+
return format_html(
|
|
128
|
+
'<span style="color: {}; font-weight: bold;">{}</span>',
|
|
129
|
+
color,
|
|
130
|
+
obj.status_code
|
|
131
|
+
)
|
|
132
|
+
status_code_colored.short_description = 'Status'
|
|
133
|
+
|
|
134
|
+
def path_truncated(self, obj):
|
|
135
|
+
"""Display truncated path for readability."""
|
|
136
|
+
max_length = 50
|
|
137
|
+
if len(obj.path) > max_length:
|
|
138
|
+
return obj.path[:max_length] + '...'
|
|
139
|
+
return obj.path
|
|
140
|
+
path_truncated.short_description = 'Path'
|
|
141
|
+
|
|
142
|
+
def user_display(self, obj):
|
|
143
|
+
"""Display user information."""
|
|
144
|
+
if obj.requested_user_id:
|
|
145
|
+
return f"{obj.requested_user_id}"
|
|
146
|
+
return "Anonymous"
|
|
147
|
+
user_display.short_description = 'User'
|
|
148
|
+
|
|
149
|
+
def response_time_display(self, obj):
|
|
150
|
+
"""Display response time with color coding."""
|
|
151
|
+
if obj.response_time_ms is None:
|
|
152
|
+
return '-'
|
|
153
|
+
|
|
154
|
+
# Color code based on response time
|
|
155
|
+
if obj.response_time_ms < 100:
|
|
156
|
+
color = '#28a745' # Green - fast
|
|
157
|
+
elif obj.response_time_ms < 500:
|
|
158
|
+
color = '#ffc107' # Yellow - medium
|
|
159
|
+
elif obj.response_time_ms < 1000:
|
|
160
|
+
color = '#fd7e14' # Orange - slow
|
|
161
|
+
else:
|
|
162
|
+
color = '#dc3545' # Red - very slow
|
|
163
|
+
|
|
164
|
+
# Format the time value first, then pass to format_html
|
|
165
|
+
time_str = f"{obj.response_time_ms:.2f} ms"
|
|
166
|
+
return format_html(
|
|
167
|
+
'<span style="color: {};">{}</span>',
|
|
168
|
+
color,
|
|
169
|
+
time_str
|
|
170
|
+
)
|
|
171
|
+
response_time_display.short_description = 'Response Time'
|
|
172
|
+
|
|
173
|
+
def request_headers_formatted(self, obj):
|
|
174
|
+
"""Display formatted request headers."""
|
|
175
|
+
if obj.request_headers:
|
|
176
|
+
import json
|
|
177
|
+
return format_html(
|
|
178
|
+
'<pre style="max-height: 300px; overflow: auto;">{}</pre>',
|
|
179
|
+
json.dumps(obj.request_headers, indent=2)
|
|
180
|
+
)
|
|
181
|
+
return '-'
|
|
182
|
+
request_headers_formatted.short_description = 'Request Headers'
|
|
183
|
+
|
|
184
|
+
def request_body_formatted(self, obj):
|
|
185
|
+
"""Display formatted request body."""
|
|
186
|
+
if obj.request_body:
|
|
187
|
+
return format_html(
|
|
188
|
+
'<pre style="max-height: 400px; overflow: auto;">{}</pre>',
|
|
189
|
+
obj.request_body
|
|
190
|
+
)
|
|
191
|
+
return '-'
|
|
192
|
+
request_body_formatted.short_description = 'Request Body'
|
|
193
|
+
|
|
194
|
+
def response_body_formatted(self, obj):
|
|
195
|
+
"""Display formatted response body."""
|
|
196
|
+
if obj.response_body:
|
|
197
|
+
return format_html(
|
|
198
|
+
'<pre style="max-height: 400px; overflow: auto;">{}</pre>',
|
|
199
|
+
obj.response_body
|
|
200
|
+
)
|
|
201
|
+
return '-'
|
|
202
|
+
response_body_formatted.short_description = 'Response Body'
|
|
203
|
+
|
|
204
|
+
def exception_formatted(self, obj):
|
|
205
|
+
"""Display formatted exception traceback."""
|
|
206
|
+
if obj.exception:
|
|
207
|
+
return format_html(
|
|
208
|
+
'<pre style="color: #dc3545; max-height: 500px; overflow: auto;">{}</pre>',
|
|
209
|
+
obj.exception
|
|
210
|
+
)
|
|
211
|
+
return '-'
|
|
212
|
+
exception_formatted.short_description = 'Exception Traceback'
|
|
213
|
+
|
|
214
|
+
def get_queryset(self, request):
|
|
215
|
+
"""Optimize queryset."""
|
|
216
|
+
qs = super().get_queryset(request)
|
|
217
|
+
return qs
|
|
File without changes
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced Analytics Queries for Enterprise-Level Insights
|
|
3
|
+
Focus on Read/Write operations, API performance, and actionable metrics.
|
|
4
|
+
"""
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from django.db.models import Count, Avg, Max, Min, Q, F, FloatField, ExpressionWrapper
|
|
7
|
+
from django.db.models.functions import TruncDate, TruncHour, TruncDay
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
from trafficmonitor.models import RequestLog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnhancedAnalyticsQueries:
|
|
13
|
+
"""
|
|
14
|
+
Enterprise-grade analytics queries focused on actionable insights.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def get_read_write_summary(start_date, end_date, **filters):
|
|
19
|
+
"""
|
|
20
|
+
Get summary of Read vs Write operations.
|
|
21
|
+
Essential for understanding application usage patterns.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
dict: {
|
|
25
|
+
'read_count': int,
|
|
26
|
+
'write_count': int,
|
|
27
|
+
'delete_count': int,
|
|
28
|
+
'read_percentage': float,
|
|
29
|
+
'write_percentage': float,
|
|
30
|
+
'avg_read_time_ms': float,
|
|
31
|
+
'avg_write_time_ms': float
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
35
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
36
|
+
|
|
37
|
+
summary = qs.aggregate(
|
|
38
|
+
read_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_READ)),
|
|
39
|
+
write_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_WRITE)),
|
|
40
|
+
delete_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_DELETE)),
|
|
41
|
+
total_count=Count('id'),
|
|
42
|
+
avg_read_time=Avg('response_time_ms', filter=Q(
|
|
43
|
+
operation_type=RequestLog.OPERATION_READ,
|
|
44
|
+
response_time_ms__isnull=False
|
|
45
|
+
)),
|
|
46
|
+
avg_write_time=Avg('response_time_ms', filter=Q(
|
|
47
|
+
operation_type=RequestLog.OPERATION_WRITE,
|
|
48
|
+
response_time_ms__isnull=False
|
|
49
|
+
)),
|
|
50
|
+
avg_delete_time=Avg('response_time_ms', filter=Q(
|
|
51
|
+
operation_type=RequestLog.OPERATION_DELETE,
|
|
52
|
+
response_time_ms__isnull=False
|
|
53
|
+
)),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Calculate percentages
|
|
57
|
+
total = summary['total_count'] or 1
|
|
58
|
+
summary['read_percentage'] = (summary['read_count'] / total) * 100
|
|
59
|
+
summary['write_percentage'] = (summary['write_count'] / total) * 100
|
|
60
|
+
summary['delete_percentage'] = (summary['delete_count'] / total) * 100
|
|
61
|
+
|
|
62
|
+
return summary
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_read_write_over_time(start_date, end_date, granularity='day', **filters):
|
|
66
|
+
"""
|
|
67
|
+
Get Read vs Write operations over time.
|
|
68
|
+
Useful for identifying usage patterns and peak times.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of dicts with period, read_count, write_count
|
|
72
|
+
"""
|
|
73
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
74
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
75
|
+
|
|
76
|
+
trunc_func = TruncHour if granularity == 'hour' else TruncDay
|
|
77
|
+
|
|
78
|
+
results = qs.annotate(
|
|
79
|
+
period=trunc_func('timestamp')
|
|
80
|
+
).values('period').annotate(
|
|
81
|
+
read_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_READ)),
|
|
82
|
+
write_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_WRITE)),
|
|
83
|
+
delete_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_DELETE)),
|
|
84
|
+
total_count=Count('id')
|
|
85
|
+
).order_by('period')
|
|
86
|
+
|
|
87
|
+
return list(results)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def get_api_health_metrics(start_date, end_date, **filters):
|
|
91
|
+
"""
|
|
92
|
+
Get API health metrics including success rates and error rates.
|
|
93
|
+
Critical for SRE/DevOps monitoring.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict with success_rate, error_rate, p50, p95, p99 response times
|
|
97
|
+
"""
|
|
98
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
99
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
100
|
+
qs = qs.filter(is_api_request=True)
|
|
101
|
+
|
|
102
|
+
total = qs.count()
|
|
103
|
+
if total == 0:
|
|
104
|
+
return {
|
|
105
|
+
'total_requests': 0,
|
|
106
|
+
'success_rate': 0,
|
|
107
|
+
'error_rate': 0,
|
|
108
|
+
'p50_response_time': 0,
|
|
109
|
+
'p95_response_time': 0,
|
|
110
|
+
'p99_response_time': 0,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Success and error rates
|
|
114
|
+
success_count = qs.filter(status_code__lt=400).count()
|
|
115
|
+
error_count = qs.filter(status_code__gte=400).count()
|
|
116
|
+
|
|
117
|
+
# Response time percentiles (approximation using aggregates)
|
|
118
|
+
response_times = qs.filter(response_time_ms__isnull=False).values_list(
|
|
119
|
+
'response_time_ms', flat=True
|
|
120
|
+
).order_by('response_time_ms')
|
|
121
|
+
|
|
122
|
+
response_times_list = list(response_times)
|
|
123
|
+
count = len(response_times_list)
|
|
124
|
+
|
|
125
|
+
p50 = response_times_list[int(count * 0.50)] if count > 0 else 0
|
|
126
|
+
p95 = response_times_list[int(count * 0.95)] if count > 0 else 0
|
|
127
|
+
p99 = response_times_list[int(count * 0.99)] if count > 0 else 0
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
'total_requests': total,
|
|
131
|
+
'success_count': success_count,
|
|
132
|
+
'error_count': error_count,
|
|
133
|
+
'success_rate': (success_count / total) * 100,
|
|
134
|
+
'error_rate': (error_count / total) * 100,
|
|
135
|
+
'p50_response_time': round(p50, 2),
|
|
136
|
+
'p95_response_time': round(p95, 2),
|
|
137
|
+
'p99_response_time': round(p99, 2),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def get_endpoint_category_breakdown(start_date, end_date, limit=10, **filters):
|
|
142
|
+
"""
|
|
143
|
+
Get request breakdown by endpoint category.
|
|
144
|
+
Helps identify which microservices/modules are most used.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of dicts with category, count, avg_response_time, error_rate
|
|
148
|
+
"""
|
|
149
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
150
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
151
|
+
qs = qs.filter(endpoint_category__isnull=False)
|
|
152
|
+
|
|
153
|
+
results = qs.values('endpoint_category').annotate(
|
|
154
|
+
total_count=Count('id'),
|
|
155
|
+
success_count=Count('id', filter=Q(status_code__lt=400)),
|
|
156
|
+
error_count=Count('id', filter=Q(status_code__gte=400)),
|
|
157
|
+
avg_response_time=Avg('response_time_ms', filter=Q(response_time_ms__isnull=False)),
|
|
158
|
+
max_response_time=Max('response_time_ms'),
|
|
159
|
+
).order_by('-total_count')[:limit]
|
|
160
|
+
|
|
161
|
+
# Add error_rate calculation
|
|
162
|
+
for item in results:
|
|
163
|
+
total = item['total_count'] or 1
|
|
164
|
+
item['error_rate'] = (item['error_count'] / total) * 100
|
|
165
|
+
item['success_rate'] = (item['success_count'] / total) * 100
|
|
166
|
+
|
|
167
|
+
return list(results)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def get_user_activity_detailed(start_date, end_date, limit=20, **filters):
|
|
171
|
+
"""
|
|
172
|
+
Get detailed user activity with Read/Write breakdown.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of dicts with user_id, total_requests, read_count, write_count
|
|
176
|
+
"""
|
|
177
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
178
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
179
|
+
qs = qs.filter(requested_user_id__isnull=False)
|
|
180
|
+
|
|
181
|
+
results = qs.values('requested_user_id').annotate(
|
|
182
|
+
total_count=Count('id'),
|
|
183
|
+
read_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_READ)),
|
|
184
|
+
write_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_WRITE)),
|
|
185
|
+
delete_count=Count('id', filter=Q(operation_type=RequestLog.OPERATION_DELETE)),
|
|
186
|
+
avg_response_time=Avg('response_time_ms', filter=Q(response_time_ms__isnull=False)),
|
|
187
|
+
error_count=Count('id', filter=Q(status_code__gte=400)),
|
|
188
|
+
).order_by('-total_count')[:limit]
|
|
189
|
+
|
|
190
|
+
# Add percentages
|
|
191
|
+
for item in results:
|
|
192
|
+
total = item['total_count'] or 1
|
|
193
|
+
item['read_percentage'] = (item['read_count'] / total) * 100
|
|
194
|
+
item['write_percentage'] = (item['write_count'] / total) * 100
|
|
195
|
+
item['error_rate'] = (item['error_count'] / total) * 100
|
|
196
|
+
|
|
197
|
+
return list(results)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def get_performance_outliers(start_date, end_date, threshold_percentile=95, limit=10, **filters):
|
|
201
|
+
"""
|
|
202
|
+
Identify endpoints with performance issues (beyond 95th percentile).
|
|
203
|
+
Critical for performance optimization.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of endpoints exceeding performance threshold
|
|
207
|
+
"""
|
|
208
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
209
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
210
|
+
qs = qs.filter(response_time_ms__isnull=False)
|
|
211
|
+
|
|
212
|
+
# Calculate 95th percentile threshold
|
|
213
|
+
response_times = list(qs.values_list('response_time_ms', flat=True).order_by('response_time_ms'))
|
|
214
|
+
if not response_times:
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
threshold = response_times[int(len(response_times) * (threshold_percentile / 100))]
|
|
218
|
+
|
|
219
|
+
# Find endpoints exceeding threshold
|
|
220
|
+
outliers = qs.filter(
|
|
221
|
+
response_time_ms__gte=threshold
|
|
222
|
+
).values('path', 'operation_type').annotate(
|
|
223
|
+
count=Count('id'),
|
|
224
|
+
avg_response_time=Avg('response_time_ms'),
|
|
225
|
+
max_response_time=Max('response_time_ms'),
|
|
226
|
+
min_response_time=Min('response_time_ms'),
|
|
227
|
+
).order_by('-avg_response_time')[:limit]
|
|
228
|
+
|
|
229
|
+
return list(outliers)
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def get_error_analysis_by_category(start_date, end_date, **filters):
|
|
233
|
+
"""
|
|
234
|
+
Analyze errors by endpoint category and status code.
|
|
235
|
+
Helps identify problematic services.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of dicts with category, status_code, count
|
|
239
|
+
"""
|
|
240
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
241
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
242
|
+
qs = qs.filter(status_code__gte=400, endpoint_category__isnull=False)
|
|
243
|
+
|
|
244
|
+
results = qs.values('endpoint_category', 'status_code').annotate(
|
|
245
|
+
count=Count('id'),
|
|
246
|
+
example_path=Max('path') # Show an example path
|
|
247
|
+
).order_by('endpoint_category', '-count')
|
|
248
|
+
|
|
249
|
+
return list(results)
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def get_throughput_metrics(start_date, end_date, **filters):
|
|
253
|
+
"""
|
|
254
|
+
Calculate throughput metrics (requests per second/minute).
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
dict with avg_rps, peak_rps, total_requests
|
|
258
|
+
"""
|
|
259
|
+
from trafficmonitor.analytics.views import AnalyticsQueryHelper
|
|
260
|
+
qs = AnalyticsQueryHelper.get_base_queryset(start_date, end_date, **filters)
|
|
261
|
+
|
|
262
|
+
total_requests = qs.count()
|
|
263
|
+
time_diff_seconds = (end_date - start_date).total_seconds()
|
|
264
|
+
|
|
265
|
+
if time_diff_seconds == 0:
|
|
266
|
+
return {'avg_rps': 0, 'total_requests': 0}
|
|
267
|
+
|
|
268
|
+
avg_rps = total_requests / time_diff_seconds
|
|
269
|
+
|
|
270
|
+
# Get peak hourly throughput
|
|
271
|
+
hourly = qs.annotate(
|
|
272
|
+
hour=TruncHour('timestamp')
|
|
273
|
+
).values('hour').annotate(
|
|
274
|
+
count=Count('id')
|
|
275
|
+
).order_by('-count').first()
|
|
276
|
+
|
|
277
|
+
peak_rph = hourly['count'] if hourly else 0
|
|
278
|
+
peak_rps = peak_rph / 3600 # Convert to per second
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
'total_requests': total_requests,
|
|
282
|
+
'avg_rps': round(avg_rps, 2),
|
|
283
|
+
'avg_rpm': round(avg_rps * 60, 2),
|
|
284
|
+
'peak_rps': round(peak_rps, 2),
|
|
285
|
+
'peak_rph': peak_rph,
|
|
286
|
+
}
|