django-cfg 1.5.20__py3-none-any.whl → 1.5.29__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/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +21 -36
- django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
- django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
- django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
- django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
- django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
- django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
- django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
- django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
- django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
- django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
- django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
- django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
- django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
- django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
- django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
- django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
- django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
- django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
- django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
- django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
- django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
- django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
- django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
- django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
- django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
- django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
- django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/management/commands/pool_status.py +153 -0
- django_cfg/middleware/pool_cleanup.py +261 -0
- django_cfg/models/api/grpc/config.py +2 -2
- django_cfg/models/infrastructure/database/config.py +16 -0
- django_cfg/models/infrastructure/database/converters.py +2 -0
- django_cfg/modules/django_admin/utils/html/composition.py +57 -13
- django_cfg/modules/django_admin/utils/html_builder.py +1 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +0 -39
- django_cfg/utils/pool_monitor.py +320 -0
- django_cfg/utils/smart_defaults.py +233 -7
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
- /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
- /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
- /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
- /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gRPC handler factory utilities.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for creating and registering gRPC servicers
|
|
5
|
+
with proper error handling and logging.
|
|
6
|
+
|
|
7
|
+
**Purpose**:
|
|
8
|
+
Simplify the boilerplate code needed to register gRPC services with servers.
|
|
9
|
+
|
|
10
|
+
**Usage Example**:
|
|
11
|
+
```python
|
|
12
|
+
from django_cfg.apps.integrations.grpc.utils.handlers import create_grpc_handler
|
|
13
|
+
|
|
14
|
+
# Your servicer class
|
|
15
|
+
class BotStreamingService(pb2_grpc.BotStreamingServiceServicer):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
# Create handler tuple
|
|
19
|
+
handlers = create_grpc_handler(
|
|
20
|
+
servicer_class=BotStreamingService,
|
|
21
|
+
add_servicer_func=pb2_grpc.add_BotStreamingServiceServicer_to_server,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Use with django-cfg
|
|
25
|
+
GRPC_HANDLERS = [handlers]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Created: 2025-11-07
|
|
29
|
+
Status: %%PRODUCTION%%
|
|
30
|
+
Phase: Phase 1 - Universal Components
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from typing import Callable, Tuple, Any, Optional, Type
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ============================================================================
|
|
40
|
+
# Handler Factory
|
|
41
|
+
# ============================================================================
|
|
42
|
+
|
|
43
|
+
def create_grpc_handler(
|
|
44
|
+
servicer_class: Type,
|
|
45
|
+
add_servicer_func: Callable[[Any, Any], None],
|
|
46
|
+
servicer_kwargs: Optional[dict] = None,
|
|
47
|
+
) -> Tuple[Callable[[Any, Any], None], Type, dict]:
|
|
48
|
+
"""
|
|
49
|
+
Create gRPC handler tuple for django-cfg GRPC_HANDLERS.
|
|
50
|
+
|
|
51
|
+
This factory creates the standardized tuple format expected by django-cfg's
|
|
52
|
+
gRPC server initialization:
|
|
53
|
+
```python
|
|
54
|
+
(add_servicer_function, servicer_class, init_kwargs)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
servicer_class: gRPC servicer class (e.g., MyStreamingService)
|
|
59
|
+
add_servicer_func: Generated add_*_to_server function from pb2_grpc
|
|
60
|
+
servicer_kwargs: Optional kwargs to pass to servicer constructor
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (add_func, servicer_class, kwargs) for GRPC_HANDLERS
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
```python
|
|
67
|
+
from .generated import bot_streaming_service_pb2_grpc
|
|
68
|
+
from .services import BotStreamingService
|
|
69
|
+
|
|
70
|
+
handlers = create_grpc_handler(
|
|
71
|
+
servicer_class=BotStreamingService,
|
|
72
|
+
add_servicer_func=bot_streaming_service_pb2_grpc.add_BotStreamingServiceServicer_to_server,
|
|
73
|
+
servicer_kwargs={'config': my_config},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# In settings.py
|
|
77
|
+
GRPC_HANDLERS = [handlers]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**django-cfg Integration**:
|
|
81
|
+
The tuple is used by django-cfg like this:
|
|
82
|
+
```python
|
|
83
|
+
for add_func, servicer_class, kwargs in GRPC_HANDLERS:
|
|
84
|
+
servicer = servicer_class(**kwargs)
|
|
85
|
+
add_func(servicer, server)
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
kwargs = servicer_kwargs or {}
|
|
89
|
+
|
|
90
|
+
logger.debug(
|
|
91
|
+
f"Created gRPC handler: {servicer_class.__name__} "
|
|
92
|
+
f"with {len(kwargs)} init kwargs"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return (add_servicer_func, servicer_class, kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_multiple_grpc_handlers(
|
|
99
|
+
handlers_config: list[dict],
|
|
100
|
+
) -> list[Tuple[Callable[[Any, Any], None], Type, dict]]:
|
|
101
|
+
"""
|
|
102
|
+
Create multiple gRPC handlers from configuration list.
|
|
103
|
+
|
|
104
|
+
Convenience function for creating multiple handlers at once.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
handlers_config: List of handler configurations, each with keys:
|
|
108
|
+
- servicer_class: Servicer class
|
|
109
|
+
- add_servicer_func: Add function
|
|
110
|
+
- servicer_kwargs: Optional init kwargs
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of handler tuples for GRPC_HANDLERS
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
```python
|
|
117
|
+
handlers = create_multiple_grpc_handlers([
|
|
118
|
+
{
|
|
119
|
+
'servicer_class': BotStreamingService,
|
|
120
|
+
'add_servicer_func': pb2_grpc.add_BotStreamingServiceServicer_to_server,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
'servicer_class': SignalStreamingService,
|
|
124
|
+
'add_servicer_func': signal_pb2_grpc.add_SignalStreamingServiceServicer_to_server,
|
|
125
|
+
'servicer_kwargs': {'timeout': 30},
|
|
126
|
+
},
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
# In settings.py
|
|
130
|
+
GRPC_HANDLERS = handlers
|
|
131
|
+
```
|
|
132
|
+
"""
|
|
133
|
+
handlers = []
|
|
134
|
+
|
|
135
|
+
for config in handlers_config:
|
|
136
|
+
handler = create_grpc_handler(
|
|
137
|
+
servicer_class=config['servicer_class'],
|
|
138
|
+
add_servicer_func=config['add_servicer_func'],
|
|
139
|
+
servicer_kwargs=config.get('servicer_kwargs'),
|
|
140
|
+
)
|
|
141
|
+
handlers.append(handler)
|
|
142
|
+
|
|
143
|
+
logger.info(f"Created {len(handlers)} gRPC handlers")
|
|
144
|
+
|
|
145
|
+
return handlers
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ============================================================================
|
|
149
|
+
# Validation Utilities
|
|
150
|
+
# ============================================================================
|
|
151
|
+
|
|
152
|
+
def validate_grpc_handler(
|
|
153
|
+
handler: Tuple[Callable, Type, dict],
|
|
154
|
+
) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Validate that handler tuple has correct structure.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
handler: Handler tuple to validate
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if valid, False otherwise
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
```python
|
|
166
|
+
handler = create_grpc_handler(MyService, add_func)
|
|
167
|
+
|
|
168
|
+
if validate_grpc_handler(handler):
|
|
169
|
+
GRPC_HANDLERS.append(handler)
|
|
170
|
+
```
|
|
171
|
+
"""
|
|
172
|
+
if not isinstance(handler, tuple):
|
|
173
|
+
logger.error(f"Handler must be tuple, got {type(handler)}")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
if len(handler) != 3:
|
|
177
|
+
logger.error(f"Handler tuple must have 3 elements, got {len(handler)}")
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
add_func, servicer_class, kwargs = handler
|
|
181
|
+
|
|
182
|
+
if not callable(add_func):
|
|
183
|
+
logger.error(f"First element (add_func) must be callable, got {type(add_func)}")
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
if not isinstance(servicer_class, type):
|
|
187
|
+
logger.error(f"Second element (servicer_class) must be class, got {type(servicer_class)}")
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
if not isinstance(kwargs, dict):
|
|
191
|
+
logger.error(f"Third element (kwargs) must be dict, got {type(kwargs)}")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def validate_grpc_handlers(
|
|
198
|
+
handlers: list,
|
|
199
|
+
) -> Tuple[bool, list[str]]:
|
|
200
|
+
"""
|
|
201
|
+
Validate list of handlers.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
handlers: List of handler tuples
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (all_valid: bool, errors: list[str])
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
```python
|
|
211
|
+
valid, errors = validate_grpc_handlers(GRPC_HANDLERS)
|
|
212
|
+
|
|
213
|
+
if not valid:
|
|
214
|
+
for error in errors:
|
|
215
|
+
logger.error(error)
|
|
216
|
+
raise ValueError("Invalid gRPC handlers")
|
|
217
|
+
```
|
|
218
|
+
"""
|
|
219
|
+
errors = []
|
|
220
|
+
|
|
221
|
+
if not isinstance(handlers, list):
|
|
222
|
+
errors.append(f"GRPC_HANDLERS must be list, got {type(handlers)}")
|
|
223
|
+
return False, errors
|
|
224
|
+
|
|
225
|
+
for i, handler in enumerate(handlers):
|
|
226
|
+
if not validate_grpc_handler(handler):
|
|
227
|
+
errors.append(f"Handler #{i} is invalid")
|
|
228
|
+
|
|
229
|
+
all_valid = len(errors) == 0
|
|
230
|
+
return all_valid, errors
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ============================================================================
|
|
234
|
+
# Exports
|
|
235
|
+
# ============================================================================
|
|
236
|
+
|
|
237
|
+
__all__ = [
|
|
238
|
+
'create_grpc_handler',
|
|
239
|
+
'create_multiple_grpc_handlers',
|
|
240
|
+
'validate_grpc_handler',
|
|
241
|
+
'validate_grpc_handlers',
|
|
242
|
+
]
|
|
@@ -394,7 +394,7 @@ def generate_proto_for_app(app_label: str, output_dir: Optional[Path] = None) ->
|
|
|
394
394
|
```
|
|
395
395
|
"""
|
|
396
396
|
# Get gRPC config from django-cfg (Pydantic)
|
|
397
|
-
from ..services.config_helper import get_grpc_config
|
|
397
|
+
from ..services.management.config_helper import get_grpc_config
|
|
398
398
|
|
|
399
399
|
grpc_config = get_grpc_config()
|
|
400
400
|
proto_config = grpc_config.proto if grpc_config else None
|
|
@@ -19,6 +19,39 @@ from rich.table import Table
|
|
|
19
19
|
from rich.text import Text
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
# ========================================================================
|
|
23
|
+
# Module-level debug mode caching (performance optimization)
|
|
24
|
+
# ========================================================================
|
|
25
|
+
|
|
26
|
+
_debug_mode: Optional[bool] = None # Cached debug mode to avoid repeated config loads
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_debug_mode() -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Get debug mode from config (cached at module level).
|
|
32
|
+
|
|
33
|
+
Loads config only once and caches the result to avoid repeated config loads.
|
|
34
|
+
This is a performance optimization - config loading can be expensive.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if debug mode is enabled, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
global _debug_mode
|
|
40
|
+
|
|
41
|
+
if _debug_mode is not None:
|
|
42
|
+
return _debug_mode
|
|
43
|
+
|
|
44
|
+
# Load config once and cache
|
|
45
|
+
try:
|
|
46
|
+
from django_cfg.core.state import get_current_config
|
|
47
|
+
config = get_current_config()
|
|
48
|
+
_debug_mode = config.debug if config and hasattr(config, 'debug') else False
|
|
49
|
+
except Exception:
|
|
50
|
+
_debug_mode = os.getenv('DEBUG', 'false').lower() in ('true', '1', 'yes')
|
|
51
|
+
|
|
52
|
+
return _debug_mode
|
|
53
|
+
|
|
54
|
+
|
|
22
55
|
class AutoTracebackHandler(logging.Handler):
|
|
23
56
|
"""
|
|
24
57
|
Custom handler that automatically adds exception info to ERROR and CRITICAL logs.
|
|
@@ -49,8 +82,8 @@ class AutoTracebackHandler(logging.Handler):
|
|
|
49
82
|
def setup_streaming_logger(
|
|
50
83
|
name: str = "grpc_streaming",
|
|
51
84
|
logs_dir: Optional[Path] = None,
|
|
52
|
-
level: int =
|
|
53
|
-
console_level: int =
|
|
85
|
+
level: Optional[int] = None,
|
|
86
|
+
console_level: Optional[int] = None
|
|
54
87
|
) -> logging.Logger:
|
|
55
88
|
"""
|
|
56
89
|
Setup dedicated logger for gRPC streaming with file and console handlers.
|
|
@@ -58,14 +91,15 @@ def setup_streaming_logger(
|
|
|
58
91
|
Follows django-cfg logging pattern:
|
|
59
92
|
- Uses os.getcwd() / 'logs' / 'grpc_streaming' for log directory
|
|
60
93
|
- Time-based log file names (streaming_YYYYMMDD_HHMMSS.log)
|
|
61
|
-
-
|
|
62
|
-
-
|
|
94
|
+
- Auto-detects debug mode for appropriate logging levels
|
|
95
|
+
- In dev/debug: files=DEBUG+, console=DEBUG+
|
|
96
|
+
- In production: files=INFO+, console=WARNING+
|
|
63
97
|
|
|
64
98
|
Args:
|
|
65
99
|
name: Logger name (default: "grpc_streaming")
|
|
66
100
|
logs_dir: Directory for log files (default: <cwd>/logs/grpc_streaming)
|
|
67
|
-
level: File logging level (default:
|
|
68
|
-
console_level: Console logging level (default:
|
|
101
|
+
level: File logging level (default: auto-detect from debug mode)
|
|
102
|
+
console_level: Console logging level (default: auto-detect from debug mode)
|
|
69
103
|
|
|
70
104
|
Returns:
|
|
71
105
|
Configured logger instance
|
|
@@ -74,14 +108,14 @@ def setup_streaming_logger(
|
|
|
74
108
|
```python
|
|
75
109
|
from django_cfg.apps.integrations.grpc.utils import setup_streaming_logger
|
|
76
110
|
|
|
77
|
-
# Basic usage
|
|
111
|
+
# Basic usage (auto-detects debug mode)
|
|
78
112
|
logger = setup_streaming_logger()
|
|
79
113
|
|
|
80
114
|
# Custom configuration
|
|
81
115
|
logger = setup_streaming_logger(
|
|
82
116
|
name="my_streaming_service",
|
|
83
117
|
logs_dir=Path("/var/log/grpc"),
|
|
84
|
-
level=logging.INFO
|
|
118
|
+
level=logging.INFO # Override auto-detection
|
|
85
119
|
)
|
|
86
120
|
|
|
87
121
|
logger.info("Service started")
|
|
@@ -93,8 +127,21 @@ def setup_streaming_logger(
|
|
|
93
127
|
- Time-based log file names
|
|
94
128
|
- No duplicate logs (propagate=False)
|
|
95
129
|
- UTF-8 encoding
|
|
130
|
+
- Debug mode auto-detection (cached for performance)
|
|
96
131
|
- Reusable across all django-cfg gRPC projects
|
|
97
132
|
"""
|
|
133
|
+
# Auto-detect debug mode (cached - loaded once)
|
|
134
|
+
debug = _get_debug_mode()
|
|
135
|
+
|
|
136
|
+
# Auto-determine logging levels based on debug mode if not explicitly provided
|
|
137
|
+
if level is None:
|
|
138
|
+
# File handlers: DEBUG in dev, INFO in production
|
|
139
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
140
|
+
|
|
141
|
+
if console_level is None:
|
|
142
|
+
# Console: DEBUG in dev (full visibility), WARNING in production (reduce noise)
|
|
143
|
+
console_level = logging.DEBUG if debug else logging.WARNING
|
|
144
|
+
|
|
98
145
|
# Create logger
|
|
99
146
|
streaming_logger = logging.getLogger(name)
|
|
100
147
|
streaming_logger.setLevel(level)
|
|
@@ -22,7 +22,7 @@ from ..serializers.charts import (
|
|
|
22
22
|
ServerUptimeChartSerializer,
|
|
23
23
|
ServiceActivityChartSerializer,
|
|
24
24
|
)
|
|
25
|
-
from ..services.
|
|
25
|
+
from ..services.rendering.charts import ChartGeneratorService
|
|
26
26
|
|
|
27
27
|
logger = get_logger("grpc.charts")
|
|
28
28
|
|
|
@@ -16,7 +16,7 @@ from rest_framework.response import Response
|
|
|
16
16
|
from ..models import GRPCRequestLog, GRPCServerStatus
|
|
17
17
|
from ..serializers.config import GRPCConfigSerializer, GRPCServerInfoSerializer
|
|
18
18
|
from ..services import ServiceDiscovery
|
|
19
|
-
from ..services.config_helper import get_grpc_config, get_grpc_server_config
|
|
19
|
+
from ..services.management.config_helper import get_grpc_config, get_grpc_server_config
|
|
20
20
|
|
|
21
21
|
logger = get_logger("grpc.config")
|
|
22
22
|
|
|
@@ -221,6 +221,17 @@ class DjangoConfig(BaseModel):
|
|
|
221
221
|
description="Database connections",
|
|
222
222
|
)
|
|
223
223
|
|
|
224
|
+
enable_pool_cleanup: bool = Field(
|
|
225
|
+
default=False,
|
|
226
|
+
description=(
|
|
227
|
+
"Enable explicit connection pool cleanup middleware. "
|
|
228
|
+
"Django already closes connections automatically, but this middleware "
|
|
229
|
+
"adds explicit guarantees and rollback on errors. "
|
|
230
|
+
"Enable only if you experience connection leaks. "
|
|
231
|
+
"Note: Not needed with ATOMIC_REQUESTS=True (default)."
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
|
|
224
235
|
# === Cache Configuration ===
|
|
225
236
|
# Redis URL - used for automatic cache configuration if cache_default is not set
|
|
226
237
|
redis_url: Optional[str] = Field(
|
|
@@ -66,6 +66,11 @@ class MiddlewareBuilder:
|
|
|
66
66
|
# Add custom user middleware
|
|
67
67
|
middleware.extend(self.config.custom_middleware)
|
|
68
68
|
|
|
69
|
+
# Add connection pool cleanup middleware LAST (if enabled)
|
|
70
|
+
# This ensures connections are returned to pool after ALL other middleware
|
|
71
|
+
if self.config.enable_pool_cleanup:
|
|
72
|
+
middleware.append('django_cfg.middleware.pool_cleanup.ConnectionPoolCleanupMiddleware')
|
|
73
|
+
|
|
69
74
|
return middleware
|
|
70
75
|
|
|
71
76
|
def _get_feature_middleware(self) -> List[str]:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django management command to display connection pool status.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python manage.py pool_status
|
|
6
|
+
python manage.py pool_status --database=secondary
|
|
7
|
+
python manage.py pool_status --json
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
from django.core.management.base import BaseCommand
|
|
13
|
+
|
|
14
|
+
from django_cfg.utils.pool_monitor import PoolMonitor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command(BaseCommand):
|
|
18
|
+
"""Display database connection pool status."""
|
|
19
|
+
|
|
20
|
+
help = 'Display database connection pool status and health information'
|
|
21
|
+
|
|
22
|
+
def add_arguments(self, parser):
|
|
23
|
+
"""Add command arguments."""
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
'--database',
|
|
26
|
+
default='default',
|
|
27
|
+
help='Database alias to check (default: default)',
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
'--json',
|
|
31
|
+
action='store_true',
|
|
32
|
+
help='Output in JSON format',
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def handle(self, *args, **options):
|
|
36
|
+
"""Execute command."""
|
|
37
|
+
database_alias = options['database']
|
|
38
|
+
json_output = options['json']
|
|
39
|
+
|
|
40
|
+
monitor = PoolMonitor(database_alias=database_alias)
|
|
41
|
+
|
|
42
|
+
if json_output:
|
|
43
|
+
self._handle_json_output(monitor)
|
|
44
|
+
else:
|
|
45
|
+
self._handle_pretty_output(monitor)
|
|
46
|
+
|
|
47
|
+
def _handle_json_output(self, monitor: PoolMonitor):
|
|
48
|
+
"""Output pool status as JSON."""
|
|
49
|
+
info = monitor.get_pool_info_dict()
|
|
50
|
+
self.stdout.write(json.dumps(info, indent=2))
|
|
51
|
+
|
|
52
|
+
def _handle_pretty_output(self, monitor: PoolMonitor):
|
|
53
|
+
"""Output pool status in pretty formatted text."""
|
|
54
|
+
stats = monitor.get_pool_stats()
|
|
55
|
+
|
|
56
|
+
if not stats:
|
|
57
|
+
self.stdout.write(self.style.WARNING('\n⚠️ Connection Pooling: Not Configured'))
|
|
58
|
+
self.stdout.write('')
|
|
59
|
+
self.stdout.write('Database is not using connection pooling.')
|
|
60
|
+
self.stdout.write('Consider enabling pooling for production environments.')
|
|
61
|
+
self.stdout.write('')
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
health = monitor.check_pool_health()
|
|
65
|
+
|
|
66
|
+
# Header
|
|
67
|
+
self.stdout.write('')
|
|
68
|
+
self.stdout.write(self.style.SUCCESS('=' * 70))
|
|
69
|
+
self.stdout.write(self.style.SUCCESS(' DATABASE CONNECTION POOL STATUS'))
|
|
70
|
+
self.stdout.write(self.style.SUCCESS('=' * 70))
|
|
71
|
+
self.stdout.write('')
|
|
72
|
+
|
|
73
|
+
# Deployment Information
|
|
74
|
+
mode = 'ASGI' if stats['is_asgi'] else 'WSGI'
|
|
75
|
+
mode_icon = '🚀' if stats['is_asgi'] else '🐍'
|
|
76
|
+
|
|
77
|
+
self.stdout.write(self.style.HTTP_INFO('Deployment Information:'))
|
|
78
|
+
self.stdout.write(f" Mode: {mode_icon} {mode}")
|
|
79
|
+
self.stdout.write(f" Environment: {stats['environment'].title()}")
|
|
80
|
+
self.stdout.write(f" Database: {monitor.database_alias}")
|
|
81
|
+
self.stdout.write(f" Backend: {stats['backend']}")
|
|
82
|
+
self.stdout.write('')
|
|
83
|
+
|
|
84
|
+
# Pool Configuration
|
|
85
|
+
self.stdout.write(self.style.HTTP_INFO('Pool Configuration:'))
|
|
86
|
+
self.stdout.write(f" Min Size: {stats['pool_min_size']:3d} connections")
|
|
87
|
+
self.stdout.write(f" Max Size: {stats['pool_max_size']:3d} connections")
|
|
88
|
+
self.stdout.write(f" Timeout: {stats['pool_timeout']:3d} seconds")
|
|
89
|
+
self.stdout.write(f" Max Lifetime: {stats['max_lifetime']:4d} seconds ({stats['max_lifetime'] // 60} min)")
|
|
90
|
+
self.stdout.write(f" Max Idle: {stats['max_idle']:4d} seconds ({stats['max_idle'] // 60} min)")
|
|
91
|
+
self.stdout.write('')
|
|
92
|
+
|
|
93
|
+
# Current Status (if available)
|
|
94
|
+
if stats['pool_size'] is not None:
|
|
95
|
+
self.stdout.write(self.style.HTTP_INFO('Current Status:'))
|
|
96
|
+
self.stdout.write(f" Current Size: {stats['pool_size']:3d} connections")
|
|
97
|
+
if stats['pool_available'] is not None:
|
|
98
|
+
self.stdout.write(f" Available: {stats['pool_available']:3d} connections")
|
|
99
|
+
self.stdout.write(f" Capacity: {health['capacity_percent']:.1f}% used")
|
|
100
|
+
self.stdout.write('')
|
|
101
|
+
else:
|
|
102
|
+
self.stdout.write(self.style.WARNING('Current Status: Not available (pool not active yet)'))
|
|
103
|
+
self.stdout.write('')
|
|
104
|
+
|
|
105
|
+
# Health Check
|
|
106
|
+
status_styles = {
|
|
107
|
+
'healthy': self.style.SUCCESS,
|
|
108
|
+
'warning': self.style.WARNING,
|
|
109
|
+
'critical': self.style.ERROR,
|
|
110
|
+
'unavailable': self.style.NOTICE,
|
|
111
|
+
}
|
|
112
|
+
status_icons = {
|
|
113
|
+
'healthy': '✅',
|
|
114
|
+
'warning': '⚠️',
|
|
115
|
+
'critical': '🔴',
|
|
116
|
+
'unavailable': '⚪',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
status_style = status_styles.get(health['status'], self.style.NOTICE)
|
|
120
|
+
status_icon = status_icons.get(health['status'], '❓')
|
|
121
|
+
status_text = health['status'].upper()
|
|
122
|
+
|
|
123
|
+
self.stdout.write(self.style.HTTP_INFO('Health Check:'))
|
|
124
|
+
self.stdout.write(f" Status: {status_icon} " + status_style(status_text))
|
|
125
|
+
|
|
126
|
+
if health['issues']:
|
|
127
|
+
self.stdout.write('')
|
|
128
|
+
self.stdout.write(self.style.WARNING(' Issues Detected:'))
|
|
129
|
+
for issue in health['issues']:
|
|
130
|
+
self.stdout.write(self.style.WARNING(f" • {issue}"))
|
|
131
|
+
|
|
132
|
+
if health['recommendations']:
|
|
133
|
+
self.stdout.write('')
|
|
134
|
+
self.stdout.write(self.style.NOTICE(' Recommendations:'))
|
|
135
|
+
for rec in health['recommendations']:
|
|
136
|
+
self.stdout.write(f" • {rec}")
|
|
137
|
+
|
|
138
|
+
# Footer
|
|
139
|
+
self.stdout.write('')
|
|
140
|
+
self.stdout.write(self.style.SUCCESS('=' * 70))
|
|
141
|
+
self.stdout.write('')
|
|
142
|
+
|
|
143
|
+
# Overall summary
|
|
144
|
+
if health['healthy']:
|
|
145
|
+
self.stdout.write(self.style.SUCCESS('✅ Pool is healthy and operating normally'))
|
|
146
|
+
elif health['status'] == 'warning':
|
|
147
|
+
self.stdout.write(self.style.WARNING('⚠️ Pool is functional but requires attention'))
|
|
148
|
+
elif health['status'] == 'critical':
|
|
149
|
+
self.stdout.write(self.style.ERROR('🔴 Pool is in critical state - immediate action required'))
|
|
150
|
+
else:
|
|
151
|
+
self.stdout.write(self.style.NOTICE('ℹ️ Pool status check completed'))
|
|
152
|
+
|
|
153
|
+
self.stdout.write('')
|