django-cfg 1.4.120__py3-none-any.whl → 1.5.2__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 (182) hide show
  1. django_cfg/__init__.py +8 -4
  2. django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
  3. django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
  4. django_cfg/apps/dashboard/serializers/__init__.py +0 -12
  5. django_cfg/apps/dashboard/serializers/activity.py +1 -1
  6. django_cfg/apps/dashboard/services/__init__.py +0 -2
  7. django_cfg/apps/dashboard/services/charts_service.py +4 -3
  8. django_cfg/apps/dashboard/services/statistics_service.py +11 -2
  9. django_cfg/apps/dashboard/services/system_health_service.py +64 -106
  10. django_cfg/apps/dashboard/urls.py +0 -2
  11. django_cfg/apps/dashboard/views/__init__.py +0 -2
  12. django_cfg/apps/dashboard/views/commands_views.py +3 -6
  13. django_cfg/apps/dashboard/views/overview_views.py +14 -13
  14. django_cfg/apps/grpc/__init__.py +9 -0
  15. django_cfg/apps/grpc/admin/__init__.py +11 -0
  16. django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
  17. django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
  18. django_cfg/apps/grpc/apps.py +28 -0
  19. django_cfg/apps/grpc/auth/__init__.py +9 -0
  20. django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
  21. django_cfg/apps/grpc/interceptors/__init__.py +19 -0
  22. django_cfg/apps/grpc/interceptors/errors.py +241 -0
  23. django_cfg/apps/grpc/interceptors/logging.py +270 -0
  24. django_cfg/apps/grpc/interceptors/metrics.py +306 -0
  25. django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
  26. django_cfg/apps/grpc/management/__init__.py +1 -0
  27. django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
  28. django_cfg/apps/grpc/managers/__init__.py +10 -0
  29. django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
  30. django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
  31. django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
  32. django_cfg/apps/grpc/models/__init__.py +9 -0
  33. django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
  34. django_cfg/apps/grpc/serializers/__init__.py +23 -0
  35. django_cfg/apps/grpc/serializers/health.py +18 -0
  36. django_cfg/apps/grpc/serializers/requests.py +18 -0
  37. django_cfg/apps/grpc/serializers/services.py +50 -0
  38. django_cfg/apps/grpc/serializers/stats.py +22 -0
  39. django_cfg/apps/grpc/services/__init__.py +16 -0
  40. django_cfg/apps/grpc/services/base.py +375 -0
  41. django_cfg/apps/grpc/services/discovery.py +415 -0
  42. django_cfg/apps/grpc/urls.py +23 -0
  43. django_cfg/apps/grpc/utils/__init__.py +13 -0
  44. django_cfg/apps/grpc/utils/proto_gen.py +423 -0
  45. django_cfg/apps/grpc/views/__init__.py +9 -0
  46. django_cfg/apps/grpc/views/monitoring.py +497 -0
  47. django_cfg/apps/knowbase/apps.py +2 -2
  48. django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
  49. django_cfg/apps/maintenance/admin/site_admin.py +5 -4
  50. django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
  51. django_cfg/apps/payments/admin/balance_admin.py +26 -36
  52. django_cfg/apps/payments/admin/payment_admin.py +65 -85
  53. django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
  54. django_cfg/apps/rq/__init__.py +9 -0
  55. django_cfg/apps/rq/apps.py +80 -0
  56. django_cfg/apps/rq/management/__init__.py +1 -0
  57. django_cfg/apps/rq/management/commands/__init__.py +1 -0
  58. django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
  59. django_cfg/apps/rq/management/commands/rqstats.py +33 -0
  60. django_cfg/apps/rq/management/commands/rqworker.py +31 -0
  61. django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
  62. django_cfg/apps/rq/serializers/__init__.py +40 -0
  63. django_cfg/apps/rq/serializers/health.py +60 -0
  64. django_cfg/apps/rq/serializers/job.py +100 -0
  65. django_cfg/apps/rq/serializers/queue.py +80 -0
  66. django_cfg/apps/rq/serializers/schedule.py +178 -0
  67. django_cfg/apps/rq/serializers/testing.py +139 -0
  68. django_cfg/apps/rq/serializers/worker.py +58 -0
  69. django_cfg/apps/rq/services/__init__.py +25 -0
  70. django_cfg/apps/rq/services/config_helper.py +233 -0
  71. django_cfg/apps/rq/services/models/README.md +417 -0
  72. django_cfg/apps/rq/services/models/__init__.py +30 -0
  73. django_cfg/apps/rq/services/models/event.py +123 -0
  74. django_cfg/apps/rq/services/models/job.py +99 -0
  75. django_cfg/apps/rq/services/models/queue.py +92 -0
  76. django_cfg/apps/rq/services/models/worker.py +104 -0
  77. django_cfg/apps/rq/services/rq_converters.py +183 -0
  78. django_cfg/apps/rq/tasks/__init__.py +23 -0
  79. django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
  80. django_cfg/apps/rq/urls.py +54 -0
  81. django_cfg/apps/rq/views/__init__.py +19 -0
  82. django_cfg/apps/rq/views/jobs.py +882 -0
  83. django_cfg/apps/rq/views/monitoring.py +248 -0
  84. django_cfg/apps/rq/views/queues.py +261 -0
  85. django_cfg/apps/rq/views/schedule.py +400 -0
  86. django_cfg/apps/rq/views/testing.py +761 -0
  87. django_cfg/apps/rq/views/workers.py +195 -0
  88. django_cfg/apps/urls.py +13 -8
  89. django_cfg/config.py +106 -0
  90. django_cfg/core/base/config_model.py +16 -26
  91. django_cfg/core/builders/apps_builder.py +7 -11
  92. django_cfg/core/generation/integration_generators/__init__.py +3 -6
  93. django_cfg/core/generation/integration_generators/django_rq.py +80 -0
  94. django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
  95. django_cfg/core/generation/orchestrator.py +15 -15
  96. django_cfg/core/integration/display/startup.py +6 -20
  97. django_cfg/mixins/__init__.py +2 -0
  98. django_cfg/mixins/superadmin_api.py +59 -0
  99. django_cfg/models/__init__.py +3 -3
  100. django_cfg/models/api/grpc/__init__.py +59 -0
  101. django_cfg/models/api/grpc/config.py +364 -0
  102. django_cfg/models/django/__init__.py +3 -3
  103. django_cfg/models/django/django_rq.py +621 -0
  104. django_cfg/models/django/revolution_legacy.py +1 -1
  105. django_cfg/modules/base.py +19 -6
  106. django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
  107. django_cfg/modules/django_admin/config/background_task_config.py +4 -4
  108. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  109. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  110. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  111. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  112. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  113. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  114. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  115. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  116. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  117. django_cfg/modules/django_admin/utils/html/composition.py +205 -0
  118. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  119. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  120. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  121. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  122. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  123. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  124. django_cfg/modules/django_unfold/navigation.py +21 -18
  125. django_cfg/pyproject.toml +4 -6
  126. django_cfg/registry/core.py +4 -7
  127. django_cfg/registry/modules.py +6 -0
  128. django_cfg/static/frontend/admin.zip +0 -0
  129. django_cfg/templates/admin/constance/includes/results_list.html +73 -0
  130. django_cfg/templates/admin/index.html +187 -62
  131. django_cfg/templatetags/django_cfg.py +61 -1
  132. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
  133. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
  134. django_cfg/apps/dashboard/permissions.py +0 -48
  135. django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
  136. django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
  137. django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
  138. django_cfg/apps/tasks/__init__.py +0 -64
  139. django_cfg/apps/tasks/admin/__init__.py +0 -4
  140. django_cfg/apps/tasks/admin/task_log.py +0 -265
  141. django_cfg/apps/tasks/apps.py +0 -15
  142. django_cfg/apps/tasks/filters/__init__.py +0 -10
  143. django_cfg/apps/tasks/filters/task_log.py +0 -121
  144. django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
  145. django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
  146. django_cfg/apps/tasks/models/__init__.py +0 -4
  147. django_cfg/apps/tasks/models/task_log.py +0 -246
  148. django_cfg/apps/tasks/serializers/__init__.py +0 -28
  149. django_cfg/apps/tasks/serializers/task_log.py +0 -249
  150. django_cfg/apps/tasks/services/__init__.py +0 -10
  151. django_cfg/apps/tasks/services/client/__init__.py +0 -7
  152. django_cfg/apps/tasks/services/client/client.py +0 -234
  153. django_cfg/apps/tasks/services/config_helper.py +0 -63
  154. django_cfg/apps/tasks/services/sync.py +0 -204
  155. django_cfg/apps/tasks/urls.py +0 -16
  156. django_cfg/apps/tasks/views/__init__.py +0 -10
  157. django_cfg/apps/tasks/views/task_log.py +0 -41
  158. django_cfg/apps/tasks/views/task_log_base.py +0 -41
  159. django_cfg/apps/tasks/views/task_log_overview.py +0 -100
  160. django_cfg/apps/tasks/views/task_log_related.py +0 -41
  161. django_cfg/apps/tasks/views/task_log_stats.py +0 -91
  162. django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
  163. django_cfg/core/generation/integration_generators/django_q2.py +0 -133
  164. django_cfg/core/generation/integration_generators/tasks.py +0 -88
  165. django_cfg/models/django/django_q2.py +0 -514
  166. django_cfg/models/tasks/__init__.py +0 -49
  167. django_cfg/models/tasks/backends.py +0 -122
  168. django_cfg/models/tasks/config.py +0 -209
  169. django_cfg/models/tasks/utils.py +0 -162
  170. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  171. django_cfg/modules/django_q2/README.md +0 -140
  172. django_cfg/modules/django_q2/__init__.py +0 -8
  173. django_cfg/modules/django_q2/apps.py +0 -107
  174. django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
  175. django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
  176. /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
  177. /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
  178. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  179. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  180. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
  181. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
  182. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,270 @@
1
+ """
2
+ Logging Interceptor for gRPC.
3
+
4
+ Provides comprehensive logging for gRPC requests and responses.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ from typing import Callable
12
+
13
+ import grpc
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LoggingInterceptor(grpc.ServerInterceptor):
19
+ """
20
+ gRPC interceptor for request/response logging.
21
+
22
+ Features:
23
+ - Logs all incoming requests
24
+ - Logs response status and timing
25
+ - Logs errors and exceptions
26
+ - Structured logging with metadata
27
+ - Performance tracking
28
+
29
+ Example:
30
+ ```python
31
+ # In Django settings (auto-configured in dev mode)
32
+ GRPC_FRAMEWORK = {
33
+ "SERVER_INTERCEPTORS": [
34
+ "django_cfg.apps.grpc.interceptors.LoggingInterceptor",
35
+ ]
36
+ }
37
+ ```
38
+
39
+ Log Format:
40
+ [gRPC] METHOD | STATUS | TIME | DETAILS
41
+ """
42
+
43
+ def intercept_service(
44
+ self,
45
+ continuation: Callable,
46
+ handler_call_details: grpc.HandlerCallDetails,
47
+ ) -> grpc.RpcMethodHandler:
48
+ """
49
+ Intercept gRPC service call for logging.
50
+
51
+ Args:
52
+ continuation: Function to invoke the next interceptor or handler
53
+ handler_call_details: Details about the RPC call
54
+
55
+ Returns:
56
+ RPC method handler with logging
57
+ """
58
+ method_name = handler_call_details.method
59
+ peer = self._extract_peer(handler_call_details.invocation_metadata)
60
+
61
+ # Log incoming request
62
+ logger.info(f"[gRPC] ➡️ {method_name} | peer={peer}")
63
+
64
+ # Get handler and wrap it
65
+ handler = continuation(handler_call_details)
66
+
67
+ if handler is None:
68
+ logger.warning(f"[gRPC] ⚠️ {method_name} | No handler found")
69
+ return None
70
+
71
+ # Wrap handler methods to log responses
72
+ return self._wrap_handler(handler, method_name, peer)
73
+
74
+ def _wrap_handler(
75
+ self,
76
+ handler: grpc.RpcMethodHandler,
77
+ method_name: str,
78
+ peer: str,
79
+ ) -> grpc.RpcMethodHandler:
80
+ """
81
+ Wrap handler to add logging.
82
+
83
+ Args:
84
+ handler: Original RPC method handler
85
+ method_name: gRPC method name
86
+ peer: Client peer information
87
+
88
+ Returns:
89
+ Wrapped RPC method handler
90
+ """
91
+ def wrap_unary_unary(behavior):
92
+ def wrapper(request, context):
93
+ start_time = time.time()
94
+ try:
95
+ response = behavior(request, context)
96
+ duration = (time.time() - start_time) * 1000 # ms
97
+ logger.info(
98
+ f"[gRPC] ✅ {method_name} | "
99
+ f"status=OK | "
100
+ f"time={duration:.2f}ms | "
101
+ f"peer={peer}"
102
+ )
103
+ return response
104
+ except Exception as e:
105
+ duration = (time.time() - start_time) * 1000 # ms
106
+ logger.error(
107
+ f"[gRPC] ❌ {method_name} | "
108
+ f"status=ERROR | "
109
+ f"time={duration:.2f}ms | "
110
+ f"error={type(e).__name__}: {str(e)} | "
111
+ f"peer={peer}",
112
+ exc_info=True
113
+ )
114
+ raise
115
+ return wrapper
116
+
117
+ def wrap_unary_stream(behavior):
118
+ def wrapper(request, context):
119
+ start_time = time.time()
120
+ message_count = 0
121
+ try:
122
+ for response in behavior(request, context):
123
+ message_count += 1
124
+ yield response
125
+ duration = (time.time() - start_time) * 1000 # ms
126
+ logger.info(
127
+ f"[gRPC] ✅ {method_name} (stream) | "
128
+ f"status=OK | "
129
+ f"messages={message_count} | "
130
+ f"time={duration:.2f}ms | "
131
+ f"peer={peer}"
132
+ )
133
+ except Exception as e:
134
+ duration = (time.time() - start_time) * 1000 # ms
135
+ logger.error(
136
+ f"[gRPC] ❌ {method_name} (stream) | "
137
+ f"status=ERROR | "
138
+ f"messages={message_count} | "
139
+ f"time={duration:.2f}ms | "
140
+ f"error={type(e).__name__}: {str(e)} | "
141
+ f"peer={peer}",
142
+ exc_info=True
143
+ )
144
+ raise
145
+ return wrapper
146
+
147
+ def wrap_stream_unary(behavior):
148
+ def wrapper(request_iterator, context):
149
+ start_time = time.time()
150
+ message_count = 0
151
+ try:
152
+ # Count messages
153
+ requests = []
154
+ for req in request_iterator:
155
+ message_count += 1
156
+ requests.append(req)
157
+
158
+ # Process
159
+ response = behavior(iter(requests), context)
160
+ duration = (time.time() - start_time) * 1000 # ms
161
+ logger.info(
162
+ f"[gRPC] ✅ {method_name} (client stream) | "
163
+ f"status=OK | "
164
+ f"messages={message_count} | "
165
+ f"time={duration:.2f}ms | "
166
+ f"peer={peer}"
167
+ )
168
+ return response
169
+ except Exception as e:
170
+ duration = (time.time() - start_time) * 1000 # ms
171
+ logger.error(
172
+ f"[gRPC] ❌ {method_name} (client stream) | "
173
+ f"status=ERROR | "
174
+ f"messages={message_count} | "
175
+ f"time={duration:.2f}ms | "
176
+ f"error={type(e).__name__}: {str(e)} | "
177
+ f"peer={peer}",
178
+ exc_info=True
179
+ )
180
+ raise
181
+ return wrapper
182
+
183
+ def wrap_stream_stream(behavior):
184
+ def wrapper(request_iterator, context):
185
+ start_time = time.time()
186
+ in_count = 0
187
+ out_count = 0
188
+ try:
189
+ # Count input messages
190
+ requests = []
191
+ for req in request_iterator:
192
+ in_count += 1
193
+ requests.append(req)
194
+
195
+ # Process and count output
196
+ for response in behavior(iter(requests), context):
197
+ out_count += 1
198
+ yield response
199
+
200
+ duration = (time.time() - start_time) * 1000 # ms
201
+ logger.info(
202
+ f"[gRPC] ✅ {method_name} (bidi stream) | "
203
+ f"status=OK | "
204
+ f"in={in_count} out={out_count} | "
205
+ f"time={duration:.2f}ms | "
206
+ f"peer={peer}"
207
+ )
208
+ except Exception as e:
209
+ duration = (time.time() - start_time) * 1000 # ms
210
+ logger.error(
211
+ f"[gRPC] ❌ {method_name} (bidi stream) | "
212
+ f"status=ERROR | "
213
+ f"in={in_count} out={out_count} | "
214
+ f"time={duration:.2f}ms | "
215
+ f"error={type(e).__name__}: {str(e)} | "
216
+ f"peer={peer}",
217
+ exc_info=True
218
+ )
219
+ raise
220
+ return wrapper
221
+
222
+ # Return wrapped handler based on type
223
+ if handler.unary_unary:
224
+ return grpc.unary_unary_rpc_method_handler(
225
+ wrap_unary_unary(handler.unary_unary),
226
+ request_deserializer=handler.request_deserializer,
227
+ response_serializer=handler.response_serializer,
228
+ )
229
+ elif handler.unary_stream:
230
+ return grpc.unary_stream_rpc_method_handler(
231
+ wrap_unary_stream(handler.unary_stream),
232
+ request_deserializer=handler.request_deserializer,
233
+ response_serializer=handler.response_serializer,
234
+ )
235
+ elif handler.stream_unary:
236
+ return grpc.stream_unary_rpc_method_handler(
237
+ wrap_stream_unary(handler.stream_unary),
238
+ request_deserializer=handler.request_deserializer,
239
+ response_serializer=handler.response_serializer,
240
+ )
241
+ elif handler.stream_stream:
242
+ return grpc.stream_stream_rpc_method_handler(
243
+ wrap_stream_stream(handler.stream_stream),
244
+ request_deserializer=handler.request_deserializer,
245
+ response_serializer=handler.response_serializer,
246
+ )
247
+ else:
248
+ return handler
249
+
250
+ def _extract_peer(self, metadata: tuple) -> str:
251
+ """
252
+ Extract peer information from metadata.
253
+
254
+ Args:
255
+ metadata: gRPC invocation metadata
256
+
257
+ Returns:
258
+ Peer identifier string
259
+ """
260
+ if not metadata:
261
+ return "unknown"
262
+
263
+ # Convert to dict for easier access
264
+ metadata_dict = dict(metadata)
265
+
266
+ # Try to get user-agent or return unknown
267
+ return metadata_dict.get("user-agent", "unknown")
268
+
269
+
270
+ __all__ = ["LoggingInterceptor"]
@@ -0,0 +1,306 @@
1
+ """
2
+ Metrics Interceptor for gRPC.
3
+
4
+ Tracks request counts, response times, and error rates.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ from collections import defaultdict
12
+ from typing import Callable
13
+
14
+ import grpc
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MetricsCollector:
20
+ """
21
+ Thread-safe metrics collector for gRPC.
22
+
23
+ Tracks:
24
+ - Request counts per method
25
+ - Response times per method
26
+ - Error counts per method
27
+ - Total requests/errors
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize metrics collector."""
32
+ self.request_counts = defaultdict(int)
33
+ self.error_counts = defaultdict(int)
34
+ self.response_times = defaultdict(list)
35
+ self.total_requests = 0
36
+ self.total_errors = 0
37
+
38
+ def record_request(self, method: str):
39
+ """Record a request."""
40
+ self.request_counts[method] += 1
41
+ self.total_requests += 1
42
+
43
+ def record_error(self, method: str):
44
+ """Record an error."""
45
+ self.error_counts[method] += 1
46
+ self.total_errors += 1
47
+
48
+ def record_response_time(self, method: str, duration_ms: float):
49
+ """Record response time."""
50
+ self.response_times[method].append(duration_ms)
51
+
52
+ def get_stats(self, method: str = None) -> dict:
53
+ """
54
+ Get statistics for a method or all methods.
55
+
56
+ Args:
57
+ method: Specific method name, or None for all
58
+
59
+ Returns:
60
+ Dictionary with statistics
61
+ """
62
+ if method:
63
+ times = self.response_times.get(method, [])
64
+ return {
65
+ "requests": self.request_counts.get(method, 0),
66
+ "errors": self.error_counts.get(method, 0),
67
+ "avg_time_ms": sum(times) / len(times) if times else 0,
68
+ "min_time_ms": min(times) if times else 0,
69
+ "max_time_ms": max(times) if times else 0,
70
+ }
71
+ else:
72
+ return {
73
+ "total_requests": self.total_requests,
74
+ "total_errors": self.total_errors,
75
+ "error_rate": (
76
+ self.total_errors / self.total_requests
77
+ if self.total_requests > 0
78
+ else 0
79
+ ),
80
+ "methods": {
81
+ method: self.get_stats(method)
82
+ for method in self.request_counts.keys()
83
+ },
84
+ }
85
+
86
+ def reset(self):
87
+ """Reset all metrics."""
88
+ self.request_counts.clear()
89
+ self.error_counts.clear()
90
+ self.response_times.clear()
91
+ self.total_requests = 0
92
+ self.total_errors = 0
93
+
94
+
95
+ # Global metrics collector instance
96
+ _metrics = MetricsCollector()
97
+
98
+
99
+ def get_metrics(method: str = None) -> dict:
100
+ """
101
+ Get metrics for a method or all methods.
102
+
103
+ Args:
104
+ method: Specific method name, or None for all
105
+
106
+ Returns:
107
+ Dictionary with metrics
108
+
109
+ Example:
110
+ ```python
111
+ from django_cfg.apps.grpc.interceptors.metrics import get_metrics
112
+
113
+ # Get all metrics
114
+ all_stats = get_metrics()
115
+
116
+ # Get metrics for specific method
117
+ stats = get_metrics("/myapp.MyService/MyMethod")
118
+ print(f"Requests: {stats['requests']}")
119
+ print(f"Avg time: {stats['avg_time_ms']:.2f}ms")
120
+ ```
121
+ """
122
+ return _metrics.get_stats(method)
123
+
124
+
125
+ def reset_metrics():
126
+ """
127
+ Reset all metrics.
128
+
129
+ Example:
130
+ ```python
131
+ from django_cfg.apps.grpc.interceptors.metrics import reset_metrics
132
+ reset_metrics()
133
+ ```
134
+ """
135
+ _metrics.reset()
136
+
137
+
138
+ class MetricsInterceptor(grpc.ServerInterceptor):
139
+ """
140
+ gRPC interceptor for metrics collection.
141
+
142
+ Features:
143
+ - Tracks request counts
144
+ - Tracks response times
145
+ - Tracks error rates
146
+ - Per-method statistics
147
+ - Global statistics
148
+
149
+ Example:
150
+ ```python
151
+ # In Django settings (auto-configured in dev mode)
152
+ GRPC_FRAMEWORK = {
153
+ "SERVER_INTERCEPTORS": [
154
+ "django_cfg.apps.grpc.interceptors.MetricsInterceptor",
155
+ ]
156
+ }
157
+ ```
158
+
159
+ Access Metrics:
160
+ ```python
161
+ from django_cfg.apps.grpc.interceptors.metrics import get_metrics
162
+
163
+ stats = get_metrics()
164
+ print(f"Total requests: {stats['total_requests']}")
165
+ print(f"Error rate: {stats['error_rate']:.2%}")
166
+ ```
167
+ """
168
+
169
+ def __init__(self):
170
+ """Initialize metrics interceptor."""
171
+ self.collector = _metrics
172
+
173
+ def intercept_service(
174
+ self,
175
+ continuation: Callable,
176
+ handler_call_details: grpc.HandlerCallDetails,
177
+ ) -> grpc.RpcMethodHandler:
178
+ """
179
+ Intercept gRPC service call for metrics collection.
180
+
181
+ Args:
182
+ continuation: Function to invoke the next interceptor or handler
183
+ handler_call_details: Details about the RPC call
184
+
185
+ Returns:
186
+ RPC method handler with metrics
187
+ """
188
+ method_name = handler_call_details.method
189
+
190
+ # Record request
191
+ self.collector.record_request(method_name)
192
+
193
+ # Get handler and wrap it
194
+ handler = continuation(handler_call_details)
195
+
196
+ if handler is None:
197
+ return None
198
+
199
+ # Wrap handler methods to track metrics
200
+ return self._wrap_handler(handler, method_name)
201
+
202
+ def _wrap_handler(
203
+ self,
204
+ handler: grpc.RpcMethodHandler,
205
+ method_name: str,
206
+ ) -> grpc.RpcMethodHandler:
207
+ """
208
+ Wrap handler to track metrics.
209
+
210
+ Args:
211
+ handler: Original RPC method handler
212
+ method_name: gRPC method name
213
+
214
+ Returns:
215
+ Wrapped RPC method handler
216
+ """
217
+ def wrap_unary_unary(behavior):
218
+ def wrapper(request, context):
219
+ start_time = time.time()
220
+ try:
221
+ response = behavior(request, context)
222
+ duration_ms = (time.time() - start_time) * 1000
223
+ self.collector.record_response_time(method_name, duration_ms)
224
+ return response
225
+ except Exception as e:
226
+ duration_ms = (time.time() - start_time) * 1000
227
+ self.collector.record_response_time(method_name, duration_ms)
228
+ self.collector.record_error(method_name)
229
+ raise
230
+ return wrapper
231
+
232
+ def wrap_unary_stream(behavior):
233
+ def wrapper(request, context):
234
+ start_time = time.time()
235
+ try:
236
+ for response in behavior(request, context):
237
+ yield response
238
+ duration_ms = (time.time() - start_time) * 1000
239
+ self.collector.record_response_time(method_name, duration_ms)
240
+ except Exception as e:
241
+ duration_ms = (time.time() - start_time) * 1000
242
+ self.collector.record_response_time(method_name, duration_ms)
243
+ self.collector.record_error(method_name)
244
+ raise
245
+ return wrapper
246
+
247
+ def wrap_stream_unary(behavior):
248
+ def wrapper(request_iterator, context):
249
+ start_time = time.time()
250
+ try:
251
+ response = behavior(request_iterator, context)
252
+ duration_ms = (time.time() - start_time) * 1000
253
+ self.collector.record_response_time(method_name, duration_ms)
254
+ return response
255
+ except Exception as e:
256
+ duration_ms = (time.time() - start_time) * 1000
257
+ self.collector.record_response_time(method_name, duration_ms)
258
+ self.collector.record_error(method_name)
259
+ raise
260
+ return wrapper
261
+
262
+ def wrap_stream_stream(behavior):
263
+ def wrapper(request_iterator, context):
264
+ start_time = time.time()
265
+ try:
266
+ for response in behavior(request_iterator, context):
267
+ yield response
268
+ duration_ms = (time.time() - start_time) * 1000
269
+ self.collector.record_response_time(method_name, duration_ms)
270
+ except Exception as e:
271
+ duration_ms = (time.time() - start_time) * 1000
272
+ self.collector.record_response_time(method_name, duration_ms)
273
+ self.collector.record_error(method_name)
274
+ raise
275
+ return wrapper
276
+
277
+ # Return wrapped handler based on type
278
+ if handler.unary_unary:
279
+ return grpc.unary_unary_rpc_method_handler(
280
+ wrap_unary_unary(handler.unary_unary),
281
+ request_deserializer=handler.request_deserializer,
282
+ response_serializer=handler.response_serializer,
283
+ )
284
+ elif handler.unary_stream:
285
+ return grpc.unary_stream_rpc_method_handler(
286
+ wrap_unary_stream(handler.unary_stream),
287
+ request_deserializer=handler.request_deserializer,
288
+ response_serializer=handler.response_serializer,
289
+ )
290
+ elif handler.stream_unary:
291
+ return grpc.stream_unary_rpc_method_handler(
292
+ wrap_stream_unary(handler.stream_unary),
293
+ request_deserializer=handler.request_deserializer,
294
+ response_serializer=handler.response_serializer,
295
+ )
296
+ elif handler.stream_stream:
297
+ return grpc.stream_stream_rpc_method_handler(
298
+ wrap_stream_stream(handler.stream_stream),
299
+ request_deserializer=handler.request_deserializer,
300
+ response_serializer=handler.response_serializer,
301
+ )
302
+ else:
303
+ return handler
304
+
305
+
306
+ __all__ = ["MetricsInterceptor", "MetricsCollector", "get_metrics", "reset_metrics"]