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,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global Service Registry for Streaming Services
|
|
3
|
+
|
|
4
|
+
Provides a global registry to store and retrieve BidirectionalStreamingService instances.
|
|
5
|
+
This enables same-process command sending from anywhere in the application.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
# In your gRPC service initialization (handlers/__init__.py)
|
|
9
|
+
from django_cfg.apps.integrations.grpc.commands.registry import register_streaming_service
|
|
10
|
+
|
|
11
|
+
def grpc_handlers(server):
|
|
12
|
+
servicer = YourStreamingService()
|
|
13
|
+
register_streaming_service("your_service", servicer._streaming_service)
|
|
14
|
+
# ... rest of setup
|
|
15
|
+
|
|
16
|
+
# In your command client
|
|
17
|
+
from django_cfg.apps.integrations.grpc.commands.registry import get_streaming_service
|
|
18
|
+
|
|
19
|
+
service = get_streaming_service("your_service")
|
|
20
|
+
client = CommandClient(client_id="123", streaming_service=service)
|
|
21
|
+
|
|
22
|
+
Documentation: See @commands/README.md § Global Service Registry
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from threading import RLock
|
|
27
|
+
from typing import Any, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Global registry of streaming services
|
|
32
|
+
# Key: service name (e.g., "bots", "signals")
|
|
33
|
+
# Value: BidirectionalStreamingService instance
|
|
34
|
+
_streaming_services: Dict[str, Any] = {}
|
|
35
|
+
|
|
36
|
+
# Thread-safe lock for registry access
|
|
37
|
+
_registry_lock = RLock()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_streaming_service(name: str, service: Any) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Register a streaming service in the global registry.
|
|
43
|
+
|
|
44
|
+
This should be called when the gRPC service is initialized,
|
|
45
|
+
typically in the grpc_handlers() function.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name: Service name (e.g., "bots", "signals", "notifications")
|
|
49
|
+
service: BidirectionalStreamingService instance
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
from your_app.grpc.server import YourStreamingService
|
|
53
|
+
|
|
54
|
+
def grpc_handlers(server):
|
|
55
|
+
servicer = YourStreamingService()
|
|
56
|
+
register_streaming_service("your_service", servicer._streaming_service)
|
|
57
|
+
# ... rest of setup
|
|
58
|
+
"""
|
|
59
|
+
with _registry_lock:
|
|
60
|
+
if name in _streaming_services:
|
|
61
|
+
logger.warning(
|
|
62
|
+
f"Streaming service '{name}' already registered, overwriting"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_streaming_services[name] = service
|
|
66
|
+
logger.info(f"Registered streaming service: {name}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_streaming_service(name: str) -> Optional[Any]:
|
|
70
|
+
"""
|
|
71
|
+
Get a streaming service from the global registry.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: Service name
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
BidirectionalStreamingService instance or None if not found
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
service = get_streaming_service("bots")
|
|
81
|
+
if service:
|
|
82
|
+
client = CommandClient(client_id="123", streaming_service=service)
|
|
83
|
+
else:
|
|
84
|
+
# Service not registered, will use cross-process mode
|
|
85
|
+
client = CommandClient(client_id="123", grpc_port=50051)
|
|
86
|
+
"""
|
|
87
|
+
with _registry_lock:
|
|
88
|
+
service = _streaming_services.get(name)
|
|
89
|
+
|
|
90
|
+
if service is None:
|
|
91
|
+
logger.debug(
|
|
92
|
+
f"Streaming service '{name}' not found in registry. "
|
|
93
|
+
f"Available: {list(_streaming_services.keys())}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return service
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def unregister_streaming_service(name: str) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Unregister a streaming service from the global registry.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
name: Service name
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if service was unregistered, False if not found
|
|
108
|
+
"""
|
|
109
|
+
with _registry_lock:
|
|
110
|
+
if name in _streaming_services:
|
|
111
|
+
del _streaming_services[name]
|
|
112
|
+
logger.info(f"Unregistered streaming service: {name}")
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def list_streaming_services() -> List[str]:
|
|
118
|
+
"""
|
|
119
|
+
List all registered streaming service names.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of service names
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> list_streaming_services()
|
|
126
|
+
['bots', 'signals', 'notifications']
|
|
127
|
+
"""
|
|
128
|
+
with _registry_lock:
|
|
129
|
+
return list(_streaming_services.keys())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_registered(name: str) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Check if a streaming service is registered.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
name: Service name
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if registered, False otherwise
|
|
141
|
+
"""
|
|
142
|
+
with _registry_lock:
|
|
143
|
+
return name in _streaming_services
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def clear_registry() -> None:
|
|
147
|
+
"""
|
|
148
|
+
Clear all registered services.
|
|
149
|
+
|
|
150
|
+
Warning: This should only be used in tests or during shutdown.
|
|
151
|
+
"""
|
|
152
|
+
with _registry_lock:
|
|
153
|
+
count = len(_streaming_services)
|
|
154
|
+
_streaming_services.clear()
|
|
155
|
+
logger.info(f"Cleared {count} streaming services from registry")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Convenience alias for backward compatibility
|
|
159
|
+
set_streaming_service = register_streaming_service
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
__all__ = [
|
|
163
|
+
'register_streaming_service',
|
|
164
|
+
'get_streaming_service',
|
|
165
|
+
'unregister_streaming_service',
|
|
166
|
+
'list_streaming_services',
|
|
167
|
+
'is_registered',
|
|
168
|
+
'clear_registry',
|
|
169
|
+
'set_streaming_service', # alias
|
|
170
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gRPC service discovery and registry.
|
|
3
|
+
|
|
4
|
+
This package provides tools for discovering and registering gRPC services
|
|
5
|
+
in a distributed environment.
|
|
6
|
+
|
|
7
|
+
**Components**:
|
|
8
|
+
- discovery: Service discovery mechanisms
|
|
9
|
+
- registry: Service registration and management
|
|
10
|
+
|
|
11
|
+
**Usage Example**:
|
|
12
|
+
```python
|
|
13
|
+
from django_cfg.apps.integrations.grpc.services.discovery import (
|
|
14
|
+
ServiceDiscovery,
|
|
15
|
+
ServiceRegistry,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Register service
|
|
19
|
+
registry = ServiceRegistry()
|
|
20
|
+
registry.register(service_name="my-service", host="localhost", port=50051)
|
|
21
|
+
|
|
22
|
+
# Discover services
|
|
23
|
+
discovery = ServiceDiscovery()
|
|
24
|
+
services = discovery.discover_all()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Created: 2025-11-07
|
|
28
|
+
Status: %%PRODUCTION%%
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Export discovery components
|
|
32
|
+
from .discovery import ServiceDiscovery, discover_and_register_services
|
|
33
|
+
from .registry import ServiceRegistryManager
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'ServiceDiscovery',
|
|
37
|
+
'ServiceRegistryManager',
|
|
38
|
+
'discover_and_register_services',
|
|
39
|
+
]
|
|
@@ -11,7 +11,7 @@ from django.apps import apps
|
|
|
11
11
|
|
|
12
12
|
from django_cfg.modules.django_logging import get_logger
|
|
13
13
|
|
|
14
|
-
from .config_helper import get_enabled_apps, get_grpc_config
|
|
14
|
+
from ..management.config_helper import get_enabled_apps, get_grpc_config
|
|
15
15
|
|
|
16
16
|
logger = get_logger("grpc.discovery")
|
|
17
17
|
|
|
@@ -443,70 +443,77 @@ class ServiceDiscovery:
|
|
|
443
443
|
logger.error(f"Error extracting metadata from {service_class}: {e}", exc_info=True)
|
|
444
444
|
return None
|
|
445
445
|
|
|
446
|
-
def
|
|
446
|
+
def get_handlers_hooks(self) -> List[Any]:
|
|
447
447
|
"""
|
|
448
|
-
Get the handlers hook function from config.
|
|
448
|
+
Get the handlers hook function(s) from config.
|
|
449
449
|
|
|
450
450
|
Returns:
|
|
451
|
-
|
|
451
|
+
List of handlers hook functions (empty list if none)
|
|
452
452
|
|
|
453
453
|
Example:
|
|
454
454
|
>>> discovery = ServiceDiscovery()
|
|
455
|
-
>>>
|
|
456
|
-
>>>
|
|
457
|
-
...
|
|
455
|
+
>>> handlers_hooks = discovery.get_handlers_hooks()
|
|
456
|
+
>>> for hook in handlers_hooks:
|
|
457
|
+
... hook(server)
|
|
458
458
|
"""
|
|
459
|
-
logger.warning(f"🔍
|
|
459
|
+
logger.warning(f"🔍 get_handlers_hooks called")
|
|
460
460
|
if not self.config:
|
|
461
461
|
logger.warning("❌ No gRPC config available")
|
|
462
|
-
return
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
logger.warning(f"🔍
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
462
|
+
return []
|
|
463
|
+
|
|
464
|
+
handlers_hook_paths = self.config.handlers_hook
|
|
465
|
+
logger.warning(f"🔍 handlers_hook_paths = '{handlers_hook_paths}'")
|
|
466
|
+
|
|
467
|
+
# Convert single string to list
|
|
468
|
+
if isinstance(handlers_hook_paths, str):
|
|
469
|
+
if not handlers_hook_paths:
|
|
470
|
+
logger.debug("No handlers_hook configured")
|
|
471
|
+
return []
|
|
472
|
+
handlers_hook_paths = [handlers_hook_paths]
|
|
473
|
+
|
|
474
|
+
hooks = []
|
|
475
|
+
for handlers_hook_path in handlers_hook_paths:
|
|
476
|
+
# Resolve {ROOT_URLCONF} placeholder
|
|
477
|
+
if "{ROOT_URLCONF}" in handlers_hook_path:
|
|
478
|
+
try:
|
|
479
|
+
from django.conf import settings
|
|
480
|
+
root_urlconf = settings.ROOT_URLCONF
|
|
481
|
+
handlers_hook_path = handlers_hook_path.replace("{ROOT_URLCONF}", root_urlconf)
|
|
482
|
+
logger.debug(f"Resolved handlers_hook: {handlers_hook_path}")
|
|
483
|
+
except Exception as e:
|
|
484
|
+
logger.warning(f"Could not resolve {{ROOT_URLCONF}}: {e}")
|
|
485
|
+
continue
|
|
470
486
|
|
|
471
|
-
# Resolve {ROOT_URLCONF} placeholder
|
|
472
|
-
if "{ROOT_URLCONF}" in handlers_hook_path:
|
|
473
487
|
try:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
logger.debug(f"Resolved handlers_hook: {handlers_hook_path}")
|
|
478
|
-
except Exception as e:
|
|
479
|
-
logger.warning(f"Could not resolve {{ROOT_URLCONF}}: {e}")
|
|
480
|
-
return None
|
|
488
|
+
# Import the module containing the hook
|
|
489
|
+
module_path, func_name = handlers_hook_path.rsplit(".", 1)
|
|
490
|
+
module = importlib.import_module(module_path)
|
|
481
491
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
module_path, func_name = handlers_hook_path.rsplit(".", 1)
|
|
485
|
-
module = importlib.import_module(module_path)
|
|
492
|
+
# Get the hook function
|
|
493
|
+
handlers_hook = getattr(module, func_name)
|
|
486
494
|
|
|
487
|
-
|
|
488
|
-
|
|
495
|
+
if not callable(handlers_hook):
|
|
496
|
+
logger.warning(f"handlers_hook {handlers_hook_path} is not callable")
|
|
497
|
+
continue
|
|
489
498
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
return None
|
|
499
|
+
logger.info(f"Loaded handlers hook: {handlers_hook_path}")
|
|
500
|
+
hooks.append(handlers_hook)
|
|
493
501
|
|
|
494
|
-
|
|
495
|
-
|
|
502
|
+
except ImportError as e:
|
|
503
|
+
logger.warning(f"Failed to import handlers hook module {handlers_hook_path}: {e}")
|
|
504
|
+
continue
|
|
505
|
+
except AttributeError as e:
|
|
506
|
+
logger.warning(f"Handlers hook function not found in {handlers_hook_path}: {e}")
|
|
507
|
+
logger.debug(f"This is optional - the hook function does not exist")
|
|
508
|
+
continue
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logger.error(
|
|
511
|
+
f"Error loading handlers hook {handlers_hook_path}: {e}",
|
|
512
|
+
exc_info=True,
|
|
513
|
+
)
|
|
514
|
+
continue
|
|
496
515
|
|
|
497
|
-
|
|
498
|
-
logger.warning(f"Failed to import handlers hook module {handlers_hook_path}: {e}")
|
|
499
|
-
return None
|
|
500
|
-
except AttributeError as e:
|
|
501
|
-
logger.warning(f"Handlers hook function not found in {handlers_hook_path}: {e}")
|
|
502
|
-
logger.debug(f"This is optional - the hook function '{func_name}' does not exist in module '{module_path}'")
|
|
503
|
-
return None
|
|
504
|
-
except Exception as e:
|
|
505
|
-
logger.error(
|
|
506
|
-
f"Error loading handlers hook {handlers_hook_path}: {e}",
|
|
507
|
-
exc_info=True,
|
|
508
|
-
)
|
|
509
|
-
return None
|
|
516
|
+
return hooks
|
|
510
517
|
|
|
511
518
|
|
|
512
519
|
def discover_and_register_services(server: Any) -> int:
|
|
@@ -540,15 +547,15 @@ def discover_and_register_services(server: Any) -> int:
|
|
|
540
547
|
discovery = ServiceDiscovery()
|
|
541
548
|
count = 0
|
|
542
549
|
|
|
543
|
-
# Try handlers
|
|
544
|
-
|
|
545
|
-
|
|
550
|
+
# Try handlers hooks first (can be multiple)
|
|
551
|
+
handlers_hooks = discovery.get_handlers_hooks()
|
|
552
|
+
for hook in handlers_hooks:
|
|
546
553
|
try:
|
|
547
|
-
|
|
548
|
-
logger.info("Successfully called handlers hook")
|
|
554
|
+
hook(server)
|
|
555
|
+
logger.info(f"Successfully called handlers hook: {hook.__name__}")
|
|
549
556
|
count += 1 # We don't know exact count, but at least 1
|
|
550
557
|
except Exception as e:
|
|
551
|
-
logger.error(f"Error calling handlers hook: {e}", exc_info=True)
|
|
558
|
+
logger.error(f"Error calling handlers hook {hook.__name__}: {e}", exc_info=True)
|
|
552
559
|
|
|
553
560
|
# Discover and register services
|
|
554
561
|
services = discovery.discover_services()
|
|
@@ -10,7 +10,7 @@ from typing import Dict, List, Optional
|
|
|
10
10
|
from django.db import models
|
|
11
11
|
from django.db.models import Avg, Count
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from ...models import GRPCRequestLog, GRPCServerStatus
|
|
14
14
|
from django_cfg.modules.django_logging import get_logger
|
|
15
15
|
|
|
16
16
|
logger = get_logger("grpc.service_registry")
|
|
@@ -31,7 +31,7 @@ class ServiceRegistryManager:
|
|
|
31
31
|
|
|
32
32
|
def get_current_server(self) -> Optional[GRPCServerStatus]:
|
|
33
33
|
"""
|
|
34
|
-
Get the currently running gRPC server instance.
|
|
34
|
+
Get the currently running gRPC server instance (SYNC).
|
|
35
35
|
|
|
36
36
|
Returns:
|
|
37
37
|
GRPCServerStatus instance if server is running, None otherwise
|
|
@@ -45,6 +45,22 @@ class ServiceRegistryManager:
|
|
|
45
45
|
logger.error(f"Error getting current server: {e}", exc_info=True)
|
|
46
46
|
return None
|
|
47
47
|
|
|
48
|
+
async def aget_current_server(self) -> Optional[GRPCServerStatus]:
|
|
49
|
+
"""
|
|
50
|
+
Get the currently running gRPC server instance (ASYNC - Django 5.2).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
GRPCServerStatus instance if server is running, None otherwise
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
current_server = await GRPCServerStatus.objects.aget_current_server()
|
|
57
|
+
if current_server and current_server.is_running:
|
|
58
|
+
return current_server
|
|
59
|
+
return None
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Error getting current server: {e}", exc_info=True)
|
|
62
|
+
return None
|
|
63
|
+
|
|
48
64
|
def get_all_services(self) -> List[Dict]:
|
|
49
65
|
"""
|
|
50
66
|
Get all registered services.
|
|
@@ -100,7 +116,7 @@ class ServiceRegistryManager:
|
|
|
100
116
|
self, service_name: str, hours: int = 24
|
|
101
117
|
) -> Dict:
|
|
102
118
|
"""
|
|
103
|
-
Get statistics for a specific service.
|
|
119
|
+
Get statistics for a specific service (SYNC).
|
|
104
120
|
|
|
105
121
|
Args:
|
|
106
122
|
service_name: Service name
|
|
@@ -142,9 +158,56 @@ class ServiceRegistryManager:
|
|
|
142
158
|
"avg_duration_ms": round(stats["avg_duration"] or 0, 2),
|
|
143
159
|
}
|
|
144
160
|
|
|
161
|
+
async def aget_service_statistics(
|
|
162
|
+
self, service_name: str, hours: int = 24
|
|
163
|
+
) -> Dict:
|
|
164
|
+
"""
|
|
165
|
+
Get statistics for a specific service (ASYNC - Django 5.2).
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
service_name: Service name
|
|
169
|
+
hours: Statistics period in hours (default: 24)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary with service statistics
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> manager = ServiceRegistryManager()
|
|
176
|
+
>>> stats = await manager.aget_service_statistics("apps.CryptoService", hours=24)
|
|
177
|
+
>>> stats['total']
|
|
178
|
+
150
|
|
179
|
+
>>> stats['successful']
|
|
180
|
+
145
|
|
181
|
+
>>> stats['success_rate']
|
|
182
|
+
96.67
|
|
183
|
+
"""
|
|
184
|
+
# Django 5.2+ async ORM: Use native async aggregate
|
|
185
|
+
stats = await (
|
|
186
|
+
GRPCRequestLog.objects.filter(service_name=service_name)
|
|
187
|
+
.recent(hours)
|
|
188
|
+
.aaggregate(
|
|
189
|
+
total=Count("id"),
|
|
190
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
191
|
+
errors=Count("id", filter=models.Q(status="error")),
|
|
192
|
+
avg_duration=Avg("duration_ms"),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
total = stats["total"] or 0
|
|
197
|
+
successful = stats["successful"] or 0
|
|
198
|
+
success_rate = (successful / total * 100) if total > 0 else 0.0
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"total": total,
|
|
202
|
+
"successful": successful,
|
|
203
|
+
"errors": stats["errors"] or 0,
|
|
204
|
+
"success_rate": round(success_rate, 2),
|
|
205
|
+
"avg_duration_ms": round(stats["avg_duration"] or 0, 2),
|
|
206
|
+
}
|
|
207
|
+
|
|
145
208
|
def get_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
|
|
146
209
|
"""
|
|
147
|
-
Get all services with their statistics.
|
|
210
|
+
Get all services with their statistics (SYNC).
|
|
148
211
|
|
|
149
212
|
Args:
|
|
150
213
|
hours: Statistics period in hours (default: 24)
|
|
@@ -206,11 +269,77 @@ class ServiceRegistryManager:
|
|
|
206
269
|
|
|
207
270
|
return services_with_stats
|
|
208
271
|
|
|
272
|
+
async def aget_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
|
|
273
|
+
"""
|
|
274
|
+
Get all services with their statistics (ASYNC - Django 5.2).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
hours: Statistics period in hours (default: 24)
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of services with statistics
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> manager = ServiceRegistryManager()
|
|
284
|
+
>>> services = await manager.aget_all_services_with_stats(hours=24)
|
|
285
|
+
>>> services[0]['name']
|
|
286
|
+
'apps.CryptoService'
|
|
287
|
+
>>> services[0]['total_requests']
|
|
288
|
+
150
|
|
289
|
+
"""
|
|
290
|
+
# Get all services (sync operation - from cache or filesystem)
|
|
291
|
+
services = self.get_all_services()
|
|
292
|
+
services_with_stats = []
|
|
293
|
+
|
|
294
|
+
# Django 5.2+ async ORM: Use native async aggregate
|
|
295
|
+
for service in services:
|
|
296
|
+
service_name = service.get("name")
|
|
297
|
+
|
|
298
|
+
# Get stats from GRPCRequestLog using native async aggregate
|
|
299
|
+
stats = await (
|
|
300
|
+
GRPCRequestLog.objects.filter(service_name=service_name)
|
|
301
|
+
.recent(hours)
|
|
302
|
+
.aaggregate(
|
|
303
|
+
total=Count("id"),
|
|
304
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
305
|
+
avg_duration=Avg("duration_ms"),
|
|
306
|
+
last_activity=models.Max("created_at"),
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Calculate success rate
|
|
311
|
+
total = stats["total"] or 0
|
|
312
|
+
successful = stats["successful"] or 0
|
|
313
|
+
success_rate = (successful / total * 100) if total > 0 else 0.0
|
|
314
|
+
|
|
315
|
+
# Extract package name
|
|
316
|
+
package = service_name.split(".")[0] if "." in service_name else ""
|
|
317
|
+
|
|
318
|
+
# Build dict directly
|
|
319
|
+
service_summary = {
|
|
320
|
+
"name": service_name,
|
|
321
|
+
"full_name": service.get("full_name", f"/{service_name}"),
|
|
322
|
+
"package": package,
|
|
323
|
+
"methods_count": len(service.get("methods", [])),
|
|
324
|
+
"total_requests": total,
|
|
325
|
+
"success_rate": round(success_rate, 2),
|
|
326
|
+
"avg_duration_ms": round(stats["avg_duration"] or 0, 2),
|
|
327
|
+
"last_activity_at": (
|
|
328
|
+
stats["last_activity"].isoformat()
|
|
329
|
+
if stats["last_activity"]
|
|
330
|
+
else None
|
|
331
|
+
),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
services_with_stats.append(service_summary)
|
|
335
|
+
|
|
336
|
+
return services_with_stats
|
|
337
|
+
|
|
209
338
|
def get_service_methods_with_stats(
|
|
210
339
|
self, service_name: str
|
|
211
340
|
) -> List[Dict]:
|
|
212
341
|
"""
|
|
213
|
-
Get all methods for a service with statistics.
|
|
342
|
+
Get all methods for a service with statistics (SYNC).
|
|
214
343
|
|
|
215
344
|
Args:
|
|
216
345
|
service_name: Service name
|
|
@@ -286,6 +415,88 @@ class ServiceRegistryManager:
|
|
|
286
415
|
|
|
287
416
|
return methods_list
|
|
288
417
|
|
|
418
|
+
async def aget_service_methods_with_stats(
|
|
419
|
+
self, service_name: str
|
|
420
|
+
) -> List[Dict]:
|
|
421
|
+
"""
|
|
422
|
+
Get all methods for a service with statistics (ASYNC - Django 5.2).
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
service_name: Service name
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
List of methods with statistics
|
|
429
|
+
|
|
430
|
+
Example:
|
|
431
|
+
>>> manager = ServiceRegistryManager()
|
|
432
|
+
>>> methods = await manager.aget_service_methods_with_stats("apps.CryptoService")
|
|
433
|
+
>>> methods[0]['name']
|
|
434
|
+
'GetCoin'
|
|
435
|
+
>>> methods[0]['stats']['total_requests']
|
|
436
|
+
50
|
|
437
|
+
"""
|
|
438
|
+
service = self.get_service_by_name(service_name)
|
|
439
|
+
if not service:
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
# Django 5.2+ async ORM: Use native async operations
|
|
443
|
+
methods_list = []
|
|
444
|
+
for method_name in service.get("methods", []):
|
|
445
|
+
# Get durations for percentile calculation using async list comprehension
|
|
446
|
+
durations = [
|
|
447
|
+
duration async for duration in
|
|
448
|
+
GRPCRequestLog.objects.filter(
|
|
449
|
+
service_name=service_name,
|
|
450
|
+
method_name=method_name,
|
|
451
|
+
duration_ms__isnull=False,
|
|
452
|
+
).values_list("duration_ms", flat=True)
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
# Get aggregate stats using native async aggregate
|
|
456
|
+
stats = await GRPCRequestLog.objects.filter(
|
|
457
|
+
service_name=service_name,
|
|
458
|
+
method_name=method_name,
|
|
459
|
+
).aaggregate(
|
|
460
|
+
total=Count("id"),
|
|
461
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
462
|
+
errors=Count("id", filter=models.Q(status="error")),
|
|
463
|
+
avg_duration=Avg("duration_ms"),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Calculate percentiles
|
|
467
|
+
p50, p95, p99 = self._calculate_percentiles(durations)
|
|
468
|
+
|
|
469
|
+
# Calculate success rate
|
|
470
|
+
total = stats["total"] or 0
|
|
471
|
+
successful = stats["successful"] or 0
|
|
472
|
+
success_rate = (successful / total * 100) if total > 0 else 0.0
|
|
473
|
+
|
|
474
|
+
# Build method stats dict
|
|
475
|
+
method_stats = {
|
|
476
|
+
"total_requests": total,
|
|
477
|
+
"successful": successful,
|
|
478
|
+
"errors": stats["errors"] or 0,
|
|
479
|
+
"success_rate": round(success_rate, 2),
|
|
480
|
+
"avg_duration_ms": round(stats["avg_duration"] or 0, 2),
|
|
481
|
+
"p50_duration_ms": p50,
|
|
482
|
+
"p95_duration_ms": p95,
|
|
483
|
+
"p99_duration_ms": p99,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
# Build method summary dict
|
|
487
|
+
method_summary = {
|
|
488
|
+
"name": method_name,
|
|
489
|
+
"full_name": f"/{service_name}/{method_name}",
|
|
490
|
+
"service_name": service_name,
|
|
491
|
+
"request_type": "",
|
|
492
|
+
"response_type": "",
|
|
493
|
+
"stats": method_stats,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
methods_list.append(method_summary)
|
|
497
|
+
|
|
498
|
+
return methods_list
|
|
499
|
+
|
|
289
500
|
def _calculate_percentiles(self, values):
|
|
290
501
|
"""
|
|
291
502
|
Calculate p50, p95, p99 percentiles.
|
|
@@ -109,7 +109,7 @@ def get_metrics(method: str = None) -> dict:
|
|
|
109
109
|
|
|
110
110
|
Example:
|
|
111
111
|
```python
|
|
112
|
-
from django_cfg.apps.integrations.grpc.interceptors.metrics import get_metrics
|
|
112
|
+
from django_cfg.apps.integrations.grpc.services.interceptors.metrics import get_metrics
|
|
113
113
|
|
|
114
114
|
# Get all metrics
|
|
115
115
|
all_stats = get_metrics()
|
|
@@ -129,7 +129,7 @@ def reset_metrics():
|
|
|
129
129
|
|
|
130
130
|
Example:
|
|
131
131
|
```python
|
|
132
|
-
from django_cfg.apps.integrations.grpc.interceptors.metrics import reset_metrics
|
|
132
|
+
from django_cfg.apps.integrations.grpc.services.interceptors.metrics import reset_metrics
|
|
133
133
|
reset_metrics()
|
|
134
134
|
```
|
|
135
135
|
"""
|
|
@@ -159,7 +159,7 @@ class MetricsInterceptor(grpc.aio.ServerInterceptor):
|
|
|
159
159
|
|
|
160
160
|
Access Metrics:
|
|
161
161
|
```python
|
|
162
|
-
from django_cfg.apps.integrations.grpc.interceptors.metrics import get_metrics
|
|
162
|
+
from django_cfg.apps.integrations.grpc.services.interceptors.metrics import get_metrics
|
|
163
163
|
|
|
164
164
|
stats = get_metrics()
|
|
165
165
|
print(f"Total requests: {stats['total_requests']}")
|