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
django_cfg/__init__.py
CHANGED
|
@@ -32,11 +32,11 @@ Example:
|
|
|
32
32
|
default_app_config = "django_cfg.apps.DjangoCfgConfig"
|
|
33
33
|
|
|
34
34
|
# Version information
|
|
35
|
-
__version__ = "1.
|
|
35
|
+
__version__ = "1.5.1"
|
|
36
36
|
__license__ = "MIT"
|
|
37
37
|
|
|
38
38
|
# Import registry for organized lazy loading
|
|
39
|
-
from .config import LIB_NAME
|
|
39
|
+
from .config import LIB_NAME, is_feature_available, require_feature, register_feature
|
|
40
40
|
from .registry import DJANGO_CFG_REGISTRY
|
|
41
41
|
|
|
42
42
|
# Get author from library config
|
|
@@ -55,5 +55,9 @@ def __getattr__(name: str):
|
|
|
55
55
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
# Export all registered components
|
|
59
|
-
__all__ = list(DJANGO_CFG_REGISTRY.keys())
|
|
58
|
+
# Export all registered components + feature detection
|
|
59
|
+
__all__ = list(DJANGO_CFG_REGISTRY.keys()) + [
|
|
60
|
+
"is_feature_available",
|
|
61
|
+
"require_feature",
|
|
62
|
+
"register_feature",
|
|
63
|
+
]
|
|
@@ -96,7 +96,7 @@ class CentrifugoLogAdmin(PydanticAdmin):
|
|
|
96
96
|
|
|
97
97
|
try:
|
|
98
98
|
formatted = json.dumps(obj.data, indent=2)
|
|
99
|
-
return
|
|
99
|
+
return self.html.code_block(formatted, language="json", max_height="400px")
|
|
100
100
|
except Exception:
|
|
101
101
|
return str(obj.data)
|
|
102
102
|
|
|
@@ -106,37 +106,22 @@ class CentrifugoLogAdmin(PydanticAdmin):
|
|
|
106
106
|
"""Display error information if publish failed."""
|
|
107
107
|
if obj.is_successful or obj.status == "pending":
|
|
108
108
|
return self.html.inline(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
]
|
|
109
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
110
|
+
self.html.text("No errors", variant="success"),
|
|
111
|
+
separator=" "
|
|
113
112
|
)
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.html.inline(
|
|
120
|
-
[
|
|
121
|
-
self.html.span("Error Code:", "font-semibold"),
|
|
122
|
-
self.html.badge(obj.error_code, variant="danger", icon=Icons.ERROR),
|
|
123
|
-
],
|
|
124
|
-
separator=" ",
|
|
125
|
-
)
|
|
126
|
-
)
|
|
114
|
+
error_code_line = self.html.key_value(
|
|
115
|
+
"Error Code",
|
|
116
|
+
self.html.badge(obj.error_code, variant="danger", icon=Icons.ERROR)
|
|
117
|
+
) if obj.error_code else None
|
|
127
118
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.html.span("Message:", "font-semibold"),
|
|
133
|
-
self.html.span(obj.error_message, "text-red-600"),
|
|
134
|
-
],
|
|
135
|
-
separator=" ",
|
|
136
|
-
)
|
|
137
|
-
)
|
|
119
|
+
error_msg_line = self.html.key_value(
|
|
120
|
+
"Message",
|
|
121
|
+
self.html.text(obj.error_message, variant="danger")
|
|
122
|
+
) if obj.error_message else None
|
|
138
123
|
|
|
139
|
-
return
|
|
124
|
+
return self.html.breakdown(error_code_line, error_msg_line) if (error_code_line or error_msg_line) else self.html.empty()
|
|
140
125
|
|
|
141
126
|
error_details_display.short_description = "Error Details"
|
|
142
127
|
|
|
@@ -145,57 +130,34 @@ class CentrifugoLogAdmin(PydanticAdmin):
|
|
|
145
130
|
if not obj.wait_for_ack:
|
|
146
131
|
return self.html.empty("No ACK tracking")
|
|
147
132
|
|
|
148
|
-
stats = []
|
|
149
|
-
|
|
150
133
|
# ACK timeout
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
self.html.span("Timeout:", "font-semibold"),
|
|
156
|
-
self.html.span(f"{obj.ack_timeout}s", "text-gray-600"),
|
|
157
|
-
],
|
|
158
|
-
separator=" ",
|
|
159
|
-
)
|
|
160
|
-
)
|
|
134
|
+
timeout_line = self.html.key_value(
|
|
135
|
+
"Timeout",
|
|
136
|
+
f"{obj.ack_timeout}s"
|
|
137
|
+
) if obj.ack_timeout else None
|
|
161
138
|
|
|
162
139
|
# ACKs received
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
self.html.span("ACKs Received:", "font-semibold"),
|
|
167
|
-
self.html.badge(str(obj.acks_received), variant="info"),
|
|
168
|
-
],
|
|
169
|
-
separator=" ",
|
|
170
|
-
)
|
|
140
|
+
received_line = self.html.key_value(
|
|
141
|
+
"ACKs Received",
|
|
142
|
+
self.html.badge(str(obj.acks_received), variant="info")
|
|
171
143
|
)
|
|
172
144
|
|
|
173
145
|
# ACKs expected (if known)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
146
|
+
expected_line = self.html.key_value(
|
|
147
|
+
"ACKs Expected",
|
|
148
|
+
str(obj.acks_expected)
|
|
149
|
+
) if obj.acks_expected else None
|
|
150
|
+
|
|
151
|
+
# Delivery rate
|
|
152
|
+
rate_line = None
|
|
153
|
+
if obj.acks_expected and obj.delivery_rate is not None:
|
|
154
|
+
rate_pct = obj.delivery_rate * 100
|
|
155
|
+
rate_line = self.html.key_value(
|
|
156
|
+
"Delivery Rate",
|
|
157
|
+
self.html.number(rate_pct, precision=1, suffix="%")
|
|
183
158
|
)
|
|
184
159
|
|
|
185
|
-
|
|
186
|
-
if obj.delivery_rate is not None:
|
|
187
|
-
rate_pct = obj.delivery_rate * 100
|
|
188
|
-
stats.append(
|
|
189
|
-
self.html.inline(
|
|
190
|
-
[
|
|
191
|
-
self.html.span("Delivery Rate:", "font-semibold"),
|
|
192
|
-
self.html.span(f"{rate_pct:.1f}%", "text-blue-600"),
|
|
193
|
-
],
|
|
194
|
-
separator=" ",
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
return "<br>".join(stats) if stats else self.html.empty()
|
|
160
|
+
return self.html.breakdown(timeout_line, received_line, expected_line, rate_line)
|
|
199
161
|
|
|
200
162
|
delivery_stats_display.short_description = "Delivery Statistics"
|
|
201
163
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Admin configuration for gRPC models.
|
|
3
|
+
|
|
4
|
+
Declarative AdminConfig using PydanticAdmin patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django_cfg.modules.django_admin import (
|
|
8
|
+
AdminConfig,
|
|
9
|
+
BadgeField,
|
|
10
|
+
DateTimeField,
|
|
11
|
+
Icons,
|
|
12
|
+
UserField,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ..models import GRPCRequestLog
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Declarative configuration for GRPCRequestLog
|
|
19
|
+
grpcrequestlog_config = AdminConfig(
|
|
20
|
+
model=GRPCRequestLog,
|
|
21
|
+
# Performance optimization
|
|
22
|
+
select_related=["user"],
|
|
23
|
+
|
|
24
|
+
# List display
|
|
25
|
+
list_display=[
|
|
26
|
+
"full_method",
|
|
27
|
+
"service_badge",
|
|
28
|
+
"method_badge",
|
|
29
|
+
"status",
|
|
30
|
+
"grpc_status_code_display",
|
|
31
|
+
"user",
|
|
32
|
+
"duration_display",
|
|
33
|
+
"created_at",
|
|
34
|
+
"completed_at"
|
|
35
|
+
],
|
|
36
|
+
|
|
37
|
+
# Auto-generated display methods
|
|
38
|
+
display_fields=[
|
|
39
|
+
BadgeField(name="service_name", title="Service", variant="info", icon=Icons.API),
|
|
40
|
+
BadgeField(name="method_name", title="Method", variant="secondary", icon=Icons.CODE),
|
|
41
|
+
BadgeField(
|
|
42
|
+
name="status",
|
|
43
|
+
title="Status",
|
|
44
|
+
label_map={
|
|
45
|
+
"pending": "warning",
|
|
46
|
+
"success": "success",
|
|
47
|
+
"error": "danger",
|
|
48
|
+
"cancelled": "secondary",
|
|
49
|
+
"timeout": "danger",
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
UserField(name="user", title="User", header=True),
|
|
53
|
+
DateTimeField(name="created_at", title="Created", ordering="created_at"),
|
|
54
|
+
DateTimeField(name="completed_at", title="Completed", ordering="completed_at"),
|
|
55
|
+
],
|
|
56
|
+
# Filters
|
|
57
|
+
list_filter=["status", "grpc_status_code", "service_name", "method_name", "is_authenticated", "created_at"],
|
|
58
|
+
search_fields=[
|
|
59
|
+
"request_id",
|
|
60
|
+
"service_name",
|
|
61
|
+
"method_name",
|
|
62
|
+
"full_method",
|
|
63
|
+
"user__username",
|
|
64
|
+
"user__email",
|
|
65
|
+
"error_message",
|
|
66
|
+
"client_ip",
|
|
67
|
+
],
|
|
68
|
+
# Autocomplete for user field
|
|
69
|
+
autocomplete_fields=["user"],
|
|
70
|
+
# Readonly fields
|
|
71
|
+
readonly_fields=[
|
|
72
|
+
"id",
|
|
73
|
+
"request_id",
|
|
74
|
+
"created_at",
|
|
75
|
+
"completed_at",
|
|
76
|
+
"request_data_display",
|
|
77
|
+
"response_data_display",
|
|
78
|
+
"error_details_display",
|
|
79
|
+
"performance_stats_display",
|
|
80
|
+
"client_info_display",
|
|
81
|
+
],
|
|
82
|
+
# Date hierarchy
|
|
83
|
+
date_hierarchy="created_at",
|
|
84
|
+
# Per page
|
|
85
|
+
list_per_page=50,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["grpcrequestlog_config"]
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gRPC Request Log Admin.
|
|
3
|
+
|
|
4
|
+
PydanticAdmin for GRPCRequestLog model with custom computed fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from django.contrib import admin
|
|
10
|
+
from django_cfg.modules.django_admin import Icons, computed_field
|
|
11
|
+
from django_cfg.modules.django_admin.base import PydanticAdmin
|
|
12
|
+
|
|
13
|
+
from ..models import GRPCRequestLog
|
|
14
|
+
from .config import grpcrequestlog_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@admin.register(GRPCRequestLog)
|
|
18
|
+
class GRPCRequestLogAdmin(PydanticAdmin):
|
|
19
|
+
"""
|
|
20
|
+
gRPC request log admin with analytics and filtering.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Color-coded status badges
|
|
24
|
+
- Performance metrics visualization
|
|
25
|
+
- Duration display with performance indicators
|
|
26
|
+
- Formatted JSON for request/response data
|
|
27
|
+
- Error details with highlighted display
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
config = grpcrequestlog_config
|
|
31
|
+
|
|
32
|
+
@computed_field("Service", ordering="service_name")
|
|
33
|
+
def service_badge(self, obj):
|
|
34
|
+
"""Display service name as badge."""
|
|
35
|
+
return self.html.badge(obj.service_name, variant="info", icon=Icons.API)
|
|
36
|
+
|
|
37
|
+
@computed_field("Method", ordering="method_name")
|
|
38
|
+
def method_badge(self, obj):
|
|
39
|
+
"""Display method name as badge."""
|
|
40
|
+
return self.html.badge(obj.method_name, variant="secondary", icon=Icons.CODE)
|
|
41
|
+
|
|
42
|
+
@computed_field("gRPC Status", ordering="grpc_status_code")
|
|
43
|
+
def grpc_status_code_display(self, obj):
|
|
44
|
+
"""Display gRPC status code with color coding."""
|
|
45
|
+
if not obj.grpc_status_code:
|
|
46
|
+
return self.html.empty()
|
|
47
|
+
|
|
48
|
+
# Color code based on status
|
|
49
|
+
if obj.grpc_status_code == "OK":
|
|
50
|
+
variant = "success"
|
|
51
|
+
icon = Icons.CHECK_CIRCLE
|
|
52
|
+
elif obj.grpc_status_code in ["CANCELLED", "DEADLINE_EXCEEDED"]:
|
|
53
|
+
variant = "warning"
|
|
54
|
+
icon = Icons.TIMER
|
|
55
|
+
else:
|
|
56
|
+
variant = "danger"
|
|
57
|
+
icon = Icons.ERROR
|
|
58
|
+
|
|
59
|
+
return self.html.badge(obj.grpc_status_code, variant=variant, icon=icon)
|
|
60
|
+
|
|
61
|
+
@computed_field("Duration", ordering="duration_ms")
|
|
62
|
+
def duration_display(self, obj):
|
|
63
|
+
"""Display duration with color coding based on speed."""
|
|
64
|
+
if obj.duration_ms is None:
|
|
65
|
+
return self.html.empty()
|
|
66
|
+
|
|
67
|
+
# Color code based on duration
|
|
68
|
+
if obj.duration_ms < 100:
|
|
69
|
+
variant = "success" # Fast
|
|
70
|
+
icon = Icons.SPEED
|
|
71
|
+
elif obj.duration_ms < 1000:
|
|
72
|
+
variant = "warning" # Moderate
|
|
73
|
+
icon = Icons.TIMER
|
|
74
|
+
else:
|
|
75
|
+
variant = "danger" # Slow
|
|
76
|
+
icon = Icons.ERROR
|
|
77
|
+
|
|
78
|
+
return self.html.badge(f"{obj.duration_ms}ms", variant=variant, icon=icon)
|
|
79
|
+
|
|
80
|
+
def request_data_display(self, obj):
|
|
81
|
+
"""Display formatted JSON request data."""
|
|
82
|
+
if not obj.request_data:
|
|
83
|
+
return self.html.empty("No request data logged")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
formatted = json.dumps(obj.request_data, indent=2)
|
|
87
|
+
return self.html.code_block(formatted, language="json", max_height="400px")
|
|
88
|
+
except Exception:
|
|
89
|
+
return str(obj.request_data)
|
|
90
|
+
|
|
91
|
+
request_data_display.short_description = "Request Data"
|
|
92
|
+
|
|
93
|
+
def response_data_display(self, obj):
|
|
94
|
+
"""Display formatted JSON response data."""
|
|
95
|
+
if not obj.response_data:
|
|
96
|
+
return self.html.empty("No response data logged")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
formatted = json.dumps(obj.response_data, indent=2)
|
|
100
|
+
return self.html.code_block(formatted, language="json", max_height="400px")
|
|
101
|
+
except Exception:
|
|
102
|
+
return str(obj.response_data)
|
|
103
|
+
|
|
104
|
+
response_data_display.short_description = "Response Data"
|
|
105
|
+
|
|
106
|
+
def error_details_display(self, obj):
|
|
107
|
+
"""Display error information if request failed."""
|
|
108
|
+
if obj.is_successful or obj.status == "pending":
|
|
109
|
+
return self.html.inline(
|
|
110
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
111
|
+
self.html.text("No errors", variant="success"),
|
|
112
|
+
separator=" "
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# gRPC status code
|
|
116
|
+
code_line = self.html.key_value(
|
|
117
|
+
"gRPC Status",
|
|
118
|
+
self.html.badge(obj.grpc_status_code, variant="danger", icon=Icons.ERROR)
|
|
119
|
+
) if obj.grpc_status_code else None
|
|
120
|
+
|
|
121
|
+
# Error message
|
|
122
|
+
msg_line = self.html.key_value(
|
|
123
|
+
"Message",
|
|
124
|
+
self.html.text(obj.error_message, variant="danger")
|
|
125
|
+
) if obj.error_message else None
|
|
126
|
+
|
|
127
|
+
# Error details
|
|
128
|
+
details_line = None
|
|
129
|
+
if obj.error_details:
|
|
130
|
+
try:
|
|
131
|
+
formatted = json.dumps(obj.error_details, indent=2)
|
|
132
|
+
details_line = self.html.key_value(
|
|
133
|
+
"Details",
|
|
134
|
+
self.html.code_block(formatted, language="json", max_height="200px")
|
|
135
|
+
)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return self.html.breakdown(code_line, msg_line, details_line) if (code_line or msg_line) else self.html.empty()
|
|
140
|
+
|
|
141
|
+
error_details_display.short_description = "Error Details"
|
|
142
|
+
|
|
143
|
+
def performance_stats_display(self, obj):
|
|
144
|
+
"""Display performance statistics."""
|
|
145
|
+
# Duration
|
|
146
|
+
duration_line = self.html.key_value(
|
|
147
|
+
"Duration",
|
|
148
|
+
self.html.number(obj.duration_ms, suffix="ms") if obj.duration_ms else "N/A"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Request size
|
|
152
|
+
request_size_line = self.html.key_value(
|
|
153
|
+
"Request Size",
|
|
154
|
+
self.html.number(obj.request_size, suffix=" bytes") if obj.request_size else "N/A"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Response size
|
|
158
|
+
response_size_line = self.html.key_value(
|
|
159
|
+
"Response Size",
|
|
160
|
+
self.html.number(obj.response_size, suffix=" bytes") if obj.response_size else "N/A"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Authentication
|
|
164
|
+
auth_line = self.html.key_value(
|
|
165
|
+
"Authenticated",
|
|
166
|
+
self.html.badge("Yes" if obj.is_authenticated else "No",
|
|
167
|
+
variant="success" if obj.is_authenticated else "secondary")
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return self.html.breakdown(duration_line, request_size_line, response_size_line, auth_line)
|
|
171
|
+
|
|
172
|
+
performance_stats_display.short_description = "Performance Statistics"
|
|
173
|
+
|
|
174
|
+
def client_info_display(self, obj):
|
|
175
|
+
"""Display client information."""
|
|
176
|
+
# Client IP
|
|
177
|
+
ip_line = self.html.key_value(
|
|
178
|
+
"Client IP",
|
|
179
|
+
obj.client_ip if obj.client_ip else "N/A"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# User Agent
|
|
183
|
+
ua_line = self.html.key_value(
|
|
184
|
+
"User Agent",
|
|
185
|
+
obj.user_agent if obj.user_agent else "N/A"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Peer
|
|
189
|
+
peer_line = self.html.key_value(
|
|
190
|
+
"Peer",
|
|
191
|
+
self.html.text(obj.peer, variant="secondary") if obj.peer else "N/A"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return self.html.breakdown(ip_line, ua_line, peer_line)
|
|
195
|
+
|
|
196
|
+
client_info_display.short_description = "Client Information"
|
|
197
|
+
|
|
198
|
+
# Fieldsets for detail view
|
|
199
|
+
def get_fieldsets(self, request, obj=None):
|
|
200
|
+
"""Dynamic fieldsets based on object state."""
|
|
201
|
+
fieldsets = [
|
|
202
|
+
(
|
|
203
|
+
"Request Information",
|
|
204
|
+
{"fields": ("id", "request_id", "full_method", "service_name", "method_name", "status")},
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
"User Context",
|
|
208
|
+
{"fields": ("user", "is_authenticated")},
|
|
209
|
+
),
|
|
210
|
+
(
|
|
211
|
+
"Performance",
|
|
212
|
+
{"fields": ("performance_stats_display", "duration_ms", "created_at", "completed_at")},
|
|
213
|
+
),
|
|
214
|
+
(
|
|
215
|
+
"Client Information",
|
|
216
|
+
{"fields": ("client_info_display", "client_ip", "user_agent", "peer"), "classes": ("collapse",)},
|
|
217
|
+
),
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# Add request/response data sections if available
|
|
221
|
+
if obj and obj.request_data:
|
|
222
|
+
fieldsets.insert(
|
|
223
|
+
3,
|
|
224
|
+
(
|
|
225
|
+
"Request Data",
|
|
226
|
+
{"fields": ("request_data_display",), "classes": ("collapse",)},
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if obj and obj.response_data:
|
|
231
|
+
fieldsets.insert(
|
|
232
|
+
4,
|
|
233
|
+
(
|
|
234
|
+
"Response Data",
|
|
235
|
+
{"fields": ("response_data_display",), "classes": ("collapse",)},
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Add error section only if failed
|
|
240
|
+
if obj and not obj.is_successful and obj.status != "pending":
|
|
241
|
+
fieldsets.insert(
|
|
242
|
+
5,
|
|
243
|
+
(
|
|
244
|
+
"Error Details",
|
|
245
|
+
{"fields": ("error_details_display", "grpc_status_code", "error_message", "error_details")},
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return fieldsets
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
__all__ = ["GRPCRequestLogAdmin"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django app configuration for gRPC integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GRPCAppConfig(AppConfig):
|
|
9
|
+
"""
|
|
10
|
+
Django app config for gRPC integration.
|
|
11
|
+
|
|
12
|
+
Provides:
|
|
13
|
+
- gRPC server with Django ORM integration
|
|
14
|
+
- JWT authentication
|
|
15
|
+
- Request logging to database
|
|
16
|
+
- Admin interface for monitoring
|
|
17
|
+
- REST API for metrics
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
21
|
+
name = 'django_cfg.apps.grpc'
|
|
22
|
+
verbose_name = 'gRPC Integration'
|
|
23
|
+
|
|
24
|
+
def ready(self):
|
|
25
|
+
"""Called when Django starts."""
|
|
26
|
+
# Import signal handlers if needed
|
|
27
|
+
# from . import signals
|
|
28
|
+
pass
|