django-cfg 1.5.14__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/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -116
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +259 -0
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +56 -1
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +315 -26
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/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/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/services/centrifugo/transformers.py +89 -0
- 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} +67 -54
- django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
- django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py +541 -0
- 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/SERVER_LOGGING.md +164 -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 +261 -13
- django_cfg/apps/integrations/grpc/views/charts.py +1 -1
- django_cfg/apps/integrations/grpc/views/config.py +1 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/core/base/config_model.py +11 -0
- django_cfg/core/builders/middleware_builder.py +5 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -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/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/groups/manager.py +25 -18
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- 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.14.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/RECORD +118 -74
- /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.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.14.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.29.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
|
|
|
@@ -53,6 +53,10 @@ class ServiceDiscovery:
|
|
|
53
53
|
# Get config from django-cfg using Pydantic2 pattern
|
|
54
54
|
self.config = get_grpc_config()
|
|
55
55
|
|
|
56
|
+
logger.warning(f"🔍 ServiceDiscovery.__init__: config = {self.config}")
|
|
57
|
+
if self.config:
|
|
58
|
+
logger.warning(f"🔍 handlers_hook = {self.config.handlers_hook}")
|
|
59
|
+
|
|
56
60
|
if self.config:
|
|
57
61
|
self.auto_register = self.config.auto_register_apps
|
|
58
62
|
self.enabled_apps = self.config.enabled_apps if self.config.auto_register_apps else []
|
|
@@ -439,68 +443,77 @@ class ServiceDiscovery:
|
|
|
439
443
|
logger.error(f"Error extracting metadata from {service_class}: {e}", exc_info=True)
|
|
440
444
|
return None
|
|
441
445
|
|
|
442
|
-
def
|
|
446
|
+
def get_handlers_hooks(self) -> List[Any]:
|
|
443
447
|
"""
|
|
444
|
-
Get the handlers hook function from config.
|
|
448
|
+
Get the handlers hook function(s) from config.
|
|
445
449
|
|
|
446
450
|
Returns:
|
|
447
|
-
|
|
451
|
+
List of handlers hook functions (empty list if none)
|
|
448
452
|
|
|
449
453
|
Example:
|
|
450
454
|
>>> discovery = ServiceDiscovery()
|
|
451
|
-
>>>
|
|
452
|
-
>>>
|
|
453
|
-
...
|
|
455
|
+
>>> handlers_hooks = discovery.get_handlers_hooks()
|
|
456
|
+
>>> for hook in handlers_hooks:
|
|
457
|
+
... hook(server)
|
|
454
458
|
"""
|
|
459
|
+
logger.warning(f"🔍 get_handlers_hooks called")
|
|
455
460
|
if not self.config:
|
|
456
|
-
logger.
|
|
457
|
-
return
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
461
|
+
logger.warning("❌ No gRPC config available")
|
|
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
|
|
464
486
|
|
|
465
|
-
# Resolve {ROOT_URLCONF} placeholder
|
|
466
|
-
if "{ROOT_URLCONF}" in handlers_hook_path:
|
|
467
487
|
try:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
logger.debug(f"Resolved handlers_hook: {handlers_hook_path}")
|
|
472
|
-
except Exception as e:
|
|
473
|
-
logger.warning(f"Could not resolve {{ROOT_URLCONF}}: {e}")
|
|
474
|
-
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)
|
|
475
491
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
module_path, func_name = handlers_hook_path.rsplit(".", 1)
|
|
479
|
-
module = importlib.import_module(module_path)
|
|
492
|
+
# Get the hook function
|
|
493
|
+
handlers_hook = getattr(module, func_name)
|
|
480
494
|
|
|
481
|
-
|
|
482
|
-
|
|
495
|
+
if not callable(handlers_hook):
|
|
496
|
+
logger.warning(f"handlers_hook {handlers_hook_path} is not callable")
|
|
497
|
+
continue
|
|
483
498
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return None
|
|
499
|
+
logger.info(f"Loaded handlers hook: {handlers_hook_path}")
|
|
500
|
+
hooks.append(handlers_hook)
|
|
487
501
|
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
490
515
|
|
|
491
|
-
|
|
492
|
-
logger.warning(f"Failed to import handlers hook module {handlers_hook_path}: {e}")
|
|
493
|
-
return None
|
|
494
|
-
except AttributeError as e:
|
|
495
|
-
logger.warning(f"Handlers hook function not found in {handlers_hook_path}: {e}")
|
|
496
|
-
logger.debug(f"This is optional - the hook function '{func_name}' does not exist in module '{module_path}'")
|
|
497
|
-
return None
|
|
498
|
-
except Exception as e:
|
|
499
|
-
logger.error(
|
|
500
|
-
f"Error loading handlers hook {handlers_hook_path}: {e}",
|
|
501
|
-
exc_info=True,
|
|
502
|
-
)
|
|
503
|
-
return None
|
|
516
|
+
return hooks
|
|
504
517
|
|
|
505
518
|
|
|
506
519
|
def discover_and_register_services(server: Any) -> int:
|
|
@@ -534,15 +547,15 @@ def discover_and_register_services(server: Any) -> int:
|
|
|
534
547
|
discovery = ServiceDiscovery()
|
|
535
548
|
count = 0
|
|
536
549
|
|
|
537
|
-
# Try handlers
|
|
538
|
-
|
|
539
|
-
|
|
550
|
+
# Try handlers hooks first (can be multiple)
|
|
551
|
+
handlers_hooks = discovery.get_handlers_hooks()
|
|
552
|
+
for hook in handlers_hooks:
|
|
540
553
|
try:
|
|
541
|
-
|
|
542
|
-
logger.info("Successfully called handlers hook")
|
|
554
|
+
hook(server)
|
|
555
|
+
logger.info(f"Successfully called handlers hook: {hook.__name__}")
|
|
543
556
|
count += 1 # We don't know exact count, but at least 1
|
|
544
557
|
except Exception as e:
|
|
545
|
-
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)
|
|
546
559
|
|
|
547
560
|
# Discover and register services
|
|
548
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,39 @@ 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: Native async aggregate is not available yet (Django 5.2 limitation)
|
|
185
|
+
# Use asgiref.sync.sync_to_async as recommended by Django docs
|
|
186
|
+
from asgiref.sync import sync_to_async
|
|
187
|
+
|
|
188
|
+
# Wrap the sync version in sync_to_async
|
|
189
|
+
return await sync_to_async(self.get_service_statistics)(service_name, hours)
|
|
190
|
+
|
|
145
191
|
def get_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
|
|
146
192
|
"""
|
|
147
|
-
Get all services with their statistics.
|
|
193
|
+
Get all services with their statistics (SYNC).
|
|
148
194
|
|
|
149
195
|
Args:
|
|
150
196
|
hours: Statistics period in hours (default: 24)
|
|
@@ -206,11 +252,84 @@ class ServiceRegistryManager:
|
|
|
206
252
|
|
|
207
253
|
return services_with_stats
|
|
208
254
|
|
|
255
|
+
async def aget_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
|
|
256
|
+
"""
|
|
257
|
+
Get all services with their statistics (ASYNC - Django 5.2).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
hours: Statistics period in hours (default: 24)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of services with statistics
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> manager = ServiceRegistryManager()
|
|
267
|
+
>>> services = await manager.aget_all_services_with_stats(hours=24)
|
|
268
|
+
>>> services[0]['name']
|
|
269
|
+
'apps.CryptoService'
|
|
270
|
+
>>> services[0]['total_requests']
|
|
271
|
+
150
|
|
272
|
+
"""
|
|
273
|
+
# Get all services (sync operation - from cache or filesystem)
|
|
274
|
+
services = self.get_all_services()
|
|
275
|
+
services_with_stats = []
|
|
276
|
+
|
|
277
|
+
# Django 5.2: Use sync_to_async for aggregate queries that aren't natively async yet
|
|
278
|
+
from asgiref.sync import sync_to_async
|
|
279
|
+
|
|
280
|
+
# Create async version of the aggregate operation
|
|
281
|
+
@sync_to_async
|
|
282
|
+
def get_service_stats_sync(service_name: str):
|
|
283
|
+
return (
|
|
284
|
+
GRPCRequestLog.objects.filter(service_name=service_name)
|
|
285
|
+
.recent(hours)
|
|
286
|
+
.aggregate(
|
|
287
|
+
total=Count("id"),
|
|
288
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
289
|
+
avg_duration=Avg("duration_ms"),
|
|
290
|
+
last_activity=models.Max("created_at"),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
for service in services:
|
|
295
|
+
service_name = service.get("name")
|
|
296
|
+
|
|
297
|
+
# Get stats from GRPCRequestLog (async)
|
|
298
|
+
stats = await get_service_stats_sync(service_name)
|
|
299
|
+
|
|
300
|
+
# Calculate success rate
|
|
301
|
+
total = stats["total"] or 0
|
|
302
|
+
successful = stats["successful"] or 0
|
|
303
|
+
success_rate = (successful / total * 100) if total > 0 else 0.0
|
|
304
|
+
|
|
305
|
+
# Extract package name
|
|
306
|
+
package = service_name.split(".")[0] if "." in service_name else ""
|
|
307
|
+
|
|
308
|
+
# Build dict directly
|
|
309
|
+
service_summary = {
|
|
310
|
+
"name": service_name,
|
|
311
|
+
"full_name": service.get("full_name", f"/{service_name}"),
|
|
312
|
+
"package": package,
|
|
313
|
+
"methods_count": len(service.get("methods", [])),
|
|
314
|
+
"total_requests": total,
|
|
315
|
+
"success_rate": round(success_rate, 2),
|
|
316
|
+
"avg_duration_ms": round(stats["avg_duration"] or 0, 2),
|
|
317
|
+
"last_activity_at": (
|
|
318
|
+
stats["last_activity"].isoformat()
|
|
319
|
+
if stats["last_activity"]
|
|
320
|
+
else None
|
|
321
|
+
),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
services_with_stats.append(service_summary)
|
|
325
|
+
|
|
326
|
+
return services_with_stats
|
|
327
|
+
|
|
209
328
|
def get_service_methods_with_stats(
|
|
210
329
|
self, service_name: str
|
|
211
330
|
) -> List[Dict]:
|
|
212
331
|
"""
|
|
213
|
-
Get all methods for a service with statistics.
|
|
332
|
+
Get all methods for a service with statistics (SYNC).
|
|
214
333
|
|
|
215
334
|
Args:
|
|
216
335
|
service_name: Service name
|
|
@@ -286,6 +405,97 @@ class ServiceRegistryManager:
|
|
|
286
405
|
|
|
287
406
|
return methods_list
|
|
288
407
|
|
|
408
|
+
async def aget_service_methods_with_stats(
|
|
409
|
+
self, service_name: str
|
|
410
|
+
) -> List[Dict]:
|
|
411
|
+
"""
|
|
412
|
+
Get all methods for a service with statistics (ASYNC - Django 5.2).
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
service_name: Service name
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
List of methods with statistics
|
|
419
|
+
|
|
420
|
+
Example:
|
|
421
|
+
>>> manager = ServiceRegistryManager()
|
|
422
|
+
>>> methods = await manager.aget_service_methods_with_stats("apps.CryptoService")
|
|
423
|
+
>>> methods[0]['name']
|
|
424
|
+
'GetCoin'
|
|
425
|
+
>>> methods[0]['stats']['total_requests']
|
|
426
|
+
50
|
|
427
|
+
"""
|
|
428
|
+
service = self.get_service_by_name(service_name)
|
|
429
|
+
if not service:
|
|
430
|
+
return []
|
|
431
|
+
|
|
432
|
+
# Django 5.2: Use sync_to_async for complex queries with aggregates and values_list
|
|
433
|
+
from asgiref.sync import sync_to_async
|
|
434
|
+
|
|
435
|
+
@sync_to_async
|
|
436
|
+
def get_method_durations(svc_name: str, method_name: str):
|
|
437
|
+
return list(
|
|
438
|
+
GRPCRequestLog.objects.filter(
|
|
439
|
+
service_name=svc_name,
|
|
440
|
+
method_name=method_name,
|
|
441
|
+
duration_ms__isnull=False,
|
|
442
|
+
).values_list("duration_ms", flat=True)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@sync_to_async
|
|
446
|
+
def get_method_stats(svc_name: str, method_name: str):
|
|
447
|
+
return GRPCRequestLog.objects.filter(
|
|
448
|
+
service_name=svc_name,
|
|
449
|
+
method_name=method_name,
|
|
450
|
+
).aggregate(
|
|
451
|
+
total=Count("id"),
|
|
452
|
+
successful=Count("id", filter=models.Q(status="success")),
|
|
453
|
+
errors=Count("id", filter=models.Q(status="error")),
|
|
454
|
+
avg_duration=Avg("duration_ms"),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
methods_list = []
|
|
458
|
+
for method_name in service.get("methods", []):
|
|
459
|
+
# Get durations for percentile calculation (async)
|
|
460
|
+
durations = await get_method_durations(service_name, method_name)
|
|
461
|
+
|
|
462
|
+
# Get aggregate stats (async)
|
|
463
|
+
stats = await get_method_stats(service_name, method_name)
|
|
464
|
+
|
|
465
|
+
# Calculate percentiles
|
|
466
|
+
p50, p95, p99 = self._calculate_percentiles(durations)
|
|
467
|
+
|
|
468
|
+
# Calculate success rate
|
|
469
|
+
total = stats["total"] or 0
|
|
470
|
+
successful = stats["successful"] or 0
|
|
471
|
+
success_rate = (successful / total * 100) if total > 0 else 0.0
|
|
472
|
+
|
|
473
|
+
# Build method stats dict
|
|
474
|
+
method_stats = {
|
|
475
|
+
"total_requests": total,
|
|
476
|
+
"successful": successful,
|
|
477
|
+
"errors": stats["errors"] or 0,
|
|
478
|
+
"success_rate": round(success_rate, 2),
|
|
479
|
+
"avg_duration_ms": round(stats["avg_duration"] or 0, 2),
|
|
480
|
+
"p50_duration_ms": p50,
|
|
481
|
+
"p95_duration_ms": p95,
|
|
482
|
+
"p99_duration_ms": p99,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Build method summary dict
|
|
486
|
+
method_summary = {
|
|
487
|
+
"name": method_name,
|
|
488
|
+
"full_name": f"/{service_name}/{method_name}",
|
|
489
|
+
"service_name": service_name,
|
|
490
|
+
"request_type": "",
|
|
491
|
+
"response_type": "",
|
|
492
|
+
"stats": method_stats,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
methods_list.append(method_summary)
|
|
496
|
+
|
|
497
|
+
return methods_list
|
|
498
|
+
|
|
289
499
|
def _calculate_percentiles(self, values):
|
|
290
500
|
"""
|
|
291
501
|
Calculate p50, p95, p99 percentiles.
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
|
-
gRPC interceptors for logging, metrics, and
|
|
2
|
+
gRPC interceptors for logging, metrics, error handling, and Centrifugo publishing.
|
|
3
3
|
|
|
4
4
|
Provides production-ready interceptors for gRPC services.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from .centrifugo import CentrifugoInterceptor
|
|
7
8
|
from .errors import ErrorHandlingInterceptor
|
|
8
9
|
from .logging import LoggingInterceptor
|
|
9
10
|
from .metrics import MetricsInterceptor, get_metrics, reset_metrics
|
|
10
11
|
from .request_logger import RequestLoggerInterceptor
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
14
|
+
"CentrifugoInterceptor",
|
|
13
15
|
"LoggingInterceptor",
|
|
14
16
|
"MetricsInterceptor",
|
|
15
17
|
"ErrorHandlingInterceptor",
|