jettask 0.2.1__py3-none-any.whl → 0.2.4__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.
- jettask/constants.py +213 -0
- jettask/core/app.py +525 -205
- jettask/core/cli.py +193 -185
- jettask/core/consumer_manager.py +126 -34
- jettask/core/context.py +3 -0
- jettask/core/enums.py +137 -0
- jettask/core/event_pool.py +501 -168
- jettask/core/message.py +147 -0
- jettask/core/offline_worker_recovery.py +181 -114
- jettask/core/task.py +10 -174
- jettask/core/task_batch.py +153 -0
- jettask/core/unified_manager_base.py +243 -0
- jettask/core/worker_scanner.py +54 -54
- jettask/executors/asyncio.py +184 -64
- jettask/webui/backend/config.py +51 -0
- jettask/webui/backend/data_access.py +2083 -92
- jettask/webui/backend/data_api.py +3294 -0
- jettask/webui/backend/dependencies.py +261 -0
- jettask/webui/backend/init_meta_db.py +158 -0
- jettask/webui/backend/main.py +1358 -69
- jettask/webui/backend/main_unified.py +78 -0
- jettask/webui/backend/main_v2.py +394 -0
- jettask/webui/backend/namespace_api.py +295 -0
- jettask/webui/backend/namespace_api_old.py +294 -0
- jettask/webui/backend/namespace_data_access.py +611 -0
- jettask/webui/backend/queue_backlog_api.py +727 -0
- jettask/webui/backend/queue_stats_v2.py +521 -0
- jettask/webui/backend/redis_monitor_api.py +476 -0
- jettask/webui/backend/unified_api_router.py +1601 -0
- jettask/webui/db_init.py +204 -32
- jettask/webui/frontend/package-lock.json +492 -1
- jettask/webui/frontend/package.json +4 -1
- jettask/webui/frontend/src/App.css +105 -7
- jettask/webui/frontend/src/App.jsx +49 -20
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +34 -10
- jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/main.jsx +1 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
- jettask/webui/frontend/src/pages/Queues.jsx +5 -1
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/services/api.js +7 -5
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/multi_namespace_consumer.py +543 -0
- jettask/webui/pg_consumer.py +983 -246
- jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
- jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
- jettask/webui/static/dist/index.html +2 -2
- jettask/webui/task_center.py +216 -0
- jettask/webui/task_center_client.py +150 -0
- jettask/webui/unified_consumer_manager.py +193 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
- jettask-0.2.4.dist-info/RECORD +134 -0
- jettask/webui/pg_consumer_slow.py +0 -1099
- jettask/webui/pg_consumer_test.py +0 -678
- jettask/webui/static/dist/assets/index-823408e8.css +0 -1
- jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
- jettask/webui/test_pg_consumer_recovery.py +0 -547
- jettask/webui/test_recovery_simple.py +0 -492
- jettask/webui/test_self_recovery.py +0 -467
- jettask-0.2.1.dist-info/RECORD +0 -91
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,476 @@
|
|
1
|
+
"""
|
2
|
+
Redis监控API - 提供Redis性能和负载监控数据
|
3
|
+
"""
|
4
|
+
from fastapi import APIRouter, HTTPException
|
5
|
+
from typing import Dict, Optional, List, Any
|
6
|
+
from datetime import datetime, timedelta
|
7
|
+
import logging
|
8
|
+
import asyncio
|
9
|
+
import time
|
10
|
+
import psutil
|
11
|
+
from namespace_data_access import get_namespace_data_access
|
12
|
+
import traceback
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
router = APIRouter(prefix="/api/redis", tags=["redis-monitor"])
|
17
|
+
|
18
|
+
|
19
|
+
def parse_redis_info(info_str: str) -> Dict[str, Any]:
|
20
|
+
"""解析Redis INFO命令的输出"""
|
21
|
+
sections = {}
|
22
|
+
current_section = None
|
23
|
+
|
24
|
+
for line in info_str.split('\n'):
|
25
|
+
line = line.strip()
|
26
|
+
if not line or line.startswith('#'):
|
27
|
+
# 处理节标题
|
28
|
+
if line.startswith('#'):
|
29
|
+
section_name = line[1:].strip().lower()
|
30
|
+
current_section = section_name
|
31
|
+
sections[current_section] = {}
|
32
|
+
continue
|
33
|
+
|
34
|
+
# 解析键值对
|
35
|
+
if ':' in line:
|
36
|
+
key, value = line.split(':', 1)
|
37
|
+
if current_section:
|
38
|
+
sections[current_section][key] = value
|
39
|
+
else:
|
40
|
+
sections[key] = value
|
41
|
+
|
42
|
+
return sections
|
43
|
+
|
44
|
+
|
45
|
+
def calculate_metrics(info_data: Dict[str, Any]) -> Dict[str, Any]:
|
46
|
+
"""从Redis INFO数据计算监控指标"""
|
47
|
+
metrics = {
|
48
|
+
'timestamp': datetime.now().isoformat(),
|
49
|
+
'status': 'healthy',
|
50
|
+
'cpu': {},
|
51
|
+
'memory': {},
|
52
|
+
'stats': {},
|
53
|
+
'persistence': {},
|
54
|
+
'replication': {},
|
55
|
+
'clients': {},
|
56
|
+
'keyspace': {},
|
57
|
+
'server': {}
|
58
|
+
}
|
59
|
+
|
60
|
+
# redis.asyncio 返回的是扁平的字典,直接从中提取数据
|
61
|
+
# CPU相关指标
|
62
|
+
metrics['cpu'] = {
|
63
|
+
'used_cpu_sys': float(info_data.get('used_cpu_sys', 0)),
|
64
|
+
'used_cpu_user': float(info_data.get('used_cpu_user', 0)),
|
65
|
+
'used_cpu_sys_children': float(info_data.get('used_cpu_sys_children', 0)),
|
66
|
+
'used_cpu_user_children': float(info_data.get('used_cpu_user_children', 0)),
|
67
|
+
'used_cpu_total': float(info_data.get('used_cpu_sys', 0)) +
|
68
|
+
float(info_data.get('used_cpu_user', 0)),
|
69
|
+
}
|
70
|
+
|
71
|
+
# 内存相关指标
|
72
|
+
used_memory = int(info_data.get('used_memory', 0))
|
73
|
+
max_memory = info_data.get('maxmemory', '0')
|
74
|
+
max_memory = int(max_memory) if max_memory != '0' else None
|
75
|
+
|
76
|
+
metrics['memory'] = {
|
77
|
+
'used_memory': used_memory,
|
78
|
+
'used_memory_human': info_data.get('used_memory_human', '0B'),
|
79
|
+
'used_memory_rss': int(info_data.get('used_memory_rss', 0)),
|
80
|
+
'used_memory_rss_human': info_data.get('used_memory_rss_human', '0B'),
|
81
|
+
'used_memory_peak': int(info_data.get('used_memory_peak', 0)),
|
82
|
+
'used_memory_peak_human': info_data.get('used_memory_peak_human', '0B'),
|
83
|
+
'used_memory_overhead': int(info_data.get('used_memory_overhead', 0)),
|
84
|
+
'used_memory_dataset': int(info_data.get('used_memory_dataset', 0)),
|
85
|
+
'mem_fragmentation_ratio': float(info_data.get('mem_fragmentation_ratio', 1.0)),
|
86
|
+
'maxmemory': max_memory,
|
87
|
+
'maxmemory_human': info_data.get('maxmemory_human', '0B'),
|
88
|
+
'maxmemory_policy': info_data.get('maxmemory_policy', 'noeviction'),
|
89
|
+
}
|
90
|
+
|
91
|
+
# 计算内存使用率
|
92
|
+
if max_memory and max_memory > 0:
|
93
|
+
# 如果Redis设置了内存限制,使用配置的限制
|
94
|
+
metrics['memory']['usage_percentage'] = round((used_memory / max_memory) * 100, 2)
|
95
|
+
metrics['memory']['total_memory'] = max_memory
|
96
|
+
metrics['memory']['total_memory_human'] = info_data.get('maxmemory_human', '0B')
|
97
|
+
else:
|
98
|
+
# 如果Redis没有设置内存限制,使用系统总内存进行计算
|
99
|
+
try:
|
100
|
+
system_memory = psutil.virtual_memory()
|
101
|
+
total_memory = system_memory.total
|
102
|
+
metrics['memory']['usage_percentage'] = round((used_memory / total_memory) * 100, 2)
|
103
|
+
metrics['memory']['total_memory'] = total_memory
|
104
|
+
# 转换为人类可读格式
|
105
|
+
def format_bytes(bytes_value):
|
106
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
107
|
+
if bytes_value < 1024.0:
|
108
|
+
return f"{bytes_value:.1f}{unit}"
|
109
|
+
bytes_value /= 1024.0
|
110
|
+
return f"{bytes_value:.1f}PB"
|
111
|
+
metrics['memory']['total_memory_human'] = format_bytes(total_memory)
|
112
|
+
except Exception as e:
|
113
|
+
logger.warning(f"无法获取系统内存信息: {e}")
|
114
|
+
traceback.print_exc()
|
115
|
+
metrics['memory']['usage_percentage'] = None
|
116
|
+
metrics['memory']['total_memory'] = None
|
117
|
+
metrics['memory']['total_memory_human'] = 'Unknown'
|
118
|
+
|
119
|
+
# 统计信息
|
120
|
+
metrics['stats'] = {
|
121
|
+
'total_connections_received': int(info_data.get('total_connections_received', 0)),
|
122
|
+
'total_commands_processed': int(info_data.get('total_commands_processed', 0)),
|
123
|
+
'instantaneous_ops_per_sec': int(info_data.get('instantaneous_ops_per_sec', 0)),
|
124
|
+
'total_net_input_bytes': int(info_data.get('total_net_input_bytes', 0)),
|
125
|
+
'total_net_output_bytes': int(info_data.get('total_net_output_bytes', 0)),
|
126
|
+
'instantaneous_input_kbps': float(info_data.get('instantaneous_input_kbps', 0)),
|
127
|
+
'instantaneous_output_kbps': float(info_data.get('instantaneous_output_kbps', 0)),
|
128
|
+
'rejected_connections': int(info_data.get('rejected_connections', 0)),
|
129
|
+
'expired_keys': int(info_data.get('expired_keys', 0)),
|
130
|
+
'evicted_keys': int(info_data.get('evicted_keys', 0)),
|
131
|
+
'keyspace_hits': int(info_data.get('keyspace_hits', 0)),
|
132
|
+
'keyspace_misses': int(info_data.get('keyspace_misses', 0)),
|
133
|
+
}
|
134
|
+
|
135
|
+
# 计算命中率
|
136
|
+
hits = metrics['stats']['keyspace_hits']
|
137
|
+
misses = metrics['stats']['keyspace_misses']
|
138
|
+
if hits + misses > 0:
|
139
|
+
metrics['stats']['hit_rate'] = round((hits / (hits + misses)) * 100, 2)
|
140
|
+
else:
|
141
|
+
metrics['stats']['hit_rate'] = 0
|
142
|
+
|
143
|
+
# 持久化信息
|
144
|
+
metrics['persistence'] = {
|
145
|
+
'rdb_last_save_time': int(info_data.get('rdb_last_save_time', 0)),
|
146
|
+
'rdb_changes_since_last_save': int(info_data.get('rdb_changes_since_last_save', 0)),
|
147
|
+
'rdb_bgsave_in_progress': int(info_data.get('rdb_bgsave_in_progress', 0)),
|
148
|
+
'rdb_last_bgsave_status': info_data.get('rdb_last_bgsave_status', 'ok'),
|
149
|
+
'aof_enabled': int(info_data.get('aof_enabled', 0)),
|
150
|
+
'aof_rewrite_in_progress': int(info_data.get('aof_rewrite_in_progress', 0)),
|
151
|
+
'aof_last_rewrite_time_sec': int(info_data.get('aof_last_rewrite_time_sec', -1)),
|
152
|
+
}
|
153
|
+
|
154
|
+
# 复制信息
|
155
|
+
metrics['replication'] = {
|
156
|
+
'role': info_data.get('role', 'master'),
|
157
|
+
'connected_slaves': int(info_data.get('connected_slaves', 0)),
|
158
|
+
'master_repl_offset': int(info_data.get('master_repl_offset', 0)) if 'master_repl_offset' in info_data else None,
|
159
|
+
}
|
160
|
+
|
161
|
+
# 客户端连接信息
|
162
|
+
metrics['clients'] = {
|
163
|
+
'connected_clients': int(info_data.get('connected_clients', 0)),
|
164
|
+
'blocked_clients': int(info_data.get('blocked_clients', 0)),
|
165
|
+
'tracking_clients': int(info_data.get('tracking_clients', 0)),
|
166
|
+
}
|
167
|
+
|
168
|
+
# 键空间信息
|
169
|
+
total_keys = 0
|
170
|
+
total_expires = 0
|
171
|
+
db_stats = {}
|
172
|
+
|
173
|
+
for key, value in info_data.items():
|
174
|
+
if key.startswith('db') and key[2:].isdigit():
|
175
|
+
# 解析格式: keys=100,expires=10,avg_ttl=3600
|
176
|
+
if isinstance(value, str):
|
177
|
+
parts = value.split(',')
|
178
|
+
db_info = {}
|
179
|
+
for part in parts:
|
180
|
+
if '=' in part:
|
181
|
+
k, v = part.split('=')
|
182
|
+
db_info[k] = int(v) if v.isdigit() else v
|
183
|
+
else:
|
184
|
+
db_info = value
|
185
|
+
|
186
|
+
db_stats[key] = db_info
|
187
|
+
total_keys += db_info.get('keys', 0)
|
188
|
+
total_expires += db_info.get('expires', 0)
|
189
|
+
|
190
|
+
metrics['keyspace'] = {
|
191
|
+
'databases': db_stats,
|
192
|
+
'total_keys': total_keys,
|
193
|
+
'total_expires': total_expires,
|
194
|
+
}
|
195
|
+
|
196
|
+
# 服务器信息
|
197
|
+
metrics['server'] = {
|
198
|
+
'redis_version': info_data.get('redis_version', 'unknown'),
|
199
|
+
'redis_mode': info_data.get('redis_mode', 'standalone'),
|
200
|
+
'uptime_in_seconds': int(info_data.get('uptime_in_seconds', 0)),
|
201
|
+
'uptime_in_days': int(info_data.get('uptime_in_days', 0)),
|
202
|
+
}
|
203
|
+
|
204
|
+
return metrics
|
205
|
+
|
206
|
+
|
207
|
+
@router.get("/monitor/{namespace}")
|
208
|
+
async def get_redis_monitor(namespace: str) -> Dict[str, Any]:
|
209
|
+
"""
|
210
|
+
获取指定命名空间的Redis监控数据
|
211
|
+
|
212
|
+
包括:
|
213
|
+
- CPU使用情况
|
214
|
+
- 内存使用情况
|
215
|
+
- 连接数
|
216
|
+
- 命令处理速率
|
217
|
+
- 键空间统计
|
218
|
+
- 持久化状态
|
219
|
+
- 复制状态
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
namespace_data = get_namespace_data_access()
|
223
|
+
connection = await namespace_data.manager.get_connection(namespace)
|
224
|
+
|
225
|
+
if not connection:
|
226
|
+
raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
|
227
|
+
|
228
|
+
# 获取Redis客户端
|
229
|
+
redis_client = await connection.get_redis_client(decode=True)
|
230
|
+
|
231
|
+
# 执行INFO命令获取Redis状态信息
|
232
|
+
info_data = await redis_client.info()
|
233
|
+
|
234
|
+
# 如果返回的是字符串,需要解析;如果已经是字典,直接使用
|
235
|
+
if isinstance(info_data, str):
|
236
|
+
info_data = parse_redis_info(info_data)
|
237
|
+
|
238
|
+
# 计算监控指标
|
239
|
+
metrics = calculate_metrics(info_data)
|
240
|
+
|
241
|
+
# 添加命名空间信息
|
242
|
+
metrics['namespace'] = namespace
|
243
|
+
|
244
|
+
# 获取慢查询日志(最近10条)
|
245
|
+
try:
|
246
|
+
slowlog = await redis_client.slowlog_get(10)
|
247
|
+
metrics['slowlog'] = []
|
248
|
+
|
249
|
+
for log_entry in slowlog:
|
250
|
+
if isinstance(log_entry, dict):
|
251
|
+
# 新版Redis返回字典格式
|
252
|
+
command = log_entry.get('command', '')
|
253
|
+
if isinstance(command, bytes):
|
254
|
+
command = command.decode('utf-8', errors='ignore')
|
255
|
+
|
256
|
+
# 限制命令长度
|
257
|
+
if len(command) > 100:
|
258
|
+
command = command[:100] + '...'
|
259
|
+
|
260
|
+
metrics['slowlog'].append({
|
261
|
+
'id': log_entry.get('id', 0),
|
262
|
+
'timestamp': datetime.fromtimestamp(log_entry.get('start_time', 0)).isoformat(),
|
263
|
+
'duration_us': log_entry.get('duration', 0),
|
264
|
+
'command': command,
|
265
|
+
})
|
266
|
+
elif isinstance(log_entry, (list, tuple)) and len(log_entry) >= 4:
|
267
|
+
# 旧版Redis返回列表格式 [id, timestamp, duration, command]
|
268
|
+
command_parts = log_entry[3] if len(log_entry) > 3 else []
|
269
|
+
command = ' '.join(str(arg) for arg in command_parts[:5])
|
270
|
+
if len(command_parts) > 5:
|
271
|
+
command += '...'
|
272
|
+
|
273
|
+
metrics['slowlog'].append({
|
274
|
+
'id': log_entry[0],
|
275
|
+
'timestamp': datetime.fromtimestamp(log_entry[1]).isoformat(),
|
276
|
+
'duration_us': log_entry[2],
|
277
|
+
'command': command,
|
278
|
+
})
|
279
|
+
|
280
|
+
except Exception as e:
|
281
|
+
logger.warning(f"获取慢查询日志失败: {e}")
|
282
|
+
traceback.print_exc()
|
283
|
+
metrics['slowlog'] = []
|
284
|
+
|
285
|
+
return {
|
286
|
+
'success': True,
|
287
|
+
'data': metrics
|
288
|
+
}
|
289
|
+
|
290
|
+
except HTTPException:
|
291
|
+
raise
|
292
|
+
except Exception as e:
|
293
|
+
logger.error(f"获取Redis监控数据失败: {e}")
|
294
|
+
traceback.print_exc()
|
295
|
+
raise HTTPException(status_code=500, detail=f"获取Redis监控数据失败: {str(e)}")
|
296
|
+
|
297
|
+
|
298
|
+
@router.get("/config/{namespace}")
|
299
|
+
async def get_redis_config(namespace: str) -> Dict[str, Any]:
|
300
|
+
"""
|
301
|
+
获取Redis配置信息
|
302
|
+
"""
|
303
|
+
try:
|
304
|
+
namespace_data = get_namespace_data_access()
|
305
|
+
connection = await namespace_data.manager.get_connection(namespace)
|
306
|
+
|
307
|
+
if not connection:
|
308
|
+
raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
|
309
|
+
|
310
|
+
# 获取Redis客户端
|
311
|
+
redis_client = await connection.get_redis_client(decode=True)
|
312
|
+
|
313
|
+
# 获取所有配置
|
314
|
+
config = await redis_client.config_get()
|
315
|
+
|
316
|
+
# 组织配置信息
|
317
|
+
important_configs = {
|
318
|
+
'maxmemory': config.get('maxmemory', '0'),
|
319
|
+
'maxmemory-policy': config.get('maxmemory-policy', 'noeviction'),
|
320
|
+
'timeout': config.get('timeout', '0'),
|
321
|
+
'tcp-keepalive': config.get('tcp-keepalive', '0'),
|
322
|
+
'databases': config.get('databases', '16'),
|
323
|
+
'save': config.get('save', ''),
|
324
|
+
'appendonly': config.get('appendonly', 'no'),
|
325
|
+
'appendfsync': config.get('appendfsync', 'everysec'),
|
326
|
+
'slowlog-log-slower-than': config.get('slowlog-log-slower-than', '10000'),
|
327
|
+
'slowlog-max-len': config.get('slowlog-max-len', '128'),
|
328
|
+
}
|
329
|
+
|
330
|
+
return {
|
331
|
+
'success': True,
|
332
|
+
'data': {
|
333
|
+
'namespace': namespace,
|
334
|
+
'important_configs': important_configs,
|
335
|
+
'all_configs': config
|
336
|
+
}
|
337
|
+
}
|
338
|
+
|
339
|
+
except HTTPException:
|
340
|
+
raise
|
341
|
+
except Exception as e:
|
342
|
+
logger.error(f"获取Redis配置失败: {e}")
|
343
|
+
traceback.print_exc()
|
344
|
+
raise HTTPException(status_code=500, detail=f"获取Redis配置失败: {str(e)}")
|
345
|
+
|
346
|
+
|
347
|
+
@router.post("/command/{namespace}")
|
348
|
+
async def execute_redis_command(
|
349
|
+
namespace: str,
|
350
|
+
command: str,
|
351
|
+
args: List[str] = None
|
352
|
+
) -> Dict[str, Any]:
|
353
|
+
"""
|
354
|
+
执行Redis命令(仅限安全的只读命令)
|
355
|
+
"""
|
356
|
+
# 允许的安全命令白名单
|
357
|
+
SAFE_COMMANDS = {
|
358
|
+
'ping', 'info', 'dbsize', 'lastsave', 'type', 'ttl', 'pttl',
|
359
|
+
'exists', 'keys', 'scan', 'get', 'mget', 'strlen', 'getrange',
|
360
|
+
'llen', 'lrange', 'lindex', 'scard', 'smembers', 'sismember',
|
361
|
+
'zcard', 'zrange', 'zrevrange', 'zrank', 'zrevrank', 'zscore',
|
362
|
+
'hlen', 'hkeys', 'hvals', 'hget', 'hmget', 'hgetall', 'hexists',
|
363
|
+
'memory', 'client', 'config', 'slowlog'
|
364
|
+
}
|
365
|
+
|
366
|
+
command_lower = command.lower()
|
367
|
+
|
368
|
+
# 检查命令是否在白名单中
|
369
|
+
if command_lower not in SAFE_COMMANDS:
|
370
|
+
raise HTTPException(
|
371
|
+
status_code=403,
|
372
|
+
detail=f"命令 {command} 不被允许。仅支持只读命令。"
|
373
|
+
)
|
374
|
+
|
375
|
+
try:
|
376
|
+
namespace_data = get_namespace_data_access()
|
377
|
+
connection = await namespace_data.manager.get_connection(namespace)
|
378
|
+
|
379
|
+
if not connection:
|
380
|
+
raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
|
381
|
+
|
382
|
+
# 获取Redis客户端
|
383
|
+
redis_client = await connection.get_redis_client(decode=True)
|
384
|
+
|
385
|
+
# 执行命令
|
386
|
+
if args:
|
387
|
+
result = await redis_client.execute_command(command, *args)
|
388
|
+
else:
|
389
|
+
result = await redis_client.execute_command(command)
|
390
|
+
|
391
|
+
# 处理不同类型的返回值
|
392
|
+
if isinstance(result, bytes):
|
393
|
+
result = result.decode('utf-8')
|
394
|
+
elif isinstance(result, (list, tuple)):
|
395
|
+
result = [r.decode('utf-8') if isinstance(r, bytes) else r for r in result]
|
396
|
+
|
397
|
+
return {
|
398
|
+
'success': True,
|
399
|
+
'data': {
|
400
|
+
'command': f"{command} {' '.join(args or [])}".strip(),
|
401
|
+
'result': result
|
402
|
+
}
|
403
|
+
}
|
404
|
+
|
405
|
+
except HTTPException:
|
406
|
+
raise
|
407
|
+
except Exception as e:
|
408
|
+
logger.error(f"执行Redis命令失败: {e}")
|
409
|
+
traceback.print_exc()
|
410
|
+
raise HTTPException(status_code=500, detail=f"执行Redis命令失败: {str(e)}")
|
411
|
+
|
412
|
+
|
413
|
+
@router.get("/performance/{namespace}")
|
414
|
+
async def get_redis_performance(
|
415
|
+
namespace: str,
|
416
|
+
duration_seconds: int = 5
|
417
|
+
) -> Dict[str, Any]:
|
418
|
+
"""
|
419
|
+
获取Redis性能测试数据
|
420
|
+
通过执行多次PING命令来测试延迟
|
421
|
+
"""
|
422
|
+
try:
|
423
|
+
namespace_data = get_namespace_data_access()
|
424
|
+
connection = await namespace_data.manager.get_connection(namespace)
|
425
|
+
|
426
|
+
if not connection:
|
427
|
+
raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
|
428
|
+
|
429
|
+
# 获取Redis客户端
|
430
|
+
redis_client = await connection.get_redis_client(decode=True)
|
431
|
+
|
432
|
+
# 性能测试
|
433
|
+
latencies = []
|
434
|
+
test_count = 100
|
435
|
+
|
436
|
+
for _ in range(test_count):
|
437
|
+
start_time = time.perf_counter()
|
438
|
+
await redis_client.ping()
|
439
|
+
end_time = time.perf_counter()
|
440
|
+
latency_ms = (end_time - start_time) * 1000
|
441
|
+
latencies.append(latency_ms)
|
442
|
+
await asyncio.sleep(duration_seconds / test_count)
|
443
|
+
|
444
|
+
# 计算统计数据
|
445
|
+
latencies.sort()
|
446
|
+
avg_latency = sum(latencies) / len(latencies)
|
447
|
+
min_latency = latencies[0]
|
448
|
+
max_latency = latencies[-1]
|
449
|
+
p50_latency = latencies[int(len(latencies) * 0.5)]
|
450
|
+
p95_latency = latencies[int(len(latencies) * 0.95)]
|
451
|
+
p99_latency = latencies[int(len(latencies) * 0.99)]
|
452
|
+
|
453
|
+
return {
|
454
|
+
'success': True,
|
455
|
+
'data': {
|
456
|
+
'namespace': namespace,
|
457
|
+
'test_count': test_count,
|
458
|
+
'duration_seconds': duration_seconds,
|
459
|
+
'latency_ms': {
|
460
|
+
'avg': round(avg_latency, 3),
|
461
|
+
'min': round(min_latency, 3),
|
462
|
+
'max': round(max_latency, 3),
|
463
|
+
'p50': round(p50_latency, 3),
|
464
|
+
'p95': round(p95_latency, 3),
|
465
|
+
'p99': round(p99_latency, 3),
|
466
|
+
},
|
467
|
+
'timestamp': datetime.now().isoformat()
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
except HTTPException:
|
472
|
+
raise
|
473
|
+
except Exception as e:
|
474
|
+
logger.error(f"Redis性能测试失败: {e}")
|
475
|
+
traceback.print_exc()
|
476
|
+
raise HTTPException(status_code=500, detail=f"Redis性能测试失败: {str(e)}")
|