django-cfg 1.4.62__py3-none-any.whl → 1.4.63__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 +1 -1
- django_cfg/apps/accounts/services/otp_service.py +3 -14
- django_cfg/apps/centrifugo/__init__.py +57 -0
- django_cfg/apps/centrifugo/admin/__init__.py +13 -0
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
- django_cfg/apps/centrifugo/admin/config.py +82 -0
- django_cfg/apps/centrifugo/apps.py +31 -0
- django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
- django_cfg/apps/centrifugo/codegen/README.md +242 -0
- django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
- django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
- django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
- django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
- django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
- django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
- django_cfg/apps/centrifugo/decorators.py +137 -0
- django_cfg/apps/centrifugo/management/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
- django_cfg/apps/centrifugo/managers/__init__.py +12 -0
- django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
- django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
- django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
- django_cfg/apps/centrifugo/models/__init__.py +11 -0
- django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
- django_cfg/apps/centrifugo/registry.py +106 -0
- django_cfg/apps/centrifugo/router.py +125 -0
- django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
- django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
- django_cfg/apps/centrifugo/serializers/channels.py +26 -0
- django_cfg/apps/centrifugo/serializers/health.py +17 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
- django_cfg/apps/centrifugo/serializers/stats.py +21 -0
- django_cfg/apps/centrifugo/services/__init__.py +12 -0
- django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
- django_cfg/apps/centrifugo/services/client/client.py +577 -0
- django_cfg/apps/centrifugo/services/client/config.py +228 -0
- django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
- django_cfg/apps/centrifugo/services/config_helper.py +63 -0
- django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
- django_cfg/apps/centrifugo/services/logging.py +677 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
- django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
- django_cfg/apps/centrifugo/urls.py +31 -0
- django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
- django_cfg/apps/centrifugo/views/__init__.py +15 -0
- django_cfg/apps/centrifugo/views/admin_api.py +374 -0
- django_cfg/apps/centrifugo/views/dashboard.py +15 -0
- django_cfg/apps/centrifugo/views/monitoring.py +286 -0
- django_cfg/apps/centrifugo/views/testing_api.py +422 -0
- django_cfg/apps/support/utils/support_email_service.py +5 -18
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
- django_cfg/apps/urls.py +5 -5
- django_cfg/core/base/config_model.py +4 -44
- django_cfg/core/builders/apps_builder.py +2 -2
- django_cfg/core/generation/integration_generators/third_party.py +8 -8
- django_cfg/core/utils/__init__.py +5 -0
- django_cfg/core/utils/url_helpers.py +73 -0
- django_cfg/modules/base.py +7 -7
- django_cfg/modules/django_client/core/__init__.py +2 -1
- django_cfg/modules/django_client/core/config/config.py +8 -0
- django_cfg/modules/django_client/core/generator/__init__.py +42 -2
- django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
- django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
- django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
- django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
- django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
- django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
- django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
- django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
- django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
- django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
- django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
- django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
- django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
- django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
- django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
- django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
- django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
- django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
- django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
- django_cfg/modules/django_client/system/schema_parser.py +5 -1
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
- django_cfg/modules/django_unfold/dashboard.py +25 -19
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/core.py +2 -0
- django_cfg/registry/modules.py +2 -2
- django_cfg/static/js/api/centrifugo/client.mjs +164 -0
- django_cfg/static/js/api/centrifugo/index.mjs +13 -0
- django_cfg/static/js/api/index.mjs +5 -5
- django_cfg/static/js/api/types.mjs +89 -26
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -70
- django_cfg/apps/ipc/README.md +0 -346
- django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
- django_cfg/apps/ipc/TESTING.md +0 -539
- django_cfg/apps/ipc/__init__.py +0 -60
- django_cfg/apps/ipc/admin.py +0 -232
- django_cfg/apps/ipc/apps.py +0 -98
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -229
- django_cfg/apps/ipc/serializers/__init__.py +0 -29
- django_cfg/apps/ipc/serializers/serializers.py +0 -343
- django_cfg/apps/ipc/services/__init__.py +0 -7
- django_cfg/apps/ipc/services/client/__init__.py +0 -23
- django_cfg/apps/ipc/services/client/client.py +0 -621
- django_cfg/apps/ipc/services/client/config.py +0 -214
- django_cfg/apps/ipc/services/client/exceptions.py +0 -201
- django_cfg/apps/ipc/services/logging.py +0 -239
- django_cfg/apps/ipc/services/monitor.py +0 -466
- django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
- django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
- django_cfg/apps/ipc/urls.py +0 -23
- django_cfg/apps/ipc/views/__init__.py +0 -13
- django_cfg/apps/ipc/views/dashboard.py +0 -15
- django_cfg/apps/ipc/views/monitoring.py +0 -251
- django_cfg/apps/ipc/views/testing.py +0 -285
- django_cfg/static/js/api/ipc/client.mjs +0 -114
- django_cfg/static/js/api/ipc/index.mjs +0 -13
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Monitoring ViewSet.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for monitoring Centrifugo publish statistics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.db.models import Avg, Count
|
|
11
|
+
from django_cfg.modules.django_logging import get_logger
|
|
12
|
+
from drf_spectacular.types import OpenApiTypes
|
|
13
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
14
|
+
from rest_framework import status, viewsets
|
|
15
|
+
from rest_framework.authentication import SessionAuthentication
|
|
16
|
+
from rest_framework.decorators import action
|
|
17
|
+
from rest_framework.permissions import IsAdminUser
|
|
18
|
+
from rest_framework.response import Response
|
|
19
|
+
|
|
20
|
+
from ..models import CentrifugoLog
|
|
21
|
+
from ..serializers import (
|
|
22
|
+
ChannelListSerializer,
|
|
23
|
+
ChannelStatsSerializer,
|
|
24
|
+
HealthCheckSerializer,
|
|
25
|
+
OverviewStatsSerializer,
|
|
26
|
+
RecentPublishesSerializer,
|
|
27
|
+
)
|
|
28
|
+
from ..services import get_centrifugo_config
|
|
29
|
+
|
|
30
|
+
logger = get_logger("centrifugo.monitoring")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CentrifugoMonitorViewSet(viewsets.ViewSet):
|
|
34
|
+
"""
|
|
35
|
+
ViewSet for Centrifugo monitoring and statistics.
|
|
36
|
+
|
|
37
|
+
Provides comprehensive monitoring data for Centrifugo publishes including:
|
|
38
|
+
- Health checks
|
|
39
|
+
- Overview statistics
|
|
40
|
+
- Recent publishes
|
|
41
|
+
- Channel-level statistics
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
authentication_classes = [SessionAuthentication]
|
|
45
|
+
permission_classes = [IsAdminUser]
|
|
46
|
+
|
|
47
|
+
@extend_schema(
|
|
48
|
+
tags=["Centrifugo Monitoring"],
|
|
49
|
+
summary="Get Centrifugo health status",
|
|
50
|
+
description="Returns the current health status of the Centrifugo client.",
|
|
51
|
+
responses={
|
|
52
|
+
200: HealthCheckSerializer,
|
|
53
|
+
503: {"description": "Service unavailable"},
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
@action(detail=False, methods=["get"], url_path="health")
|
|
57
|
+
def health(self, request):
|
|
58
|
+
"""Get health status of Centrifugo client."""
|
|
59
|
+
try:
|
|
60
|
+
config = get_centrifugo_config()
|
|
61
|
+
|
|
62
|
+
if not config:
|
|
63
|
+
return Response(
|
|
64
|
+
{"error": "Centrifugo not configured"},
|
|
65
|
+
status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
health_data = {
|
|
69
|
+
"status": "healthy",
|
|
70
|
+
"wrapper_url": config.wrapper_url,
|
|
71
|
+
"has_api_key": config.centrifugo_api_key is not None,
|
|
72
|
+
"timestamp": datetime.now().isoformat(),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
serializer = HealthCheckSerializer(**health_data)
|
|
76
|
+
return Response(serializer.model_dump())
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Health check error: {e}", exc_info=True)
|
|
80
|
+
return Response(
|
|
81
|
+
{"error": "Internal server error"},
|
|
82
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@extend_schema(
|
|
86
|
+
tags=["Centrifugo Monitoring"],
|
|
87
|
+
summary="Get overview statistics",
|
|
88
|
+
description="Returns overview statistics for Centrifugo publishes.",
|
|
89
|
+
parameters=[
|
|
90
|
+
OpenApiParameter(
|
|
91
|
+
name="hours",
|
|
92
|
+
type=OpenApiTypes.INT,
|
|
93
|
+
location=OpenApiParameter.QUERY,
|
|
94
|
+
description="Statistics period in hours (default: 24)",
|
|
95
|
+
required=False,
|
|
96
|
+
),
|
|
97
|
+
],
|
|
98
|
+
responses={
|
|
99
|
+
200: OverviewStatsSerializer,
|
|
100
|
+
400: {"description": "Invalid parameters"},
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
@action(detail=False, methods=["get"], url_path="overview")
|
|
104
|
+
def overview(self, request):
|
|
105
|
+
"""Get overview statistics for Centrifugo publishes."""
|
|
106
|
+
try:
|
|
107
|
+
hours = int(request.GET.get("hours", 24))
|
|
108
|
+
hours = min(max(hours, 1), 168) # 1 hour to 1 week
|
|
109
|
+
|
|
110
|
+
stats = CentrifugoLog.objects.get_statistics(hours=hours)
|
|
111
|
+
stats["period_hours"] = hours
|
|
112
|
+
|
|
113
|
+
serializer = OverviewStatsSerializer(**stats)
|
|
114
|
+
return Response(serializer.model_dump())
|
|
115
|
+
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
logger.warning(f"Overview stats validation error: {e}")
|
|
118
|
+
return Response(
|
|
119
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"Overview stats error: {e}", exc_info=True)
|
|
123
|
+
return Response(
|
|
124
|
+
{"error": "Internal server error"},
|
|
125
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@extend_schema(
|
|
129
|
+
tags=["Centrifugo Monitoring"],
|
|
130
|
+
summary="Get recent publishes",
|
|
131
|
+
description="Returns a list of recent Centrifugo publishes with their details.",
|
|
132
|
+
parameters=[
|
|
133
|
+
OpenApiParameter(
|
|
134
|
+
name="count",
|
|
135
|
+
type=OpenApiTypes.INT,
|
|
136
|
+
location=OpenApiParameter.QUERY,
|
|
137
|
+
description="Number of publishes to return (default: 50, max: 200)",
|
|
138
|
+
required=False,
|
|
139
|
+
),
|
|
140
|
+
OpenApiParameter(
|
|
141
|
+
name="channel",
|
|
142
|
+
type=OpenApiTypes.STR,
|
|
143
|
+
location=OpenApiParameter.QUERY,
|
|
144
|
+
description="Filter by channel name",
|
|
145
|
+
required=False,
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
responses={
|
|
149
|
+
200: RecentPublishesSerializer,
|
|
150
|
+
400: {"description": "Invalid parameters"},
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
@action(detail=False, methods=["get"], url_path="publishes")
|
|
154
|
+
def publishes(self, request):
|
|
155
|
+
"""Get recent Centrifugo publishes."""
|
|
156
|
+
try:
|
|
157
|
+
count = int(request.GET.get("count", 50))
|
|
158
|
+
count = min(count, 200) # Max 200
|
|
159
|
+
|
|
160
|
+
channel = request.GET.get("channel")
|
|
161
|
+
|
|
162
|
+
queryset = CentrifugoLog.objects.all()
|
|
163
|
+
|
|
164
|
+
if channel:
|
|
165
|
+
queryset = queryset.filter(channel=channel)
|
|
166
|
+
|
|
167
|
+
publishes_list = list(
|
|
168
|
+
queryset.order_by("-created_at")[:count].values(
|
|
169
|
+
"message_id",
|
|
170
|
+
"channel",
|
|
171
|
+
"status",
|
|
172
|
+
"wait_for_ack",
|
|
173
|
+
"acks_received",
|
|
174
|
+
"acks_expected",
|
|
175
|
+
"duration_ms",
|
|
176
|
+
"created_at",
|
|
177
|
+
"completed_at",
|
|
178
|
+
"error_code",
|
|
179
|
+
"error_message",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Convert datetime to ISO format
|
|
184
|
+
for pub in publishes_list:
|
|
185
|
+
if pub["created_at"]:
|
|
186
|
+
pub["created_at"] = pub["created_at"].isoformat()
|
|
187
|
+
if pub["completed_at"]:
|
|
188
|
+
pub["completed_at"] = pub["completed_at"].isoformat()
|
|
189
|
+
|
|
190
|
+
total = queryset.count()
|
|
191
|
+
|
|
192
|
+
response_data = {
|
|
193
|
+
"publishes": publishes_list,
|
|
194
|
+
"count": len(publishes_list),
|
|
195
|
+
"total_available": total,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
serializer = RecentPublishesSerializer(**response_data)
|
|
199
|
+
return Response(serializer.model_dump())
|
|
200
|
+
|
|
201
|
+
except ValueError as e:
|
|
202
|
+
logger.warning(f"Recent publishes validation error: {e}")
|
|
203
|
+
return Response(
|
|
204
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"Recent publishes error: {e}", exc_info=True)
|
|
208
|
+
return Response(
|
|
209
|
+
{"error": "Internal server error"},
|
|
210
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
@extend_schema(
|
|
214
|
+
tags=["Centrifugo Monitoring"],
|
|
215
|
+
summary="Get channel statistics",
|
|
216
|
+
description="Returns statistics grouped by channel.",
|
|
217
|
+
parameters=[
|
|
218
|
+
OpenApiParameter(
|
|
219
|
+
name="hours",
|
|
220
|
+
type=OpenApiTypes.INT,
|
|
221
|
+
location=OpenApiParameter.QUERY,
|
|
222
|
+
description="Statistics period in hours (default: 24)",
|
|
223
|
+
required=False,
|
|
224
|
+
),
|
|
225
|
+
],
|
|
226
|
+
responses={
|
|
227
|
+
200: ChannelListSerializer,
|
|
228
|
+
400: {"description": "Invalid parameters"},
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
@action(detail=False, methods=["get"], url_path="channels")
|
|
232
|
+
def channels(self, request):
|
|
233
|
+
"""Get statistics per channel."""
|
|
234
|
+
try:
|
|
235
|
+
hours = int(request.GET.get("hours", 24))
|
|
236
|
+
hours = min(max(hours, 1), 168)
|
|
237
|
+
|
|
238
|
+
# Get channel statistics
|
|
239
|
+
channel_stats = (
|
|
240
|
+
CentrifugoLog.objects.recent(hours)
|
|
241
|
+
.values("channel")
|
|
242
|
+
.annotate(
|
|
243
|
+
total=Count("id"),
|
|
244
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
245
|
+
failed=Count("id", filter=models.Q(status="failed")),
|
|
246
|
+
avg_duration_ms=Avg("duration_ms"),
|
|
247
|
+
avg_acks=Avg("acks_received"),
|
|
248
|
+
)
|
|
249
|
+
.order_by("-total")
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
channels_list = []
|
|
253
|
+
for stats in channel_stats:
|
|
254
|
+
channels_list.append(
|
|
255
|
+
ChannelStatsSerializer(
|
|
256
|
+
channel=stats["channel"],
|
|
257
|
+
total=stats["total"],
|
|
258
|
+
successful=stats["successful"],
|
|
259
|
+
failed=stats["failed"],
|
|
260
|
+
avg_duration_ms=round(stats["avg_duration_ms"] or 0, 2),
|
|
261
|
+
avg_acks=round(stats["avg_acks"] or 0, 2),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
response_data = {
|
|
266
|
+
"channels": [ch.model_dump() for ch in channels_list],
|
|
267
|
+
"total_channels": len(channels_list),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
serializer = ChannelListSerializer(**response_data)
|
|
271
|
+
return Response(serializer.model_dump())
|
|
272
|
+
|
|
273
|
+
except ValueError as e:
|
|
274
|
+
logger.warning(f"Channel stats validation error: {e}")
|
|
275
|
+
return Response(
|
|
276
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
277
|
+
)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Channel stats error: {e}", exc_info=True)
|
|
280
|
+
return Response(
|
|
281
|
+
{"error": "Internal server error"},
|
|
282
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
__all__ = ["CentrifugoMonitorViewSet"]
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Testing API.
|
|
3
|
+
|
|
4
|
+
Provides endpoints for live testing of Centrifugo integration from dashboard.
|
|
5
|
+
Includes connection tokens, publish proxying, and ACK management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import jwt
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from django_cfg.modules.django_logging import get_logger
|
|
17
|
+
from drf_spectacular.utils import extend_schema
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
from rest_framework import status, viewsets
|
|
20
|
+
from rest_framework.authentication import SessionAuthentication
|
|
21
|
+
from rest_framework.decorators import action
|
|
22
|
+
from rest_framework.permissions import IsAdminUser
|
|
23
|
+
from rest_framework.response import Response
|
|
24
|
+
|
|
25
|
+
from ..services import get_centrifugo_config
|
|
26
|
+
from ..services.client import CentrifugoClient
|
|
27
|
+
|
|
28
|
+
logger = get_logger("centrifugo.testing_api")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ========================================================================
|
|
32
|
+
# Request/Response Models
|
|
33
|
+
# ========================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConnectionTokenRequest(BaseModel):
|
|
37
|
+
"""Request model for connection token generation."""
|
|
38
|
+
|
|
39
|
+
user_id: str = Field(..., description="User ID for the connection")
|
|
40
|
+
channels: list[str] = Field(
|
|
41
|
+
default_factory=list, description="List of channels to authorize"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConnectionTokenResponse(BaseModel):
|
|
46
|
+
"""Response model for connection token."""
|
|
47
|
+
|
|
48
|
+
token: str = Field(..., description="JWT token for WebSocket connection")
|
|
49
|
+
centrifugo_url: str = Field(..., description="Centrifugo WebSocket URL")
|
|
50
|
+
expires_at: str = Field(..., description="Token expiration time (ISO 8601)")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PublishTestRequest(BaseModel):
|
|
54
|
+
"""Request model for test message publishing."""
|
|
55
|
+
|
|
56
|
+
channel: str = Field(..., description="Target channel name")
|
|
57
|
+
data: Dict[str, Any] = Field(..., description="Message data (any JSON object)")
|
|
58
|
+
wait_for_ack: bool = Field(
|
|
59
|
+
default=False, description="Wait for client acknowledgment"
|
|
60
|
+
)
|
|
61
|
+
ack_timeout: int = Field(
|
|
62
|
+
default=10, ge=1, le=60, description="ACK timeout in seconds"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PublishTestResponse(BaseModel):
|
|
67
|
+
"""Response model for test message publishing."""
|
|
68
|
+
|
|
69
|
+
success: bool = Field(..., description="Whether publish succeeded")
|
|
70
|
+
message_id: str = Field(..., description="Unique message ID")
|
|
71
|
+
channel: str = Field(..., description="Target channel")
|
|
72
|
+
acks_received: int = Field(default=0, description="Number of ACKs received")
|
|
73
|
+
delivered: bool = Field(default=False, description="Whether message was delivered")
|
|
74
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ManualAckRequest(BaseModel):
|
|
78
|
+
"""Request model for manual ACK sending."""
|
|
79
|
+
|
|
80
|
+
message_id: str = Field(..., description="Message ID to acknowledge")
|
|
81
|
+
client_id: str = Field(..., description="Client ID sending the ACK")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ManualAckResponse(BaseModel):
|
|
85
|
+
"""Response model for manual ACK."""
|
|
86
|
+
|
|
87
|
+
success: bool = Field(..., description="Whether ACK was sent successfully")
|
|
88
|
+
message_id: str = Field(..., description="Message ID that was acknowledged")
|
|
89
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ========================================================================
|
|
93
|
+
# Testing API ViewSet
|
|
94
|
+
# ========================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CentrifugoTestingAPIViewSet(viewsets.ViewSet):
|
|
98
|
+
"""
|
|
99
|
+
Centrifugo Testing API ViewSet.
|
|
100
|
+
|
|
101
|
+
Provides endpoints for interactive testing of Centrifugo integration
|
|
102
|
+
from the dashboard. Includes connection token generation, test message
|
|
103
|
+
publishing, and manual ACK management.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
authentication_classes = [SessionAuthentication]
|
|
107
|
+
permission_classes = [IsAdminUser]
|
|
108
|
+
|
|
109
|
+
def __init__(self, *args, **kwargs):
|
|
110
|
+
super().__init__(*args, **kwargs)
|
|
111
|
+
self._http_client = None
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
115
|
+
"""Get or create HTTP client for wrapper API calls."""
|
|
116
|
+
if self._http_client is None:
|
|
117
|
+
config = get_centrifugo_config()
|
|
118
|
+
if not config:
|
|
119
|
+
raise ValueError("Centrifugo not configured")
|
|
120
|
+
|
|
121
|
+
headers = {"Content-Type": "application/json"}
|
|
122
|
+
if config.wrapper_api_key:
|
|
123
|
+
headers["X-API-Key"] = config.wrapper_api_key
|
|
124
|
+
|
|
125
|
+
# Use wrapper URL as base
|
|
126
|
+
base_url = config.wrapper_url.rstrip("/")
|
|
127
|
+
|
|
128
|
+
self._http_client = httpx.AsyncClient(
|
|
129
|
+
base_url=base_url, headers=headers, timeout=httpx.Timeout(30.0)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return self._http_client
|
|
133
|
+
|
|
134
|
+
@extend_schema(
|
|
135
|
+
tags=["Centrifugo Testing"],
|
|
136
|
+
summary="Generate connection token",
|
|
137
|
+
description="Generate JWT token for WebSocket connection to Centrifugo.",
|
|
138
|
+
request=ConnectionTokenRequest,
|
|
139
|
+
responses={
|
|
140
|
+
200: ConnectionTokenResponse,
|
|
141
|
+
400: {"description": "Invalid request"},
|
|
142
|
+
500: {"description": "Server error"},
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
@action(detail=False, methods=["post"], url_path="connection-token")
|
|
146
|
+
def connection_token(self, request):
|
|
147
|
+
"""
|
|
148
|
+
Generate JWT token for WebSocket connection.
|
|
149
|
+
|
|
150
|
+
Returns token that can be used to connect to Centrifugo from browser.
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
config = get_centrifugo_config()
|
|
154
|
+
if not config:
|
|
155
|
+
return Response(
|
|
156
|
+
{"error": "Centrifugo not configured"},
|
|
157
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Parse request
|
|
161
|
+
req_data = ConnectionTokenRequest(**request.data)
|
|
162
|
+
|
|
163
|
+
# Generate JWT token
|
|
164
|
+
now = int(time.time())
|
|
165
|
+
exp = now + 3600 # 1 hour
|
|
166
|
+
|
|
167
|
+
payload = {
|
|
168
|
+
"sub": req_data.user_id,
|
|
169
|
+
"exp": exp,
|
|
170
|
+
"iat": now,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Add channels if provided
|
|
174
|
+
if req_data.channels:
|
|
175
|
+
payload["channels"] = req_data.channels
|
|
176
|
+
|
|
177
|
+
# Use HMAC secret from config or Django SECRET_KEY
|
|
178
|
+
secret = config.centrifugo_token_hmac_secret or settings.SECRET_KEY
|
|
179
|
+
|
|
180
|
+
token = jwt.encode(payload, secret, algorithm="HS256")
|
|
181
|
+
|
|
182
|
+
response = ConnectionTokenResponse(
|
|
183
|
+
token=token,
|
|
184
|
+
centrifugo_url=config.centrifugo_url,
|
|
185
|
+
expires_at=datetime.utcfromtimestamp(exp).isoformat() + "Z",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return Response(response.model_dump())
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Failed to generate connection token: {e}", exc_info=True)
|
|
192
|
+
return Response(
|
|
193
|
+
{"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@extend_schema(
|
|
197
|
+
tags=["Centrifugo Testing"],
|
|
198
|
+
summary="Publish test message",
|
|
199
|
+
description="Publish test message to Centrifugo via wrapper with optional ACK tracking.",
|
|
200
|
+
request=PublishTestRequest,
|
|
201
|
+
responses={
|
|
202
|
+
200: PublishTestResponse,
|
|
203
|
+
400: {"description": "Invalid request"},
|
|
204
|
+
500: {"description": "Server error"},
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
@action(detail=False, methods=["post"], url_path="publish-test")
|
|
208
|
+
def publish_test(self, request):
|
|
209
|
+
"""
|
|
210
|
+
Publish test message via wrapper.
|
|
211
|
+
|
|
212
|
+
Proxies request to Centrifugo wrapper with ACK tracking support.
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
req_data = PublishTestRequest(**request.data)
|
|
216
|
+
|
|
217
|
+
# Call wrapper API
|
|
218
|
+
result = asyncio.run(
|
|
219
|
+
self._publish_to_wrapper(
|
|
220
|
+
channel=req_data.channel,
|
|
221
|
+
data=req_data.data,
|
|
222
|
+
wait_for_ack=req_data.wait_for_ack,
|
|
223
|
+
ack_timeout=req_data.ack_timeout,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
response = PublishTestResponse(
|
|
228
|
+
success=result.get("published", False),
|
|
229
|
+
message_id=result.get("message_id", ""),
|
|
230
|
+
channel=result.get("channel", req_data.channel),
|
|
231
|
+
acks_received=result.get("acks_received", 0),
|
|
232
|
+
delivered=result.get("delivered", False),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return Response(response.model_dump())
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Failed to publish test message: {e}", exc_info=True)
|
|
239
|
+
return Response(
|
|
240
|
+
PublishTestResponse(
|
|
241
|
+
success=False,
|
|
242
|
+
message_id="",
|
|
243
|
+
channel=request.data.get("channel", ""),
|
|
244
|
+
error=str(e),
|
|
245
|
+
).model_dump(),
|
|
246
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@extend_schema(
|
|
250
|
+
tags=["Centrifugo Testing"],
|
|
251
|
+
summary="Send manual ACK",
|
|
252
|
+
description="Manually send ACK for a message to the wrapper. Pass message_id in request body.",
|
|
253
|
+
request=ManualAckRequest,
|
|
254
|
+
responses={
|
|
255
|
+
200: ManualAckResponse,
|
|
256
|
+
400: {"description": "Invalid request"},
|
|
257
|
+
500: {"description": "Server error"},
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
@action(detail=False, methods=["post"], url_path="send-ack")
|
|
261
|
+
def send_ack(self, request):
|
|
262
|
+
"""
|
|
263
|
+
Send manual ACK for message.
|
|
264
|
+
|
|
265
|
+
Proxies ACK to wrapper for testing ACK flow.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
req_data = ManualAckRequest(**request.data)
|
|
269
|
+
|
|
270
|
+
# Send ACK to wrapper
|
|
271
|
+
result = asyncio.run(
|
|
272
|
+
self._send_ack_to_wrapper(
|
|
273
|
+
message_id=req_data.message_id, client_id=req_data.client_id
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
response = ManualAckResponse(
|
|
278
|
+
success=result.get("status") == "ok",
|
|
279
|
+
message_id=req_data.message_id,
|
|
280
|
+
error=result.get("message") if result.get("status") != "ok" else None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return Response(response.model_dump())
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.error(f"Failed to send ACK: {e}", exc_info=True)
|
|
287
|
+
return Response(
|
|
288
|
+
ManualAckResponse(
|
|
289
|
+
success=False,
|
|
290
|
+
message_id=request.data.get("message_id", ""),
|
|
291
|
+
error=str(e)
|
|
292
|
+
).model_dump(),
|
|
293
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def _publish_to_wrapper(
|
|
297
|
+
self, channel: str, data: Dict[str, Any], wait_for_ack: bool, ack_timeout: int
|
|
298
|
+
) -> Dict[str, Any]:
|
|
299
|
+
"""
|
|
300
|
+
Publish message to wrapper API.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
channel: Target channel
|
|
304
|
+
data: Message data
|
|
305
|
+
wait_for_ack: Whether to wait for ACK
|
|
306
|
+
ack_timeout: ACK timeout in seconds
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Wrapper API response
|
|
310
|
+
"""
|
|
311
|
+
payload = {
|
|
312
|
+
"channel": channel,
|
|
313
|
+
"data": data,
|
|
314
|
+
"wait_for_ack": wait_for_ack,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if wait_for_ack:
|
|
318
|
+
payload["ack_timeout"] = ack_timeout
|
|
319
|
+
|
|
320
|
+
response = await self.http_client.post("/api/publish", json=payload)
|
|
321
|
+
response.raise_for_status()
|
|
322
|
+
return response.json()
|
|
323
|
+
|
|
324
|
+
async def _send_ack_to_wrapper(
|
|
325
|
+
self, message_id: str, client_id: str
|
|
326
|
+
) -> Dict[str, Any]:
|
|
327
|
+
"""
|
|
328
|
+
Send ACK to wrapper API.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
message_id: Message ID to acknowledge
|
|
332
|
+
client_id: Client ID sending the ACK
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Wrapper API response
|
|
336
|
+
"""
|
|
337
|
+
payload = {"client_id": client_id}
|
|
338
|
+
|
|
339
|
+
response = await self.http_client.post(
|
|
340
|
+
f"/api/ack/{message_id}", json=payload
|
|
341
|
+
)
|
|
342
|
+
response.raise_for_status()
|
|
343
|
+
return response.json()
|
|
344
|
+
|
|
345
|
+
@extend_schema(
|
|
346
|
+
tags=["Centrifugo Testing"],
|
|
347
|
+
summary="Publish with database logging",
|
|
348
|
+
description="Publish message using CentrifugoClient with database logging. This will create CentrifugoLog records.",
|
|
349
|
+
request=PublishTestRequest,
|
|
350
|
+
responses={
|
|
351
|
+
200: PublishTestResponse,
|
|
352
|
+
400: {"description": "Invalid request"},
|
|
353
|
+
500: {"description": "Server error"},
|
|
354
|
+
},
|
|
355
|
+
)
|
|
356
|
+
@action(detail=False, methods=["post"], url_path="publish-with-logging")
|
|
357
|
+
def publish_with_logging(self, request):
|
|
358
|
+
"""
|
|
359
|
+
Publish message using CentrifugoClient with database logging.
|
|
360
|
+
|
|
361
|
+
This endpoint uses the production CentrifugoClient which logs all
|
|
362
|
+
publishes to the database (CentrifugoLog model).
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
req_data = PublishTestRequest(**request.data)
|
|
366
|
+
|
|
367
|
+
# Use CentrifugoClient for publishing
|
|
368
|
+
client = CentrifugoClient()
|
|
369
|
+
|
|
370
|
+
# Publish message
|
|
371
|
+
result = asyncio.run(
|
|
372
|
+
client.publish_with_ack(
|
|
373
|
+
channel=req_data.channel,
|
|
374
|
+
data=req_data.data,
|
|
375
|
+
ack_timeout=req_data.ack_timeout if req_data.wait_for_ack else None,
|
|
376
|
+
user=request.user if request.user.is_authenticated else None,
|
|
377
|
+
caller_ip=request.META.get("REMOTE_ADDR"),
|
|
378
|
+
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
379
|
+
)
|
|
380
|
+
if req_data.wait_for_ack
|
|
381
|
+
else client.publish(
|
|
382
|
+
channel=req_data.channel,
|
|
383
|
+
data=req_data.data,
|
|
384
|
+
user=request.user if request.user.is_authenticated else None,
|
|
385
|
+
caller_ip=request.META.get("REMOTE_ADDR"),
|
|
386
|
+
user_agent=request.META.get("HTTP_USER_AGENT"),
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Convert PublishResponse to dict
|
|
391
|
+
response_data = {
|
|
392
|
+
"success": result.published,
|
|
393
|
+
"message_id": result.message_id,
|
|
394
|
+
"channel": req_data.channel,
|
|
395
|
+
"delivered": result.delivered if req_data.wait_for_ack else None,
|
|
396
|
+
"acks_received": result.acks_received if req_data.wait_for_ack else 0,
|
|
397
|
+
"logged_to_database": True, # CentrifugoClient always logs
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return Response(response_data)
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f"Failed to publish with logging: {e}", exc_info=True)
|
|
404
|
+
return Response(
|
|
405
|
+
{"success": False, "error": str(e)},
|
|
406
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def __del__(self):
|
|
410
|
+
"""Cleanup HTTP client on deletion."""
|
|
411
|
+
if self._http_client:
|
|
412
|
+
try:
|
|
413
|
+
loop = asyncio.get_event_loop()
|
|
414
|
+
if loop.is_running():
|
|
415
|
+
loop.create_task(self._http_client.aclose())
|
|
416
|
+
else:
|
|
417
|
+
loop.run_until_complete(self._http_client.aclose())
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
__all__ = ["CentrifugoTestingAPIViewSet"]
|