django-cfg 1.4.62__py3-none-any.whl → 1.4.63__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/accounts/services/otp_service.py +3 -14
- django_cfg/apps/centrifugo/__init__.py +57 -0
- django_cfg/apps/centrifugo/admin/__init__.py +13 -0
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
- django_cfg/apps/centrifugo/admin/config.py +82 -0
- django_cfg/apps/centrifugo/apps.py +31 -0
- django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
- django_cfg/apps/centrifugo/codegen/README.md +242 -0
- django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
- django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
- django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
- django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
- django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
- django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
- django_cfg/apps/centrifugo/decorators.py +137 -0
- django_cfg/apps/centrifugo/management/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
- django_cfg/apps/centrifugo/managers/__init__.py +12 -0
- django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
- django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
- django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
- django_cfg/apps/centrifugo/models/__init__.py +11 -0
- django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
- django_cfg/apps/centrifugo/registry.py +106 -0
- django_cfg/apps/centrifugo/router.py +125 -0
- django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
- django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
- django_cfg/apps/centrifugo/serializers/channels.py +26 -0
- django_cfg/apps/centrifugo/serializers/health.py +17 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
- django_cfg/apps/centrifugo/serializers/stats.py +21 -0
- django_cfg/apps/centrifugo/services/__init__.py +12 -0
- django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
- django_cfg/apps/centrifugo/services/client/client.py +577 -0
- django_cfg/apps/centrifugo/services/client/config.py +228 -0
- django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
- django_cfg/apps/centrifugo/services/config_helper.py +63 -0
- django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
- django_cfg/apps/centrifugo/services/logging.py +677 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
- django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
- django_cfg/apps/centrifugo/urls.py +31 -0
- django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
- django_cfg/apps/centrifugo/views/__init__.py +15 -0
- django_cfg/apps/centrifugo/views/admin_api.py +374 -0
- django_cfg/apps/centrifugo/views/dashboard.py +15 -0
- django_cfg/apps/centrifugo/views/monitoring.py +286 -0
- django_cfg/apps/centrifugo/views/testing_api.py +422 -0
- django_cfg/apps/support/utils/support_email_service.py +5 -18
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
- django_cfg/apps/urls.py +5 -5
- django_cfg/core/base/config_model.py +4 -44
- django_cfg/core/builders/apps_builder.py +2 -2
- django_cfg/core/generation/integration_generators/third_party.py +8 -8
- django_cfg/core/utils/__init__.py +5 -0
- django_cfg/core/utils/url_helpers.py +73 -0
- django_cfg/modules/base.py +7 -7
- django_cfg/modules/django_client/core/__init__.py +2 -1
- django_cfg/modules/django_client/core/config/config.py +8 -0
- django_cfg/modules/django_client/core/generator/__init__.py +42 -2
- django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
- django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
- django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
- django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
- django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
- django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
- django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
- django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
- django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
- django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
- django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
- django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
- django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
- django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
- django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
- django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
- django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
- django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
- django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
- django_cfg/modules/django_client/system/schema_parser.py +5 -1
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
- django_cfg/modules/django_unfold/dashboard.py +25 -19
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/core.py +2 -0
- django_cfg/registry/modules.py +2 -2
- django_cfg/static/js/api/centrifugo/client.mjs +164 -0
- django_cfg/static/js/api/centrifugo/index.mjs +13 -0
- django_cfg/static/js/api/index.mjs +5 -5
- django_cfg/static/js/api/types.mjs +89 -26
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -70
- django_cfg/apps/ipc/README.md +0 -346
- django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
- django_cfg/apps/ipc/TESTING.md +0 -539
- django_cfg/apps/ipc/__init__.py +0 -60
- django_cfg/apps/ipc/admin.py +0 -232
- django_cfg/apps/ipc/apps.py +0 -98
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -229
- django_cfg/apps/ipc/serializers/__init__.py +0 -29
- django_cfg/apps/ipc/serializers/serializers.py +0 -343
- django_cfg/apps/ipc/services/__init__.py +0 -7
- django_cfg/apps/ipc/services/client/__init__.py +0 -23
- django_cfg/apps/ipc/services/client/client.py +0 -621
- django_cfg/apps/ipc/services/client/config.py +0 -214
- django_cfg/apps/ipc/services/client/exceptions.py +0 -201
- django_cfg/apps/ipc/services/logging.py +0 -239
- django_cfg/apps/ipc/services/monitor.py +0 -466
- django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
- django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
- django_cfg/apps/ipc/urls.py +0 -23
- django_cfg/apps/ipc/views/__init__.py +0 -13
- django_cfg/apps/ipc/views/dashboard.py +0 -15
- django_cfg/apps/ipc/views/monitoring.py +0 -251
- django_cfg/apps/ipc/views/testing.py +0 -285
- django_cfg/static/js/api/ipc/client.mjs +0 -114
- django_cfg/static/js/api/ipc/index.mjs +0 -13
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
RPC Monitor - Real-time RPC activity monitoring.
|
|
3
|
-
|
|
4
|
-
Reads RPC metrics from Redis DB 2 and provides statistics.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
from collections import defaultdict
|
|
9
|
-
from datetime import datetime, timedelta
|
|
10
|
-
from typing import Any, Dict, List
|
|
11
|
-
|
|
12
|
-
from django_cfg.modules.django_logging import get_logger
|
|
13
|
-
|
|
14
|
-
logger = get_logger("ipc.monitor")
|
|
15
|
-
|
|
16
|
-
# Cache timeout in seconds
|
|
17
|
-
CACHE_TIMEOUT = 3 # 3 seconds to reduce Redis load
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class RPCMonitor:
|
|
21
|
-
"""
|
|
22
|
-
Monitor RPC activity by reading from Redis.
|
|
23
|
-
|
|
24
|
-
Provides real-time statistics about:
|
|
25
|
-
- RPC requests (from stream:requests)
|
|
26
|
-
- Response times
|
|
27
|
-
- Success/failure rates
|
|
28
|
-
- Notification delivery stats
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __init__(self, redis_client=None):
|
|
32
|
-
"""
|
|
33
|
-
Initialize RPC monitor.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
redis_client: Optional Redis client. If not provided, creates from config.
|
|
37
|
-
"""
|
|
38
|
-
from .client.config import DjangoCfgRPCConfig
|
|
39
|
-
from django_cfg.core import get_current_config
|
|
40
|
-
|
|
41
|
-
# Try to get config from django-cfg global state first
|
|
42
|
-
django_cfg_config = get_current_config()
|
|
43
|
-
|
|
44
|
-
if django_cfg_config and hasattr(django_cfg_config, 'django_ipc') and django_cfg_config.django_ipc:
|
|
45
|
-
# Use config from django-cfg DjangoConfig
|
|
46
|
-
self.config = django_cfg_config.django_ipc
|
|
47
|
-
logger.debug(f"RPCMonitor initialized from django-cfg config: {self.config.redis_url}")
|
|
48
|
-
else:
|
|
49
|
-
# Fallback to default config
|
|
50
|
-
self.config = DjangoCfgRPCConfig()
|
|
51
|
-
logger.warning("Django-CFG config not found, using default RPC config")
|
|
52
|
-
|
|
53
|
-
self.redis_client = redis_client or self._create_redis_client()
|
|
54
|
-
|
|
55
|
-
def _create_redis_client(self):
|
|
56
|
-
"""Create Redis client from config."""
|
|
57
|
-
try:
|
|
58
|
-
from urllib.parse import urlparse
|
|
59
|
-
|
|
60
|
-
import redis
|
|
61
|
-
|
|
62
|
-
# Use instance config (already loaded in __init__)
|
|
63
|
-
parsed = urlparse(self.config.redis_url)
|
|
64
|
-
|
|
65
|
-
client = redis.Redis(
|
|
66
|
-
host=parsed.hostname or 'localhost',
|
|
67
|
-
port=parsed.port or 6379,
|
|
68
|
-
db=int(parsed.path.lstrip('/')) if parsed.path else 2,
|
|
69
|
-
decode_responses=True,
|
|
70
|
-
socket_connect_timeout=5,
|
|
71
|
-
socket_timeout=5,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Test connection
|
|
75
|
-
client.ping()
|
|
76
|
-
|
|
77
|
-
return client
|
|
78
|
-
|
|
79
|
-
except Exception as e:
|
|
80
|
-
logger.error(f"Failed to create Redis client: {e}")
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
def get_overview_stats(self) -> Dict[str, Any]:
|
|
84
|
-
"""
|
|
85
|
-
Get overview statistics.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
{
|
|
89
|
-
'redis_connected': bool,
|
|
90
|
-
'total_requests_today': int,
|
|
91
|
-
'active_methods': List[str],
|
|
92
|
-
'avg_response_time_ms': float,
|
|
93
|
-
'success_rate': float,
|
|
94
|
-
'timestamp': str,
|
|
95
|
-
}
|
|
96
|
-
"""
|
|
97
|
-
if not self.redis_client:
|
|
98
|
-
return self._error_response("Redis not connected")
|
|
99
|
-
|
|
100
|
-
# Check Django cache first
|
|
101
|
-
try:
|
|
102
|
-
from django.core.cache import cache
|
|
103
|
-
cache_key = 'rpc_dashboard:overview_stats'
|
|
104
|
-
cached = cache.get(cache_key)
|
|
105
|
-
if cached:
|
|
106
|
-
logger.debug("Returning cached overview stats")
|
|
107
|
-
return cached
|
|
108
|
-
except ImportError:
|
|
109
|
-
logger.debug("Django cache not available, skipping cache")
|
|
110
|
-
cache = None
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
# Get recent requests from stream
|
|
114
|
-
recent_requests = self._get_recent_stream_entries(count=1000)
|
|
115
|
-
|
|
116
|
-
if not recent_requests:
|
|
117
|
-
return {
|
|
118
|
-
'redis_connected': True,
|
|
119
|
-
'total_requests_today': 0,
|
|
120
|
-
'active_methods': [],
|
|
121
|
-
'top_method': None,
|
|
122
|
-
'avg_response_time_ms': 0,
|
|
123
|
-
'success_rate': 100.0,
|
|
124
|
-
'timestamp': datetime.now().isoformat(),
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
# Calculate statistics
|
|
128
|
-
methods = defaultdict(int)
|
|
129
|
-
response_times = []
|
|
130
|
-
|
|
131
|
-
for entry in recent_requests:
|
|
132
|
-
payload = entry.get('payload', {})
|
|
133
|
-
method = payload.get('method', 'unknown')
|
|
134
|
-
methods[method] += 1
|
|
135
|
-
|
|
136
|
-
# Try to extract response time (if available)
|
|
137
|
-
# This would require storing response metadata
|
|
138
|
-
|
|
139
|
-
stats = {
|
|
140
|
-
'redis_connected': True,
|
|
141
|
-
'total_requests_today': len(recent_requests),
|
|
142
|
-
'active_methods': list(methods.keys()),
|
|
143
|
-
'top_method': max(methods.items(), key=lambda x: x[1])[0] if methods else None,
|
|
144
|
-
'method_counts': dict(methods),
|
|
145
|
-
'avg_response_time_ms': sum(response_times) / len(response_times) if response_times else 0,
|
|
146
|
-
'success_rate': 98.5, # TODO: Calculate from actual data
|
|
147
|
-
'timestamp': datetime.now().isoformat(),
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
# Cache the result
|
|
151
|
-
if cache:
|
|
152
|
-
cache.set(cache_key, stats, timeout=CACHE_TIMEOUT)
|
|
153
|
-
logger.debug(f"Cached overview stats for {CACHE_TIMEOUT}s")
|
|
154
|
-
|
|
155
|
-
return stats
|
|
156
|
-
|
|
157
|
-
except Exception as e:
|
|
158
|
-
logger.error(f"Error getting overview stats: {e}")
|
|
159
|
-
return self._error_response(str(e))
|
|
160
|
-
|
|
161
|
-
def get_recent_requests(self, count: int = 50) -> List[Dict[str, Any]]:
|
|
162
|
-
"""
|
|
163
|
-
Get recent RPC requests.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
count: Number of recent requests to return
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
List of request dicts with metadata
|
|
170
|
-
"""
|
|
171
|
-
if not self.redis_client:
|
|
172
|
-
return []
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
entries = self._get_recent_stream_entries(count=count)
|
|
176
|
-
|
|
177
|
-
return [
|
|
178
|
-
{
|
|
179
|
-
'id': entry.get('id'),
|
|
180
|
-
'timestamp': self._timestamp_to_datetime(entry.get('id')),
|
|
181
|
-
'method': entry.get('payload', {}).get('method'),
|
|
182
|
-
'params': entry.get('payload', {}).get('params'),
|
|
183
|
-
'correlation_id': entry.get('payload', {}).get('correlation_id'),
|
|
184
|
-
}
|
|
185
|
-
for entry in entries
|
|
186
|
-
]
|
|
187
|
-
|
|
188
|
-
except Exception as e:
|
|
189
|
-
logger.error(f"Error getting recent requests: {e}")
|
|
190
|
-
return []
|
|
191
|
-
|
|
192
|
-
def get_notification_stats(self) -> Dict[str, Any]:
|
|
193
|
-
"""
|
|
194
|
-
Get notification-specific statistics.
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
{
|
|
198
|
-
'total_sent': int,
|
|
199
|
-
'delivery_rate': float,
|
|
200
|
-
'by_type': Dict[str, int],
|
|
201
|
-
'recent': List[Dict],
|
|
202
|
-
}
|
|
203
|
-
"""
|
|
204
|
-
if not self.redis_client:
|
|
205
|
-
return self._error_response("Redis not connected")
|
|
206
|
-
|
|
207
|
-
# Check cache
|
|
208
|
-
try:
|
|
209
|
-
from django.core.cache import cache
|
|
210
|
-
cache_key = 'rpc_dashboard:notification_stats'
|
|
211
|
-
cached = cache.get(cache_key)
|
|
212
|
-
if cached:
|
|
213
|
-
return cached
|
|
214
|
-
except ImportError:
|
|
215
|
-
cache = None
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
# Get requests with method="send_notification"
|
|
219
|
-
recent_requests = self._get_recent_stream_entries(count=1000)
|
|
220
|
-
|
|
221
|
-
notifications = [
|
|
222
|
-
r for r in recent_requests
|
|
223
|
-
if r.get('payload', {}).get('method') == 'send_notification'
|
|
224
|
-
]
|
|
225
|
-
|
|
226
|
-
# Group by type
|
|
227
|
-
by_type = defaultdict(int)
|
|
228
|
-
for notif in notifications:
|
|
229
|
-
params = notif.get('payload', {}).get('params', {})
|
|
230
|
-
notif_type = params.get('type', 'unknown')
|
|
231
|
-
by_type[notif_type] += 1
|
|
232
|
-
|
|
233
|
-
stats = {
|
|
234
|
-
'total_sent': len(notifications),
|
|
235
|
-
'delivery_rate': 95.0, # TODO: Calculate from responses
|
|
236
|
-
'by_type': dict(by_type),
|
|
237
|
-
'recent': [
|
|
238
|
-
{
|
|
239
|
-
'timestamp': self._timestamp_to_datetime(n.get('id')),
|
|
240
|
-
'type': n.get('payload', {}).get('params', {}).get('type'),
|
|
241
|
-
'user_id': n.get('payload', {}).get('params', {}).get('user_id'),
|
|
242
|
-
'message': n.get('payload', {}).get('params', {}).get('message', '')[:50],
|
|
243
|
-
}
|
|
244
|
-
for n in notifications[:20]
|
|
245
|
-
],
|
|
246
|
-
'timestamp': datetime.now().isoformat(),
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
# Cache the result
|
|
250
|
-
if cache:
|
|
251
|
-
cache.set(cache_key, stats, timeout=CACHE_TIMEOUT)
|
|
252
|
-
|
|
253
|
-
return stats
|
|
254
|
-
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.error(f"Error getting notification stats: {e}")
|
|
257
|
-
return self._error_response(str(e))
|
|
258
|
-
|
|
259
|
-
def get_total_requests_count(self) -> int:
|
|
260
|
-
"""
|
|
261
|
-
Get total number of requests in the stream.
|
|
262
|
-
|
|
263
|
-
Returns:
|
|
264
|
-
Total count of requests in the stream
|
|
265
|
-
"""
|
|
266
|
-
if not self.redis_client:
|
|
267
|
-
return 0
|
|
268
|
-
|
|
269
|
-
try:
|
|
270
|
-
# Get stream length
|
|
271
|
-
stream_len = self.redis_client.xlen(self.config.request_stream)
|
|
272
|
-
return stream_len or 0
|
|
273
|
-
except Exception as e:
|
|
274
|
-
logger.error(f"Error getting total requests count: {e}")
|
|
275
|
-
return 0
|
|
276
|
-
|
|
277
|
-
def get_method_stats(self) -> List[Dict[str, Any]]:
|
|
278
|
-
"""
|
|
279
|
-
Get statistics grouped by RPC method.
|
|
280
|
-
|
|
281
|
-
Returns:
|
|
282
|
-
List of method stats:
|
|
283
|
-
[
|
|
284
|
-
{
|
|
285
|
-
'method': str,
|
|
286
|
-
'count': int,
|
|
287
|
-
'percentage': float,
|
|
288
|
-
'avg_time_ms': float,
|
|
289
|
-
},
|
|
290
|
-
...
|
|
291
|
-
]
|
|
292
|
-
"""
|
|
293
|
-
if not self.redis_client:
|
|
294
|
-
return []
|
|
295
|
-
|
|
296
|
-
# Check cache
|
|
297
|
-
try:
|
|
298
|
-
from django.core.cache import cache
|
|
299
|
-
cache_key = 'rpc_dashboard:method_stats'
|
|
300
|
-
cached = cache.get(cache_key)
|
|
301
|
-
if cached:
|
|
302
|
-
return cached
|
|
303
|
-
except ImportError:
|
|
304
|
-
cache = None
|
|
305
|
-
|
|
306
|
-
try:
|
|
307
|
-
recent_requests = self._get_recent_stream_entries(count=1000)
|
|
308
|
-
|
|
309
|
-
method_counts = defaultdict(int)
|
|
310
|
-
total = len(recent_requests)
|
|
311
|
-
|
|
312
|
-
for entry in recent_requests:
|
|
313
|
-
method = entry.get('payload', {}).get('method', 'unknown')
|
|
314
|
-
method_counts[method] += 1
|
|
315
|
-
|
|
316
|
-
stats = [
|
|
317
|
-
{
|
|
318
|
-
'method': method,
|
|
319
|
-
'count': count,
|
|
320
|
-
'percentage': round((count / total) * 100, 1) if total > 0 else 0,
|
|
321
|
-
'avg_time_ms': 45, # TODO: Calculate from actual data
|
|
322
|
-
}
|
|
323
|
-
for method, count in sorted(
|
|
324
|
-
method_counts.items(),
|
|
325
|
-
key=lambda x: x[1],
|
|
326
|
-
reverse=True
|
|
327
|
-
)
|
|
328
|
-
]
|
|
329
|
-
|
|
330
|
-
# Cache the result
|
|
331
|
-
if cache:
|
|
332
|
-
cache.set(cache_key, stats, timeout=CACHE_TIMEOUT)
|
|
333
|
-
|
|
334
|
-
return stats
|
|
335
|
-
|
|
336
|
-
except Exception as e:
|
|
337
|
-
logger.error(f"Error getting method stats: {e}")
|
|
338
|
-
return []
|
|
339
|
-
|
|
340
|
-
def _get_recent_stream_entries(self, count: int = 100, stream_key: str = None) -> List[Dict]:
|
|
341
|
-
"""
|
|
342
|
-
Get recent entries from Redis stream.
|
|
343
|
-
|
|
344
|
-
Args:
|
|
345
|
-
count: Number of entries to retrieve
|
|
346
|
-
stream_key: Stream key (defaults to config.request_stream)
|
|
347
|
-
|
|
348
|
-
Returns:
|
|
349
|
-
List of parsed stream entries
|
|
350
|
-
"""
|
|
351
|
-
if not self.redis_client:
|
|
352
|
-
return []
|
|
353
|
-
|
|
354
|
-
try:
|
|
355
|
-
# Use instance config
|
|
356
|
-
stream_key = stream_key or self.config.request_stream
|
|
357
|
-
|
|
358
|
-
# Validate stream_key (security: prevent Redis key injection)
|
|
359
|
-
ALLOWED_STREAMS = ['stream:requests', 'stream:responses', 'stream:rpc_requests', 'stream:rpc_responses']
|
|
360
|
-
if stream_key not in ALLOWED_STREAMS:
|
|
361
|
-
logger.warning(f"Invalid stream key attempted: {stream_key}")
|
|
362
|
-
raise ValueError(f"Stream key not allowed: {stream_key}")
|
|
363
|
-
|
|
364
|
-
# XREVRANGE to get latest entries
|
|
365
|
-
entries = self.redis_client.xrevrange(stream_key, count=count)
|
|
366
|
-
|
|
367
|
-
parsed = []
|
|
368
|
-
for entry_id, fields in entries:
|
|
369
|
-
try:
|
|
370
|
-
payload_str = fields.get('payload', '{}')
|
|
371
|
-
payload = json.loads(payload_str) if isinstance(payload_str, str) else payload_str
|
|
372
|
-
|
|
373
|
-
parsed.append({
|
|
374
|
-
'id': entry_id,
|
|
375
|
-
'payload': payload,
|
|
376
|
-
'fields': fields,
|
|
377
|
-
})
|
|
378
|
-
except json.JSONDecodeError:
|
|
379
|
-
logger.warning(f"Failed to parse payload for entry {entry_id}")
|
|
380
|
-
continue
|
|
381
|
-
|
|
382
|
-
return parsed
|
|
383
|
-
|
|
384
|
-
except Exception as e:
|
|
385
|
-
logger.error(f"Error reading stream: {e}")
|
|
386
|
-
return []
|
|
387
|
-
|
|
388
|
-
def _timestamp_to_datetime(self, stream_id: str) -> str:
|
|
389
|
-
"""
|
|
390
|
-
Convert Redis stream ID to datetime string.
|
|
391
|
-
|
|
392
|
-
Args:
|
|
393
|
-
stream_id: Redis stream ID (e.g., "1234567890123-0")
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
ISO datetime string
|
|
397
|
-
"""
|
|
398
|
-
try:
|
|
399
|
-
timestamp_ms = int(stream_id.split('-')[0])
|
|
400
|
-
dt = datetime.fromtimestamp(timestamp_ms / 1000.0)
|
|
401
|
-
return dt.isoformat()
|
|
402
|
-
except (ValueError, IndexError):
|
|
403
|
-
return datetime.now().isoformat()
|
|
404
|
-
|
|
405
|
-
def _error_response(self, error_msg: str) -> Dict[str, Any]:
|
|
406
|
-
"""Create error response dict."""
|
|
407
|
-
return {
|
|
408
|
-
'redis_connected': False,
|
|
409
|
-
'error': error_msg,
|
|
410
|
-
'timestamp': datetime.now().isoformat(),
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
def health_check(self) -> Dict[str, Any]:
|
|
414
|
-
"""
|
|
415
|
-
Check RPC monitoring health.
|
|
416
|
-
|
|
417
|
-
Returns:
|
|
418
|
-
{
|
|
419
|
-
'redis_connected': bool,
|
|
420
|
-
'stream_exists': bool,
|
|
421
|
-
'recent_activity': bool,
|
|
422
|
-
'error': Optional[str],
|
|
423
|
-
}
|
|
424
|
-
"""
|
|
425
|
-
if not self.redis_client:
|
|
426
|
-
return {
|
|
427
|
-
'redis_connected': False,
|
|
428
|
-
'stream_exists': False,
|
|
429
|
-
'recent_activity': False,
|
|
430
|
-
'error': 'Redis client not initialized',
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
try:
|
|
434
|
-
# Check Redis connection
|
|
435
|
-
self.redis_client.ping()
|
|
436
|
-
|
|
437
|
-
# Check if stream exists (use instance config)
|
|
438
|
-
stream_len = self.redis_client.xlen(self.config.request_stream)
|
|
439
|
-
stream_exists = stream_len is not None
|
|
440
|
-
|
|
441
|
-
# Check for recent activity (last 5 minutes)
|
|
442
|
-
recent_activity = False
|
|
443
|
-
if stream_exists and stream_len > 0:
|
|
444
|
-
latest = self.redis_client.xrevrange(self.config.request_stream, count=1)
|
|
445
|
-
if latest:
|
|
446
|
-
latest_id = latest[0][0]
|
|
447
|
-
timestamp_ms = int(latest_id.split('-')[0])
|
|
448
|
-
dt = datetime.fromtimestamp(timestamp_ms / 1000.0)
|
|
449
|
-
recent_activity = (datetime.now() - dt) < timedelta(minutes=5)
|
|
450
|
-
|
|
451
|
-
return {
|
|
452
|
-
'redis_connected': True,
|
|
453
|
-
'stream_exists': stream_exists,
|
|
454
|
-
'stream_length': stream_len or 0,
|
|
455
|
-
'recent_activity': recent_activity,
|
|
456
|
-
'error': None,
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
except Exception as e:
|
|
460
|
-
logger.error(f"Health check failed: {e}")
|
|
461
|
-
return {
|
|
462
|
-
'redis_connected': False,
|
|
463
|
-
'stream_exists': False,
|
|
464
|
-
'recent_activity': False,
|
|
465
|
-
'error': str(e),
|
|
466
|
-
}
|