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.
- django_cfg/__init__.py +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
- 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/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/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/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- 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/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +13 -8
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +16 -26
- django_cfg/core/builders/apps_builder.py +7 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +15 -15
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +19 -6
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- 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 +205 -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 +21 -18
- django_cfg/pyproject.toml +4 -6
- django_cfg/registry/core.py +4 -7
- django_cfg/registry/modules.py +6 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/task_log.py +0 -265
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
- /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
- /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.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request Logger Interceptor for gRPC.
|
|
3
|
+
|
|
4
|
+
Automatically logs all gRPC requests to the database for monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
14
|
+
import grpc
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RequestLoggerInterceptor(grpc.ServerInterceptor):
|
|
20
|
+
"""
|
|
21
|
+
gRPC interceptor for request logging to database.
|
|
22
|
+
|
|
23
|
+
Features:
|
|
24
|
+
- Logs all requests to GRPCRequestLog model
|
|
25
|
+
- Captures timing, status, and error information
|
|
26
|
+
- Links requests to authenticated users
|
|
27
|
+
- Captures client metadata
|
|
28
|
+
- Tracks request/response sizes
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
```python
|
|
32
|
+
# In Django settings (auto-configured)
|
|
33
|
+
GRPC_FRAMEWORK = {
|
|
34
|
+
"SERVER_INTERCEPTORS": [
|
|
35
|
+
"django_cfg.apps.grpc.interceptors.RequestLoggerInterceptor",
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Database Schema:
|
|
41
|
+
All requests are logged to GRPCRequestLog model.
|
|
42
|
+
Use admin interface or REST API to view logs.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, log_request_data: bool = False, log_response_data: bool = False):
|
|
46
|
+
"""
|
|
47
|
+
Initialize request logger.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
log_request_data: Whether to log request data (default: False)
|
|
51
|
+
log_response_data: Whether to log response data (default: False)
|
|
52
|
+
"""
|
|
53
|
+
self.log_request_data = log_request_data
|
|
54
|
+
self.log_response_data = log_response_data
|
|
55
|
+
|
|
56
|
+
def intercept_service(
|
|
57
|
+
self,
|
|
58
|
+
continuation: Callable,
|
|
59
|
+
handler_call_details: grpc.HandlerCallDetails,
|
|
60
|
+
) -> grpc.RpcMethodHandler:
|
|
61
|
+
"""
|
|
62
|
+
Intercept gRPC service call for logging.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
continuation: Function to invoke the next interceptor or handler
|
|
66
|
+
handler_call_details: Details about the RPC call
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
RPC method handler with logging
|
|
70
|
+
"""
|
|
71
|
+
# Generate unique request ID
|
|
72
|
+
request_id = str(uuid.uuid4())
|
|
73
|
+
|
|
74
|
+
# Extract method info
|
|
75
|
+
full_method = handler_call_details.method
|
|
76
|
+
service_name, method_name = self._parse_method(full_method)
|
|
77
|
+
|
|
78
|
+
# Extract client metadata
|
|
79
|
+
metadata_dict = dict(handler_call_details.invocation_metadata)
|
|
80
|
+
peer = metadata_dict.get("peer", "unknown")
|
|
81
|
+
user_agent = metadata_dict.get("user-agent", None)
|
|
82
|
+
|
|
83
|
+
# Get handler and wrap it
|
|
84
|
+
handler = continuation(handler_call_details)
|
|
85
|
+
|
|
86
|
+
if handler is None:
|
|
87
|
+
logger.warning(f"[gRPC Logger] No handler found for {full_method}")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Wrap handler methods to log to database
|
|
91
|
+
return self._wrap_handler(
|
|
92
|
+
handler,
|
|
93
|
+
request_id,
|
|
94
|
+
service_name,
|
|
95
|
+
method_name,
|
|
96
|
+
full_method,
|
|
97
|
+
peer,
|
|
98
|
+
user_agent,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _wrap_handler(
|
|
102
|
+
self,
|
|
103
|
+
handler: grpc.RpcMethodHandler,
|
|
104
|
+
request_id: str,
|
|
105
|
+
service_name: str,
|
|
106
|
+
method_name: str,
|
|
107
|
+
full_method: str,
|
|
108
|
+
peer: str,
|
|
109
|
+
user_agent: str,
|
|
110
|
+
) -> grpc.RpcMethodHandler:
|
|
111
|
+
"""
|
|
112
|
+
Wrap handler to add database logging.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
handler: Original RPC method handler
|
|
116
|
+
request_id: Unique request ID
|
|
117
|
+
service_name: Service name
|
|
118
|
+
method_name: Method name
|
|
119
|
+
full_method: Full method path
|
|
120
|
+
peer: Client peer
|
|
121
|
+
user_agent: User agent
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Wrapped RPC method handler
|
|
125
|
+
"""
|
|
126
|
+
def wrap_unary_unary(behavior):
|
|
127
|
+
def wrapper(request, context):
|
|
128
|
+
start_time = time.time()
|
|
129
|
+
|
|
130
|
+
# Create log entry
|
|
131
|
+
log_entry = self._create_log_entry(
|
|
132
|
+
request_id=request_id,
|
|
133
|
+
service_name=service_name,
|
|
134
|
+
method_name=method_name,
|
|
135
|
+
full_method=full_method,
|
|
136
|
+
peer=peer,
|
|
137
|
+
user_agent=user_agent,
|
|
138
|
+
context=context,
|
|
139
|
+
request=request if self.log_request_data else None,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
response = behavior(request, context)
|
|
144
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
145
|
+
|
|
146
|
+
# Mark as successful
|
|
147
|
+
self._mark_success(
|
|
148
|
+
log_entry,
|
|
149
|
+
duration_ms=duration_ms,
|
|
150
|
+
response=response if self.log_response_data else None,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return response
|
|
154
|
+
except Exception as e:
|
|
155
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
156
|
+
|
|
157
|
+
# Mark as error
|
|
158
|
+
self._mark_error(
|
|
159
|
+
log_entry,
|
|
160
|
+
error=e,
|
|
161
|
+
context=context,
|
|
162
|
+
duration_ms=duration_ms,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
return wrapper
|
|
168
|
+
|
|
169
|
+
def wrap_unary_stream(behavior):
|
|
170
|
+
def wrapper(request, context):
|
|
171
|
+
start_time = time.time()
|
|
172
|
+
|
|
173
|
+
# Create log entry
|
|
174
|
+
log_entry = self._create_log_entry(
|
|
175
|
+
request_id=request_id,
|
|
176
|
+
service_name=service_name,
|
|
177
|
+
method_name=method_name,
|
|
178
|
+
full_method=full_method,
|
|
179
|
+
peer=peer,
|
|
180
|
+
user_agent=user_agent,
|
|
181
|
+
context=context,
|
|
182
|
+
request=request if self.log_request_data else None,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
response_count = 0
|
|
187
|
+
for response in behavior(request, context):
|
|
188
|
+
response_count += 1
|
|
189
|
+
yield response
|
|
190
|
+
|
|
191
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
192
|
+
|
|
193
|
+
# Mark as successful
|
|
194
|
+
self._mark_success(
|
|
195
|
+
log_entry,
|
|
196
|
+
duration_ms=duration_ms,
|
|
197
|
+
response_data={"message_count": response_count} if not self.log_response_data else None,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
202
|
+
|
|
203
|
+
# Mark as error
|
|
204
|
+
self._mark_error(
|
|
205
|
+
log_entry,
|
|
206
|
+
error=e,
|
|
207
|
+
context=context,
|
|
208
|
+
duration_ms=duration_ms,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
return wrapper
|
|
214
|
+
|
|
215
|
+
def wrap_stream_unary(behavior):
|
|
216
|
+
def wrapper(request_iterator, context):
|
|
217
|
+
start_time = time.time()
|
|
218
|
+
|
|
219
|
+
# Create log entry
|
|
220
|
+
log_entry = self._create_log_entry(
|
|
221
|
+
request_id=request_id,
|
|
222
|
+
service_name=service_name,
|
|
223
|
+
method_name=method_name,
|
|
224
|
+
full_method=full_method,
|
|
225
|
+
peer=peer,
|
|
226
|
+
user_agent=user_agent,
|
|
227
|
+
context=context,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Count requests
|
|
232
|
+
requests = []
|
|
233
|
+
request_count = 0
|
|
234
|
+
for req in request_iterator:
|
|
235
|
+
request_count += 1
|
|
236
|
+
requests.append(req)
|
|
237
|
+
|
|
238
|
+
# Process
|
|
239
|
+
response = behavior(iter(requests), context)
|
|
240
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
241
|
+
|
|
242
|
+
# Mark as successful
|
|
243
|
+
self._mark_success(
|
|
244
|
+
log_entry,
|
|
245
|
+
duration_ms=duration_ms,
|
|
246
|
+
request_data={"message_count": request_count} if not self.log_request_data else None,
|
|
247
|
+
response=response if self.log_response_data else None,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return response
|
|
251
|
+
except Exception as e:
|
|
252
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
253
|
+
|
|
254
|
+
# Mark as error
|
|
255
|
+
self._mark_error(
|
|
256
|
+
log_entry,
|
|
257
|
+
error=e,
|
|
258
|
+
context=context,
|
|
259
|
+
duration_ms=duration_ms,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
return wrapper
|
|
265
|
+
|
|
266
|
+
def wrap_stream_stream(behavior):
|
|
267
|
+
def wrapper(request_iterator, context):
|
|
268
|
+
start_time = time.time()
|
|
269
|
+
|
|
270
|
+
# Create log entry
|
|
271
|
+
log_entry = self._create_log_entry(
|
|
272
|
+
request_id=request_id,
|
|
273
|
+
service_name=service_name,
|
|
274
|
+
method_name=method_name,
|
|
275
|
+
full_method=full_method,
|
|
276
|
+
peer=peer,
|
|
277
|
+
user_agent=user_agent,
|
|
278
|
+
context=context,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
# Count requests
|
|
283
|
+
requests = []
|
|
284
|
+
request_count = 0
|
|
285
|
+
for req in request_iterator:
|
|
286
|
+
request_count += 1
|
|
287
|
+
requests.append(req)
|
|
288
|
+
|
|
289
|
+
# Process and count responses
|
|
290
|
+
response_count = 0
|
|
291
|
+
for response in behavior(iter(requests), context):
|
|
292
|
+
response_count += 1
|
|
293
|
+
yield response
|
|
294
|
+
|
|
295
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
296
|
+
|
|
297
|
+
# Mark as successful
|
|
298
|
+
self._mark_success(
|
|
299
|
+
log_entry,
|
|
300
|
+
duration_ms=duration_ms,
|
|
301
|
+
response_data={"request_count": request_count, "response_count": response_count} if not self.log_response_data else None,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
306
|
+
|
|
307
|
+
# Mark as error
|
|
308
|
+
self._mark_error(
|
|
309
|
+
log_entry,
|
|
310
|
+
error=e,
|
|
311
|
+
context=context,
|
|
312
|
+
duration_ms=duration_ms,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
raise
|
|
316
|
+
|
|
317
|
+
return wrapper
|
|
318
|
+
|
|
319
|
+
# Return wrapped handler based on type
|
|
320
|
+
if handler.unary_unary:
|
|
321
|
+
return grpc.unary_unary_rpc_method_handler(
|
|
322
|
+
wrap_unary_unary(handler.unary_unary),
|
|
323
|
+
request_deserializer=handler.request_deserializer,
|
|
324
|
+
response_serializer=handler.response_serializer,
|
|
325
|
+
)
|
|
326
|
+
elif handler.unary_stream:
|
|
327
|
+
return grpc.unary_stream_rpc_method_handler(
|
|
328
|
+
wrap_unary_stream(handler.unary_stream),
|
|
329
|
+
request_deserializer=handler.request_deserializer,
|
|
330
|
+
response_serializer=handler.response_serializer,
|
|
331
|
+
)
|
|
332
|
+
elif handler.stream_unary:
|
|
333
|
+
return grpc.stream_unary_rpc_method_handler(
|
|
334
|
+
wrap_stream_unary(handler.stream_unary),
|
|
335
|
+
request_deserializer=handler.request_deserializer,
|
|
336
|
+
response_serializer=handler.response_serializer,
|
|
337
|
+
)
|
|
338
|
+
elif handler.stream_stream:
|
|
339
|
+
return grpc.stream_stream_rpc_method_handler(
|
|
340
|
+
wrap_stream_stream(handler.stream_stream),
|
|
341
|
+
request_deserializer=handler.request_deserializer,
|
|
342
|
+
response_serializer=handler.response_serializer,
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
return handler
|
|
346
|
+
|
|
347
|
+
def _create_log_entry(
|
|
348
|
+
self,
|
|
349
|
+
request_id: str,
|
|
350
|
+
service_name: str,
|
|
351
|
+
method_name: str,
|
|
352
|
+
full_method: str,
|
|
353
|
+
peer: str,
|
|
354
|
+
user_agent: str,
|
|
355
|
+
context: grpc.ServicerContext,
|
|
356
|
+
request=None,
|
|
357
|
+
):
|
|
358
|
+
"""Create initial log entry in database."""
|
|
359
|
+
try:
|
|
360
|
+
from ..models import GRPCRequestLog
|
|
361
|
+
|
|
362
|
+
# Get user from context (set by JWTAuthInterceptor)
|
|
363
|
+
user = getattr(context, "user", None)
|
|
364
|
+
is_authenticated = user is not None
|
|
365
|
+
|
|
366
|
+
# Extract client IP from peer
|
|
367
|
+
client_ip = self._extract_ip_from_peer(peer)
|
|
368
|
+
|
|
369
|
+
# Create log entry
|
|
370
|
+
log_entry = GRPCRequestLog.objects.create(
|
|
371
|
+
request_id=request_id,
|
|
372
|
+
service_name=service_name,
|
|
373
|
+
method_name=method_name,
|
|
374
|
+
full_method=full_method,
|
|
375
|
+
user=user if is_authenticated else None,
|
|
376
|
+
is_authenticated=is_authenticated,
|
|
377
|
+
client_ip=client_ip,
|
|
378
|
+
user_agent=user_agent,
|
|
379
|
+
peer=peer,
|
|
380
|
+
request_data=self._serialize_message(request) if request else None,
|
|
381
|
+
status="pending",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return log_entry
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.error(f"Failed to create log entry: {e}", exc_info=True)
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
def _mark_success(
|
|
391
|
+
self,
|
|
392
|
+
log_entry,
|
|
393
|
+
duration_ms: int,
|
|
394
|
+
response=None,
|
|
395
|
+
request_data: dict = None,
|
|
396
|
+
response_data: dict = None,
|
|
397
|
+
):
|
|
398
|
+
"""Mark log entry as successful."""
|
|
399
|
+
if log_entry is None:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
from ..models import GRPCRequestLog
|
|
404
|
+
|
|
405
|
+
# Prepare response data
|
|
406
|
+
if response:
|
|
407
|
+
response_data = self._serialize_message(response)
|
|
408
|
+
|
|
409
|
+
GRPCRequestLog.objects.mark_success(
|
|
410
|
+
log_entry,
|
|
411
|
+
duration_ms=duration_ms,
|
|
412
|
+
response_data=response_data,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"Failed to mark success: {e}", exc_info=True)
|
|
417
|
+
|
|
418
|
+
def _mark_error(
|
|
419
|
+
self,
|
|
420
|
+
log_entry,
|
|
421
|
+
error: Exception,
|
|
422
|
+
context: grpc.ServicerContext,
|
|
423
|
+
duration_ms: int,
|
|
424
|
+
):
|
|
425
|
+
"""Mark log entry as error."""
|
|
426
|
+
if log_entry is None:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
from ..models import GRPCRequestLog
|
|
431
|
+
|
|
432
|
+
# Get gRPC status code
|
|
433
|
+
grpc_code = self._get_grpc_code(error, context)
|
|
434
|
+
|
|
435
|
+
GRPCRequestLog.objects.mark_error(
|
|
436
|
+
log_entry,
|
|
437
|
+
grpc_status_code=grpc_code,
|
|
438
|
+
error_message=str(error),
|
|
439
|
+
error_details={"type": type(error).__name__},
|
|
440
|
+
duration_ms=duration_ms,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.error(f"Failed to mark error: {e}", exc_info=True)
|
|
445
|
+
|
|
446
|
+
def _parse_method(self, full_method: str) -> tuple[str, str]:
|
|
447
|
+
"""
|
|
448
|
+
Parse full method path into service and method names.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
full_method: Full method path (e.g., /myapp.UserService/GetUser)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
(service_name, method_name) tuple
|
|
455
|
+
"""
|
|
456
|
+
try:
|
|
457
|
+
parts = full_method.strip("/").split("/")
|
|
458
|
+
if len(parts) >= 2:
|
|
459
|
+
return parts[0], parts[1]
|
|
460
|
+
else:
|
|
461
|
+
return full_method, "unknown"
|
|
462
|
+
except Exception:
|
|
463
|
+
return full_method, "unknown"
|
|
464
|
+
|
|
465
|
+
def _extract_ip_from_peer(self, peer: str) -> str | None:
|
|
466
|
+
"""
|
|
467
|
+
Extract IP address from peer string.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
peer: Peer string (e.g., ipv4:127.0.0.1:12345)
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
IP address or None
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
if ":" in peer:
|
|
477
|
+
parts = peer.split(":")
|
|
478
|
+
# Handle ipv4:x.x.x.x:port format
|
|
479
|
+
if len(parts) >= 3 and parts[0] in ["ipv4", "ipv6"]:
|
|
480
|
+
return parts[1]
|
|
481
|
+
# Handle x.x.x.x:port format
|
|
482
|
+
elif len(parts) == 2:
|
|
483
|
+
return parts[0]
|
|
484
|
+
except Exception:
|
|
485
|
+
pass
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
def _get_grpc_code(self, error: Exception, context: grpc.ServicerContext) -> str:
|
|
489
|
+
"""Get gRPC status code from error."""
|
|
490
|
+
try:
|
|
491
|
+
# Check if error is a gRPC error
|
|
492
|
+
if hasattr(error, "code"):
|
|
493
|
+
return error.code().name
|
|
494
|
+
|
|
495
|
+
# Try to get from context
|
|
496
|
+
if hasattr(context, "_state") and hasattr(context._state, "code"):
|
|
497
|
+
return context._state.code.name
|
|
498
|
+
|
|
499
|
+
# Default to UNKNOWN
|
|
500
|
+
return "UNKNOWN"
|
|
501
|
+
except Exception:
|
|
502
|
+
return "UNKNOWN"
|
|
503
|
+
|
|
504
|
+
def _serialize_message(self, message) -> dict | None:
|
|
505
|
+
"""Serialize protobuf message to dict."""
|
|
506
|
+
try:
|
|
507
|
+
# Try to use MessageToDict from google.protobuf
|
|
508
|
+
from google.protobuf.json_format import MessageToDict
|
|
509
|
+
return MessageToDict(message)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.debug(f"Failed to serialize message: {e}")
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
__all__ = ["RequestLoggerInterceptor"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Management commands for gRPC app."""
|