django-cfg 1.5.8__py3-none-any.whl → 1.5.20__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/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centrifugo Publishing Interceptor for gRPC.
|
|
3
|
+
|
|
4
|
+
Automatically publishes gRPC call metadata to Centrifugo WebSocket channels.
|
|
5
|
+
Works alongside CentrifugoBridgeMixin for complete event visibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime, timezone as tz
|
|
13
|
+
from typing import Callable, Optional, Any, Dict
|
|
14
|
+
|
|
15
|
+
import grpc
|
|
16
|
+
import grpc.aio
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CentrifugoInterceptor(grpc.aio.ServerInterceptor):
|
|
22
|
+
"""
|
|
23
|
+
Async gRPC interceptor that publishes call metadata to Centrifugo.
|
|
24
|
+
|
|
25
|
+
Automatically publishes:
|
|
26
|
+
- RPC method invocations (start/end)
|
|
27
|
+
- Timing information
|
|
28
|
+
- Status codes
|
|
29
|
+
- Message counts
|
|
30
|
+
- Error information
|
|
31
|
+
- Client peer information
|
|
32
|
+
|
|
33
|
+
Works in parallel with CentrifugoBridgeMixin:
|
|
34
|
+
- Interceptor: Publishes RPC-level metadata (method, timing, status)
|
|
35
|
+
- Mixin: Publishes message-level data (protobuf field contents)
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
```python
|
|
39
|
+
# In Django settings
|
|
40
|
+
GRPC_FRAMEWORK = {
|
|
41
|
+
"SERVER_INTERCEPTORS": [
|
|
42
|
+
"django_cfg.apps.integrations.grpc.interceptors.CentrifugoInterceptor",
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Channel naming:
|
|
48
|
+
- RPC calls: `grpc#{service}#{method}#meta`
|
|
49
|
+
- Errors: `grpc#{service}#{method}#errors`
|
|
50
|
+
|
|
51
|
+
Published metadata:
|
|
52
|
+
{
|
|
53
|
+
"event_type": "rpc_start" | "rpc_end" | "rpc_error",
|
|
54
|
+
"method": "/service.Service/Method",
|
|
55
|
+
"service": "service.Service",
|
|
56
|
+
"method_name": "Method",
|
|
57
|
+
"peer": "ipv4:127.0.0.1:12345",
|
|
58
|
+
"timestamp": "2025-11-05T...",
|
|
59
|
+
"duration_ms": 123.45, # Only on rpc_end
|
|
60
|
+
"status": "OK" | "ERROR",
|
|
61
|
+
"message_count": 10, # For streaming
|
|
62
|
+
"error": {...}, # Only on error
|
|
63
|
+
}
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
enabled: bool = True,
|
|
69
|
+
publish_start: bool = False,
|
|
70
|
+
publish_end: bool = True,
|
|
71
|
+
publish_errors: bool = True,
|
|
72
|
+
publish_stream_messages: bool = False,
|
|
73
|
+
channel_template: str = "grpc#{service}#{method}#meta",
|
|
74
|
+
error_channel_template: str = "grpc#{service}#{method}#errors",
|
|
75
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Initialize Centrifugo interceptor.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
enabled: Enable/disable publishing
|
|
82
|
+
publish_start: Publish RPC start events
|
|
83
|
+
publish_end: Publish RPC completion events
|
|
84
|
+
publish_errors: Publish RPC error events
|
|
85
|
+
publish_stream_messages: Publish each streaming message (can be noisy)
|
|
86
|
+
channel_template: Channel name template for metadata
|
|
87
|
+
error_channel_template: Channel name template for errors
|
|
88
|
+
metadata: Additional metadata to include in all events
|
|
89
|
+
"""
|
|
90
|
+
self.enabled = enabled
|
|
91
|
+
self.publish_start = publish_start
|
|
92
|
+
self.publish_end = publish_end
|
|
93
|
+
self.publish_errors = publish_errors
|
|
94
|
+
self.publish_stream_messages = publish_stream_messages
|
|
95
|
+
self.channel_template = channel_template
|
|
96
|
+
self.error_channel_template = error_channel_template
|
|
97
|
+
self.metadata = metadata or {}
|
|
98
|
+
|
|
99
|
+
self._centrifugo_publisher: Optional[Any] = None
|
|
100
|
+
self._initialize_publisher()
|
|
101
|
+
|
|
102
|
+
def _initialize_publisher(self):
|
|
103
|
+
"""Initialize Centrifugo publisher lazily with direct client."""
|
|
104
|
+
if not self.enabled:
|
|
105
|
+
logger.debug("CentrifugoInterceptor disabled")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
from django_cfg.apps.integrations.centrifugo.services import get_centrifugo_publisher
|
|
110
|
+
# Use Publisher with DirectClient (use_direct=True by default)
|
|
111
|
+
# This bypasses wrapper and goes directly to Centrifugo
|
|
112
|
+
self._centrifugo_publisher = get_centrifugo_publisher()
|
|
113
|
+
logger.info("CentrifugoInterceptor initialized with DirectCentrifugoClient")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(
|
|
116
|
+
f"Failed to initialize Centrifugo publisher in interceptor: {e}. "
|
|
117
|
+
f"Interceptor will continue without publishing."
|
|
118
|
+
)
|
|
119
|
+
self.enabled = False
|
|
120
|
+
|
|
121
|
+
async def intercept_service(
|
|
122
|
+
self,
|
|
123
|
+
continuation: Callable,
|
|
124
|
+
handler_call_details: grpc.HandlerCallDetails,
|
|
125
|
+
) -> grpc.RpcMethodHandler:
|
|
126
|
+
"""
|
|
127
|
+
Intercept async gRPC service call for Centrifugo publishing.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
continuation: Function to invoke the next interceptor or handler
|
|
131
|
+
handler_call_details: Details about the RPC call
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
RPC method handler with Centrifugo publishing
|
|
135
|
+
"""
|
|
136
|
+
if not self.enabled or not self._centrifugo_publisher:
|
|
137
|
+
# Pass through without interception
|
|
138
|
+
return await continuation(handler_call_details)
|
|
139
|
+
|
|
140
|
+
method_name = handler_call_details.method
|
|
141
|
+
peer = self._extract_peer(handler_call_details.invocation_metadata)
|
|
142
|
+
service_name, method_short = self._parse_method_name(method_name)
|
|
143
|
+
|
|
144
|
+
# Publish start event
|
|
145
|
+
if self.publish_start:
|
|
146
|
+
await self._publish_event(
|
|
147
|
+
event_type="rpc_start",
|
|
148
|
+
method=method_name,
|
|
149
|
+
service=service_name,
|
|
150
|
+
method_name=method_short,
|
|
151
|
+
peer=peer,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Get handler and wrap it
|
|
155
|
+
handler = await continuation(handler_call_details)
|
|
156
|
+
|
|
157
|
+
if handler is None:
|
|
158
|
+
logger.warning(f"[CentrifugoInterceptor] No handler found for {method_name}")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Wrap handler methods to publish events
|
|
162
|
+
return self._wrap_handler(handler, method_name, service_name, method_short, peer)
|
|
163
|
+
|
|
164
|
+
def _wrap_handler(
|
|
165
|
+
self,
|
|
166
|
+
handler: grpc.RpcMethodHandler,
|
|
167
|
+
method_name: str,
|
|
168
|
+
service_name: str,
|
|
169
|
+
method_short: str,
|
|
170
|
+
peer: str,
|
|
171
|
+
) -> grpc.RpcMethodHandler:
|
|
172
|
+
"""
|
|
173
|
+
Wrap handler to add Centrifugo publishing.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
handler: Original RPC method handler
|
|
177
|
+
method_name: Full gRPC method name
|
|
178
|
+
service_name: Service name
|
|
179
|
+
method_short: Short method name
|
|
180
|
+
peer: Client peer information
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Wrapped RPC method handler
|
|
184
|
+
"""
|
|
185
|
+
# Determine handler type and wrap accordingly
|
|
186
|
+
if handler.unary_unary:
|
|
187
|
+
wrapped = self._wrap_unary_unary(
|
|
188
|
+
handler.unary_unary, method_name, service_name, method_short, peer
|
|
189
|
+
)
|
|
190
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
191
|
+
wrapped,
|
|
192
|
+
request_deserializer=handler.request_deserializer,
|
|
193
|
+
response_serializer=handler.response_serializer,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if handler.unary_stream:
|
|
197
|
+
wrapped = self._wrap_unary_stream(
|
|
198
|
+
handler.unary_stream, method_name, service_name, method_short, peer
|
|
199
|
+
)
|
|
200
|
+
return grpc.unary_stream_rpc_method_handler(
|
|
201
|
+
wrapped,
|
|
202
|
+
request_deserializer=handler.request_deserializer,
|
|
203
|
+
response_serializer=handler.response_serializer,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if handler.stream_unary:
|
|
207
|
+
wrapped = self._wrap_stream_unary(
|
|
208
|
+
handler.stream_unary, method_name, service_name, method_short, peer
|
|
209
|
+
)
|
|
210
|
+
return grpc.stream_unary_rpc_method_handler(
|
|
211
|
+
wrapped,
|
|
212
|
+
request_deserializer=handler.request_deserializer,
|
|
213
|
+
response_serializer=handler.response_serializer,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if handler.stream_stream:
|
|
217
|
+
wrapped = self._wrap_stream_stream(
|
|
218
|
+
handler.stream_stream, method_name, service_name, method_short, peer
|
|
219
|
+
)
|
|
220
|
+
return grpc.stream_stream_rpc_method_handler(
|
|
221
|
+
wrapped,
|
|
222
|
+
request_deserializer=handler.request_deserializer,
|
|
223
|
+
response_serializer=handler.response_serializer,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return handler
|
|
227
|
+
|
|
228
|
+
def _wrap_unary_unary(self, behavior, method_name, service_name, method_short, peer):
|
|
229
|
+
"""Wrap unary-unary RPC."""
|
|
230
|
+
async def wrapper(request, context):
|
|
231
|
+
start_time = time.time()
|
|
232
|
+
try:
|
|
233
|
+
response = await behavior(request, context)
|
|
234
|
+
duration = (time.time() - start_time) * 1000
|
|
235
|
+
|
|
236
|
+
if self.publish_end:
|
|
237
|
+
await self._publish_event(
|
|
238
|
+
event_type="rpc_end",
|
|
239
|
+
method=method_name,
|
|
240
|
+
service=service_name,
|
|
241
|
+
method_name=method_short,
|
|
242
|
+
peer=peer,
|
|
243
|
+
duration_ms=duration,
|
|
244
|
+
status="OK",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return response
|
|
248
|
+
except Exception as e:
|
|
249
|
+
duration = (time.time() - start_time) * 1000
|
|
250
|
+
|
|
251
|
+
if self.publish_errors:
|
|
252
|
+
await self._publish_error(
|
|
253
|
+
method=method_name,
|
|
254
|
+
service=service_name,
|
|
255
|
+
method_name=method_short,
|
|
256
|
+
peer=peer,
|
|
257
|
+
duration_ms=duration,
|
|
258
|
+
error=e,
|
|
259
|
+
)
|
|
260
|
+
raise
|
|
261
|
+
|
|
262
|
+
return wrapper
|
|
263
|
+
|
|
264
|
+
def _wrap_unary_stream(self, behavior, method_name, service_name, method_short, peer):
|
|
265
|
+
"""Wrap unary-stream RPC."""
|
|
266
|
+
async def wrapper(request, context):
|
|
267
|
+
start_time = time.time()
|
|
268
|
+
message_count = 0
|
|
269
|
+
try:
|
|
270
|
+
async for response in behavior(request, context):
|
|
271
|
+
message_count += 1
|
|
272
|
+
|
|
273
|
+
if self.publish_stream_messages:
|
|
274
|
+
await self._publish_event(
|
|
275
|
+
event_type="stream_message",
|
|
276
|
+
method=method_name,
|
|
277
|
+
service=service_name,
|
|
278
|
+
method_name=method_short,
|
|
279
|
+
peer=peer,
|
|
280
|
+
message_count=message_count,
|
|
281
|
+
direction="server_to_client",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
yield response
|
|
285
|
+
|
|
286
|
+
duration = (time.time() - start_time) * 1000
|
|
287
|
+
|
|
288
|
+
if self.publish_end:
|
|
289
|
+
await self._publish_event(
|
|
290
|
+
event_type="rpc_end",
|
|
291
|
+
method=method_name,
|
|
292
|
+
service=service_name,
|
|
293
|
+
method_name=method_short,
|
|
294
|
+
peer=peer,
|
|
295
|
+
duration_ms=duration,
|
|
296
|
+
status="OK",
|
|
297
|
+
message_count=message_count,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
duration = (time.time() - start_time) * 1000
|
|
302
|
+
|
|
303
|
+
if self.publish_errors:
|
|
304
|
+
await self._publish_error(
|
|
305
|
+
method=method_name,
|
|
306
|
+
service=service_name,
|
|
307
|
+
method_name=method_short,
|
|
308
|
+
peer=peer,
|
|
309
|
+
duration_ms=duration,
|
|
310
|
+
error=e,
|
|
311
|
+
message_count=message_count,
|
|
312
|
+
)
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
return wrapper
|
|
316
|
+
|
|
317
|
+
def _wrap_stream_unary(self, behavior, method_name, service_name, method_short, peer):
|
|
318
|
+
"""Wrap stream-unary RPC."""
|
|
319
|
+
async def wrapper(request_iterator, context):
|
|
320
|
+
start_time = time.time()
|
|
321
|
+
message_count = 0
|
|
322
|
+
try:
|
|
323
|
+
# Count incoming messages
|
|
324
|
+
requests = []
|
|
325
|
+
async for req in request_iterator:
|
|
326
|
+
message_count += 1
|
|
327
|
+
|
|
328
|
+
if self.publish_stream_messages:
|
|
329
|
+
await self._publish_event(
|
|
330
|
+
event_type="stream_message",
|
|
331
|
+
method=method_name,
|
|
332
|
+
service=service_name,
|
|
333
|
+
method_name=method_short,
|
|
334
|
+
peer=peer,
|
|
335
|
+
message_count=message_count,
|
|
336
|
+
direction="client_to_server",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
requests.append(req)
|
|
340
|
+
|
|
341
|
+
# Process
|
|
342
|
+
async def request_iter():
|
|
343
|
+
for r in requests:
|
|
344
|
+
yield r
|
|
345
|
+
|
|
346
|
+
response = await behavior(request_iter(), context)
|
|
347
|
+
duration = (time.time() - start_time) * 1000
|
|
348
|
+
|
|
349
|
+
if self.publish_end:
|
|
350
|
+
await self._publish_event(
|
|
351
|
+
event_type="rpc_end",
|
|
352
|
+
method=method_name,
|
|
353
|
+
service=service_name,
|
|
354
|
+
method_name=method_short,
|
|
355
|
+
peer=peer,
|
|
356
|
+
duration_ms=duration,
|
|
357
|
+
status="OK",
|
|
358
|
+
message_count=message_count,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return response
|
|
362
|
+
except Exception as e:
|
|
363
|
+
duration = (time.time() - start_time) * 1000
|
|
364
|
+
|
|
365
|
+
if self.publish_errors:
|
|
366
|
+
await self._publish_error(
|
|
367
|
+
method=method_name,
|
|
368
|
+
service=service_name,
|
|
369
|
+
method_name=method_short,
|
|
370
|
+
peer=peer,
|
|
371
|
+
duration_ms=duration,
|
|
372
|
+
error=e,
|
|
373
|
+
message_count=message_count,
|
|
374
|
+
)
|
|
375
|
+
raise
|
|
376
|
+
|
|
377
|
+
return wrapper
|
|
378
|
+
|
|
379
|
+
def _wrap_stream_stream(self, behavior, method_name, service_name, method_short, peer):
|
|
380
|
+
"""Wrap bidirectional streaming RPC."""
|
|
381
|
+
async def wrapper(request_iterator, context):
|
|
382
|
+
start_time = time.time()
|
|
383
|
+
in_count = 0
|
|
384
|
+
out_count = 0
|
|
385
|
+
try:
|
|
386
|
+
# Wrap request iterator to count messages
|
|
387
|
+
async def counting_iterator():
|
|
388
|
+
nonlocal in_count
|
|
389
|
+
async for req in request_iterator:
|
|
390
|
+
in_count += 1
|
|
391
|
+
|
|
392
|
+
if self.publish_stream_messages:
|
|
393
|
+
await self._publish_event(
|
|
394
|
+
event_type="stream_message",
|
|
395
|
+
method=method_name,
|
|
396
|
+
service=service_name,
|
|
397
|
+
method_name=method_short,
|
|
398
|
+
peer=peer,
|
|
399
|
+
message_count=in_count,
|
|
400
|
+
direction="client_to_server",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
yield req
|
|
404
|
+
|
|
405
|
+
# Stream responses
|
|
406
|
+
async for response in behavior(counting_iterator(), context):
|
|
407
|
+
out_count += 1
|
|
408
|
+
|
|
409
|
+
if self.publish_stream_messages:
|
|
410
|
+
await self._publish_event(
|
|
411
|
+
event_type="stream_message",
|
|
412
|
+
method=method_name,
|
|
413
|
+
service=service_name,
|
|
414
|
+
method_name=method_short,
|
|
415
|
+
peer=peer,
|
|
416
|
+
message_count=out_count,
|
|
417
|
+
direction="server_to_client",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
yield response
|
|
421
|
+
|
|
422
|
+
duration = (time.time() - start_time) * 1000
|
|
423
|
+
|
|
424
|
+
if self.publish_end:
|
|
425
|
+
await self._publish_event(
|
|
426
|
+
event_type="rpc_end",
|
|
427
|
+
method=method_name,
|
|
428
|
+
service=service_name,
|
|
429
|
+
method_name=method_short,
|
|
430
|
+
peer=peer,
|
|
431
|
+
duration_ms=duration,
|
|
432
|
+
status="OK",
|
|
433
|
+
in_message_count=in_count,
|
|
434
|
+
out_message_count=out_count,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
duration = (time.time() - start_time) * 1000
|
|
439
|
+
|
|
440
|
+
if self.publish_errors:
|
|
441
|
+
await self._publish_error(
|
|
442
|
+
method=method_name,
|
|
443
|
+
service=service_name,
|
|
444
|
+
method_name=method_short,
|
|
445
|
+
peer=peer,
|
|
446
|
+
duration_ms=duration,
|
|
447
|
+
error=e,
|
|
448
|
+
in_message_count=in_count,
|
|
449
|
+
out_message_count=out_count,
|
|
450
|
+
)
|
|
451
|
+
raise
|
|
452
|
+
|
|
453
|
+
return wrapper
|
|
454
|
+
|
|
455
|
+
async def _publish_event(self, **data):
|
|
456
|
+
"""Publish event to Centrifugo via Publisher."""
|
|
457
|
+
try:
|
|
458
|
+
# Build channel name
|
|
459
|
+
channel = self.channel_template.format(
|
|
460
|
+
service=data.get('service', 'unknown'),
|
|
461
|
+
method=data.get('method_name', 'unknown'),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Use Publisher's publish_grpc_event for type-safe gRPC events
|
|
465
|
+
await self._centrifugo_publisher.publish_grpc_event(
|
|
466
|
+
channel=channel,
|
|
467
|
+
method=data.get('method', ''),
|
|
468
|
+
status=data.get('status', 'UNKNOWN'),
|
|
469
|
+
duration_ms=data.get('duration_ms', 0.0),
|
|
470
|
+
peer=data.get('peer'),
|
|
471
|
+
metadata={
|
|
472
|
+
'event_type': data.get('event_type'),
|
|
473
|
+
**self.metadata,
|
|
474
|
+
},
|
|
475
|
+
**{k: v for k, v in data.items() if k not in ['method', 'status', 'duration_ms', 'peer', 'event_type', 'service', 'method_name']},
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
logger.debug(f"Published gRPC event to {channel}: {data.get('event_type')}")
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.warning(f"Failed to publish gRPC event to Centrifugo: {e}")
|
|
482
|
+
|
|
483
|
+
async def _publish_error(self, error: Exception, **data):
|
|
484
|
+
"""Publish error to Centrifugo via Publisher."""
|
|
485
|
+
try:
|
|
486
|
+
# Build error channel name
|
|
487
|
+
channel = self.error_channel_template.format(
|
|
488
|
+
service=data.get('service', 'unknown'),
|
|
489
|
+
method=data.get('method_name', 'unknown'),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Use Publisher's publish_grpc_event with error status
|
|
493
|
+
await self._centrifugo_publisher.publish_grpc_event(
|
|
494
|
+
channel=channel,
|
|
495
|
+
method=data.get('method', ''),
|
|
496
|
+
status='ERROR',
|
|
497
|
+
duration_ms=data.get('duration_ms', 0.0),
|
|
498
|
+
peer=data.get('peer'),
|
|
499
|
+
metadata={
|
|
500
|
+
'event_type': 'rpc_error',
|
|
501
|
+
'error': {
|
|
502
|
+
'type': type(error).__name__,
|
|
503
|
+
'message': str(error),
|
|
504
|
+
},
|
|
505
|
+
**self.metadata,
|
|
506
|
+
},
|
|
507
|
+
**{k: v for k, v in data.items() if k not in ['method', 'duration_ms', 'peer', 'error', 'service', 'method_name']},
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
logger.debug(f"Published gRPC error to {channel}")
|
|
511
|
+
|
|
512
|
+
except Exception as e:
|
|
513
|
+
logger.warning(f"Failed to publish gRPC error to Centrifugo: {e}")
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _extract_peer(invocation_metadata) -> str:
|
|
517
|
+
"""Extract peer information from metadata."""
|
|
518
|
+
if invocation_metadata:
|
|
519
|
+
for key, value in invocation_metadata:
|
|
520
|
+
if key == "x-forwarded-for":
|
|
521
|
+
return value
|
|
522
|
+
return "unknown"
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _parse_method_name(full_method: str) -> tuple[str, str]:
|
|
526
|
+
"""
|
|
527
|
+
Parse full gRPC method name.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
full_method: e.g., "/trading_bots.BotStreamingService/ConnectBot"
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
(service_name, method_name): ("trading_bots.BotStreamingService", "ConnectBot")
|
|
534
|
+
"""
|
|
535
|
+
parts = full_method.strip("/").split("/")
|
|
536
|
+
if len(parts) == 2:
|
|
537
|
+
return parts[0], parts[1]
|
|
538
|
+
return "unknown", full_method
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
__all__ = ["CentrifugoInterceptor"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Logging Interceptor for gRPC.
|
|
2
|
+
Async Logging Interceptor for gRPC.
|
|
3
3
|
|
|
4
|
-
Provides comprehensive logging for gRPC requests and responses.
|
|
4
|
+
Provides comprehensive logging for async gRPC requests and responses.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -11,13 +11,14 @@ import time
|
|
|
11
11
|
from typing import Callable
|
|
12
12
|
|
|
13
13
|
import grpc
|
|
14
|
+
import grpc.aio
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class LoggingInterceptor(grpc.ServerInterceptor):
|
|
19
|
+
class LoggingInterceptor(grpc.aio.ServerInterceptor):
|
|
19
20
|
"""
|
|
20
|
-
gRPC interceptor for request/response logging.
|
|
21
|
+
Async gRPC interceptor for request/response logging.
|
|
21
22
|
|
|
22
23
|
Features:
|
|
23
24
|
- Logs all incoming requests
|
|
@@ -25,6 +26,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
25
26
|
- Logs errors and exceptions
|
|
26
27
|
- Structured logging with metadata
|
|
27
28
|
- Performance tracking
|
|
29
|
+
- Async/await support
|
|
28
30
|
|
|
29
31
|
Example:
|
|
30
32
|
```python
|
|
@@ -40,13 +42,13 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
40
42
|
[gRPC] METHOD | STATUS | TIME | DETAILS
|
|
41
43
|
"""
|
|
42
44
|
|
|
43
|
-
def intercept_service(
|
|
45
|
+
async def intercept_service(
|
|
44
46
|
self,
|
|
45
47
|
continuation: Callable,
|
|
46
48
|
handler_call_details: grpc.HandlerCallDetails,
|
|
47
49
|
) -> grpc.RpcMethodHandler:
|
|
48
50
|
"""
|
|
49
|
-
Intercept gRPC service call for logging.
|
|
51
|
+
Intercept async gRPC service call for logging.
|
|
50
52
|
|
|
51
53
|
Args:
|
|
52
54
|
continuation: Function to invoke the next interceptor or handler
|
|
@@ -62,7 +64,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
62
64
|
logger.info(f"[gRPC] ➡️ {method_name} | peer={peer}")
|
|
63
65
|
|
|
64
66
|
# Get handler and wrap it
|
|
65
|
-
handler = continuation(handler_call_details)
|
|
67
|
+
handler = await continuation(handler_call_details)
|
|
66
68
|
|
|
67
69
|
if handler is None:
|
|
68
70
|
logger.warning(f"[gRPC] ⚠️ {method_name} | No handler found")
|
|
@@ -181,19 +183,14 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
181
183
|
return wrapper
|
|
182
184
|
|
|
183
185
|
def wrap_stream_stream(behavior):
|
|
184
|
-
|
|
186
|
+
# All behaviors are async now
|
|
187
|
+
async def async_wrapper(request_iterator, context):
|
|
185
188
|
start_time = time.time()
|
|
186
|
-
in_count = 0
|
|
187
189
|
out_count = 0
|
|
188
190
|
try:
|
|
189
|
-
|
|
190
|
-
requests = []
|
|
191
|
-
for req in request_iterator:
|
|
192
|
-
in_count += 1
|
|
193
|
-
requests.append(req)
|
|
191
|
+
logger.info(f"[gRPC] 🔄 {method_name} (bidi stream) | peer={peer}")
|
|
194
192
|
|
|
195
|
-
|
|
196
|
-
for response in behavior(iter(requests), context):
|
|
193
|
+
async for response in behavior(request_iterator, context):
|
|
197
194
|
out_count += 1
|
|
198
195
|
yield response
|
|
199
196
|
|
|
@@ -201,7 +198,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
201
198
|
logger.info(
|
|
202
199
|
f"[gRPC] ✅ {method_name} (bidi stream) | "
|
|
203
200
|
f"status=OK | "
|
|
204
|
-
f"
|
|
201
|
+
f"out={out_count} | "
|
|
205
202
|
f"time={duration:.2f}ms | "
|
|
206
203
|
f"peer={peer}"
|
|
207
204
|
)
|
|
@@ -210,14 +207,14 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
210
207
|
logger.error(
|
|
211
208
|
f"[gRPC] ❌ {method_name} (bidi stream) | "
|
|
212
209
|
f"status=ERROR | "
|
|
213
|
-
f"
|
|
210
|
+
f"out={out_count} | "
|
|
214
211
|
f"time={duration:.2f}ms | "
|
|
215
212
|
f"error={type(e).__name__}: {str(e)} | "
|
|
216
213
|
f"peer={peer}",
|
|
217
214
|
exc_info=True
|
|
218
215
|
)
|
|
219
216
|
raise
|
|
220
|
-
return
|
|
217
|
+
return async_wrapper
|
|
221
218
|
|
|
222
219
|
# Return wrapped handler based on type
|
|
223
220
|
if handler.unary_unary:
|
|
@@ -233,7 +230,7 @@ class LoggingInterceptor(grpc.ServerInterceptor):
|
|
|
233
230
|
response_serializer=handler.response_serializer,
|
|
234
231
|
)
|
|
235
232
|
elif handler.stream_unary:
|
|
236
|
-
return grpc.
|
|
233
|
+
return grpc.stream_stream_rpc_method_handler(
|
|
237
234
|
wrap_stream_unary(handler.stream_unary),
|
|
238
235
|
request_deserializer=handler.request_deserializer,
|
|
239
236
|
response_serializer=handler.response_serializer,
|