django-cfg 1.4.59__py3-none-any.whl → 1.4.60__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/ipc/RPC_LOGGING.md +321 -0
- django_cfg/apps/ipc/TESTING.md +539 -0
- django_cfg/apps/ipc/__init__.py +12 -3
- django_cfg/apps/ipc/admin.py +212 -0
- django_cfg/apps/ipc/migrations/0001_initial.py +137 -0
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +221 -0
- django_cfg/apps/ipc/serializers/__init__.py +10 -0
- django_cfg/apps/ipc/serializers/serializers.py +114 -0
- django_cfg/apps/ipc/services/client/client.py +83 -4
- django_cfg/apps/ipc/services/logging.py +239 -0
- django_cfg/apps/ipc/services/monitor.py +5 -3
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +269 -0
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +259 -0
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +375 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +22 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +9 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +9 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +23 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/stat_cards.html +50 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +47 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/tab_navigation.html +29 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +184 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +56 -0
- django_cfg/apps/ipc/urls.py +4 -2
- django_cfg/apps/ipc/views/__init__.py +7 -2
- django_cfg/apps/ipc/views/dashboard.py +1 -1
- django_cfg/apps/ipc/views/{viewsets.py → monitoring.py} +17 -11
- django_cfg/apps/ipc/views/testing.py +285 -0
- django_cfg/modules/django_client/system/generate_mjs_clients.py +1 -1
- django_cfg/modules/django_dashboard/sections/widgets.py +209 -0
- django_cfg/modules/django_unfold/callbacks/main.py +43 -18
- django_cfg/modules/django_unfold/dashboard.py +41 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/js/api/index.mjs +8 -3
- django_cfg/static/js/api/ipc/client.mjs +40 -0
- django_cfg/static/js/api/knowbase/client.mjs +309 -0
- django_cfg/static/js/api/knowbase/index.mjs +13 -0
- django_cfg/static/js/api/payments/client.mjs +46 -1215
- django_cfg/static/js/api/types.mjs +164 -337
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +13 -1
- django_cfg/templates/admin/sections/widgets_section.html +129 -0
- django_cfg/templates/admin/snippets/tabs/widgets_tab.html +38 -0
- {django_cfg-1.4.59.dist-info → django_cfg-1.4.60.dist-info}/METADATA +1 -1
- {django_cfg-1.4.59.dist-info → django_cfg-1.4.60.dist-info}/RECORD +52 -28
- django_cfg/apps/ipc/templates/django_cfg_ipc/dashboard.html +0 -202
- /django_cfg/apps/ipc/static/django_cfg_ipc/js/{dashboard.mjs → dashboard.mjs.old} +0 -0
- /django_cfg/apps/ipc/templates/django_cfg_ipc/{base.html → layout/base.html} +0 -0
- {django_cfg-1.4.59.dist-info → django_cfg-1.4.60.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.59.dist-info → django_cfg-1.4.60.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.59.dist-info → django_cfg-1.4.60.dist-info}/licenses/LICENSE +0 -0
|
@@ -61,6 +61,7 @@ class OverviewStatsSerializer(serializers.Serializer):
|
|
|
61
61
|
help_text="List of active RPC methods"
|
|
62
62
|
)
|
|
63
63
|
top_method = serializers.CharField(
|
|
64
|
+
required=False,
|
|
64
65
|
allow_null=True,
|
|
65
66
|
help_text="Most frequently called method"
|
|
66
67
|
)
|
|
@@ -227,3 +228,116 @@ class MethodStatsSerializer(serializers.Serializer):
|
|
|
227
228
|
total_calls = serializers.IntegerField(
|
|
228
229
|
help_text="Total calls across all methods"
|
|
229
230
|
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Testing Serializers
|
|
234
|
+
|
|
235
|
+
class TestRPCRequestSerializer(serializers.Serializer):
|
|
236
|
+
"""Serializer for test RPC request input."""
|
|
237
|
+
|
|
238
|
+
method = serializers.CharField(
|
|
239
|
+
help_text="RPC method to call"
|
|
240
|
+
)
|
|
241
|
+
params = serializers.JSONField(
|
|
242
|
+
help_text="Parameters for the RPC call"
|
|
243
|
+
)
|
|
244
|
+
timeout = serializers.IntegerField(
|
|
245
|
+
default=10,
|
|
246
|
+
min_value=1,
|
|
247
|
+
max_value=60,
|
|
248
|
+
help_text="Timeout in seconds"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class TestRPCResponseSerializer(serializers.Serializer):
|
|
253
|
+
"""Serializer for test RPC response."""
|
|
254
|
+
|
|
255
|
+
success = serializers.BooleanField(
|
|
256
|
+
help_text="Whether the call was successful"
|
|
257
|
+
)
|
|
258
|
+
duration_ms = serializers.FloatField(
|
|
259
|
+
help_text="Call duration in milliseconds"
|
|
260
|
+
)
|
|
261
|
+
response = serializers.JSONField(
|
|
262
|
+
allow_null=True,
|
|
263
|
+
required=False,
|
|
264
|
+
help_text="Response data from RPC call"
|
|
265
|
+
)
|
|
266
|
+
error = serializers.CharField(
|
|
267
|
+
allow_null=True,
|
|
268
|
+
required=False,
|
|
269
|
+
help_text="Error message if failed"
|
|
270
|
+
)
|
|
271
|
+
correlation_id = serializers.CharField(
|
|
272
|
+
help_text="Correlation ID for tracking"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class LoadTestRequestSerializer(serializers.Serializer):
|
|
277
|
+
"""Serializer for load test request input."""
|
|
278
|
+
|
|
279
|
+
method = serializers.CharField(
|
|
280
|
+
help_text="RPC method to test"
|
|
281
|
+
)
|
|
282
|
+
total_requests = serializers.IntegerField(
|
|
283
|
+
min_value=1,
|
|
284
|
+
max_value=10000,
|
|
285
|
+
help_text="Total number of requests to send"
|
|
286
|
+
)
|
|
287
|
+
concurrency = serializers.IntegerField(
|
|
288
|
+
min_value=1,
|
|
289
|
+
max_value=100,
|
|
290
|
+
help_text="Number of concurrent requests"
|
|
291
|
+
)
|
|
292
|
+
params = serializers.JSONField(
|
|
293
|
+
required=False,
|
|
294
|
+
default=dict,
|
|
295
|
+
help_text="Parameters template for RPC calls"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class LoadTestResponseSerializer(serializers.Serializer):
|
|
300
|
+
"""Serializer for load test response."""
|
|
301
|
+
|
|
302
|
+
test_id = serializers.CharField(
|
|
303
|
+
help_text="Unique test ID"
|
|
304
|
+
)
|
|
305
|
+
started = serializers.BooleanField(
|
|
306
|
+
help_text="Whether test was started successfully"
|
|
307
|
+
)
|
|
308
|
+
message = serializers.CharField(
|
|
309
|
+
help_text="Status message"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class LoadTestStatusSerializer(serializers.Serializer):
|
|
314
|
+
"""Serializer for load test status."""
|
|
315
|
+
|
|
316
|
+
test_id = serializers.CharField(
|
|
317
|
+
allow_null=True,
|
|
318
|
+
help_text="Unique test ID"
|
|
319
|
+
)
|
|
320
|
+
running = serializers.BooleanField(
|
|
321
|
+
help_text="Whether test is currently running"
|
|
322
|
+
)
|
|
323
|
+
progress = serializers.IntegerField(
|
|
324
|
+
help_text="Number of completed requests"
|
|
325
|
+
)
|
|
326
|
+
total = serializers.IntegerField(
|
|
327
|
+
help_text="Total number of requests"
|
|
328
|
+
)
|
|
329
|
+
success_count = serializers.IntegerField(
|
|
330
|
+
help_text="Number of successful requests"
|
|
331
|
+
)
|
|
332
|
+
failed_count = serializers.IntegerField(
|
|
333
|
+
help_text="Number of failed requests"
|
|
334
|
+
)
|
|
335
|
+
avg_duration_ms = serializers.FloatField(
|
|
336
|
+
help_text="Average duration in milliseconds"
|
|
337
|
+
)
|
|
338
|
+
elapsed_time = serializers.FloatField(
|
|
339
|
+
help_text="Total elapsed time in seconds"
|
|
340
|
+
)
|
|
341
|
+
rps = serializers.FloatField(
|
|
342
|
+
help_text="Requests per second"
|
|
343
|
+
)
|
|
@@ -10,11 +10,11 @@ Works with or without django-cfg-rpc installed:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
-
import logging
|
|
14
13
|
from typing import Any, Dict, Optional, Type, TypeVar
|
|
15
14
|
from uuid import uuid4
|
|
16
15
|
|
|
17
16
|
import redis
|
|
17
|
+
from django_cfg.modules.django_logging import get_logger
|
|
18
18
|
|
|
19
19
|
from .exceptions import (
|
|
20
20
|
HAS_DJANGO_CFG_RPC,
|
|
@@ -24,7 +24,7 @@ from .exceptions import (
|
|
|
24
24
|
RPCTimeoutError,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
logger =
|
|
27
|
+
logger = get_logger("ipc.client")
|
|
28
28
|
|
|
29
29
|
# Try to import Pydantic from django-cfg-rpc
|
|
30
30
|
if HAS_DJANGO_CFG_RPC:
|
|
@@ -178,6 +178,9 @@ class DjangoCfgRPCClient:
|
|
|
178
178
|
params: Any,
|
|
179
179
|
result_model: Optional[Type[TResult]] = None,
|
|
180
180
|
timeout: Optional[int] = None,
|
|
181
|
+
user: Optional[Any] = None,
|
|
182
|
+
caller_ip: Optional[str] = None,
|
|
183
|
+
user_agent: Optional[str] = None,
|
|
181
184
|
) -> Any:
|
|
182
185
|
"""
|
|
183
186
|
Make synchronous RPC call to django-cfg-rpc server.
|
|
@@ -187,6 +190,9 @@ class DjangoCfgRPCClient:
|
|
|
187
190
|
params: Pydantic model or dict with parameters
|
|
188
191
|
result_model: Expected result model class (optional)
|
|
189
192
|
timeout: Optional timeout override (seconds)
|
|
193
|
+
user: Django User instance for logging (optional)
|
|
194
|
+
caller_ip: IP address for logging (optional)
|
|
195
|
+
user_agent: User agent for logging (optional)
|
|
190
196
|
|
|
191
197
|
Returns:
|
|
192
198
|
Pydantic result model instance (if result_model provided) or dict
|
|
@@ -203,10 +209,13 @@ class DjangoCfgRPCClient:
|
|
|
203
209
|
... params=NotificationRequest(user_id="123", type="info",
|
|
204
210
|
... title="Hello", message="World"),
|
|
205
211
|
... result_model=NotificationResponse,
|
|
206
|
-
... timeout=10
|
|
212
|
+
... timeout=10,
|
|
213
|
+
... user=request.user
|
|
207
214
|
... )
|
|
208
215
|
>>> print(result.delivered) # True/False
|
|
209
216
|
"""
|
|
217
|
+
import time
|
|
218
|
+
|
|
210
219
|
timeout = timeout or self.default_timeout
|
|
211
220
|
|
|
212
221
|
# Generate correlation ID
|
|
@@ -223,11 +232,13 @@ class DjangoCfgRPCClient:
|
|
|
223
232
|
else:
|
|
224
233
|
params_json = json.dumps({"data": params})
|
|
225
234
|
|
|
235
|
+
params_dict = json.loads(params_json)
|
|
236
|
+
|
|
226
237
|
# Build RPC request payload
|
|
227
238
|
request_payload = {
|
|
228
239
|
"type": "rpc",
|
|
229
240
|
"method": method,
|
|
230
|
-
"params":
|
|
241
|
+
"params": params_dict, # Embedded as dict
|
|
231
242
|
"correlation_id": cid,
|
|
232
243
|
"reply_to": reply_key, # Redis List key for response
|
|
233
244
|
"timeout": timeout,
|
|
@@ -236,6 +247,25 @@ class DjangoCfgRPCClient:
|
|
|
236
247
|
if self.log_calls:
|
|
237
248
|
logger.debug(f"RPC call: {method} (cid={cid})")
|
|
238
249
|
|
|
250
|
+
# Start timing for logging
|
|
251
|
+
start_time = time.time()
|
|
252
|
+
log_entry = None
|
|
253
|
+
|
|
254
|
+
# Create log entry if logging enabled
|
|
255
|
+
try:
|
|
256
|
+
from ..logging import RPCLogger
|
|
257
|
+
log_entry = RPCLogger.create_log(
|
|
258
|
+
correlation_id=cid,
|
|
259
|
+
method=method,
|
|
260
|
+
params=params_dict,
|
|
261
|
+
user=user,
|
|
262
|
+
caller_ip=caller_ip,
|
|
263
|
+
user_agent=user_agent,
|
|
264
|
+
)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
# Don't fail RPC call if logging fails
|
|
267
|
+
logger.warning(f"Failed to create RPC log: {e}")
|
|
268
|
+
|
|
239
269
|
try:
|
|
240
270
|
# Send request to Redis Stream
|
|
241
271
|
message_id = self._redis.xadd(
|
|
@@ -253,7 +283,17 @@ class DjangoCfgRPCClient:
|
|
|
253
283
|
|
|
254
284
|
if response_data is None:
|
|
255
285
|
# Timeout occurred
|
|
286
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
256
287
|
logger.warning(f"RPC timeout: {method} (cid={cid}, timeout={timeout}s)")
|
|
288
|
+
|
|
289
|
+
# Log timeout
|
|
290
|
+
if log_entry:
|
|
291
|
+
try:
|
|
292
|
+
from ..logging import RPCLogger
|
|
293
|
+
RPCLogger.mark_timeout(log_entry, timeout)
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
257
297
|
raise RPCTimeoutError(
|
|
258
298
|
f"RPC call '{method}' timed out after {timeout}s",
|
|
259
299
|
method=method,
|
|
@@ -272,18 +312,57 @@ class DjangoCfgRPCClient:
|
|
|
272
312
|
# Check response type
|
|
273
313
|
if response_dict.get("type") == "error":
|
|
274
314
|
# Error response
|
|
315
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
275
316
|
error_data = response_dict.get("error", {})
|
|
317
|
+
|
|
318
|
+
# Log error
|
|
319
|
+
if log_entry:
|
|
320
|
+
try:
|
|
321
|
+
from ..logging import RPCLogger
|
|
322
|
+
RPCLogger.mark_failed(
|
|
323
|
+
log_entry,
|
|
324
|
+
error_data.get("code", "unknown"),
|
|
325
|
+
error_data.get("message", "Unknown error"),
|
|
326
|
+
duration_ms
|
|
327
|
+
)
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
276
331
|
raise RPCRemoteError(error_data)
|
|
277
332
|
|
|
278
333
|
# Extract result
|
|
279
334
|
result_data = response_dict.get("result")
|
|
280
335
|
|
|
281
336
|
if result_data is None:
|
|
337
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
338
|
+
|
|
339
|
+
# Log error
|
|
340
|
+
if log_entry:
|
|
341
|
+
try:
|
|
342
|
+
from ..logging import RPCLogger
|
|
343
|
+
RPCLogger.mark_failed(
|
|
344
|
+
log_entry,
|
|
345
|
+
"internal_error",
|
|
346
|
+
"Response has no result field",
|
|
347
|
+
duration_ms
|
|
348
|
+
)
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
282
352
|
raise RPCRemoteError({
|
|
283
353
|
"code": "internal_error",
|
|
284
354
|
"message": "Response has no result field",
|
|
285
355
|
})
|
|
286
356
|
|
|
357
|
+
# Success - log it
|
|
358
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
359
|
+
if log_entry:
|
|
360
|
+
try:
|
|
361
|
+
from ..logging import RPCLogger
|
|
362
|
+
RPCLogger.mark_success(log_entry, result_data, duration_ms)
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
287
366
|
# Deserialize result if model provided
|
|
288
367
|
if result_model and HAS_DJANGO_CFG_RPC:
|
|
289
368
|
try:
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RPC Logging helper for tracking RPC calls.
|
|
3
|
+
|
|
4
|
+
Provides async-safe logging of RPC calls to database.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django_cfg.modules.django_logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("ipc.rpc")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RPCLogger:
|
|
16
|
+
"""
|
|
17
|
+
Helper class for logging RPC calls to database.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
>>> log_entry = RPCLogger.create_log(
|
|
21
|
+
... correlation_id="abc123",
|
|
22
|
+
... method="send_notification",
|
|
23
|
+
... params={"user_id": "123"},
|
|
24
|
+
... user=request.user if authenticated else None
|
|
25
|
+
... )
|
|
26
|
+
>>> # ... make RPC call ...
|
|
27
|
+
>>> RPCLogger.mark_success(log_entry, response_data, duration_ms=150)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def is_logging_enabled() -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Check if RPC logging is enabled in settings.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
bool: True if logging is enabled
|
|
37
|
+
"""
|
|
38
|
+
# Check if IPC app is installed
|
|
39
|
+
if 'django_cfg.apps.ipc' not in settings.INSTALLED_APPS:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
# Check if logging is explicitly disabled
|
|
43
|
+
if hasattr(settings, 'DJANGO_CFG_RPC'):
|
|
44
|
+
return settings.DJANGO_CFG_RPC.get('ENABLE_LOGGING', True)
|
|
45
|
+
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def create_log(
|
|
50
|
+
correlation_id: str,
|
|
51
|
+
method: str,
|
|
52
|
+
params: Dict[str, Any],
|
|
53
|
+
user: Optional[Any] = None,
|
|
54
|
+
caller_ip: Optional[str] = None,
|
|
55
|
+
user_agent: Optional[str] = None,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Create RPC log entry in pending state.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
correlation_id: UUID correlation ID from RPC request
|
|
62
|
+
method: RPC method name
|
|
63
|
+
params: Parameters sent to RPC method
|
|
64
|
+
user: Django User instance (optional)
|
|
65
|
+
caller_ip: IP address of caller (optional)
|
|
66
|
+
user_agent: User agent string (optional)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
RPCLog instance or None if logging disabled
|
|
70
|
+
"""
|
|
71
|
+
if not RPCLogger.is_logging_enabled():
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from ..models import RPCLog
|
|
76
|
+
|
|
77
|
+
log_entry = RPCLog.objects.create(
|
|
78
|
+
correlation_id=correlation_id,
|
|
79
|
+
method=method,
|
|
80
|
+
params=params,
|
|
81
|
+
user=user,
|
|
82
|
+
caller_ip=caller_ip,
|
|
83
|
+
user_agent=user_agent,
|
|
84
|
+
status=RPCLog.StatusChoices.PENDING
|
|
85
|
+
)
|
|
86
|
+
return log_entry
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to create RPC log: {e}", exc_info=True)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def mark_success(log_entry, response_data: Dict[str, Any], duration_ms: Optional[int] = None):
|
|
94
|
+
"""
|
|
95
|
+
Mark RPC log as successful.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
log_entry: RPCLog instance
|
|
99
|
+
response_data: Response data from RPC call
|
|
100
|
+
duration_ms: Duration in milliseconds (optional)
|
|
101
|
+
"""
|
|
102
|
+
if log_entry is None:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
log_entry.mark_success(response_data, duration_ms)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Failed to mark RPC log as success: {e}", exc_info=True)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def mark_failed(log_entry, error_code: str, error_message: str, duration_ms: Optional[int] = None):
|
|
112
|
+
"""
|
|
113
|
+
Mark RPC log as failed.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
log_entry: RPCLog instance
|
|
117
|
+
error_code: Error code
|
|
118
|
+
error_message: Error message
|
|
119
|
+
duration_ms: Duration in milliseconds (optional)
|
|
120
|
+
"""
|
|
121
|
+
if log_entry is None:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
log_entry.mark_failed(error_code, error_message, duration_ms)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Failed to mark RPC log as failed: {e}", exc_info=True)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def mark_timeout(log_entry, timeout_seconds: int):
|
|
131
|
+
"""
|
|
132
|
+
Mark RPC log as timed out.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
log_entry: RPCLog instance
|
|
136
|
+
timeout_seconds: Timeout duration in seconds
|
|
137
|
+
"""
|
|
138
|
+
if log_entry is None:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
log_entry.mark_timeout(timeout_seconds)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"Failed to mark RPC log as timeout: {e}", exc_info=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class RPCLogContext:
|
|
148
|
+
"""
|
|
149
|
+
Context manager for automatically logging RPC calls.
|
|
150
|
+
|
|
151
|
+
Usage:
|
|
152
|
+
>>> with RPCLogContext(
|
|
153
|
+
... correlation_id="abc123",
|
|
154
|
+
... method="send_notification",
|
|
155
|
+
... params={"user_id": "123"}
|
|
156
|
+
... ) as log_ctx:
|
|
157
|
+
... result = rpc.call(...)
|
|
158
|
+
... log_ctx.set_response(result)
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
correlation_id: str,
|
|
164
|
+
method: str,
|
|
165
|
+
params: Dict[str, Any],
|
|
166
|
+
user: Optional[Any] = None,
|
|
167
|
+
caller_ip: Optional[str] = None,
|
|
168
|
+
user_agent: Optional[str] = None,
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
Initialize RPC log context.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
correlation_id: UUID correlation ID
|
|
175
|
+
method: RPC method name
|
|
176
|
+
params: Parameters for RPC call
|
|
177
|
+
user: Django User instance (optional)
|
|
178
|
+
caller_ip: IP address (optional)
|
|
179
|
+
user_agent: User agent string (optional)
|
|
180
|
+
"""
|
|
181
|
+
self.correlation_id = correlation_id
|
|
182
|
+
self.method = method
|
|
183
|
+
self.params = params
|
|
184
|
+
self.user = user
|
|
185
|
+
self.caller_ip = caller_ip
|
|
186
|
+
self.user_agent = user_agent
|
|
187
|
+
self.log_entry = None
|
|
188
|
+
self.start_time = None
|
|
189
|
+
self.response = None
|
|
190
|
+
|
|
191
|
+
def __enter__(self):
|
|
192
|
+
"""Start timing and create log entry."""
|
|
193
|
+
self.start_time = time.time()
|
|
194
|
+
self.log_entry = RPCLogger.create_log(
|
|
195
|
+
correlation_id=self.correlation_id,
|
|
196
|
+
method=self.method,
|
|
197
|
+
params=self.params,
|
|
198
|
+
user=self.user,
|
|
199
|
+
caller_ip=self.caller_ip,
|
|
200
|
+
user_agent=self.user_agent,
|
|
201
|
+
)
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
205
|
+
"""Mark log as success or failed based on exception."""
|
|
206
|
+
if self.log_entry is None:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
duration_ms = int((time.time() - self.start_time) * 1000) if self.start_time else None
|
|
210
|
+
|
|
211
|
+
if exc_type is None:
|
|
212
|
+
# Success - use response if set
|
|
213
|
+
RPCLogger.mark_success(self.log_entry, self.response or {}, duration_ms)
|
|
214
|
+
else:
|
|
215
|
+
# Failed - extract error details
|
|
216
|
+
error_code = exc_type.__name__ if exc_type else 'unknown'
|
|
217
|
+
error_message = str(exc_val) if exc_val else 'Unknown error'
|
|
218
|
+
|
|
219
|
+
# Check if it's a timeout
|
|
220
|
+
if 'timeout' in error_code.lower():
|
|
221
|
+
timeout_seconds = duration_ms // 1000 if duration_ms else 30
|
|
222
|
+
RPCLogger.mark_timeout(self.log_entry, timeout_seconds)
|
|
223
|
+
else:
|
|
224
|
+
RPCLogger.mark_failed(self.log_entry, error_code, error_message, duration_ms)
|
|
225
|
+
|
|
226
|
+
# Don't suppress exceptions
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def set_response(self, response: Dict[str, Any]):
|
|
230
|
+
"""
|
|
231
|
+
Set response data for successful call.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
response: Response data from RPC call
|
|
235
|
+
"""
|
|
236
|
+
self.response = response
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ['RPCLogger', 'RPCLogContext']
|
|
@@ -5,12 +5,13 @@ Reads RPC metrics from Redis DB 2 and provides statistics.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
import logging
|
|
9
8
|
from collections import defaultdict
|
|
10
9
|
from datetime import datetime, timedelta
|
|
11
10
|
from typing import Any, Dict, List
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
from django_cfg.modules.django_logging import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger("ipc.monitor")
|
|
14
15
|
|
|
15
16
|
# Cache timeout in seconds
|
|
16
17
|
CACHE_TIMEOUT = 3 # 3 seconds to reduce Redis load
|
|
@@ -117,6 +118,7 @@ class RPCMonitor:
|
|
|
117
118
|
'redis_connected': True,
|
|
118
119
|
'total_requests_today': 0,
|
|
119
120
|
'active_methods': [],
|
|
121
|
+
'top_method': None,
|
|
120
122
|
'avg_response_time_ms': 0,
|
|
121
123
|
'success_rate': 100.0,
|
|
122
124
|
'timestamp': datetime.now().isoformat(),
|
|
@@ -354,7 +356,7 @@ class RPCMonitor:
|
|
|
354
356
|
stream_key = stream_key or self.config.request_stream
|
|
355
357
|
|
|
356
358
|
# Validate stream_key (security: prevent Redis key injection)
|
|
357
|
-
ALLOWED_STREAMS = ['stream:requests', 'stream:responses']
|
|
359
|
+
ALLOWED_STREAMS = ['stream:requests', 'stream:responses', 'stream:rpc_requests', 'stream:rpc_responses']
|
|
358
360
|
if stream_key not in ALLOWED_STREAMS:
|
|
359
361
|
logger.warning(f"Invalid stream key attempted: {stream_key}")
|
|
360
362
|
raise ValueError(f"Stream key not allowed: {stream_key}")
|