django-cfg 1.5.8__py3-none-any.whl → 1.5.20__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 +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Streaming Logger Utilities for gRPC Services.
|
|
3
|
+
|
|
4
|
+
Provides reusable logger configuration for gRPC streaming services.
|
|
5
|
+
Follows django-cfg logging patterns for consistency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from django.utils import timezone
|
|
14
|
+
|
|
15
|
+
# Rich for beautiful console output
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AutoTracebackHandler(logging.Handler):
|
|
23
|
+
"""
|
|
24
|
+
Custom handler that automatically adds exception info to ERROR and CRITICAL logs.
|
|
25
|
+
|
|
26
|
+
This ensures full tracebacks are always logged for errors, even if exc_info=True
|
|
27
|
+
is not explicitly specified.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, base_handler: logging.Handler):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.base_handler = base_handler
|
|
33
|
+
self.setLevel(base_handler.level)
|
|
34
|
+
self.setFormatter(base_handler.formatter)
|
|
35
|
+
|
|
36
|
+
def emit(self, record: logging.LogRecord):
|
|
37
|
+
"""Emit log record, automatically adding exc_info for errors."""
|
|
38
|
+
# If ERROR or CRITICAL and no exc_info yet, add current exception if any
|
|
39
|
+
if record.levelno >= logging.ERROR and not record.exc_info:
|
|
40
|
+
# Check if we're in exception context
|
|
41
|
+
exc_info = sys.exc_info()
|
|
42
|
+
if exc_info[0] is not None:
|
|
43
|
+
record.exc_info = exc_info
|
|
44
|
+
|
|
45
|
+
# Delegate to base handler
|
|
46
|
+
self.base_handler.emit(record)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def setup_streaming_logger(
|
|
50
|
+
name: str = "grpc_streaming",
|
|
51
|
+
logs_dir: Optional[Path] = None,
|
|
52
|
+
level: int = logging.DEBUG,
|
|
53
|
+
console_level: int = logging.INFO
|
|
54
|
+
) -> logging.Logger:
|
|
55
|
+
"""
|
|
56
|
+
Setup dedicated logger for gRPC streaming with file and console handlers.
|
|
57
|
+
|
|
58
|
+
Follows django-cfg logging pattern:
|
|
59
|
+
- Uses os.getcwd() / 'logs' / 'grpc_streaming' for log directory
|
|
60
|
+
- Time-based log file names (streaming_YYYYMMDD_HHMMSS.log)
|
|
61
|
+
- Detailed file logging (DEBUG level by default)
|
|
62
|
+
- Concise console logging (INFO level by default)
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name: Logger name (default: "grpc_streaming")
|
|
66
|
+
logs_dir: Directory for log files (default: <cwd>/logs/grpc_streaming)
|
|
67
|
+
level: File logging level (default: DEBUG)
|
|
68
|
+
console_level: Console logging level (default: INFO)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Configured logger instance
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
```python
|
|
75
|
+
from django_cfg.apps.integrations.grpc.utils import setup_streaming_logger
|
|
76
|
+
|
|
77
|
+
# Basic usage
|
|
78
|
+
logger = setup_streaming_logger()
|
|
79
|
+
|
|
80
|
+
# Custom configuration
|
|
81
|
+
logger = setup_streaming_logger(
|
|
82
|
+
name="my_streaming_service",
|
|
83
|
+
logs_dir=Path("/var/log/grpc"),
|
|
84
|
+
level=logging.INFO
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
logger.info("Service started")
|
|
88
|
+
logger.debug("Detailed debug info")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Features:
|
|
92
|
+
- Automatic log directory creation
|
|
93
|
+
- Time-based log file names
|
|
94
|
+
- No duplicate logs (propagate=False)
|
|
95
|
+
- UTF-8 encoding
|
|
96
|
+
- Reusable across all django-cfg gRPC projects
|
|
97
|
+
"""
|
|
98
|
+
# Create logger
|
|
99
|
+
streaming_logger = logging.getLogger(name)
|
|
100
|
+
streaming_logger.setLevel(level)
|
|
101
|
+
|
|
102
|
+
# Avoid duplicate handlers if logger already configured
|
|
103
|
+
if streaming_logger.handlers:
|
|
104
|
+
return streaming_logger
|
|
105
|
+
|
|
106
|
+
# Determine logs directory using django-cfg pattern
|
|
107
|
+
if logs_dir is None:
|
|
108
|
+
# Pattern from django_cfg.modules.django_logging:
|
|
109
|
+
# current_dir = Path(os.getcwd())
|
|
110
|
+
# logs_dir = current_dir / 'logs' / 'grpc_streaming'
|
|
111
|
+
current_dir = Path(os.getcwd())
|
|
112
|
+
logs_dir = current_dir / 'logs' / 'grpc_streaming'
|
|
113
|
+
|
|
114
|
+
# Create logs directory
|
|
115
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
# Create log filename with timestamp
|
|
118
|
+
log_filename = f'streaming_{timezone.now().strftime("%Y%m%d_%H%M%S")}.log'
|
|
119
|
+
log_file_path = logs_dir / log_filename
|
|
120
|
+
|
|
121
|
+
# File handler - detailed logs with auto-traceback
|
|
122
|
+
base_file_handler = logging.FileHandler(
|
|
123
|
+
log_file_path,
|
|
124
|
+
encoding='utf-8'
|
|
125
|
+
)
|
|
126
|
+
base_file_handler.setLevel(level)
|
|
127
|
+
file_formatter = logging.Formatter(
|
|
128
|
+
'%(asctime)s | %(levelname)-8s | %(message)s',
|
|
129
|
+
datefmt='%H:%M:%S'
|
|
130
|
+
)
|
|
131
|
+
base_file_handler.setFormatter(file_formatter)
|
|
132
|
+
|
|
133
|
+
# Wrap with auto-traceback handler for automatic exc_info on errors
|
|
134
|
+
file_handler = AutoTracebackHandler(base_file_handler)
|
|
135
|
+
streaming_logger.addHandler(file_handler)
|
|
136
|
+
|
|
137
|
+
# Console handler - important messages only (also with auto-traceback)
|
|
138
|
+
base_console_handler = logging.StreamHandler()
|
|
139
|
+
base_console_handler.setLevel(console_level)
|
|
140
|
+
console_formatter = logging.Formatter('%(levelname)s: %(message)s')
|
|
141
|
+
base_console_handler.setFormatter(console_formatter)
|
|
142
|
+
|
|
143
|
+
# Wrap console handler with auto-traceback too
|
|
144
|
+
console_handler = AutoTracebackHandler(base_console_handler)
|
|
145
|
+
streaming_logger.addHandler(console_handler)
|
|
146
|
+
|
|
147
|
+
# Prevent propagation to avoid duplicate logs
|
|
148
|
+
streaming_logger.propagate = False
|
|
149
|
+
|
|
150
|
+
# Log initialization
|
|
151
|
+
streaming_logger.info("=" * 80)
|
|
152
|
+
streaming_logger.info(f"🌊 {name.title()} Logger Initialized")
|
|
153
|
+
streaming_logger.info(f"📁 Log file: {log_file_path}")
|
|
154
|
+
streaming_logger.info("=" * 80)
|
|
155
|
+
|
|
156
|
+
return streaming_logger
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_streaming_logger(name: str = "grpc_streaming") -> logging.Logger:
|
|
160
|
+
"""
|
|
161
|
+
Get existing streaming logger or create new one.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: Logger name (default: "grpc_streaming")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Logger instance
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
```python
|
|
171
|
+
from django_cfg.apps.integrations.grpc.utils import get_streaming_logger
|
|
172
|
+
|
|
173
|
+
logger = get_streaming_logger()
|
|
174
|
+
logger.info("Using existing logger")
|
|
175
|
+
```
|
|
176
|
+
"""
|
|
177
|
+
logger = logging.getLogger(name)
|
|
178
|
+
|
|
179
|
+
# If not configured yet, set it up
|
|
180
|
+
if not logger.handlers:
|
|
181
|
+
return setup_streaming_logger(name)
|
|
182
|
+
|
|
183
|
+
return logger
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def log_server_start(
|
|
187
|
+
logger: logging.Logger,
|
|
188
|
+
server_type: str = "Server",
|
|
189
|
+
mode: str = "Development",
|
|
190
|
+
hotreload_enabled: bool = False,
|
|
191
|
+
use_rich: bool = True,
|
|
192
|
+
**extra_info
|
|
193
|
+
):
|
|
194
|
+
"""
|
|
195
|
+
Log server startup with timestamp and configuration using Rich panels.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
logger: Logger instance to use
|
|
199
|
+
server_type: Type of server (e.g., "gRPC Server", "WebSocket Server")
|
|
200
|
+
mode: Running mode (Development/Production)
|
|
201
|
+
hotreload_enabled: Whether hot-reload is enabled
|
|
202
|
+
use_rich: Use Rich for beautiful output (default: True)
|
|
203
|
+
**extra_info: Additional key-value pairs to log
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
```python
|
|
207
|
+
from django_cfg.apps.integrations.grpc.utils import log_server_start
|
|
208
|
+
from datetime import datetime
|
|
209
|
+
|
|
210
|
+
start_time = log_server_start(
|
|
211
|
+
logger,
|
|
212
|
+
server_type="gRPC Server",
|
|
213
|
+
mode="Development",
|
|
214
|
+
hotreload_enabled=True,
|
|
215
|
+
host="0.0.0.0",
|
|
216
|
+
port=50051
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
datetime object of start time for later use in log_server_shutdown
|
|
222
|
+
"""
|
|
223
|
+
from datetime import datetime
|
|
224
|
+
|
|
225
|
+
start_time = datetime.now()
|
|
226
|
+
|
|
227
|
+
if use_rich:
|
|
228
|
+
# Create Rich table for server info
|
|
229
|
+
console = Console()
|
|
230
|
+
|
|
231
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
232
|
+
table.add_column("Key", style="cyan")
|
|
233
|
+
table.add_column("Value", style="white")
|
|
234
|
+
|
|
235
|
+
table.add_row("⏰ Started at", start_time.strftime('%Y-%m-%d %H:%M:%S'))
|
|
236
|
+
table.add_row("Mode", f"[{'red' if mode == 'Production' else 'green'}]{mode}[/]")
|
|
237
|
+
table.add_row("Hotreload", f"[{'yellow' if hotreload_enabled else 'dim'}]{'Enabled ⚡' if hotreload_enabled else 'Disabled'}[/]")
|
|
238
|
+
|
|
239
|
+
# Add extra info
|
|
240
|
+
for key, value in extra_info.items():
|
|
241
|
+
key_display = key.replace('_', ' ').title()
|
|
242
|
+
table.add_row(key_display, str(value))
|
|
243
|
+
|
|
244
|
+
# Create panel
|
|
245
|
+
panel = Panel(
|
|
246
|
+
table,
|
|
247
|
+
title=f"[bold green]🚀 {server_type} Starting[/bold green]",
|
|
248
|
+
border_style="green",
|
|
249
|
+
padding=(1, 2)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
console.print(panel)
|
|
253
|
+
|
|
254
|
+
if hotreload_enabled:
|
|
255
|
+
console.print(
|
|
256
|
+
"[yellow]⚠️ Hotreload active - connections may be dropped on code changes[/yellow]",
|
|
257
|
+
style="bold"
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
# Fallback to simple logging
|
|
261
|
+
logger.info("=" * 80)
|
|
262
|
+
logger.info(f"🚀 {server_type} Starting")
|
|
263
|
+
logger.info(f" ⏰ Started at: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
264
|
+
logger.info(f" Mode: {mode}")
|
|
265
|
+
logger.info(f" Hotreload: {'Enabled' if hotreload_enabled else 'Disabled'}")
|
|
266
|
+
|
|
267
|
+
for key, value in extra_info.items():
|
|
268
|
+
key_display = key.replace('_', ' ').title()
|
|
269
|
+
logger.info(f" {key_display}: {value}")
|
|
270
|
+
|
|
271
|
+
if hotreload_enabled:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"⚠️ Hotreload active - connections may be dropped on code changes"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
logger.info("=" * 80)
|
|
277
|
+
|
|
278
|
+
return start_time
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def log_server_shutdown(
|
|
282
|
+
logger: logging.Logger,
|
|
283
|
+
start_time,
|
|
284
|
+
server_type: str = "Server",
|
|
285
|
+
reason: str = None,
|
|
286
|
+
use_rich: bool = True,
|
|
287
|
+
**extra_info
|
|
288
|
+
):
|
|
289
|
+
"""
|
|
290
|
+
Log server shutdown with uptime calculation using Rich panels.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
logger: Logger instance to use
|
|
294
|
+
start_time: datetime object from log_server_start()
|
|
295
|
+
server_type: Type of server (e.g., "gRPC Server", "WebSocket Server")
|
|
296
|
+
reason: Shutdown reason (e.g., "Keyboard interrupt", "Hotreload")
|
|
297
|
+
use_rich: Use Rich for beautiful output (default: True)
|
|
298
|
+
**extra_info: Additional key-value pairs to log
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
```python
|
|
302
|
+
from django_cfg.apps.integrations.grpc.utils import log_server_shutdown
|
|
303
|
+
|
|
304
|
+
log_server_shutdown(
|
|
305
|
+
logger,
|
|
306
|
+
start_time,
|
|
307
|
+
server_type="gRPC Server",
|
|
308
|
+
reason="Hotreload triggered",
|
|
309
|
+
active_connections=5
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
"""
|
|
313
|
+
from datetime import datetime
|
|
314
|
+
|
|
315
|
+
end_time = datetime.now()
|
|
316
|
+
uptime = end_time - start_time
|
|
317
|
+
uptime_seconds = int(uptime.total_seconds())
|
|
318
|
+
|
|
319
|
+
# Format uptime
|
|
320
|
+
hours = uptime_seconds // 3600
|
|
321
|
+
minutes = (uptime_seconds % 3600) // 60
|
|
322
|
+
seconds = uptime_seconds % 60
|
|
323
|
+
uptime_str = f"{hours}h {minutes}m {seconds}s"
|
|
324
|
+
|
|
325
|
+
if use_rich:
|
|
326
|
+
# Create Rich table for shutdown info
|
|
327
|
+
console = Console()
|
|
328
|
+
|
|
329
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
330
|
+
table.add_column("Key", style="cyan")
|
|
331
|
+
table.add_column("Value", style="white")
|
|
332
|
+
|
|
333
|
+
if reason:
|
|
334
|
+
table.add_row("📋 Reason", reason)
|
|
335
|
+
|
|
336
|
+
table.add_row("⏱️ Uptime", f"[bold]{uptime_str}[/bold]")
|
|
337
|
+
table.add_row("🕐 Stopped at", end_time.strftime('%Y-%m-%d %H:%M:%S'))
|
|
338
|
+
|
|
339
|
+
# Add extra info
|
|
340
|
+
for key, value in extra_info.items():
|
|
341
|
+
key_display = key.replace('_', ' ').title()
|
|
342
|
+
table.add_row(key_display, str(value))
|
|
343
|
+
|
|
344
|
+
# Create panel
|
|
345
|
+
panel = Panel(
|
|
346
|
+
table,
|
|
347
|
+
title=f"[bold red]🧹 Shutting down {server_type}[/bold red]",
|
|
348
|
+
border_style="red",
|
|
349
|
+
padding=(1, 2)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
console.print(panel)
|
|
353
|
+
console.print("[green]✅ Server shutdown complete[/green]", style="bold")
|
|
354
|
+
else:
|
|
355
|
+
# Fallback to simple logging
|
|
356
|
+
logger.info("=" * 80)
|
|
357
|
+
logger.info(f"🧹 Shutting down {server_type}...")
|
|
358
|
+
|
|
359
|
+
if reason:
|
|
360
|
+
logger.info(f" 📋 Reason: {reason}")
|
|
361
|
+
|
|
362
|
+
logger.info(f" ⏱️ Uptime: {uptime_str}")
|
|
363
|
+
logger.info(f" 🕐 Stopped at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
364
|
+
|
|
365
|
+
for key, value in extra_info.items():
|
|
366
|
+
key_display = key.replace('_', ' ').title()
|
|
367
|
+
logger.info(f" {key_display}: {value}")
|
|
368
|
+
|
|
369
|
+
logger.info("✅ Server shutdown complete")
|
|
370
|
+
logger.info("=" * 80)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
__all__ = [
|
|
374
|
+
"setup_streaming_logger",
|
|
375
|
+
"get_streaming_logger",
|
|
376
|
+
"log_server_start",
|
|
377
|
+
"log_server_shutdown",
|
|
378
|
+
]
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Views for gRPC monitoring API.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from .api_keys import GRPCApiKeyViewSet
|
|
5
6
|
from .config import GRPCConfigViewSet
|
|
6
7
|
from .monitoring import GRPCMonitorViewSet
|
|
8
|
+
from .proto_files import GRPCProtoFilesViewSet
|
|
7
9
|
from .services import GRPCServiceViewSet
|
|
8
10
|
from .testing import GRPCTestingViewSet
|
|
9
11
|
|
|
@@ -12,4 +14,6 @@ __all__ = [
|
|
|
12
14
|
"GRPCConfigViewSet",
|
|
13
15
|
"GRPCServiceViewSet",
|
|
14
16
|
"GRPCTestingViewSet",
|
|
17
|
+
"GRPCApiKeyViewSet",
|
|
18
|
+
"GRPCProtoFilesViewSet",
|
|
15
19
|
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gRPC API Keys ViewSet.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for viewing API keys.
|
|
5
|
+
Create/Update/Delete operations handled through Django Admin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.db.models import Count, Q, Sum
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
from django_cfg.mixins import AdminAPIMixin
|
|
11
|
+
from django_cfg.modules.django_logging import get_logger
|
|
12
|
+
from drf_spectacular.types import OpenApiTypes
|
|
13
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
14
|
+
from rest_framework import status, viewsets
|
|
15
|
+
from rest_framework.decorators import action
|
|
16
|
+
from rest_framework.response import Response
|
|
17
|
+
|
|
18
|
+
from ..models import GrpcApiKey
|
|
19
|
+
from ..serializers.api_keys import (
|
|
20
|
+
ApiKeyListSerializer,
|
|
21
|
+
ApiKeySerializer,
|
|
22
|
+
ApiKeyStatsSerializer,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = get_logger("grpc.api_keys")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GRPCApiKeyViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
29
|
+
"""
|
|
30
|
+
ViewSet for gRPC API Keys (Read-Only).
|
|
31
|
+
|
|
32
|
+
Provides listing and statistics for API keys.
|
|
33
|
+
Create/Update/Delete operations are handled through Django Admin.
|
|
34
|
+
|
|
35
|
+
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
queryset = GrpcApiKey.objects.none()
|
|
39
|
+
serializer_class = ApiKeySerializer
|
|
40
|
+
|
|
41
|
+
@extend_schema(
|
|
42
|
+
tags=["gRPC API Keys"],
|
|
43
|
+
summary="List API keys",
|
|
44
|
+
description="Returns a list of all API keys with their details. Uses standard DRF pagination.",
|
|
45
|
+
parameters=[
|
|
46
|
+
OpenApiParameter(
|
|
47
|
+
name="is_active",
|
|
48
|
+
type=OpenApiTypes.BOOL,
|
|
49
|
+
location=OpenApiParameter.QUERY,
|
|
50
|
+
description="Filter by active status",
|
|
51
|
+
required=False,
|
|
52
|
+
),
|
|
53
|
+
OpenApiParameter(
|
|
54
|
+
name="key_type",
|
|
55
|
+
type=OpenApiTypes.STR,
|
|
56
|
+
location=OpenApiParameter.QUERY,
|
|
57
|
+
description="Filter by key type (service, cli, webhook, internal, development)",
|
|
58
|
+
required=False,
|
|
59
|
+
),
|
|
60
|
+
OpenApiParameter(
|
|
61
|
+
name="user_id",
|
|
62
|
+
type=OpenApiTypes.INT,
|
|
63
|
+
location=OpenApiParameter.QUERY,
|
|
64
|
+
description="Filter by user ID",
|
|
65
|
+
required=False,
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
responses={
|
|
69
|
+
200: ApiKeySerializer(many=True),
|
|
70
|
+
400: {"description": "Invalid parameters"},
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
def list(self, request):
|
|
74
|
+
"""List all API keys."""
|
|
75
|
+
try:
|
|
76
|
+
# Build queryset with filters
|
|
77
|
+
queryset = GrpcApiKey.objects.select_related("user", "created_by").all()
|
|
78
|
+
|
|
79
|
+
# Apply filters
|
|
80
|
+
is_active = request.GET.get("is_active")
|
|
81
|
+
if is_active is not None:
|
|
82
|
+
is_active_bool = is_active.lower() in ("true", "1", "yes")
|
|
83
|
+
queryset = queryset.filter(is_active=is_active_bool)
|
|
84
|
+
|
|
85
|
+
key_type = request.GET.get("key_type")
|
|
86
|
+
if key_type:
|
|
87
|
+
queryset = queryset.filter(key_type=key_type)
|
|
88
|
+
|
|
89
|
+
user_id = request.GET.get("user_id")
|
|
90
|
+
if user_id:
|
|
91
|
+
queryset = queryset.filter(user_id=user_id)
|
|
92
|
+
|
|
93
|
+
# Order by most recently created
|
|
94
|
+
queryset = queryset.order_by("-created_at")
|
|
95
|
+
|
|
96
|
+
# Paginate
|
|
97
|
+
page = self.paginate_queryset(queryset)
|
|
98
|
+
if page is not None:
|
|
99
|
+
# Serialize paginated data
|
|
100
|
+
results = []
|
|
101
|
+
for key in page:
|
|
102
|
+
results.append({
|
|
103
|
+
"id": key.id,
|
|
104
|
+
"name": key.name,
|
|
105
|
+
"key_type": key.key_type,
|
|
106
|
+
"masked_key": key.masked_key,
|
|
107
|
+
"is_active": key.is_active,
|
|
108
|
+
"is_valid": key.is_valid,
|
|
109
|
+
"user_id": key.user.id,
|
|
110
|
+
"username": key.user.username,
|
|
111
|
+
"user_email": key.user.email,
|
|
112
|
+
"request_count": key.request_count,
|
|
113
|
+
"last_used_at": key.last_used_at,
|
|
114
|
+
"expires_at": key.expires_at,
|
|
115
|
+
"created_at": key.created_at,
|
|
116
|
+
"created_by": key.created_by.username if key.created_by else None,
|
|
117
|
+
})
|
|
118
|
+
return self.get_paginated_response(results)
|
|
119
|
+
|
|
120
|
+
# No pagination fallback
|
|
121
|
+
results = []
|
|
122
|
+
for key in queryset[:100]:
|
|
123
|
+
results.append({
|
|
124
|
+
"id": key.id,
|
|
125
|
+
"name": key.name,
|
|
126
|
+
"key_type": key.key_type,
|
|
127
|
+
"masked_key": key.masked_key,
|
|
128
|
+
"is_active": key.is_active,
|
|
129
|
+
"is_valid": key.is_valid,
|
|
130
|
+
"user_id": key.user.id,
|
|
131
|
+
"username": key.user.username,
|
|
132
|
+
"user_email": key.user.email,
|
|
133
|
+
"request_count": key.request_count,
|
|
134
|
+
"last_used_at": key.last_used_at,
|
|
135
|
+
"expires_at": key.expires_at,
|
|
136
|
+
"created_at": key.created_at,
|
|
137
|
+
"created_by": key.created_by.username if key.created_by else None,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return Response({"results": results, "count": len(results)})
|
|
141
|
+
|
|
142
|
+
except ValueError as e:
|
|
143
|
+
logger.warning(f"API keys list validation error: {e}")
|
|
144
|
+
return Response(
|
|
145
|
+
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
|
146
|
+
)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"API keys list error: {e}", exc_info=True)
|
|
149
|
+
return Response(
|
|
150
|
+
{"error": "Internal server error"},
|
|
151
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@extend_schema(
|
|
155
|
+
tags=["gRPC API Keys"],
|
|
156
|
+
summary="Get API key details",
|
|
157
|
+
description="Returns detailed information about a specific API key.",
|
|
158
|
+
responses={
|
|
159
|
+
200: ApiKeySerializer,
|
|
160
|
+
404: {"description": "API key not found"},
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
def retrieve(self, request, pk=None):
|
|
164
|
+
"""Get details of a specific API key."""
|
|
165
|
+
try:
|
|
166
|
+
key = GrpcApiKey.objects.select_related("user", "created_by").get(pk=pk)
|
|
167
|
+
|
|
168
|
+
data = {
|
|
169
|
+
"id": key.id,
|
|
170
|
+
"name": key.name,
|
|
171
|
+
"key_type": key.key_type,
|
|
172
|
+
"masked_key": key.masked_key,
|
|
173
|
+
"is_active": key.is_active,
|
|
174
|
+
"is_valid": key.is_valid,
|
|
175
|
+
"user_id": key.user.id,
|
|
176
|
+
"username": key.user.username,
|
|
177
|
+
"user_email": key.user.email,
|
|
178
|
+
"request_count": key.request_count,
|
|
179
|
+
"last_used_at": key.last_used_at,
|
|
180
|
+
"expires_at": key.expires_at,
|
|
181
|
+
"created_at": key.created_at,
|
|
182
|
+
"created_by": key.created_by.username if key.created_by else None,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
serializer = ApiKeySerializer(data=data)
|
|
186
|
+
serializer.is_valid(raise_exception=True)
|
|
187
|
+
return Response(serializer.data)
|
|
188
|
+
|
|
189
|
+
except GrpcApiKey.DoesNotExist:
|
|
190
|
+
return Response(
|
|
191
|
+
{"error": "API key not found"},
|
|
192
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"API key retrieve error: {e}", exc_info=True)
|
|
196
|
+
return Response(
|
|
197
|
+
{"error": "Internal server error"},
|
|
198
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@extend_schema(
|
|
202
|
+
tags=["gRPC API Keys"],
|
|
203
|
+
summary="Get API keys statistics",
|
|
204
|
+
description="Returns overall statistics about API keys usage.",
|
|
205
|
+
responses={
|
|
206
|
+
200: ApiKeyStatsSerializer,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
@action(detail=False, methods=["get"], url_path="stats")
|
|
210
|
+
def stats(self, request):
|
|
211
|
+
"""Get API keys statistics."""
|
|
212
|
+
try:
|
|
213
|
+
# Total keys
|
|
214
|
+
total_keys = GrpcApiKey.objects.count()
|
|
215
|
+
|
|
216
|
+
# Active keys
|
|
217
|
+
active_keys = GrpcApiKey.objects.filter(is_active=True).count()
|
|
218
|
+
|
|
219
|
+
# Expired keys
|
|
220
|
+
expired_keys = GrpcApiKey.objects.filter(
|
|
221
|
+
Q(is_active=True) & Q(expires_at__lt=timezone.now())
|
|
222
|
+
).count()
|
|
223
|
+
|
|
224
|
+
# Total requests across all keys
|
|
225
|
+
total_requests = GrpcApiKey.objects.aggregate(
|
|
226
|
+
total=Sum("request_count")
|
|
227
|
+
)["total"] or 0
|
|
228
|
+
|
|
229
|
+
# Keys by type
|
|
230
|
+
keys_by_type_qs = GrpcApiKey.objects.values("key_type").annotate(
|
|
231
|
+
count=Count("id")
|
|
232
|
+
)
|
|
233
|
+
keys_by_type = {item["key_type"]: item["count"] for item in keys_by_type_qs}
|
|
234
|
+
|
|
235
|
+
stats_data = {
|
|
236
|
+
"total_keys": total_keys,
|
|
237
|
+
"active_keys": active_keys,
|
|
238
|
+
"expired_keys": expired_keys,
|
|
239
|
+
"total_requests": total_requests,
|
|
240
|
+
"keys_by_type": keys_by_type,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
serializer = ApiKeyStatsSerializer(data=stats_data)
|
|
244
|
+
serializer.is_valid(raise_exception=True)
|
|
245
|
+
return Response(serializer.data)
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"API keys stats error: {e}", exc_info=True)
|
|
249
|
+
return Response(
|
|
250
|
+
{"error": "Internal server error"},
|
|
251
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = ["GRPCApiKeyViewSet"]
|