django-cfg 1.4.120__py3-none-any.whl → 1.5.1__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 +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/grpc/admin/config.py +89 -0
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/__init__.py +0 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/migrations/__init__.py +0 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/tasks/admin/task_log.py +20 -47
- django_cfg/apps/urls.py +7 -1
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +6 -0
- django_cfg/core/builders/apps_builder.py +3 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +10 -0
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/modules/base.py +15 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +198 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +28 -0
- django_cfg/pyproject.toml +3 -5
- django_cfg/registry/modules.py +6 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/RECORD +79 -30
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.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,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
|
+
]
|