django-cfg 1.5.20__py3-none-any.whl → 1.5.31__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 +90 -14
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +47 -43
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +41 -29
- 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 +22 -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} +216 -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/generator/typescript/files_generator.py +12 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -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_client/urls.py +38 -5
- django_cfg/modules/django_logging/django_logger.py +58 -19
- django_cfg/modules/django_twilio/email_otp.py +3 -1
- django_cfg/modules/django_twilio/sms.py +3 -1
- django_cfg/modules/django_twilio/unified.py +6 -2
- django_cfg/modules/django_twilio/whatsapp.py +3 -1
- django_cfg/pyproject.toml +3 -3
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/index.html +17 -57
- 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.31.dist-info}/METADATA +75 -5
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/RECORD +97 -68
- 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.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.20.dist-info → django_cfg-1.5.31.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection Pool Monitoring for Django-CFG.
|
|
3
|
+
|
|
4
|
+
Provides utilities to monitor and inspect PostgreSQL connection pool status.
|
|
5
|
+
Works with Django 5.1+ native connection pooling (psycopg3).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from django_cfg.utils.pool_monitor import PoolMonitor
|
|
9
|
+
|
|
10
|
+
# Get pool statistics
|
|
11
|
+
monitor = PoolMonitor()
|
|
12
|
+
stats = monitor.get_pool_stats()
|
|
13
|
+
|
|
14
|
+
# Check pool health
|
|
15
|
+
health = monitor.check_pool_health()
|
|
16
|
+
|
|
17
|
+
# Log pool status
|
|
18
|
+
monitor.log_pool_status()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
|
|
24
|
+
from django.conf import settings
|
|
25
|
+
from django.db import connection
|
|
26
|
+
|
|
27
|
+
from .smart_defaults import _detect_asgi_mode, get_pool_config
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger('django_cfg.pool_monitor')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PoolMonitor:
|
|
33
|
+
"""
|
|
34
|
+
Monitor and inspect database connection pool.
|
|
35
|
+
|
|
36
|
+
Provides methods to retrieve pool statistics, check health,
|
|
37
|
+
and log pool status for operational visibility.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, database_alias: str = 'default'):
|
|
41
|
+
"""
|
|
42
|
+
Initialize pool monitor.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
database_alias: Database alias to monitor (default: 'default')
|
|
46
|
+
"""
|
|
47
|
+
self.database_alias = database_alias
|
|
48
|
+
|
|
49
|
+
def get_pool_stats(self) -> Optional[Dict[str, Any]]:
|
|
50
|
+
"""
|
|
51
|
+
Get current connection pool statistics.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dict with pool statistics:
|
|
55
|
+
{
|
|
56
|
+
'pool_size': int, # Current pool size
|
|
57
|
+
'pool_available': int, # Available connections
|
|
58
|
+
'pool_min_size': int, # Configured minimum size
|
|
59
|
+
'pool_max_size': int, # Configured maximum size
|
|
60
|
+
'pool_timeout': int, # Connection timeout
|
|
61
|
+
'is_asgi': bool, # Deployment mode
|
|
62
|
+
'environment': str, # Environment name
|
|
63
|
+
'backend': str, # Database backend
|
|
64
|
+
'has_pool': bool, # Whether pooling is enabled
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Returns None if connection pooling is not configured.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> monitor = PoolMonitor()
|
|
71
|
+
>>> stats = monitor.get_pool_stats()
|
|
72
|
+
>>> print(f"Pool size: {stats['pool_size']}/{stats['pool_max_size']}")
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# Get database configuration
|
|
76
|
+
db_config = settings.DATABASES.get(self.database_alias, {})
|
|
77
|
+
backend = db_config.get('ENGINE', 'unknown')
|
|
78
|
+
|
|
79
|
+
# Check if PostgreSQL with pooling
|
|
80
|
+
if 'postgresql' not in backend:
|
|
81
|
+
logger.debug(f"Database {self.database_alias} is not PostgreSQL, pooling not available")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
pool_options = db_config.get('OPTIONS', {}).get('pool', {})
|
|
85
|
+
if not pool_options:
|
|
86
|
+
logger.debug(f"No pool configuration found for database {self.database_alias}")
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Detect environment and mode
|
|
90
|
+
is_asgi = _detect_asgi_mode()
|
|
91
|
+
environment = getattr(settings, 'ENVIRONMENT', 'production')
|
|
92
|
+
|
|
93
|
+
# Get expected pool config
|
|
94
|
+
expected_config = get_pool_config(environment, is_asgi=is_asgi)
|
|
95
|
+
|
|
96
|
+
# Try to get actual pool statistics from psycopg3
|
|
97
|
+
pool_size = None
|
|
98
|
+
pool_available = None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Access underlying psycopg3 connection pool
|
|
102
|
+
# This requires psycopg3 with connection pooling
|
|
103
|
+
db_conn = connection.connection
|
|
104
|
+
if db_conn and hasattr(db_conn, 'pgconn'):
|
|
105
|
+
pool = getattr(db_conn, 'pool', None)
|
|
106
|
+
if pool:
|
|
107
|
+
# psycopg3 ConnectionPool has these attributes
|
|
108
|
+
pool_size = getattr(pool, 'size', None)
|
|
109
|
+
pool_available = getattr(pool, 'available', None)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.debug(f"Could not retrieve live pool stats: {e}")
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
'pool_size': pool_size,
|
|
115
|
+
'pool_available': pool_available,
|
|
116
|
+
'pool_min_size': pool_options.get('min_size', expected_config['min_size']),
|
|
117
|
+
'pool_max_size': pool_options.get('max_size', expected_config['max_size']),
|
|
118
|
+
'pool_timeout': pool_options.get('timeout', expected_config['timeout']),
|
|
119
|
+
'max_lifetime': pool_options.get('max_lifetime', 3600),
|
|
120
|
+
'max_idle': pool_options.get('max_idle', 600),
|
|
121
|
+
'is_asgi': is_asgi,
|
|
122
|
+
'environment': environment,
|
|
123
|
+
'backend': backend,
|
|
124
|
+
'has_pool': True,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to get pool stats: {e}", exc_info=True)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def check_pool_health(self) -> Dict[str, Any]:
|
|
132
|
+
"""
|
|
133
|
+
Check connection pool health status.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dict with health information:
|
|
137
|
+
{
|
|
138
|
+
'healthy': bool, # Overall health status
|
|
139
|
+
'status': str, # 'healthy', 'warning', 'critical', 'unavailable'
|
|
140
|
+
'capacity_percent': float, # Pool capacity usage (0-100)
|
|
141
|
+
'issues': list, # List of detected issues
|
|
142
|
+
'recommendations': list, # Recommended actions
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Health thresholds:
|
|
146
|
+
- < 70% capacity: Healthy
|
|
147
|
+
- 70-90% capacity: Warning
|
|
148
|
+
- > 90% capacity: Critical
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> monitor = PoolMonitor()
|
|
152
|
+
>>> health = monitor.check_pool_health()
|
|
153
|
+
>>> if not health['healthy']:
|
|
154
|
+
... print(f"Issues: {', '.join(health['issues'])}")
|
|
155
|
+
"""
|
|
156
|
+
stats = self.get_pool_stats()
|
|
157
|
+
|
|
158
|
+
if not stats or not stats['has_pool']:
|
|
159
|
+
return {
|
|
160
|
+
'healthy': True, # No pool = no problem (using regular connections)
|
|
161
|
+
'status': 'unavailable',
|
|
162
|
+
'capacity_percent': 0.0,
|
|
163
|
+
'issues': ['Connection pooling not configured'],
|
|
164
|
+
'recommendations': ['Consider enabling connection pooling for production'],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
issues = []
|
|
168
|
+
recommendations = []
|
|
169
|
+
healthy = True
|
|
170
|
+
status = 'healthy'
|
|
171
|
+
|
|
172
|
+
# Calculate capacity if live stats available
|
|
173
|
+
capacity_percent = 0.0
|
|
174
|
+
if stats['pool_size'] is not None and stats['pool_max_size']:
|
|
175
|
+
capacity_percent = (stats['pool_size'] / stats['pool_max_size']) * 100
|
|
176
|
+
|
|
177
|
+
# Check capacity thresholds
|
|
178
|
+
if capacity_percent >= 90:
|
|
179
|
+
status = 'critical'
|
|
180
|
+
healthy = False
|
|
181
|
+
issues.append(f"Pool capacity critical: {capacity_percent:.1f}% used")
|
|
182
|
+
recommendations.append("Increase DB_POOL_MAX_SIZE or scale database")
|
|
183
|
+
elif capacity_percent >= 70:
|
|
184
|
+
status = 'warning'
|
|
185
|
+
issues.append(f"Pool capacity high: {capacity_percent:.1f}% used")
|
|
186
|
+
recommendations.append("Monitor pool usage and consider increasing max_size")
|
|
187
|
+
|
|
188
|
+
# Check if min_size is reasonable
|
|
189
|
+
min_size = stats['pool_min_size']
|
|
190
|
+
max_size = stats['pool_max_size']
|
|
191
|
+
|
|
192
|
+
if min_size >= max_size * 0.9:
|
|
193
|
+
issues.append(f"Min size ({min_size}) too close to max size ({max_size})")
|
|
194
|
+
recommendations.append("Reduce DB_POOL_MIN_SIZE for better resource management")
|
|
195
|
+
|
|
196
|
+
# Check timeout
|
|
197
|
+
timeout = stats['pool_timeout']
|
|
198
|
+
if timeout > 30:
|
|
199
|
+
issues.append(f"Pool timeout high: {timeout}s")
|
|
200
|
+
recommendations.append("Long timeouts may indicate slow queries or insufficient pool size")
|
|
201
|
+
|
|
202
|
+
# Check ASGI vs WSGI pool sizing
|
|
203
|
+
is_asgi = stats['is_asgi']
|
|
204
|
+
mode = 'ASGI' if is_asgi else 'WSGI'
|
|
205
|
+
|
|
206
|
+
expected_config = get_pool_config(stats['environment'], is_asgi=is_asgi)
|
|
207
|
+
if max_size < expected_config['max_size'] * 0.5:
|
|
208
|
+
issues.append(f"Pool size low for {mode} mode")
|
|
209
|
+
recommendations.append(f"Consider increasing to {expected_config['max_size']} for {mode}")
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
'healthy': healthy and len(issues) == 0,
|
|
213
|
+
'status': status,
|
|
214
|
+
'capacity_percent': capacity_percent,
|
|
215
|
+
'issues': issues,
|
|
216
|
+
'recommendations': recommendations,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
def log_pool_status(self, level: str = 'info') -> None:
|
|
220
|
+
"""
|
|
221
|
+
Log current pool status to Django logger.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
level: Log level ('debug', 'info', 'warning', 'error')
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
>>> monitor = PoolMonitor()
|
|
228
|
+
>>> monitor.log_pool_status(level='info')
|
|
229
|
+
"""
|
|
230
|
+
stats = self.get_pool_stats()
|
|
231
|
+
|
|
232
|
+
if not stats:
|
|
233
|
+
logger.debug(f"No pool statistics available for database {self.database_alias}")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
health = self.check_pool_health()
|
|
237
|
+
|
|
238
|
+
log_func = getattr(logger, level, logger.info)
|
|
239
|
+
|
|
240
|
+
mode = 'ASGI' if stats['is_asgi'] else 'WSGI'
|
|
241
|
+
status_emoji = {
|
|
242
|
+
'healthy': '✅',
|
|
243
|
+
'warning': '⚠️',
|
|
244
|
+
'critical': '🔴',
|
|
245
|
+
'unavailable': '⚪',
|
|
246
|
+
}.get(health['status'], '❓')
|
|
247
|
+
|
|
248
|
+
log_message = (
|
|
249
|
+
f"[Pool Monitor] {status_emoji} Status: {health['status'].upper()} | "
|
|
250
|
+
f"Mode: {mode} | Env: {stats['environment']} | "
|
|
251
|
+
f"Pool: {stats['pool_size'] or '?'}/{stats['pool_max_size']} | "
|
|
252
|
+
f"Capacity: {health['capacity_percent']:.1f}%"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
log_func(log_message)
|
|
256
|
+
|
|
257
|
+
if health['issues']:
|
|
258
|
+
for issue in health['issues']:
|
|
259
|
+
logger.warning(f"[Pool Monitor] Issue: {issue}")
|
|
260
|
+
|
|
261
|
+
if health['recommendations']:
|
|
262
|
+
for rec in health['recommendations']:
|
|
263
|
+
logger.info(f"[Pool Monitor] Recommendation: {rec}")
|
|
264
|
+
|
|
265
|
+
def get_pool_info_dict(self) -> Dict[str, Any]:
|
|
266
|
+
"""
|
|
267
|
+
Get complete pool information as a dictionary.
|
|
268
|
+
|
|
269
|
+
Combines statistics and health check into a single dict.
|
|
270
|
+
Useful for API responses or structured logging.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dict with complete pool information including stats and health.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> monitor = PoolMonitor()
|
|
277
|
+
>>> info = monitor.get_pool_info_dict()
|
|
278
|
+
>>> print(json.dumps(info, indent=2))
|
|
279
|
+
"""
|
|
280
|
+
stats = self.get_pool_stats()
|
|
281
|
+
health = self.check_pool_health()
|
|
282
|
+
|
|
283
|
+
if not stats:
|
|
284
|
+
return {
|
|
285
|
+
'available': False,
|
|
286
|
+
'reason': 'Connection pooling not configured',
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
'available': True,
|
|
291
|
+
'statistics': stats,
|
|
292
|
+
'health': health,
|
|
293
|
+
'timestamp': self._get_timestamp(),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _get_timestamp() -> str:
|
|
298
|
+
"""Get current timestamp in ISO format."""
|
|
299
|
+
from datetime import datetime
|
|
300
|
+
return datetime.utcnow().isoformat() + 'Z'
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Convenience function for quick pool status check
|
|
304
|
+
def get_pool_status(database_alias: str = 'default') -> Dict[str, Any]:
|
|
305
|
+
"""
|
|
306
|
+
Convenience function to quickly get pool status.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
database_alias: Database alias to check (default: 'default')
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Dict with pool information (same as PoolMonitor.get_pool_info_dict())
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
>>> from django_cfg.utils.pool_monitor import get_pool_status
|
|
316
|
+
>>> status = get_pool_status()
|
|
317
|
+
>>> print(status['health']['status'])
|
|
318
|
+
"""
|
|
319
|
+
monitor = PoolMonitor(database_alias=database_alias)
|
|
320
|
+
return monitor.get_pool_info_dict()
|
|
@@ -7,6 +7,8 @@ Following KISS principle:
|
|
|
7
7
|
- Logging handled by django_logger module
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import Any, Dict, List, Optional
|
|
12
14
|
|
|
@@ -14,9 +16,9 @@ from typing import Any, Dict, List, Optional
|
|
|
14
16
|
def get_log_filename() -> str:
|
|
15
17
|
"""
|
|
16
18
|
Determine the correct log filename based on project type.
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
Returns:
|
|
19
|
-
- 'django-cfg.log' for django-cfg projects
|
|
21
|
+
- 'django-cfg.log' for django-cfg projects
|
|
20
22
|
- 'django.log' for regular Django projects
|
|
21
23
|
"""
|
|
22
24
|
try:
|
|
@@ -35,6 +37,188 @@ def get_log_filename() -> str:
|
|
|
35
37
|
return 'django-cfg.log'
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
def _detect_asgi_mode() -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Detect if Django is running in ASGI or WSGI mode.
|
|
43
|
+
|
|
44
|
+
Detection priority:
|
|
45
|
+
1. DJANGO_ASGI environment variable (explicit override)
|
|
46
|
+
2. ASGI_APPLICATION setting (if Django is configured)
|
|
47
|
+
3. Command-line arguments (uvicorn, daphne, hypercorn)
|
|
48
|
+
4. Default: False (WSGI mode)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if ASGI mode, False if WSGI mode
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
>>> os.environ['DJANGO_ASGI'] = 'true'
|
|
55
|
+
>>> _detect_asgi_mode()
|
|
56
|
+
True
|
|
57
|
+
|
|
58
|
+
>>> 'uvicorn' in sys.argv[0]
|
|
59
|
+
>>> _detect_asgi_mode()
|
|
60
|
+
True
|
|
61
|
+
"""
|
|
62
|
+
# 1. Check explicit env var override
|
|
63
|
+
asgi_env = os.environ.get('DJANGO_ASGI', '').lower()
|
|
64
|
+
if asgi_env in ('true', '1', 'yes'):
|
|
65
|
+
return True
|
|
66
|
+
elif asgi_env in ('false', '0', 'no'):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# 2. Check Django settings for ASGI_APPLICATION
|
|
70
|
+
try:
|
|
71
|
+
from django.conf import settings
|
|
72
|
+
if hasattr(settings, 'ASGI_APPLICATION') and settings.ASGI_APPLICATION:
|
|
73
|
+
return True
|
|
74
|
+
except (ImportError, Exception):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# 3. Check command-line arguments for ASGI servers
|
|
78
|
+
command_line = ' '.join(sys.argv).lower()
|
|
79
|
+
asgi_servers = ['uvicorn', 'daphne', 'hypercorn']
|
|
80
|
+
for server in asgi_servers:
|
|
81
|
+
if server in command_line:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Default: WSGI mode
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_pool_config(environment: str = "development", is_asgi: Optional[bool] = None) -> Dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
Get connection pool configuration.
|
|
91
|
+
|
|
92
|
+
By default, uses simple environment-based configuration. Set AUTO_POOL_SIZE=true
|
|
93
|
+
to enable automatic ASGI/WSGI detection and optimization.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
environment: Environment name ('development', 'testing', 'staging', 'production')
|
|
97
|
+
is_asgi: Deployment mode. If None and AUTO_POOL_SIZE=true, auto-detects mode
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Dict with pool configuration:
|
|
101
|
+
{
|
|
102
|
+
'min_size': int, # Minimum pool size
|
|
103
|
+
'max_size': int, # Maximum pool size
|
|
104
|
+
'timeout': int, # Connection timeout (seconds)
|
|
105
|
+
'max_lifetime': int, # Max connection lifetime (seconds)
|
|
106
|
+
'max_idle': int, # Max idle time before closing (seconds)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Environment Variables:
|
|
110
|
+
DB_POOL_MIN_SIZE: Minimum pool size (default: 10)
|
|
111
|
+
DB_POOL_MAX_SIZE: Maximum pool size (default: 50)
|
|
112
|
+
DB_POOL_TIMEOUT: Connection timeout in seconds (default: 30)
|
|
113
|
+
AUTO_POOL_SIZE: Enable automatic ASGI/WSGI detection (default: false)
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
# Simple static config (default):
|
|
117
|
+
>>> get_pool_config('production')
|
|
118
|
+
{'min_size': 10, 'max_size': 50, ...}
|
|
119
|
+
|
|
120
|
+
# With auto-detection:
|
|
121
|
+
>>> os.environ['AUTO_POOL_SIZE'] = 'true'
|
|
122
|
+
>>> get_pool_config('production', is_asgi=True)
|
|
123
|
+
{'min_size': 5, 'max_size': 20, ...} # Optimized for ASGI
|
|
124
|
+
"""
|
|
125
|
+
# Check if auto-detection is enabled
|
|
126
|
+
auto_detect = os.environ.get('AUTO_POOL_SIZE', 'false').lower() in ('true', '1', 'yes')
|
|
127
|
+
|
|
128
|
+
# Simple static configuration (default)
|
|
129
|
+
if not auto_detect and is_asgi is None:
|
|
130
|
+
# Use simple env var based config
|
|
131
|
+
try:
|
|
132
|
+
min_size = int(os.environ.get('DB_POOL_MIN_SIZE', 10))
|
|
133
|
+
except ValueError:
|
|
134
|
+
min_size = 10
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
max_size = int(os.environ.get('DB_POOL_MAX_SIZE', 50))
|
|
138
|
+
except ValueError:
|
|
139
|
+
max_size = 50
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
timeout = int(os.environ.get('DB_POOL_TIMEOUT', 30))
|
|
143
|
+
except ValueError:
|
|
144
|
+
timeout = 30
|
|
145
|
+
|
|
146
|
+
# Validate
|
|
147
|
+
if min_size >= max_size:
|
|
148
|
+
min_size = max(1, max_size - 1)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
'min_size': min_size,
|
|
152
|
+
'max_size': max_size,
|
|
153
|
+
'timeout': timeout,
|
|
154
|
+
'max_lifetime': 3600, # 1 hour
|
|
155
|
+
'max_idle': 600, # 10 minutes
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Auto-detect ASGI mode if enabled and not specified
|
|
159
|
+
if is_asgi is None:
|
|
160
|
+
is_asgi = _detect_asgi_mode()
|
|
161
|
+
|
|
162
|
+
# Pool configuration matrix
|
|
163
|
+
# Format: (min_size, max_size, timeout)
|
|
164
|
+
pool_configs = {
|
|
165
|
+
'development': {
|
|
166
|
+
'asgi': (2, 10, 10),
|
|
167
|
+
'wsgi': (3, 15, 20),
|
|
168
|
+
},
|
|
169
|
+
'testing': {
|
|
170
|
+
'asgi': (1, 5, 5),
|
|
171
|
+
'wsgi': (2, 10, 10),
|
|
172
|
+
},
|
|
173
|
+
'staging': {
|
|
174
|
+
'asgi': (3, 15, 10),
|
|
175
|
+
'wsgi': (5, 30, 20),
|
|
176
|
+
},
|
|
177
|
+
'production': {
|
|
178
|
+
'asgi': (5, 20, 10),
|
|
179
|
+
'wsgi': (10, 50, 30),
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Get base configuration
|
|
184
|
+
env_key = environment.lower()
|
|
185
|
+
if env_key not in pool_configs:
|
|
186
|
+
# Fallback to development for unknown environments
|
|
187
|
+
env_key = 'development'
|
|
188
|
+
|
|
189
|
+
mode_key = 'asgi' if is_asgi else 'wsgi'
|
|
190
|
+
min_size, max_size, timeout = pool_configs[env_key][mode_key]
|
|
191
|
+
|
|
192
|
+
# Allow environment variable overrides
|
|
193
|
+
try:
|
|
194
|
+
min_size = int(os.environ.get('DB_POOL_MIN_SIZE', min_size))
|
|
195
|
+
except ValueError:
|
|
196
|
+
pass # Keep default if invalid
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
max_size = int(os.environ.get('DB_POOL_MAX_SIZE', max_size))
|
|
200
|
+
except ValueError:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
timeout = int(os.environ.get('DB_POOL_TIMEOUT', timeout))
|
|
205
|
+
except ValueError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Validate: min_size must be < max_size
|
|
209
|
+
if min_size >= max_size:
|
|
210
|
+
min_size = max(1, max_size - 1)
|
|
211
|
+
|
|
212
|
+
# Build and return pool configuration
|
|
213
|
+
return {
|
|
214
|
+
'min_size': min_size,
|
|
215
|
+
'max_size': max_size,
|
|
216
|
+
'timeout': timeout,
|
|
217
|
+
'max_lifetime': 3600, # 1 hour
|
|
218
|
+
'max_idle': 600, # 10 minutes
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
38
222
|
class SmartDefaults:
|
|
39
223
|
"""
|
|
40
224
|
Environment-aware smart defaults for Django configuration.
|
|
@@ -45,18 +229,50 @@ class SmartDefaults:
|
|
|
45
229
|
|
|
46
230
|
@staticmethod
|
|
47
231
|
def get_database_defaults(environment: str = "development", debug: bool = False, engine: str = "sqlite3") -> Dict[str, Any]:
|
|
48
|
-
"""
|
|
232
|
+
"""
|
|
233
|
+
Get database configuration defaults.
|
|
234
|
+
|
|
235
|
+
For PostgreSQL with Django 5.1+:
|
|
236
|
+
- Uses native connection pooling (recommended for ASGI/async apps)
|
|
237
|
+
- CONN_MAX_AGE = 0 (required with native pooling)
|
|
238
|
+
- ATOMIC_REQUESTS = True (default - safe and works with pooling)
|
|
239
|
+
- Pool sizes: Auto-configured based on environment and ASGI/WSGI mode
|
|
240
|
+
- Health checks: Handled automatically by psycopg3 pool
|
|
241
|
+
|
|
242
|
+
Note on Transaction Safety:
|
|
243
|
+
ATOMIC_REQUESTS=True is enabled by default, which wraps each request
|
|
244
|
+
in a database transaction. This adds ~5-10ms overhead but ensures data
|
|
245
|
+
integrity without manual transaction management.
|
|
246
|
+
|
|
247
|
+
This works perfectly fine with connection pooling. If you need to optimize
|
|
248
|
+
for read-heavy workloads, you can disable ATOMIC_REQUESTS and use selective
|
|
249
|
+
transactions via Django's @transaction.atomic decorator on write views.
|
|
250
|
+
|
|
251
|
+
References:
|
|
252
|
+
- Django 5.1+ native pooling: https://docs.djangoproject.com/en/5.2/ref/databases/#connection-pooling
|
|
253
|
+
- ASGI best practices: persistent connections should be disabled with ASGI
|
|
254
|
+
"""
|
|
49
255
|
defaults = {
|
|
50
256
|
'ENGINE': 'django.db.backends.sqlite3',
|
|
51
257
|
'NAME': Path('db') / 'db.sqlite3',
|
|
52
|
-
'ATOMIC_REQUESTS': True,
|
|
53
|
-
'CONN_MAX_AGE':
|
|
258
|
+
'ATOMIC_REQUESTS': True, # Safe default - ~5-10ms overhead acceptable for data integrity
|
|
259
|
+
'CONN_MAX_AGE': 0, # Set to 0 for native pooling (Django 5.1+)
|
|
260
|
+
'CONN_HEALTH_CHECKS': True, # Enable health checks to prevent stale connections
|
|
54
261
|
'OPTIONS': {}
|
|
55
262
|
}
|
|
56
263
|
|
|
57
264
|
# Add engine-specific options
|
|
58
265
|
if engine == "django.db.backends.postgresql":
|
|
59
|
-
|
|
266
|
+
# Native connection pooling for Django 5.1+ with psycopg >= 3.1
|
|
267
|
+
# See: https://docs.djangoproject.com/en/5.2/ref/databases/#postgresql-connection-pooling
|
|
268
|
+
|
|
269
|
+
# Get dynamic pool configuration based on environment and deployment mode
|
|
270
|
+
pool_config = get_pool_config(environment=environment, is_asgi=None)
|
|
271
|
+
|
|
272
|
+
defaults['OPTIONS'] = {
|
|
273
|
+
'connect_timeout': 20,
|
|
274
|
+
'pool': pool_config, # Dynamic pool config (ASGI/WSGI aware)
|
|
275
|
+
}
|
|
60
276
|
elif engine == "django.db.backends.sqlite3":
|
|
61
277
|
defaults['OPTIONS']['timeout'] = 20 # SQLite uses 'timeout'
|
|
62
278
|
|
|
@@ -139,7 +355,16 @@ class SmartDefaults:
|
|
|
139
355
|
|
|
140
356
|
@staticmethod
|
|
141
357
|
def get_middleware_defaults() -> List[str]:
|
|
142
|
-
"""
|
|
358
|
+
"""
|
|
359
|
+
Get middleware configuration defaults.
|
|
360
|
+
|
|
361
|
+
Note:
|
|
362
|
+
ConnectionPoolCleanupMiddleware is automatically added LAST if
|
|
363
|
+
enable_pool_cleanup=True in DjangoConfig (default).
|
|
364
|
+
|
|
365
|
+
This middleware prevents connection leaks when using native
|
|
366
|
+
connection pooling with ATOMIC_REQUESTS=False.
|
|
367
|
+
"""
|
|
143
368
|
return [
|
|
144
369
|
'corsheaders.middleware.CorsMiddleware',
|
|
145
370
|
'django.middleware.security.SecurityMiddleware',
|
|
@@ -149,6 +374,7 @@ class SmartDefaults:
|
|
|
149
374
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
150
375
|
'django.contrib.messages.middleware.MessageMiddleware',
|
|
151
376
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
377
|
+
# ConnectionPoolCleanupMiddleware added in DjangoConfig.get_middleware()
|
|
152
378
|
]
|
|
153
379
|
|
|
154
380
|
@staticmethod
|