django-cfg 1.4.120__py3-none-any.whl → 1.5.1__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/grpc/admin/config.py +89 -0
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/__init__.py +0 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/migrations/__init__.py +0 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/tasks/admin/task_log.py +20 -47
- django_cfg/apps/urls.py +7 -1
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +6 -0
- django_cfg/core/builders/apps_builder.py +3 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +10 -0
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/modules/base.py +15 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +198 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +28 -0
- django_cfg/pyproject.toml +3 -5
- django_cfg/registry/modules.py +6 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/RECORD +79 -30
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gRPC Monitoring ViewSet.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for monitoring gRPC request statistics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.db.models import Avg, Count, Max
|
|
12
|
+
from django.db.models.functions import TruncDay, TruncHour
|
|
13
|
+
from django_cfg.mixins import AdminAPIMixin
|
|
14
|
+
from django_cfg.modules.django_logging import get_logger
|
|
15
|
+
from drf_spectacular.types import OpenApiTypes
|
|
16
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
17
|
+
from rest_framework import status, viewsets
|
|
18
|
+
from rest_framework.decorators import action
|
|
19
|
+
from rest_framework.response import Response
|
|
20
|
+
|
|
21
|
+
from ..models import GRPCRequestLog
|
|
22
|
+
from ..serializers import (
|
|
23
|
+
HealthCheckSerializer,
|
|
24
|
+
MethodListSerializer,
|
|
25
|
+
MethodStatsSerializer,
|
|
26
|
+
OverviewStatsSerializer,
|
|
27
|
+
RecentRequestsSerializer,
|
|
28
|
+
ServiceListSerializer,
|
|
29
|
+
ServiceStatsSerializer,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = get_logger("grpc.monitoring")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GRPCMonitorViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
36
|
+
"""
|
|
37
|
+
ViewSet for gRPC monitoring and statistics.
|
|
38
|
+
|
|
39
|
+
Provides comprehensive monitoring data for gRPC requests including:
|
|
40
|
+
- Health checks
|
|
41
|
+
- Overview statistics
|
|
42
|
+
- Recent requests
|
|
43
|
+
- Service-level statistics
|
|
44
|
+
- Method-level statistics
|
|
45
|
+
- Timeline data
|
|
46
|
+
|
|
47
|
+
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@extend_schema(
|
|
51
|
+
tags=["gRPC Monitoring"],
|
|
52
|
+
summary="Get gRPC health status",
|
|
53
|
+
description="Returns the current health status of the gRPC server.",
|
|
54
|
+
responses={
|
|
55
|
+
200: HealthCheckSerializer,
|
|
56
|
+
503: {"description": "Service unavailable"},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
@action(detail=False, methods=["get"], url_path="health")
|
|
60
|
+
def health(self, request):
|
|
61
|
+
"""Get health status of gRPC server."""
|
|
62
|
+
try:
|
|
63
|
+
grpc_server_config = getattr(settings, "GRPC_SERVER", {})
|
|
64
|
+
grpc_framework_config = getattr(settings, "GRPC_FRAMEWORK", {})
|
|
65
|
+
|
|
66
|
+
if not grpc_server_config:
|
|
67
|
+
return Response(
|
|
68
|
+
{"error": "gRPC not configured"},
|
|
69
|
+
status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
health_data = {
|
|
73
|
+
"status": "healthy",
|
|
74
|
+
"server_host": grpc_server_config.get("host", "[::]"),
|
|
75
|
+
"server_port": grpc_server_config.get("port", 50051),
|
|
76
|
+
"enabled": True,
|
|
77
|
+
"timestamp": datetime.now().isoformat(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
serializer = HealthCheckSerializer(**health_data)
|
|
81
|
+
return Response(serializer.model_dump())
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Health check error: {e}", exc_info=True)
|
|
85
|
+
return Response(
|
|
86
|
+
{"error": "Internal server error"},
|
|
87
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@extend_schema(
|
|
91
|
+
tags=["gRPC Monitoring"],
|
|
92
|
+
summary="Get overview statistics",
|
|
93
|
+
description="Returns overview statistics for gRPC requests.",
|
|
94
|
+
parameters=[
|
|
95
|
+
OpenApiParameter(
|
|
96
|
+
name="hours",
|
|
97
|
+
type=OpenApiTypes.INT,
|
|
98
|
+
location=OpenApiParameter.QUERY,
|
|
99
|
+
description="Statistics period in hours (default: 24)",
|
|
100
|
+
required=False,
|
|
101
|
+
),
|
|
102
|
+
],
|
|
103
|
+
responses={
|
|
104
|
+
200: OverviewStatsSerializer,
|
|
105
|
+
400: {"description": "Invalid parameters"},
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
@action(detail=False, methods=["get"], url_path="overview")
|
|
109
|
+
def overview(self, request):
|
|
110
|
+
"""Get overview statistics for gRPC requests."""
|
|
111
|
+
try:
|
|
112
|
+
hours = int(request.GET.get("hours", 24))
|
|
113
|
+
hours = min(max(hours, 1), 168) # 1 hour to 1 week
|
|
114
|
+
|
|
115
|
+
stats = GRPCRequestLog.objects.get_statistics(hours=hours)
|
|
116
|
+
stats["period_hours"] = hours
|
|
117
|
+
|
|
118
|
+
serializer = OverviewStatsSerializer(**stats)
|
|
119
|
+
return Response(serializer.model_dump())
|
|
120
|
+
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
logger.warning(f"Overview stats validation error: {e}")
|
|
123
|
+
return Response(
|
|
124
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
125
|
+
)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Overview stats error: {e}", exc_info=True)
|
|
128
|
+
return Response(
|
|
129
|
+
{"error": "Internal server error"},
|
|
130
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@extend_schema(
|
|
134
|
+
tags=["gRPC Monitoring"],
|
|
135
|
+
summary="Get recent requests",
|
|
136
|
+
description="Returns a list of recent gRPC requests with their details.",
|
|
137
|
+
parameters=[
|
|
138
|
+
OpenApiParameter(
|
|
139
|
+
name="count",
|
|
140
|
+
type=OpenApiTypes.INT,
|
|
141
|
+
location=OpenApiParameter.QUERY,
|
|
142
|
+
description="Number of requests to return (default: 50, max: 200)",
|
|
143
|
+
required=False,
|
|
144
|
+
),
|
|
145
|
+
OpenApiParameter(
|
|
146
|
+
name="service",
|
|
147
|
+
type=OpenApiTypes.STR,
|
|
148
|
+
location=OpenApiParameter.QUERY,
|
|
149
|
+
description="Filter by service name",
|
|
150
|
+
required=False,
|
|
151
|
+
),
|
|
152
|
+
OpenApiParameter(
|
|
153
|
+
name="method",
|
|
154
|
+
type=OpenApiTypes.STR,
|
|
155
|
+
location=OpenApiParameter.QUERY,
|
|
156
|
+
description="Filter by method name",
|
|
157
|
+
required=False,
|
|
158
|
+
),
|
|
159
|
+
OpenApiParameter(
|
|
160
|
+
name="status",
|
|
161
|
+
type=OpenApiTypes.STR,
|
|
162
|
+
location=OpenApiParameter.QUERY,
|
|
163
|
+
description="Filter by status (success, error, timeout, pending, cancelled)",
|
|
164
|
+
required=False,
|
|
165
|
+
),
|
|
166
|
+
OpenApiParameter(
|
|
167
|
+
name="offset",
|
|
168
|
+
type=OpenApiTypes.INT,
|
|
169
|
+
location=OpenApiParameter.QUERY,
|
|
170
|
+
description="Offset for pagination (default: 0)",
|
|
171
|
+
required=False,
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
responses={
|
|
175
|
+
200: RecentRequestsSerializer,
|
|
176
|
+
400: {"description": "Invalid parameters"},
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
@action(detail=False, methods=["get"], url_path="requests")
|
|
180
|
+
def requests(self, request):
|
|
181
|
+
"""Get recent gRPC requests."""
|
|
182
|
+
try:
|
|
183
|
+
count = int(request.GET.get("count", 50))
|
|
184
|
+
count = min(count, 200) # Max 200
|
|
185
|
+
|
|
186
|
+
service_filter = request.GET.get("service")
|
|
187
|
+
method_filter = request.GET.get("method")
|
|
188
|
+
status_filter = request.GET.get("status")
|
|
189
|
+
offset = int(request.GET.get("offset", 0))
|
|
190
|
+
|
|
191
|
+
queryset = GRPCRequestLog.objects.all()
|
|
192
|
+
|
|
193
|
+
if service_filter:
|
|
194
|
+
queryset = queryset.filter(service_name=service_filter)
|
|
195
|
+
|
|
196
|
+
if method_filter:
|
|
197
|
+
queryset = queryset.filter(method_name=method_filter)
|
|
198
|
+
|
|
199
|
+
if status_filter and status_filter in ["success", "error", "timeout", "pending", "cancelled"]:
|
|
200
|
+
queryset = queryset.filter(status=status_filter)
|
|
201
|
+
|
|
202
|
+
# Get total count before slicing
|
|
203
|
+
total = queryset.count()
|
|
204
|
+
|
|
205
|
+
# Apply offset and limit
|
|
206
|
+
requests_list = list(
|
|
207
|
+
queryset.order_by("-created_at")[offset:offset + count].values(
|
|
208
|
+
"request_id",
|
|
209
|
+
"service_name",
|
|
210
|
+
"method_name",
|
|
211
|
+
"full_method",
|
|
212
|
+
"status",
|
|
213
|
+
"grpc_status_code",
|
|
214
|
+
"is_authenticated",
|
|
215
|
+
"duration_ms",
|
|
216
|
+
"created_at",
|
|
217
|
+
"completed_at",
|
|
218
|
+
"error_message",
|
|
219
|
+
"user__username",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Convert datetime to ISO format
|
|
224
|
+
for req in requests_list:
|
|
225
|
+
if req["created_at"]:
|
|
226
|
+
req["created_at"] = req["created_at"].isoformat()
|
|
227
|
+
if req["completed_at"]:
|
|
228
|
+
req["completed_at"] = req["completed_at"].isoformat()
|
|
229
|
+
|
|
230
|
+
response_data = {
|
|
231
|
+
"requests": requests_list,
|
|
232
|
+
"count": len(requests_list),
|
|
233
|
+
"total_available": total,
|
|
234
|
+
"offset": offset,
|
|
235
|
+
"has_more": (offset + count) < total,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
serializer = RecentRequestsSerializer(**response_data)
|
|
239
|
+
return Response(serializer.model_dump())
|
|
240
|
+
|
|
241
|
+
except ValueError as e:
|
|
242
|
+
logger.warning(f"Recent requests validation error: {e}")
|
|
243
|
+
return Response(
|
|
244
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Recent requests error: {e}", exc_info=True)
|
|
248
|
+
return Response(
|
|
249
|
+
{"error": "Internal server error"},
|
|
250
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@extend_schema(
|
|
254
|
+
tags=["gRPC Monitoring"],
|
|
255
|
+
summary="Get service statistics",
|
|
256
|
+
description="Returns statistics grouped by service.",
|
|
257
|
+
parameters=[
|
|
258
|
+
OpenApiParameter(
|
|
259
|
+
name="hours",
|
|
260
|
+
type=OpenApiTypes.INT,
|
|
261
|
+
location=OpenApiParameter.QUERY,
|
|
262
|
+
description="Statistics period in hours (default: 24)",
|
|
263
|
+
required=False,
|
|
264
|
+
),
|
|
265
|
+
],
|
|
266
|
+
responses={
|
|
267
|
+
200: ServiceListSerializer,
|
|
268
|
+
400: {"description": "Invalid parameters"},
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
@action(detail=False, methods=["get"], url_path="services")
|
|
272
|
+
def services(self, request):
|
|
273
|
+
"""Get statistics per service."""
|
|
274
|
+
try:
|
|
275
|
+
hours = int(request.GET.get("hours", 24))
|
|
276
|
+
hours = min(max(hours, 1), 168)
|
|
277
|
+
|
|
278
|
+
# Get service statistics
|
|
279
|
+
service_stats = (
|
|
280
|
+
GRPCRequestLog.objects.recent(hours)
|
|
281
|
+
.values("service_name")
|
|
282
|
+
.annotate(
|
|
283
|
+
total=Count("id"),
|
|
284
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
285
|
+
errors=Count("id", filter=models.Q(status="error")),
|
|
286
|
+
avg_duration_ms=Avg("duration_ms"),
|
|
287
|
+
last_activity_at=Max("created_at"),
|
|
288
|
+
)
|
|
289
|
+
.order_by("-total")
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
services_list = []
|
|
293
|
+
for stats in service_stats:
|
|
294
|
+
services_list.append(
|
|
295
|
+
ServiceStatsSerializer(
|
|
296
|
+
service_name=stats["service_name"],
|
|
297
|
+
total=stats["total"],
|
|
298
|
+
successful=stats["successful"],
|
|
299
|
+
errors=stats["errors"],
|
|
300
|
+
avg_duration_ms=round(stats["avg_duration_ms"] or 0, 2),
|
|
301
|
+
last_activity_at=stats["last_activity_at"].isoformat() if stats["last_activity_at"] else None,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
response_data = {
|
|
306
|
+
"services": [svc.model_dump() for svc in services_list],
|
|
307
|
+
"total_services": len(services_list),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
serializer = ServiceListSerializer(**response_data)
|
|
311
|
+
return Response(serializer.model_dump())
|
|
312
|
+
|
|
313
|
+
except ValueError as e:
|
|
314
|
+
logger.warning(f"Service stats validation error: {e}")
|
|
315
|
+
return Response(
|
|
316
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Service stats error: {e}", exc_info=True)
|
|
320
|
+
return Response(
|
|
321
|
+
{"error": "Internal server error"},
|
|
322
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
@extend_schema(
|
|
326
|
+
tags=["gRPC Monitoring"],
|
|
327
|
+
summary="Get method statistics",
|
|
328
|
+
description="Returns statistics grouped by method.",
|
|
329
|
+
parameters=[
|
|
330
|
+
OpenApiParameter(
|
|
331
|
+
name="hours",
|
|
332
|
+
type=OpenApiTypes.INT,
|
|
333
|
+
location=OpenApiParameter.QUERY,
|
|
334
|
+
description="Statistics period in hours (default: 24)",
|
|
335
|
+
required=False,
|
|
336
|
+
),
|
|
337
|
+
OpenApiParameter(
|
|
338
|
+
name="service",
|
|
339
|
+
type=OpenApiTypes.STR,
|
|
340
|
+
location=OpenApiParameter.QUERY,
|
|
341
|
+
description="Filter by service name",
|
|
342
|
+
required=False,
|
|
343
|
+
),
|
|
344
|
+
],
|
|
345
|
+
responses={
|
|
346
|
+
200: MethodListSerializer,
|
|
347
|
+
400: {"description": "Invalid parameters"},
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
@action(detail=False, methods=["get"], url_path="methods")
|
|
351
|
+
def methods(self, request):
|
|
352
|
+
"""Get statistics per method."""
|
|
353
|
+
try:
|
|
354
|
+
hours = int(request.GET.get("hours", 24))
|
|
355
|
+
hours = min(max(hours, 1), 168)
|
|
356
|
+
service_filter = request.GET.get("service")
|
|
357
|
+
|
|
358
|
+
# Get method statistics
|
|
359
|
+
queryset = GRPCRequestLog.objects.recent(hours)
|
|
360
|
+
|
|
361
|
+
if service_filter:
|
|
362
|
+
queryset = queryset.filter(service_name=service_filter)
|
|
363
|
+
|
|
364
|
+
method_stats = (
|
|
365
|
+
queryset
|
|
366
|
+
.values("service_name", "method_name")
|
|
367
|
+
.annotate(
|
|
368
|
+
total=Count("id"),
|
|
369
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
370
|
+
errors=Count("id", filter=models.Q(status="error")),
|
|
371
|
+
avg_duration_ms=Avg("duration_ms"),
|
|
372
|
+
last_activity_at=Max("created_at"),
|
|
373
|
+
)
|
|
374
|
+
.order_by("-total")
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
methods_list = []
|
|
378
|
+
for stats in method_stats:
|
|
379
|
+
methods_list.append(
|
|
380
|
+
MethodStatsSerializer(
|
|
381
|
+
method_name=stats["method_name"],
|
|
382
|
+
service_name=stats["service_name"],
|
|
383
|
+
total=stats["total"],
|
|
384
|
+
successful=stats["successful"],
|
|
385
|
+
errors=stats["errors"],
|
|
386
|
+
avg_duration_ms=round(stats["avg_duration_ms"] or 0, 2),
|
|
387
|
+
last_activity_at=stats["last_activity_at"].isoformat() if stats["last_activity_at"] else None,
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
response_data = {
|
|
392
|
+
"methods": [method.model_dump() for method in methods_list],
|
|
393
|
+
"total_methods": len(methods_list),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
serializer = MethodListSerializer(**response_data)
|
|
397
|
+
return Response(serializer.model_dump())
|
|
398
|
+
|
|
399
|
+
except ValueError as e:
|
|
400
|
+
logger.warning(f"Method stats validation error: {e}")
|
|
401
|
+
return Response(
|
|
402
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
403
|
+
)
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.error(f"Method stats error: {e}", exc_info=True)
|
|
406
|
+
return Response(
|
|
407
|
+
{"error": "Internal server error"},
|
|
408
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
@extend_schema(
|
|
412
|
+
tags=["gRPC Monitoring"],
|
|
413
|
+
summary="Get request timeline",
|
|
414
|
+
description="Returns hourly or daily breakdown of request counts for charts.",
|
|
415
|
+
parameters=[
|
|
416
|
+
OpenApiParameter(
|
|
417
|
+
name="hours",
|
|
418
|
+
type=OpenApiTypes.INT,
|
|
419
|
+
location=OpenApiParameter.QUERY,
|
|
420
|
+
description="Time period in hours (default: 24)",
|
|
421
|
+
required=False,
|
|
422
|
+
),
|
|
423
|
+
OpenApiParameter(
|
|
424
|
+
name="interval",
|
|
425
|
+
type=OpenApiTypes.STR,
|
|
426
|
+
location=OpenApiParameter.QUERY,
|
|
427
|
+
description="Time interval: 'hour' or 'day' (default: hour)",
|
|
428
|
+
required=False,
|
|
429
|
+
),
|
|
430
|
+
],
|
|
431
|
+
responses={
|
|
432
|
+
200: {"description": "Timeline data"},
|
|
433
|
+
400: {"description": "Invalid parameters"},
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
@action(detail=False, methods=["get"], url_path="timeline")
|
|
437
|
+
def timeline(self, request):
|
|
438
|
+
"""Get request timeline breakdown for charts."""
|
|
439
|
+
try:
|
|
440
|
+
hours = int(request.GET.get("hours", 24))
|
|
441
|
+
hours = min(max(hours, 1), 168)
|
|
442
|
+
interval = request.GET.get("interval", "hour")
|
|
443
|
+
|
|
444
|
+
if interval not in ["hour", "day"]:
|
|
445
|
+
interval = "hour"
|
|
446
|
+
|
|
447
|
+
# Determine truncation function
|
|
448
|
+
trunc_func = TruncHour if interval == "hour" else TruncDay
|
|
449
|
+
|
|
450
|
+
# Get timeline data
|
|
451
|
+
timeline_data = (
|
|
452
|
+
GRPCRequestLog.objects.recent(hours)
|
|
453
|
+
.annotate(period=trunc_func("created_at"))
|
|
454
|
+
.values("period")
|
|
455
|
+
.annotate(
|
|
456
|
+
count=Count("id"),
|
|
457
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
458
|
+
errors=Count("id", filter=models.Q(status="error")),
|
|
459
|
+
timeout=Count("id", filter=models.Q(status="timeout")),
|
|
460
|
+
cancelled=Count("id", filter=models.Q(status="cancelled")),
|
|
461
|
+
)
|
|
462
|
+
.order_by("period")
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
timeline_list = []
|
|
466
|
+
for item in timeline_data:
|
|
467
|
+
timeline_list.append({
|
|
468
|
+
"timestamp": item["period"].isoformat(),
|
|
469
|
+
"count": item["count"],
|
|
470
|
+
"successful": item["successful"],
|
|
471
|
+
"errors": item["errors"],
|
|
472
|
+
"timeout": item["timeout"],
|
|
473
|
+
"cancelled": item["cancelled"],
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
response_data = {
|
|
477
|
+
"timeline": timeline_list,
|
|
478
|
+
"period_hours": hours,
|
|
479
|
+
"interval": interval,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return Response(response_data)
|
|
483
|
+
|
|
484
|
+
except ValueError as e:
|
|
485
|
+
logger.warning(f"Timeline validation error: {e}")
|
|
486
|
+
return Response(
|
|
487
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
488
|
+
)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
logger.error(f"Timeline error: {e}", exc_info=True)
|
|
491
|
+
return Response(
|
|
492
|
+
{"error": "Internal server error"},
|
|
493
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
__all__ = ["GRPCMonitorViewSet"]
|
|
@@ -177,18 +177,17 @@ class CloudflareApiKeyAdmin(PydanticAdmin):
|
|
|
177
177
|
if not sites:
|
|
178
178
|
return "No sites using this key"
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
result = "\n".join(site_list)
|
|
180
|
+
# Declarative site list generation
|
|
181
|
+
site_items = [
|
|
182
|
+
f"{'🔧 Maintenance' if site.maintenance_active else '✅ Active'} {site.name} ({site.domain})"
|
|
183
|
+
for site in sites
|
|
184
|
+
]
|
|
186
185
|
|
|
187
186
|
total_count = obj.cloudflaresite_set.count()
|
|
188
187
|
if total_count > 10:
|
|
189
|
-
|
|
188
|
+
site_items.append(f"... and {total_count - 10} more sites")
|
|
190
189
|
|
|
191
|
-
return
|
|
190
|
+
return "\n".join(site_items)
|
|
192
191
|
sites_using_key.short_description = "Sites Using This Key"
|
|
193
192
|
|
|
194
193
|
def changelist_view(self, request, extra_context=None):
|
|
@@ -256,10 +256,11 @@ class CloudflareSiteAdmin(PydanticAdmin):
|
|
|
256
256
|
if not logs:
|
|
257
257
|
return "No maintenance logs yet"
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
# Declarative log list generation
|
|
260
|
+
log_list = [
|
|
261
|
+
f"{('✅ Success' if log.status == MaintenanceLog.Status.SUCCESS else '❌ Failed' if log.status == MaintenanceLog.Status.FAILED else '⏳ Pending')} {log.action} - {log.created_at.strftime('%Y-%m-%d %H:%M')}"
|
|
262
|
+
for log in logs
|
|
263
|
+
]
|
|
263
264
|
|
|
264
265
|
return "\n".join(log_list)
|
|
265
266
|
logs_preview.short_description = "Recent Logs"
|
|
@@ -187,48 +187,38 @@ class UserBalanceAdmin(PydanticAdmin):
|
|
|
187
187
|
if not obj.pk:
|
|
188
188
|
return "Save to see breakdown"
|
|
189
189
|
|
|
190
|
-
# Build breakdown list
|
|
191
|
-
details = []
|
|
192
|
-
|
|
193
|
-
# Current Balance
|
|
194
|
-
details.append(self.html.inline([
|
|
195
|
-
self.html.span("Current Balance:", "font-semibold"),
|
|
196
|
-
self.html.span(f"${obj.balance_usd:.2f} USD", "")
|
|
197
|
-
], separator=" "))
|
|
198
|
-
|
|
199
|
-
# Total Deposited
|
|
200
|
-
details.append(self.html.inline([
|
|
201
|
-
self.html.span("Total Deposited:", "font-semibold"),
|
|
202
|
-
self.html.span(f"${obj.total_deposited:.2f} USD", "")
|
|
203
|
-
], separator=" "))
|
|
204
|
-
|
|
205
|
-
# Total Withdrawn
|
|
206
|
-
details.append(self.html.inline([
|
|
207
|
-
self.html.span("Total Withdrawn:", "font-semibold"),
|
|
208
|
-
self.html.span(f"${obj.total_withdrawn:.2f} USD", "")
|
|
209
|
-
], separator=" "))
|
|
210
|
-
|
|
211
190
|
# Calculate net
|
|
212
191
|
net = obj.total_deposited - obj.total_withdrawn
|
|
213
|
-
details.append(self.html.inline([
|
|
214
|
-
self.html.span("Net Deposits:", "font-semibold"),
|
|
215
|
-
self.html.span(f"${net:.2f} USD", "")
|
|
216
|
-
], separator=" "))
|
|
217
|
-
|
|
218
|
-
if obj.last_transaction_at:
|
|
219
|
-
details.append(self.html.inline([
|
|
220
|
-
self.html.span("Last Transaction:", "font-semibold"),
|
|
221
|
-
self.html.span(str(obj.last_transaction_at), "")
|
|
222
|
-
], separator=" "))
|
|
223
192
|
|
|
224
193
|
# Transaction count
|
|
225
194
|
txn_count = Transaction.objects.filter(user=obj.user).count()
|
|
226
|
-
details.append(self.html.inline([
|
|
227
|
-
self.html.span("Total Transactions:", "font-semibold"),
|
|
228
|
-
self.html.span(str(txn_count), "")
|
|
229
|
-
], separator=" "))
|
|
230
195
|
|
|
231
|
-
return
|
|
196
|
+
return self.html.breakdown(
|
|
197
|
+
self.html.key_value(
|
|
198
|
+
"Current Balance",
|
|
199
|
+
self.html.number(obj.balance_usd, precision=2, prefix="$", suffix=" USD")
|
|
200
|
+
),
|
|
201
|
+
self.html.key_value(
|
|
202
|
+
"Total Deposited",
|
|
203
|
+
self.html.number(obj.total_deposited, precision=2, prefix="$", suffix=" USD")
|
|
204
|
+
),
|
|
205
|
+
self.html.key_value(
|
|
206
|
+
"Total Withdrawn",
|
|
207
|
+
self.html.number(obj.total_withdrawn, precision=2, prefix="$", suffix=" USD")
|
|
208
|
+
),
|
|
209
|
+
self.html.key_value(
|
|
210
|
+
"Net Deposits",
|
|
211
|
+
self.html.number(net, precision=2, prefix="$", suffix=" USD")
|
|
212
|
+
),
|
|
213
|
+
self.html.key_value(
|
|
214
|
+
"Last Transaction",
|
|
215
|
+
str(obj.last_transaction_at)
|
|
216
|
+
) if obj.last_transaction_at else None,
|
|
217
|
+
self.html.key_value(
|
|
218
|
+
"Total Transactions",
|
|
219
|
+
str(txn_count)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
232
222
|
|
|
233
223
|
balance_breakdown_display.short_description = "Balance Breakdown"
|
|
234
224
|
|