django-cfg 1.4.61__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.

Files changed (179) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/services/otp_service.py +3 -14
  3. django_cfg/apps/centrifugo/__init__.py +57 -0
  4. django_cfg/apps/centrifugo/admin/__init__.py +13 -0
  5. django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
  6. django_cfg/apps/centrifugo/admin/config.py +82 -0
  7. django_cfg/apps/centrifugo/apps.py +31 -0
  8. django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
  9. django_cfg/apps/centrifugo/codegen/README.md +242 -0
  10. django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
  11. django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
  12. django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
  13. django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
  14. django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
  15. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
  16. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
  17. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
  18. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
  19. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
  20. django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
  21. django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
  22. django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
  23. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
  24. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
  25. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
  26. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
  27. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
  28. django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
  29. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
  30. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
  31. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
  32. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
  33. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
  34. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
  35. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
  36. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
  37. django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
  38. django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
  39. django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
  40. django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
  41. django_cfg/apps/centrifugo/decorators.py +137 -0
  42. django_cfg/apps/centrifugo/management/__init__.py +1 -0
  43. django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
  44. django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
  45. django_cfg/apps/centrifugo/managers/__init__.py +12 -0
  46. django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
  47. django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
  48. django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
  49. django_cfg/apps/centrifugo/models/__init__.py +11 -0
  50. django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
  51. django_cfg/apps/centrifugo/registry.py +106 -0
  52. django_cfg/apps/centrifugo/router.py +125 -0
  53. django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
  54. django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
  55. django_cfg/apps/centrifugo/serializers/channels.py +26 -0
  56. django_cfg/apps/centrifugo/serializers/health.py +17 -0
  57. django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
  58. django_cfg/apps/centrifugo/serializers/stats.py +21 -0
  59. django_cfg/apps/centrifugo/services/__init__.py +12 -0
  60. django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
  61. django_cfg/apps/centrifugo/services/client/client.py +577 -0
  62. django_cfg/apps/centrifugo/services/client/config.py +228 -0
  63. django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
  64. django_cfg/apps/centrifugo/services/config_helper.py +63 -0
  65. django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
  66. django_cfg/apps/centrifugo/services/logging.py +677 -0
  67. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
  68. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
  69. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
  70. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
  71. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
  72. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
  73. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
  74. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
  75. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
  76. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
  77. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
  78. django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
  79. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
  80. django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
  81. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
  82. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
  83. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
  84. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
  85. django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
  86. django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
  87. django_cfg/apps/centrifugo/urls.py +31 -0
  88. django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
  89. django_cfg/apps/centrifugo/views/__init__.py +15 -0
  90. django_cfg/apps/centrifugo/views/admin_api.py +374 -0
  91. django_cfg/apps/centrifugo/views/dashboard.py +15 -0
  92. django_cfg/apps/centrifugo/views/monitoring.py +286 -0
  93. django_cfg/apps/centrifugo/views/testing_api.py +422 -0
  94. django_cfg/apps/support/utils/support_email_service.py +5 -18
  95. django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
  96. django_cfg/apps/urls.py +5 -5
  97. django_cfg/core/base/config_model.py +4 -44
  98. django_cfg/core/builders/apps_builder.py +2 -2
  99. django_cfg/core/generation/integration_generators/third_party.py +8 -8
  100. django_cfg/core/utils/__init__.py +5 -0
  101. django_cfg/core/utils/url_helpers.py +73 -0
  102. django_cfg/modules/base.py +7 -7
  103. django_cfg/modules/django_client/core/__init__.py +2 -1
  104. django_cfg/modules/django_client/core/config/config.py +8 -0
  105. django_cfg/modules/django_client/core/generator/__init__.py +42 -2
  106. django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
  107. django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
  108. django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
  109. django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
  110. django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
  111. django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
  112. django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
  113. django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
  114. django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
  115. django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
  116. django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
  117. django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
  118. django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
  119. django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
  120. django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
  121. django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
  122. django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
  123. django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
  124. django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
  125. django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
  126. django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
  127. django_cfg/modules/django_client/system/schema_parser.py +5 -1
  128. django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
  129. django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
  130. django_cfg/modules/django_unfold/dashboard.py +25 -19
  131. django_cfg/pyproject.toml +1 -1
  132. django_cfg/registry/core.py +2 -0
  133. django_cfg/registry/modules.py +2 -2
  134. django_cfg/static/js/api/centrifugo/client.mjs +164 -0
  135. django_cfg/static/js/api/centrifugo/index.mjs +13 -0
  136. django_cfg/static/js/api/index.mjs +5 -5
  137. django_cfg/static/js/api/types.mjs +89 -26
  138. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
  139. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -68
  140. django_cfg/apps/ipc/README.md +0 -346
  141. django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
  142. django_cfg/apps/ipc/TESTING.md +0 -539
  143. django_cfg/apps/ipc/__init__.py +0 -60
  144. django_cfg/apps/ipc/admin.py +0 -212
  145. django_cfg/apps/ipc/apps.py +0 -28
  146. django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
  147. django_cfg/apps/ipc/migrations/__init__.py +0 -0
  148. django_cfg/apps/ipc/models.py +0 -221
  149. django_cfg/apps/ipc/serializers/__init__.py +0 -29
  150. django_cfg/apps/ipc/serializers/serializers.py +0 -343
  151. django_cfg/apps/ipc/services/__init__.py +0 -7
  152. django_cfg/apps/ipc/services/client/__init__.py +0 -23
  153. django_cfg/apps/ipc/services/client/client.py +0 -621
  154. django_cfg/apps/ipc/services/client/config.py +0 -214
  155. django_cfg/apps/ipc/services/client/exceptions.py +0 -201
  156. django_cfg/apps/ipc/services/logging.py +0 -239
  157. django_cfg/apps/ipc/services/monitor.py +0 -466
  158. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
  159. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
  160. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
  161. django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
  162. django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
  163. django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
  164. django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
  165. django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
  166. django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
  167. django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
  168. django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
  169. django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
  170. django_cfg/apps/ipc/urls.py +0 -23
  171. django_cfg/apps/ipc/views/__init__.py +0 -13
  172. django_cfg/apps/ipc/views/dashboard.py +0 -15
  173. django_cfg/apps/ipc/views/monitoring.py +0 -251
  174. django_cfg/apps/ipc/views/testing.py +0 -285
  175. django_cfg/static/js/api/ipc/client.mjs +0 -114
  176. django_cfg/static/js/api/ipc/index.mjs +0 -13
  177. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
  178. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
  179. {django_cfg-1.4.61.dist-info → django_cfg-1.4.63.dist-info}/licenses/LICENSE +0 -0
@@ -1,466 +0,0 @@
1
- """
2
- RPC Monitor - Real-time RPC activity monitoring.
3
-
4
- Reads RPC metrics from Redis DB 2 and provides statistics.
5
- """
6
-
7
- import json
8
- from collections import defaultdict
9
- from datetime import datetime, timedelta
10
- from typing import Any, Dict, List
11
-
12
- from django_cfg.modules.django_logging import get_logger
13
-
14
- logger = get_logger("ipc.monitor")
15
-
16
- # Cache timeout in seconds
17
- CACHE_TIMEOUT = 3 # 3 seconds to reduce Redis load
18
-
19
-
20
- class RPCMonitor:
21
- """
22
- Monitor RPC activity by reading from Redis.
23
-
24
- Provides real-time statistics about:
25
- - RPC requests (from stream:requests)
26
- - Response times
27
- - Success/failure rates
28
- - Notification delivery stats
29
- """
30
-
31
- def __init__(self, redis_client=None):
32
- """
33
- Initialize RPC monitor.
34
-
35
- Args:
36
- redis_client: Optional Redis client. If not provided, creates from config.
37
- """
38
- from .client.config import DjangoCfgRPCConfig
39
- from django_cfg.core import get_current_config
40
-
41
- # Try to get config from django-cfg global state first
42
- django_cfg_config = get_current_config()
43
-
44
- if django_cfg_config and hasattr(django_cfg_config, 'django_ipc') and django_cfg_config.django_ipc:
45
- # Use config from django-cfg DjangoConfig
46
- self.config = django_cfg_config.django_ipc
47
- logger.debug(f"RPCMonitor initialized from django-cfg config: {self.config.redis_url}")
48
- else:
49
- # Fallback to default config
50
- self.config = DjangoCfgRPCConfig()
51
- logger.warning("Django-CFG config not found, using default RPC config")
52
-
53
- self.redis_client = redis_client or self._create_redis_client()
54
-
55
- def _create_redis_client(self):
56
- """Create Redis client from config."""
57
- try:
58
- from urllib.parse import urlparse
59
-
60
- import redis
61
-
62
- # Use instance config (already loaded in __init__)
63
- parsed = urlparse(self.config.redis_url)
64
-
65
- client = redis.Redis(
66
- host=parsed.hostname or 'localhost',
67
- port=parsed.port or 6379,
68
- db=int(parsed.path.lstrip('/')) if parsed.path else 2,
69
- decode_responses=True,
70
- socket_connect_timeout=5,
71
- socket_timeout=5,
72
- )
73
-
74
- # Test connection
75
- client.ping()
76
-
77
- return client
78
-
79
- except Exception as e:
80
- logger.error(f"Failed to create Redis client: {e}")
81
- return None
82
-
83
- def get_overview_stats(self) -> Dict[str, Any]:
84
- """
85
- Get overview statistics.
86
-
87
- Returns:
88
- {
89
- 'redis_connected': bool,
90
- 'total_requests_today': int,
91
- 'active_methods': List[str],
92
- 'avg_response_time_ms': float,
93
- 'success_rate': float,
94
- 'timestamp': str,
95
- }
96
- """
97
- if not self.redis_client:
98
- return self._error_response("Redis not connected")
99
-
100
- # Check Django cache first
101
- try:
102
- from django.core.cache import cache
103
- cache_key = 'rpc_dashboard:overview_stats'
104
- cached = cache.get(cache_key)
105
- if cached:
106
- logger.debug("Returning cached overview stats")
107
- return cached
108
- except ImportError:
109
- logger.debug("Django cache not available, skipping cache")
110
- cache = None
111
-
112
- try:
113
- # Get recent requests from stream
114
- recent_requests = self._get_recent_stream_entries(count=1000)
115
-
116
- if not recent_requests:
117
- return {
118
- 'redis_connected': True,
119
- 'total_requests_today': 0,
120
- 'active_methods': [],
121
- 'top_method': None,
122
- 'avg_response_time_ms': 0,
123
- 'success_rate': 100.0,
124
- 'timestamp': datetime.now().isoformat(),
125
- }
126
-
127
- # Calculate statistics
128
- methods = defaultdict(int)
129
- response_times = []
130
-
131
- for entry in recent_requests:
132
- payload = entry.get('payload', {})
133
- method = payload.get('method', 'unknown')
134
- methods[method] += 1
135
-
136
- # Try to extract response time (if available)
137
- # This would require storing response metadata
138
-
139
- stats = {
140
- 'redis_connected': True,
141
- 'total_requests_today': len(recent_requests),
142
- 'active_methods': list(methods.keys()),
143
- 'top_method': max(methods.items(), key=lambda x: x[1])[0] if methods else None,
144
- 'method_counts': dict(methods),
145
- 'avg_response_time_ms': sum(response_times) / len(response_times) if response_times else 0,
146
- 'success_rate': 98.5, # TODO: Calculate from actual data
147
- 'timestamp': datetime.now().isoformat(),
148
- }
149
-
150
- # Cache the result
151
- if cache:
152
- cache.set(cache_key, stats, timeout=CACHE_TIMEOUT)
153
- logger.debug(f"Cached overview stats for {CACHE_TIMEOUT}s")
154
-
155
- return stats
156
-
157
- except Exception as e:
158
- logger.error(f"Error getting overview stats: {e}")
159
- return self._error_response(str(e))
160
-
161
- def get_recent_requests(self, count: int = 50) -> List[Dict[str, Any]]:
162
- """
163
- Get recent RPC requests.
164
-
165
- Args:
166
- count: Number of recent requests to return
167
-
168
- Returns:
169
- List of request dicts with metadata
170
- """
171
- if not self.redis_client:
172
- return []
173
-
174
- try:
175
- entries = self._get_recent_stream_entries(count=count)
176
-
177
- return [
178
- {
179
- 'id': entry.get('id'),
180
- 'timestamp': self._timestamp_to_datetime(entry.get('id')),
181
- 'method': entry.get('payload', {}).get('method'),
182
- 'params': entry.get('payload', {}).get('params'),
183
- 'correlation_id': entry.get('payload', {}).get('correlation_id'),
184
- }
185
- for entry in entries
186
- ]
187
-
188
- except Exception as e:
189
- logger.error(f"Error getting recent requests: {e}")
190
- return []
191
-
192
- def get_notification_stats(self) -> Dict[str, Any]:
193
- """
194
- Get notification-specific statistics.
195
-
196
- Returns:
197
- {
198
- 'total_sent': int,
199
- 'delivery_rate': float,
200
- 'by_type': Dict[str, int],
201
- 'recent': List[Dict],
202
- }
203
- """
204
- if not self.redis_client:
205
- return self._error_response("Redis not connected")
206
-
207
- # Check cache
208
- try:
209
- from django.core.cache import cache
210
- cache_key = 'rpc_dashboard:notification_stats'
211
- cached = cache.get(cache_key)
212
- if cached:
213
- return cached
214
- except ImportError:
215
- cache = None
216
-
217
- try:
218
- # Get requests with method="send_notification"
219
- recent_requests = self._get_recent_stream_entries(count=1000)
220
-
221
- notifications = [
222
- r for r in recent_requests
223
- if r.get('payload', {}).get('method') == 'send_notification'
224
- ]
225
-
226
- # Group by type
227
- by_type = defaultdict(int)
228
- for notif in notifications:
229
- params = notif.get('payload', {}).get('params', {})
230
- notif_type = params.get('type', 'unknown')
231
- by_type[notif_type] += 1
232
-
233
- stats = {
234
- 'total_sent': len(notifications),
235
- 'delivery_rate': 95.0, # TODO: Calculate from responses
236
- 'by_type': dict(by_type),
237
- 'recent': [
238
- {
239
- 'timestamp': self._timestamp_to_datetime(n.get('id')),
240
- 'type': n.get('payload', {}).get('params', {}).get('type'),
241
- 'user_id': n.get('payload', {}).get('params', {}).get('user_id'),
242
- 'message': n.get('payload', {}).get('params', {}).get('message', '')[:50],
243
- }
244
- for n in notifications[:20]
245
- ],
246
- 'timestamp': datetime.now().isoformat(),
247
- }
248
-
249
- # Cache the result
250
- if cache:
251
- cache.set(cache_key, stats, timeout=CACHE_TIMEOUT)
252
-
253
- return stats
254
-
255
- except Exception as e:
256
- logger.error(f"Error getting notification stats: {e}")
257
- return self._error_response(str(e))
258
-
259
- def get_total_requests_count(self) -> int:
260
- """
261
- Get total number of requests in the stream.
262
-
263
- Returns:
264
- Total count of requests in the stream
265
- """
266
- if not self.redis_client:
267
- return 0
268
-
269
- try:
270
- # Get stream length
271
- stream_len = self.redis_client.xlen(self.config.request_stream)
272
- return stream_len or 0
273
- except Exception as e:
274
- logger.error(f"Error getting total requests count: {e}")
275
- return 0
276
-
277
- def get_method_stats(self) -> List[Dict[str, Any]]:
278
- """
279
- Get statistics grouped by RPC method.
280
-
281
- Returns:
282
- List of method stats:
283
- [
284
- {
285
- 'method': str,
286
- 'count': int,
287
- 'percentage': float,
288
- 'avg_time_ms': float,
289
- },
290
- ...
291
- ]
292
- """
293
- if not self.redis_client:
294
- return []
295
-
296
- # Check cache
297
- try:
298
- from django.core.cache import cache
299
- cache_key = 'rpc_dashboard:method_stats'
300
- cached = cache.get(cache_key)
301
- if cached:
302
- return cached
303
- except ImportError:
304
- cache = None
305
-
306
- try:
307
- recent_requests = self._get_recent_stream_entries(count=1000)
308
-
309
- method_counts = defaultdict(int)
310
- total = len(recent_requests)
311
-
312
- for entry in recent_requests:
313
- method = entry.get('payload', {}).get('method', 'unknown')
314
- method_counts[method] += 1
315
-
316
- stats = [
317
- {
318
- 'method': method,
319
- 'count': count,
320
- 'percentage': round((count / total) * 100, 1) if total > 0 else 0,
321
- 'avg_time_ms': 45, # TODO: Calculate from actual data
322
- }
323
- for method, count in sorted(
324
- method_counts.items(),
325
- key=lambda x: x[1],
326
- reverse=True
327
- )
328
- ]
329
-
330
- # Cache the result
331
- if cache:
332
- cache.set(cache_key, stats, timeout=CACHE_TIMEOUT)
333
-
334
- return stats
335
-
336
- except Exception as e:
337
- logger.error(f"Error getting method stats: {e}")
338
- return []
339
-
340
- def _get_recent_stream_entries(self, count: int = 100, stream_key: str = None) -> List[Dict]:
341
- """
342
- Get recent entries from Redis stream.
343
-
344
- Args:
345
- count: Number of entries to retrieve
346
- stream_key: Stream key (defaults to config.request_stream)
347
-
348
- Returns:
349
- List of parsed stream entries
350
- """
351
- if not self.redis_client:
352
- return []
353
-
354
- try:
355
- # Use instance config
356
- stream_key = stream_key or self.config.request_stream
357
-
358
- # Validate stream_key (security: prevent Redis key injection)
359
- ALLOWED_STREAMS = ['stream:requests', 'stream:responses', 'stream:rpc_requests', 'stream:rpc_responses']
360
- if stream_key not in ALLOWED_STREAMS:
361
- logger.warning(f"Invalid stream key attempted: {stream_key}")
362
- raise ValueError(f"Stream key not allowed: {stream_key}")
363
-
364
- # XREVRANGE to get latest entries
365
- entries = self.redis_client.xrevrange(stream_key, count=count)
366
-
367
- parsed = []
368
- for entry_id, fields in entries:
369
- try:
370
- payload_str = fields.get('payload', '{}')
371
- payload = json.loads(payload_str) if isinstance(payload_str, str) else payload_str
372
-
373
- parsed.append({
374
- 'id': entry_id,
375
- 'payload': payload,
376
- 'fields': fields,
377
- })
378
- except json.JSONDecodeError:
379
- logger.warning(f"Failed to parse payload for entry {entry_id}")
380
- continue
381
-
382
- return parsed
383
-
384
- except Exception as e:
385
- logger.error(f"Error reading stream: {e}")
386
- return []
387
-
388
- def _timestamp_to_datetime(self, stream_id: str) -> str:
389
- """
390
- Convert Redis stream ID to datetime string.
391
-
392
- Args:
393
- stream_id: Redis stream ID (e.g., "1234567890123-0")
394
-
395
- Returns:
396
- ISO datetime string
397
- """
398
- try:
399
- timestamp_ms = int(stream_id.split('-')[0])
400
- dt = datetime.fromtimestamp(timestamp_ms / 1000.0)
401
- return dt.isoformat()
402
- except (ValueError, IndexError):
403
- return datetime.now().isoformat()
404
-
405
- def _error_response(self, error_msg: str) -> Dict[str, Any]:
406
- """Create error response dict."""
407
- return {
408
- 'redis_connected': False,
409
- 'error': error_msg,
410
- 'timestamp': datetime.now().isoformat(),
411
- }
412
-
413
- def health_check(self) -> Dict[str, Any]:
414
- """
415
- Check RPC monitoring health.
416
-
417
- Returns:
418
- {
419
- 'redis_connected': bool,
420
- 'stream_exists': bool,
421
- 'recent_activity': bool,
422
- 'error': Optional[str],
423
- }
424
- """
425
- if not self.redis_client:
426
- return {
427
- 'redis_connected': False,
428
- 'stream_exists': False,
429
- 'recent_activity': False,
430
- 'error': 'Redis client not initialized',
431
- }
432
-
433
- try:
434
- # Check Redis connection
435
- self.redis_client.ping()
436
-
437
- # Check if stream exists (use instance config)
438
- stream_len = self.redis_client.xlen(self.config.request_stream)
439
- stream_exists = stream_len is not None
440
-
441
- # Check for recent activity (last 5 minutes)
442
- recent_activity = False
443
- if stream_exists and stream_len > 0:
444
- latest = self.redis_client.xrevrange(self.config.request_stream, count=1)
445
- if latest:
446
- latest_id = latest[0][0]
447
- timestamp_ms = int(latest_id.split('-')[0])
448
- dt = datetime.fromtimestamp(timestamp_ms / 1000.0)
449
- recent_activity = (datetime.now() - dt) < timedelta(minutes=5)
450
-
451
- return {
452
- 'redis_connected': True,
453
- 'stream_exists': stream_exists,
454
- 'stream_length': stream_len or 0,
455
- 'recent_activity': recent_activity,
456
- 'error': None,
457
- }
458
-
459
- except Exception as e:
460
- logger.error(f"Health check failed: {e}")
461
- return {
462
- 'redis_connected': False,
463
- 'stream_exists': False,
464
- 'recent_activity': False,
465
- 'error': str(e),
466
- }