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,302 @@
1
+ """
2
+ Django management command to run gRPC server.
3
+
4
+ Usage:
5
+ python manage.py rungrpc
6
+ python manage.py rungrpc --host 0.0.0.0 --port 50051
7
+ python manage.py rungrpc --workers 20
8
+ """
9
+
10
+ import logging
11
+ import signal
12
+ import sys
13
+ from concurrent import futures
14
+
15
+ import grpc
16
+ from django.conf import settings
17
+ from django.core.management.base import BaseCommand
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class Command(BaseCommand):
23
+ """
24
+ Run gRPC server with auto-discovered services.
25
+
26
+ Features:
27
+ - Auto-discovers and registers services
28
+ - Configurable host, port, and workers
29
+ - Health check support
30
+ - Reflection support
31
+ - Graceful shutdown
32
+ - Signal handling
33
+ """
34
+
35
+ help = "Run gRPC server"
36
+
37
+ def add_arguments(self, parser):
38
+ """Add command arguments."""
39
+ parser.add_argument(
40
+ "--host",
41
+ type=str,
42
+ default=None,
43
+ help="Server host (default: from settings or [::])",
44
+ )
45
+ parser.add_argument(
46
+ "--port",
47
+ type=int,
48
+ default=None,
49
+ help="Server port (default: from settings or 50051)",
50
+ )
51
+ parser.add_argument(
52
+ "--workers",
53
+ type=int,
54
+ default=None,
55
+ help="Max worker threads (default: from settings or 10)",
56
+ )
57
+ parser.add_argument(
58
+ "--no-reflection",
59
+ action="store_true",
60
+ help="Disable server reflection",
61
+ )
62
+ parser.add_argument(
63
+ "--no-health-check",
64
+ action="store_true",
65
+ help="Disable health check service",
66
+ )
67
+
68
+ def handle(self, *args, **options):
69
+ """Run gRPC server."""
70
+ # Get configuration
71
+ grpc_server_config = getattr(settings, "GRPC_SERVER", {})
72
+
73
+ # Get server parameters
74
+ host = options["host"] or grpc_server_config.get("host", "[::]")
75
+ port = options["port"] or grpc_server_config.get("port", 50051)
76
+ max_workers = options["workers"] or grpc_server_config.get("max_workers", 10)
77
+
78
+ # Server options
79
+ enable_reflection = not options["no_reflection"] and grpc_server_config.get(
80
+ "enable_reflection", False
81
+ )
82
+ enable_health_check = not options["no_health_check"] and grpc_server_config.get(
83
+ "enable_health_check", True
84
+ )
85
+
86
+ # gRPC options
87
+ grpc_options = self._build_grpc_options(grpc_server_config)
88
+
89
+ # Create server
90
+ self.stdout.write(self.style.SUCCESS(f"Creating gRPC server with {max_workers} workers..."))
91
+ server = grpc.server(
92
+ futures.ThreadPoolExecutor(max_workers=max_workers),
93
+ options=grpc_options,
94
+ interceptors=self._build_interceptors(),
95
+ )
96
+
97
+ # Add health check
98
+ if enable_health_check:
99
+ self._add_health_check(server)
100
+
101
+ # Discover and register services
102
+ self.stdout.write("Discovering services...")
103
+ service_count = self._register_services(server)
104
+
105
+ if service_count == 0:
106
+ self.stdout.write(
107
+ self.style.WARNING(
108
+ "No services registered. "
109
+ "Make sure you have services defined in your apps."
110
+ )
111
+ )
112
+ else:
113
+ self.stdout.write(
114
+ self.style.SUCCESS(f"Registered {service_count} service(s)")
115
+ )
116
+
117
+ # Add reflection
118
+ if enable_reflection:
119
+ self._add_reflection(server)
120
+ self.stdout.write(self.style.SUCCESS("Server reflection enabled"))
121
+
122
+ # Bind server
123
+ address = f"{host}:{port}"
124
+ server.add_insecure_port(address)
125
+
126
+ # Start server
127
+ self.stdout.write(self.style.SUCCESS(f"\nStarting gRPC server on {address}..."))
128
+ self.stdout.write(self.style.SUCCESS(f"Health check: {'enabled' if enable_health_check else 'disabled'}"))
129
+ self.stdout.write(self.style.SUCCESS(f"Reflection: {'enabled' if enable_reflection else 'disabled'}"))
130
+ self.stdout.write("")
131
+
132
+ server.start()
133
+
134
+ # Setup signal handlers for graceful shutdown
135
+ self._setup_signal_handlers(server)
136
+
137
+ # Keep server running
138
+ try:
139
+ self.stdout.write(self.style.SUCCESS("✅ gRPC server is running..."))
140
+ self.stdout.write("Press CTRL+C to stop")
141
+ server.wait_for_termination()
142
+ except KeyboardInterrupt:
143
+ self.stdout.write("\nShutting down...")
144
+ server.stop(grace=5)
145
+ self.stdout.write(self.style.SUCCESS("Server stopped"))
146
+
147
+ def _build_grpc_options(self, config: dict) -> list:
148
+ """
149
+ Build gRPC server options from configuration.
150
+
151
+ Args:
152
+ config: GRPC_SERVER configuration dict
153
+
154
+ Returns:
155
+ List of gRPC options tuples
156
+ """
157
+ options = []
158
+
159
+ # Message size limits
160
+ max_send = config.get("max_send_message_length", 4 * 1024 * 1024)
161
+ max_receive = config.get("max_receive_message_length", 4 * 1024 * 1024)
162
+
163
+ options.append(("grpc.max_send_message_length", max_send))
164
+ options.append(("grpc.max_receive_message_length", max_receive))
165
+
166
+ # Keepalive settings
167
+ keepalive_time = config.get("keepalive_time_ms", 7200000)
168
+ keepalive_timeout = config.get("keepalive_timeout_ms", 20000)
169
+
170
+ options.append(("grpc.keepalive_time_ms", keepalive_time))
171
+ options.append(("grpc.keepalive_timeout_ms", keepalive_timeout))
172
+ options.append(("grpc.http2.max_pings_without_data", 0))
173
+
174
+ return options
175
+
176
+ def _build_interceptors(self) -> list:
177
+ """
178
+ Build server interceptors from configuration.
179
+
180
+ Returns:
181
+ List of interceptor instances
182
+ """
183
+ grpc_framework_config = getattr(settings, "GRPC_FRAMEWORK", {})
184
+ interceptor_paths = grpc_framework_config.get("SERVER_INTERCEPTORS", [])
185
+
186
+ interceptors = []
187
+
188
+ for interceptor_path in interceptor_paths:
189
+ try:
190
+ # Import interceptor class
191
+ module_path, class_name = interceptor_path.rsplit(".", 1)
192
+
193
+ import importlib
194
+ module = importlib.import_module(module_path)
195
+ interceptor_class = getattr(module, class_name)
196
+
197
+ # Instantiate interceptor
198
+ interceptor = interceptor_class()
199
+ interceptors.append(interceptor)
200
+
201
+ logger.debug(f"Loaded interceptor: {class_name}")
202
+
203
+ except Exception as e:
204
+ logger.error(f"Failed to load interceptor {interceptor_path}: {e}")
205
+
206
+ return interceptors
207
+
208
+ def _add_health_check(self, server):
209
+ """
210
+ Add health check service to server.
211
+
212
+ Args:
213
+ server: gRPC server instance
214
+ """
215
+ try:
216
+ from grpc_health.v1 import health, health_pb2_grpc
217
+
218
+ # Create health servicer
219
+ health_servicer = health.HealthServicer()
220
+
221
+ # Mark all services as serving
222
+ health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
223
+
224
+ # Add to server
225
+ health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
226
+
227
+ logger.info("Health check service added")
228
+
229
+ except ImportError:
230
+ logger.warning(
231
+ "grpcio-health-checking not installed. "
232
+ "Install with: pip install grpcio-health-checking"
233
+ )
234
+ except Exception as e:
235
+ logger.error(f"Failed to add health check service: {e}")
236
+
237
+ def _add_reflection(self, server):
238
+ """
239
+ Add reflection service to server.
240
+
241
+ Args:
242
+ server: gRPC server instance
243
+ """
244
+ try:
245
+ from grpc_reflection.v1alpha import reflection
246
+
247
+ # Get service names
248
+ service_names = [
249
+ desc.full_name
250
+ for desc in server._state.generic_handlers[0].service_name()
251
+ ] if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers') else []
252
+
253
+ # Add reflection
254
+ reflection.enable_server_reflection(service_names, server)
255
+
256
+ logger.info("Server reflection enabled")
257
+
258
+ except ImportError:
259
+ logger.warning(
260
+ "grpcio-reflection not installed. "
261
+ "Install with: pip install grpcio-reflection"
262
+ )
263
+ except Exception as e:
264
+ logger.error(f"Failed to enable server reflection: {e}")
265
+
266
+ def _register_services(self, server) -> int:
267
+ """
268
+ Discover and register services to server.
269
+
270
+ Args:
271
+ server: gRPC server instance
272
+
273
+ Returns:
274
+ Number of services registered
275
+ """
276
+ try:
277
+ from django_cfg.modules.django_grpc.services import discover_and_register_services
278
+
279
+ count = discover_and_register_services(server)
280
+ return count
281
+
282
+ except Exception as e:
283
+ logger.error(f"Failed to register services: {e}", exc_info=True)
284
+ self.stdout.write(
285
+ self.style.ERROR(f"Error registering services: {e}")
286
+ )
287
+ return 0
288
+
289
+ def _setup_signal_handlers(self, server):
290
+ """
291
+ Setup signal handlers for graceful shutdown.
292
+
293
+ Args:
294
+ server: gRPC server instance
295
+ """
296
+ def handle_signal(sig, frame):
297
+ self.stdout.write(f"\nReceived signal {sig}, shutting down...")
298
+ server.stop(grace=5)
299
+ sys.exit(0)
300
+
301
+ signal.signal(signal.SIGINT, handle_signal)
302
+ signal.signal(signal.SIGTERM, handle_signal)
@@ -0,0 +1,10 @@
1
+ """
2
+ Managers for gRPC app models.
3
+ """
4
+
5
+ from .grpc_request_log import GRPCRequestLogManager, GRPCRequestLogQuerySet
6
+
7
+ __all__ = [
8
+ "GRPCRequestLogManager",
9
+ "GRPCRequestLogQuerySet",
10
+ ]
@@ -0,0 +1,310 @@
1
+ """
2
+ GRPCRequestLog Manager.
3
+
4
+ Custom QuerySet and Manager for GRPCRequestLog model.
5
+ """
6
+
7
+ from django.db import models
8
+ from django.utils import timezone
9
+
10
+
11
+ class GRPCRequestLogQuerySet(models.QuerySet):
12
+ """Custom QuerySet for GRPCRequestLog with filtering helpers."""
13
+
14
+ def pending(self):
15
+ """Get all pending logs."""
16
+ return self.filter(status="pending")
17
+
18
+ def successful(self):
19
+ """Get all successful logs."""
20
+ return self.filter(status="success")
21
+
22
+ def error(self):
23
+ """Get all error logs."""
24
+ return self.filter(status="error")
25
+
26
+ def cancelled(self):
27
+ """Get all cancelled logs."""
28
+ return self.filter(status="cancelled")
29
+
30
+ def timeout(self):
31
+ """Get all timeout logs."""
32
+ return self.filter(status="timeout")
33
+
34
+ def authenticated(self):
35
+ """Get logs for authenticated requests."""
36
+ return self.filter(is_authenticated=True)
37
+
38
+ def for_service(self, service_name: str):
39
+ """Get logs for specific service."""
40
+ return self.filter(service_name=service_name)
41
+
42
+ def for_method(self, method_name: str):
43
+ """Get logs for specific method."""
44
+ return self.filter(method_name=method_name)
45
+
46
+ def for_user(self, user):
47
+ """Get logs for specific user."""
48
+ return self.filter(user=user)
49
+
50
+ def recent(self, hours: int = 24):
51
+ """Get logs from recent hours."""
52
+ cutoff = timezone.now() - timezone.timedelta(hours=hours)
53
+ return self.filter(created_at__gte=cutoff)
54
+
55
+ def completed(self):
56
+ """Get all completed logs (success, error, cancelled, timeout)."""
57
+ return self.exclude(status="pending")
58
+
59
+ def by_performance(self):
60
+ """Order by duration (fastest first)."""
61
+ return self.filter(duration_ms__isnull=False).order_by("duration_ms")
62
+
63
+ def slow_requests(self, threshold_ms: int = 1000):
64
+ """Get slow requests (above threshold)."""
65
+ return self.filter(duration_ms__gt=threshold_ms)
66
+
67
+
68
+ class GRPCRequestLogManager(models.Manager):
69
+ """Custom Manager for GRPCRequestLog."""
70
+
71
+ def get_queryset(self):
72
+ """Return custom QuerySet."""
73
+ return GRPCRequestLogQuerySet(self.model, using=self._db)
74
+
75
+ def pending(self):
76
+ """Get pending logs."""
77
+ return self.get_queryset().pending()
78
+
79
+ def successful(self):
80
+ """Get successful logs."""
81
+ return self.get_queryset().successful()
82
+
83
+ def error(self):
84
+ """Get error logs."""
85
+ return self.get_queryset().error()
86
+
87
+ def cancelled(self):
88
+ """Get cancelled logs."""
89
+ return self.get_queryset().cancelled()
90
+
91
+ def timeout(self):
92
+ """Get timeout logs."""
93
+ return self.get_queryset().timeout()
94
+
95
+ def authenticated(self):
96
+ """Get authenticated requests."""
97
+ return self.get_queryset().authenticated()
98
+
99
+ def for_service(self, service_name: str):
100
+ """Get logs for service."""
101
+ return self.get_queryset().for_service(service_name)
102
+
103
+ def for_method(self, method_name: str):
104
+ """Get logs for method."""
105
+ return self.get_queryset().for_method(method_name)
106
+
107
+ def for_user(self, user):
108
+ """Get logs for user."""
109
+ return self.get_queryset().for_user(user)
110
+
111
+ def recent(self, hours: int = 24):
112
+ """Get recent logs."""
113
+ return self.get_queryset().recent(hours)
114
+
115
+ def slow_requests(self, threshold_ms: int = 1000):
116
+ """Get slow requests."""
117
+ return self.get_queryset().slow_requests(threshold_ms)
118
+
119
+ def get_statistics(self, hours: int = 24):
120
+ """
121
+ Get request statistics for recent period.
122
+
123
+ Args:
124
+ hours: Number of hours to analyze
125
+
126
+ Returns:
127
+ Dictionary with statistics
128
+ """
129
+ recent_logs = self.recent(hours)
130
+
131
+ total = recent_logs.count()
132
+ successful = recent_logs.successful().count()
133
+ errors = recent_logs.error().count()
134
+ cancelled = recent_logs.cancelled().count()
135
+ timeout_count = recent_logs.timeout().count()
136
+
137
+ success_rate = (successful / total * 100) if total > 0 else 0
138
+
139
+ avg_duration = recent_logs.filter(
140
+ duration_ms__isnull=False
141
+ ).aggregate(
142
+ models.Avg("duration_ms")
143
+ )["duration_ms__avg"] or 0
144
+
145
+ p95_duration = None
146
+ if total > 0:
147
+ # Calculate 95th percentile
148
+ sorted_durations = list(
149
+ recent_logs.filter(duration_ms__isnull=False)
150
+ .order_by("duration_ms")
151
+ .values_list("duration_ms", flat=True)
152
+ )
153
+ if sorted_durations:
154
+ p95_index = int(len(sorted_durations) * 0.95)
155
+ p95_duration = sorted_durations[p95_index] if p95_index < len(sorted_durations) else sorted_durations[-1]
156
+
157
+ return {
158
+ "total": total,
159
+ "successful": successful,
160
+ "errors": errors,
161
+ "cancelled": cancelled,
162
+ "timeout": timeout_count,
163
+ "success_rate": round(success_rate, 2),
164
+ "avg_duration_ms": round(avg_duration, 2),
165
+ "p95_duration_ms": round(p95_duration, 2) if p95_duration else None,
166
+ }
167
+
168
+ def mark_success(
169
+ self,
170
+ log_instance,
171
+ duration_ms: int | None = None,
172
+ response_size: int | None = None,
173
+ response_data: dict | None = None,
174
+ ):
175
+ """
176
+ Mark request as successful.
177
+
178
+ Args:
179
+ log_instance: GRPCRequestLog instance
180
+ duration_ms: Duration in milliseconds
181
+ response_size: Response size in bytes
182
+ response_data: Response data
183
+ """
184
+ from ..models import GRPCRequestLog
185
+
186
+ log_instance.status = GRPCRequestLog.StatusChoices.SUCCESS
187
+ log_instance.grpc_status_code = "OK"
188
+ log_instance.completed_at = timezone.now()
189
+
190
+ if duration_ms is not None:
191
+ log_instance.duration_ms = duration_ms
192
+
193
+ if response_size is not None:
194
+ log_instance.response_size = response_size
195
+
196
+ if response_data is not None:
197
+ log_instance.response_data = response_data
198
+
199
+ log_instance.save(
200
+ update_fields=[
201
+ "status",
202
+ "grpc_status_code",
203
+ "completed_at",
204
+ "duration_ms",
205
+ "response_size",
206
+ "response_data",
207
+ ]
208
+ )
209
+
210
+ def mark_error(
211
+ self,
212
+ log_instance,
213
+ grpc_status_code: str,
214
+ error_message: str,
215
+ error_details: dict | None = None,
216
+ duration_ms: int | None = None,
217
+ ):
218
+ """
219
+ Mark request as failed.
220
+
221
+ Args:
222
+ log_instance: GRPCRequestLog instance
223
+ grpc_status_code: gRPC status code
224
+ error_message: Error message
225
+ error_details: Additional error details
226
+ duration_ms: Duration in milliseconds
227
+ """
228
+ from ..models import GRPCRequestLog
229
+
230
+ log_instance.status = GRPCRequestLog.StatusChoices.ERROR
231
+ log_instance.grpc_status_code = grpc_status_code
232
+ log_instance.error_message = error_message
233
+ log_instance.completed_at = timezone.now()
234
+
235
+ if error_details is not None:
236
+ log_instance.error_details = error_details
237
+
238
+ if duration_ms is not None:
239
+ log_instance.duration_ms = duration_ms
240
+
241
+ log_instance.save(
242
+ update_fields=[
243
+ "status",
244
+ "grpc_status_code",
245
+ "error_message",
246
+ "error_details",
247
+ "completed_at",
248
+ "duration_ms",
249
+ ]
250
+ )
251
+
252
+ def mark_cancelled(
253
+ self,
254
+ log_instance,
255
+ duration_ms: int | None = None,
256
+ ):
257
+ """
258
+ Mark request as cancelled.
259
+
260
+ Args:
261
+ log_instance: GRPCRequestLog instance
262
+ duration_ms: Duration in milliseconds
263
+ """
264
+ from ..models import GRPCRequestLog
265
+
266
+ log_instance.status = GRPCRequestLog.StatusChoices.CANCELLED
267
+ log_instance.grpc_status_code = "CANCELLED"
268
+ log_instance.completed_at = timezone.now()
269
+
270
+ if duration_ms is not None:
271
+ log_instance.duration_ms = duration_ms
272
+
273
+ log_instance.save(
274
+ update_fields=["status", "grpc_status_code", "completed_at", "duration_ms"]
275
+ )
276
+
277
+ def mark_timeout(
278
+ self,
279
+ log_instance,
280
+ duration_ms: int | None = None,
281
+ ):
282
+ """
283
+ Mark request as timed out.
284
+
285
+ Args:
286
+ log_instance: GRPCRequestLog instance
287
+ duration_ms: Duration in milliseconds
288
+ """
289
+ from ..models import GRPCRequestLog
290
+
291
+ log_instance.status = GRPCRequestLog.StatusChoices.TIMEOUT
292
+ log_instance.grpc_status_code = "DEADLINE_EXCEEDED"
293
+ log_instance.error_message = "Request timed out"
294
+ log_instance.completed_at = timezone.now()
295
+
296
+ if duration_ms is not None:
297
+ log_instance.duration_ms = duration_ms
298
+
299
+ log_instance.save(
300
+ update_fields=[
301
+ "status",
302
+ "grpc_status_code",
303
+ "error_message",
304
+ "completed_at",
305
+ "duration_ms",
306
+ ]
307
+ )
308
+
309
+
310
+ __all__ = ["GRPCRequestLogManager", "GRPCRequestLogQuerySet"]
@@ -0,0 +1,69 @@
1
+ # Generated manually for django_cfg.apps.grpc
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name='GRPCRequestLog',
19
+ fields=[
20
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+ ('request_id', models.CharField(db_index=True, help_text='Unique request identifier (UUID)', max_length=100, unique=True)),
22
+ ('service_name', models.CharField(db_index=True, help_text='gRPC service name (e.g., myapp.UserService)', max_length=200)),
23
+ ('method_name', models.CharField(db_index=True, help_text='gRPC method name (e.g., GetUser)', max_length=200)),
24
+ ('full_method', models.CharField(db_index=True, help_text='Full method path (e.g., /myapp.UserService/GetUser)', max_length=400)),
25
+ ('request_size', models.IntegerField(blank=True, help_text='Request size in bytes', null=True)),
26
+ ('response_size', models.IntegerField(blank=True, help_text='Response size in bytes', null=True)),
27
+ ('request_data', models.JSONField(blank=True, help_text='Request data (if logged)', null=True)),
28
+ ('response_data', models.JSONField(blank=True, help_text='Response data (if logged)', null=True)),
29
+ ('status', models.CharField(choices=[('pending', 'Pending'), ('success', 'Success'), ('error', 'Error'), ('cancelled', 'Cancelled'), ('timeout', 'Timeout')], db_index=True, default='pending', help_text='Current status of request', max_length=20)),
30
+ ('grpc_status_code', models.CharField(blank=True, db_index=True, help_text='gRPC status code (OK, CANCELLED, INVALID_ARGUMENT, etc.)', max_length=50, null=True)),
31
+ ('error_message', models.TextField(blank=True, help_text='Error message if failed', null=True)),
32
+ ('error_details', models.JSONField(blank=True, help_text='Additional error details', null=True)),
33
+ ('duration_ms', models.IntegerField(blank=True, help_text='Total duration in milliseconds', null=True)),
34
+ ('is_authenticated', models.BooleanField(db_index=True, default=False, help_text='Whether request was authenticated')),
35
+ ('client_ip', models.GenericIPAddressField(blank=True, help_text='Client IP address', null=True)),
36
+ ('user_agent', models.TextField(blank=True, help_text='User agent from metadata', null=True)),
37
+ ('peer', models.CharField(blank=True, help_text='gRPC peer information', max_length=200, null=True)),
38
+ ('created_at', models.DateTimeField(auto_now_add=True, db_index=True, help_text='When request was received')),
39
+ ('completed_at', models.DateTimeField(blank=True, db_index=True, help_text='When request completed (success/error)', null=True)),
40
+ ('user', models.ForeignKey(blank=True, help_text='Authenticated user (if applicable)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='grpc_request_logs', to=settings.AUTH_USER_MODEL)),
41
+ ],
42
+ options={
43
+ 'verbose_name': 'gRPC Request Log',
44
+ 'verbose_name_plural': 'gRPC Request Logs',
45
+ 'db_table': 'django_cfg_grpc_request_log',
46
+ 'ordering': ['-created_at'],
47
+ },
48
+ ),
49
+ migrations.AddIndex(
50
+ model_name='grpcrequestlog',
51
+ index=models.Index(fields=['service_name', '-created_at'], name='django_cfg__service_4c4a8e_idx'),
52
+ ),
53
+ migrations.AddIndex(
54
+ model_name='grpcrequestlog',
55
+ index=models.Index(fields=['method_name', '-created_at'], name='django_cfg__method__8e1a7c_idx'),
56
+ ),
57
+ migrations.AddIndex(
58
+ model_name='grpcrequestlog',
59
+ index=models.Index(fields=['status', '-created_at'], name='django_cfg__status_f3d9a1_idx'),
60
+ ),
61
+ migrations.AddIndex(
62
+ model_name='grpcrequestlog',
63
+ index=models.Index(fields=['user', '-created_at'], name='django_cfg__user_id_9c2b4f_idx'),
64
+ ),
65
+ migrations.AddIndex(
66
+ model_name='grpcrequestlog',
67
+ index=models.Index(fields=['grpc_status_code', '-created_at'], name='django_cfg__grpc_st_a7e5c2_idx'),
68
+ ),
69
+ ]