jettask 0.2.18__py3-none-any.whl → 0.2.20__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/__init__.py +60 -2
- jettask/cli.py +314 -228
- jettask/config/__init__.py +9 -1
- jettask/config/config.py +245 -0
- jettask/config/env_loader.py +381 -0
- jettask/config/lua_scripts.py +158 -0
- jettask/config/nacos_config.py +132 -5
- jettask/core/__init__.py +1 -1
- jettask/core/app.py +1573 -666
- jettask/core/app_importer.py +33 -16
- jettask/core/container.py +532 -0
- jettask/core/task.py +1 -4
- jettask/core/unified_manager_base.py +2 -2
- jettask/executor/__init__.py +38 -0
- jettask/executor/core.py +625 -0
- jettask/executor/executor.py +338 -0
- jettask/executor/orchestrator.py +290 -0
- jettask/executor/process_entry.py +638 -0
- jettask/executor/task_executor.py +317 -0
- jettask/messaging/__init__.py +68 -0
- jettask/messaging/event_pool.py +2188 -0
- jettask/messaging/reader.py +519 -0
- jettask/messaging/registry.py +266 -0
- jettask/messaging/scanner.py +369 -0
- jettask/messaging/sender.py +312 -0
- jettask/persistence/__init__.py +118 -0
- jettask/persistence/backlog_monitor.py +567 -0
- jettask/{backend/data_access.py → persistence/base.py} +58 -57
- jettask/persistence/consumer.py +315 -0
- jettask/{core → persistence}/db_manager.py +23 -22
- jettask/persistence/maintenance.py +81 -0
- jettask/persistence/message_consumer.py +259 -0
- jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
- jettask/persistence/offline_recovery.py +196 -0
- jettask/persistence/queue_discovery.py +215 -0
- jettask/persistence/task_persistence.py +218 -0
- jettask/persistence/task_updater.py +583 -0
- jettask/scheduler/__init__.py +2 -2
- jettask/scheduler/loader.py +6 -5
- jettask/scheduler/run_scheduler.py +1 -1
- jettask/scheduler/scheduler.py +7 -7
- jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
- jettask/task/__init__.py +16 -0
- jettask/{router.py → task/router.py} +26 -8
- jettask/task/task_center/__init__.py +9 -0
- jettask/task/task_executor.py +318 -0
- jettask/task/task_registry.py +291 -0
- jettask/test_connection_monitor.py +73 -0
- jettask/utils/__init__.py +31 -1
- jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
- jettask/utils/db_connector.py +1629 -0
- jettask/{db_init.py → utils/db_init.py} +1 -1
- jettask/utils/rate_limit/__init__.py +30 -0
- jettask/utils/rate_limit/concurrency_limiter.py +665 -0
- jettask/utils/rate_limit/config.py +145 -0
- jettask/utils/rate_limit/limiter.py +41 -0
- jettask/utils/rate_limit/manager.py +269 -0
- jettask/utils/rate_limit/qps_limiter.py +154 -0
- jettask/utils/rate_limit/task_limiter.py +384 -0
- jettask/utils/serializer.py +3 -0
- jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
- jettask/utils/time_sync.py +173 -0
- jettask/webui/__init__.py +27 -0
- jettask/{api/v1 → webui/api}/alerts.py +1 -1
- jettask/{api/v1 → webui/api}/analytics.py +2 -2
- jettask/{api/v1 → webui/api}/namespaces.py +1 -1
- jettask/{api/v1 → webui/api}/overview.py +1 -1
- jettask/{api/v1 → webui/api}/queues.py +3 -3
- jettask/{api/v1 → webui/api}/scheduled.py +1 -1
- jettask/{api/v1 → webui/api}/settings.py +1 -1
- jettask/{api.py → webui/app.py} +253 -145
- jettask/webui/namespace_manager/__init__.py +10 -0
- jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
- jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
- jettask/{run.py → webui/run.py} +2 -2
- jettask/{services → webui/services}/__init__.py +1 -3
- jettask/{services → webui/services}/overview_service.py +34 -16
- jettask/{services → webui/services}/queue_service.py +1 -1
- jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
- jettask/{services → webui/services}/settings_service.py +1 -1
- jettask/worker/__init__.py +53 -0
- jettask/worker/lifecycle.py +1507 -0
- jettask/worker/manager.py +583 -0
- jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
- jettask-0.2.20.dist-info/RECORD +145 -0
- jettask/__main__.py +0 -140
- jettask/api/__init__.py +0 -103
- jettask/backend/__init__.py +0 -1
- jettask/backend/api/__init__.py +0 -3
- jettask/backend/api/v1/__init__.py +0 -17
- jettask/backend/api/v1/monitoring.py +0 -431
- jettask/backend/api/v1/namespaces.py +0 -504
- jettask/backend/api/v1/queues.py +0 -342
- jettask/backend/api/v1/tasks.py +0 -367
- jettask/backend/core/__init__.py +0 -3
- jettask/backend/core/cache.py +0 -221
- jettask/backend/core/database.py +0 -200
- jettask/backend/core/exceptions.py +0 -102
- jettask/backend/dependencies.py +0 -261
- jettask/backend/init_meta_db.py +0 -158
- jettask/backend/main.py +0 -1426
- jettask/backend/main_unified.py +0 -78
- jettask/backend/main_v2.py +0 -394
- jettask/backend/models/__init__.py +0 -3
- jettask/backend/models/requests.py +0 -236
- jettask/backend/models/responses.py +0 -230
- jettask/backend/namespace_api_old.py +0 -267
- jettask/backend/services/__init__.py +0 -3
- jettask/backend/start.py +0 -42
- jettask/backend/unified_api_router.py +0 -1541
- jettask/cleanup_deprecated_tables.sql +0 -16
- jettask/core/consumer_manager.py +0 -1695
- jettask/core/delay_scanner.py +0 -256
- jettask/core/event_pool.py +0 -1700
- jettask/core/heartbeat_process.py +0 -222
- jettask/core/task_batch.py +0 -153
- jettask/core/worker_scanner.py +0 -271
- jettask/executors/__init__.py +0 -5
- jettask/executors/asyncio.py +0 -876
- jettask/executors/base.py +0 -30
- jettask/executors/common.py +0 -148
- jettask/executors/multi_asyncio.py +0 -309
- jettask/gradio_app.py +0 -570
- jettask/integrated_gradio_app.py +0 -1088
- jettask/main.py +0 -0
- jettask/monitoring/__init__.py +0 -3
- jettask/pg_consumer.py +0 -1896
- jettask/run_monitor.py +0 -22
- jettask/run_webui.py +0 -148
- jettask/scheduler/multi_namespace_scheduler.py +0 -294
- jettask/scheduler/unified_manager.py +0 -450
- jettask/task_center_client.py +0 -150
- jettask/utils/serializer_optimized.py +0 -33
- jettask/webui_exceptions.py +0 -67
- jettask-0.2.18.dist-info/RECORD +0 -150
- /jettask/{constants.py → config/constants.py} +0 -0
- /jettask/{backend/config.py → config/task_center.py} +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
- /jettask/{models.py → persistence/models.py} +0 -0
- /jettask/scheduler/{manager.py → task_crud.py} +0 -0
- /jettask/{schema.sql → schemas/schema.sql} +0 -0
- /jettask/{task_center.py → task/task_center/client.py} +0 -0
- /jettask/{monitoring → utils}/file_watcher.py +0 -0
- /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
- /jettask/{api/v1 → webui/api}/__init__.py +0 -0
- /jettask/{webui_config.py → webui/config.py} +0 -0
- /jettask/{webui_models → webui/models}/__init__.py +0 -0
- /jettask/{webui_models → webui/models}/namespace.py +0 -0
- /jettask/{services → webui/services}/alert_service.py +0 -0
- /jettask/{services → webui/services}/analytics_service.py +0 -0
- /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
- /jettask/{services → webui/services}/task_service.py +0 -0
- /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
- /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
jettask/gradio_app.py
DELETED
@@ -1,570 +0,0 @@
|
|
1
|
-
import gradio as gr
|
2
|
-
import pandas as pd
|
3
|
-
import plotly.graph_objects as go
|
4
|
-
import plotly.express as px
|
5
|
-
from datetime import datetime, timedelta, timezone
|
6
|
-
import asyncio
|
7
|
-
import json
|
8
|
-
import logging
|
9
|
-
from typing import Dict, List, Any, Optional
|
10
|
-
import aiohttp
|
11
|
-
from functools import partial
|
12
|
-
|
13
|
-
# 设置日志
|
14
|
-
logging.basicConfig(level=logging.INFO)
|
15
|
-
logger = logging.getLogger(__name__)
|
16
|
-
|
17
|
-
# API 基础URL
|
18
|
-
API_BASE_URL = "http://localhost:8000"
|
19
|
-
|
20
|
-
|
21
|
-
class JetTaskMonitor:
|
22
|
-
"""JetTask 监控界面"""
|
23
|
-
|
24
|
-
def __init__(self):
|
25
|
-
self.current_stats = {}
|
26
|
-
self.queue_data = []
|
27
|
-
self.worker_data = []
|
28
|
-
self.task_data = []
|
29
|
-
|
30
|
-
async def fetch_api(self, endpoint: str, params: Dict = None) -> Dict:
|
31
|
-
"""异步获取API数据"""
|
32
|
-
try:
|
33
|
-
async with aiohttp.ClientSession() as session:
|
34
|
-
async with session.get(f"{API_BASE_URL}/api/{endpoint}", params=params) as response:
|
35
|
-
if response.status == 200:
|
36
|
-
return await response.json()
|
37
|
-
else:
|
38
|
-
logger.error(f"API请求失败: {endpoint}, 状态码: {response.status}")
|
39
|
-
return {}
|
40
|
-
except Exception as e:
|
41
|
-
logger.error(f"API请求异常: {endpoint}, 错误: {e}")
|
42
|
-
return {}
|
43
|
-
|
44
|
-
def fetch_global_stats(self):
|
45
|
-
"""获取全局统计数据"""
|
46
|
-
stats = asyncio.run(self.fetch_api("global-stats"))
|
47
|
-
self.current_stats = stats
|
48
|
-
|
49
|
-
# 构建显示文本
|
50
|
-
stats_text = f"""
|
51
|
-
## 📊 系统概览
|
52
|
-
|
53
|
-
### Workers
|
54
|
-
- 🟢 在线: {stats.get('online_workers', 0)} / {stats.get('total_workers', 0)}
|
55
|
-
- 📦 活跃队列: {stats.get('active_queues', 0)} / {stats.get('total_queues', 0)}
|
56
|
-
|
57
|
-
### 任务统计
|
58
|
-
- ⏳ 待处理: {stats.get('pending_tasks', 0):,}
|
59
|
-
- 🔄 运行中: {stats.get('running_tasks', 0):,}
|
60
|
-
- ✅ 已完成: {stats.get('completed_tasks', 0):,}
|
61
|
-
- ❌ 失败: {stats.get('failed_tasks', 0):,}
|
62
|
-
|
63
|
-
### 实时性能
|
64
|
-
- 📈 总任务数: {stats.get('total_tasks', 0):,}
|
65
|
-
- ⚡ 成功率: {self._calculate_success_rate(stats):.1f}%
|
66
|
-
"""
|
67
|
-
return stats_text
|
68
|
-
|
69
|
-
def _calculate_success_rate(self, stats: Dict) -> float:
|
70
|
-
"""计算成功率"""
|
71
|
-
completed = stats.get('completed_tasks', 0)
|
72
|
-
failed = stats.get('failed_tasks', 0)
|
73
|
-
total = completed + failed
|
74
|
-
return (completed / total * 100) if total > 0 else 0
|
75
|
-
|
76
|
-
def fetch_queues_data(self):
|
77
|
-
"""获取队列数据并返回DataFrame"""
|
78
|
-
queues_data = asyncio.run(self.fetch_api("queues"))
|
79
|
-
queues = queues_data.get('queues', [])
|
80
|
-
|
81
|
-
# 获取每个队列的详细信息
|
82
|
-
detailed_queues = []
|
83
|
-
for queue_name in queues:
|
84
|
-
stats = asyncio.run(self.fetch_api(f"queue/{queue_name}/stats"))
|
85
|
-
summary = asyncio.run(self.fetch_api(f"queue/{queue_name}/worker-summary"))
|
86
|
-
|
87
|
-
queue_info = {
|
88
|
-
'队列名称': queue_name,
|
89
|
-
'待处理': stats.get('messages_ready', 0),
|
90
|
-
'处理中': stats.get('messages_unacknowledged', 0),
|
91
|
-
'消费者': stats.get('consumers', 0),
|
92
|
-
'成功数': summary.get('summary', {}).get('success_count', 0),
|
93
|
-
'失败数': summary.get('summary', {}).get('failed_count', 0),
|
94
|
-
'在线Workers': summary.get('summary', {}).get('online_workers', 0),
|
95
|
-
'总Workers': summary.get('summary', {}).get('total_workers', 0),
|
96
|
-
}
|
97
|
-
detailed_queues.append(queue_info)
|
98
|
-
|
99
|
-
self.queue_data = detailed_queues
|
100
|
-
return pd.DataFrame(detailed_queues) if detailed_queues else pd.DataFrame()
|
101
|
-
|
102
|
-
def fetch_workers_data(self, queue_filter: str = "all"):
|
103
|
-
"""获取Worker数据"""
|
104
|
-
if queue_filter == "all":
|
105
|
-
# 获取所有队列的workers
|
106
|
-
queues_data = asyncio.run(self.fetch_api("queues"))
|
107
|
-
queues = queues_data.get('queues', [])
|
108
|
-
|
109
|
-
all_workers = []
|
110
|
-
for queue_name in queues:
|
111
|
-
workers = asyncio.run(self.fetch_api(f"queue/{queue_name}/workers"))
|
112
|
-
for worker in workers.get('workers', []):
|
113
|
-
worker_info = {
|
114
|
-
'队列': queue_name,
|
115
|
-
'Worker ID': worker.get('consumer_id', '-'),
|
116
|
-
'主机': worker.get('host', '-'),
|
117
|
-
'进程ID': worker.get('pid', '-'),
|
118
|
-
'状态': '🟢 在线' if worker.get('is_alive') else '🔴 离线',
|
119
|
-
'最后心跳': self._format_heartbeat(worker.get('seconds_ago', 0)),
|
120
|
-
'成功任务': worker.get('success_count', 0),
|
121
|
-
'失败任务': worker.get('failed_count', 0),
|
122
|
-
'运行中': worker.get('running_tasks', 0),
|
123
|
-
'平均处理时间': f"{worker.get('avg_processing_time', 0):.2f}s",
|
124
|
-
}
|
125
|
-
all_workers.append(worker_info)
|
126
|
-
|
127
|
-
self.worker_data = all_workers
|
128
|
-
return pd.DataFrame(all_workers) if all_workers else pd.DataFrame()
|
129
|
-
else:
|
130
|
-
# 获取特定队列的workers
|
131
|
-
workers = asyncio.run(self.fetch_api(f"queue/{queue_filter}/workers"))
|
132
|
-
workers_list = []
|
133
|
-
for worker in workers.get('workers', []):
|
134
|
-
worker_info = {
|
135
|
-
'Worker ID': worker.get('consumer_id', '-'),
|
136
|
-
'主机': worker.get('host', '-'),
|
137
|
-
'进程ID': worker.get('pid', '-'),
|
138
|
-
'状态': '🟢 在线' if worker.get('is_alive') else '🔴 离线',
|
139
|
-
'最后心跳': self._format_heartbeat(worker.get('seconds_ago', 0)),
|
140
|
-
'成功任务': worker.get('success_count', 0),
|
141
|
-
'失败任务': worker.get('failed_count', 0),
|
142
|
-
'运行中': worker.get('running_tasks', 0),
|
143
|
-
'平均处理时间': f"{worker.get('avg_processing_time', 0):.2f}s",
|
144
|
-
}
|
145
|
-
workers_list.append(worker_info)
|
146
|
-
|
147
|
-
return pd.DataFrame(workers_list) if workers_list else pd.DataFrame()
|
148
|
-
|
149
|
-
def _format_heartbeat(self, seconds_ago: int) -> str:
|
150
|
-
"""格式化心跳时间"""
|
151
|
-
if seconds_ago < 60:
|
152
|
-
return f"{seconds_ago}秒前"
|
153
|
-
elif seconds_ago < 3600:
|
154
|
-
return f"{seconds_ago // 60}分钟前"
|
155
|
-
else:
|
156
|
-
return f"{seconds_ago // 3600}小时前"
|
157
|
-
|
158
|
-
def create_queue_timeline_chart(self, time_range: str = "1h", selected_queues: List[str] = None):
|
159
|
-
"""创建队列时间线图表"""
|
160
|
-
# 计算时间范围
|
161
|
-
end_time = datetime.now(timezone.utc)
|
162
|
-
if time_range == "15m":
|
163
|
-
start_time = end_time - timedelta(minutes=15)
|
164
|
-
elif time_range == "30m":
|
165
|
-
start_time = end_time - timedelta(minutes=30)
|
166
|
-
elif time_range == "1h":
|
167
|
-
start_time = end_time - timedelta(hours=1)
|
168
|
-
elif time_range == "3h":
|
169
|
-
start_time = end_time - timedelta(hours=3)
|
170
|
-
elif time_range == "6h":
|
171
|
-
start_time = end_time - timedelta(hours=6)
|
172
|
-
elif time_range == "12h":
|
173
|
-
start_time = end_time - timedelta(hours=12)
|
174
|
-
elif time_range == "24h":
|
175
|
-
start_time = end_time - timedelta(days=1)
|
176
|
-
else:
|
177
|
-
start_time = end_time - timedelta(hours=1)
|
178
|
-
|
179
|
-
# 获取队列列表
|
180
|
-
if not selected_queues or len(selected_queues) == 0:
|
181
|
-
# 如果没有选择任何队列,返回空图表
|
182
|
-
return go.Figure().update_layout(
|
183
|
-
title=f"队列处理趋势 - {time_range} (请选择队列)",
|
184
|
-
xaxis_title="时间(本地时区)",
|
185
|
-
yaxis_title="任务数量",
|
186
|
-
template='plotly_dark',
|
187
|
-
height=500
|
188
|
-
)
|
189
|
-
else:
|
190
|
-
queues = selected_queues[:10] # 限制最多10个队列
|
191
|
-
|
192
|
-
# 获取时间线数据
|
193
|
-
params = {
|
194
|
-
'queues': ','.join(queues),
|
195
|
-
'start_time': start_time.isoformat(),
|
196
|
-
'end_time': end_time.isoformat()
|
197
|
-
}
|
198
|
-
|
199
|
-
timeline_data = asyncio.run(self.fetch_api("queues/timeline/pg", params))
|
200
|
-
|
201
|
-
# 创建图表
|
202
|
-
fig = go.Figure()
|
203
|
-
|
204
|
-
# 使用Plotly的默认颜色序列,支持更多队列
|
205
|
-
colors = px.colors.qualitative.Plotly + px.colors.qualitative.D3
|
206
|
-
|
207
|
-
for i, queue_data in enumerate(timeline_data.get('queues', [])):
|
208
|
-
queue_name = queue_data['queue']
|
209
|
-
timeline = queue_data.get('timeline', {}).get('timeline', [])
|
210
|
-
|
211
|
-
if timeline:
|
212
|
-
# 将UTC时间转换为本地时间
|
213
|
-
local_times = []
|
214
|
-
for item in timeline:
|
215
|
-
# 解析ISO格式的UTC时间
|
216
|
-
utc_time = datetime.fromisoformat(item['time'].replace('Z', '+00:00'))
|
217
|
-
# 转换为本地时间
|
218
|
-
local_time = utc_time.replace(tzinfo=timezone.utc).astimezone()
|
219
|
-
local_times.append(local_time)
|
220
|
-
|
221
|
-
counts = [item['count'] for item in timeline]
|
222
|
-
|
223
|
-
# 为hover创建格式化的时间字符串
|
224
|
-
hover_times = [t.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] for t in local_times] # 显示到毫秒
|
225
|
-
|
226
|
-
fig.add_trace(go.Scatter(
|
227
|
-
x=local_times,
|
228
|
-
y=counts,
|
229
|
-
name=queue_name,
|
230
|
-
mode='lines+markers',
|
231
|
-
line=dict(color=colors[i % len(colors)], width=2),
|
232
|
-
marker=dict(size=5),
|
233
|
-
customdata=hover_times,
|
234
|
-
hovertemplate='<b>%{fullData.name}</b><br>' +
|
235
|
-
'时间: %{customdata}<br>' +
|
236
|
-
'任务数: %{y}<br>' +
|
237
|
-
'<extra></extra>'
|
238
|
-
))
|
239
|
-
|
240
|
-
fig.update_layout(
|
241
|
-
title=f"队列处理趋势 - {time_range}",
|
242
|
-
xaxis_title="时间(本地时区)",
|
243
|
-
yaxis_title="任务数量",
|
244
|
-
hovermode='x unified',
|
245
|
-
template='plotly_dark',
|
246
|
-
height=500, # 增加高度以便更好地显示多个队列
|
247
|
-
xaxis=dict(
|
248
|
-
tickformat='%Y-%m-%d<br>%H:%M:%S.%L', # 显示日期、时间和毫秒
|
249
|
-
tickangle=-45, # 倾斜标签以避免重叠
|
250
|
-
showgrid=True,
|
251
|
-
gridcolor='rgba(128, 128, 128, 0.2)',
|
252
|
-
tickformatstops=[
|
253
|
-
dict(dtickrange=[None, 1000], value="%H:%M:%S.%L"), # 小于1秒时显示毫秒
|
254
|
-
dict(dtickrange=[1000, 60000], value="%H:%M:%S"), # 1秒到1分钟显示秒
|
255
|
-
dict(dtickrange=[60000, 3600000], value="%H:%M"), # 1分钟到1小时显示分钟
|
256
|
-
dict(dtickrange=[3600000, None], value="%Y-%m-%d<br>%H:%M") # 大于1小时显示日期和时间
|
257
|
-
]
|
258
|
-
),
|
259
|
-
yaxis=dict(
|
260
|
-
showgrid=True,
|
261
|
-
gridcolor='rgba(128, 128, 128, 0.2)',
|
262
|
-
fixedrange=True # 禁用Y轴缩放
|
263
|
-
),
|
264
|
-
legend=dict(
|
265
|
-
orientation="v",
|
266
|
-
yanchor="top",
|
267
|
-
y=0.99,
|
268
|
-
xanchor="left",
|
269
|
-
x=1.01
|
270
|
-
),
|
271
|
-
margin=dict(r=150), # 为图例留出空间
|
272
|
-
dragmode='zoom', # 设置为缩放模式,允许框选
|
273
|
-
selectdirection='h' # 只允许水平选择(只能框选X轴)
|
274
|
-
)
|
275
|
-
|
276
|
-
return fig
|
277
|
-
|
278
|
-
def fetch_tasks_data(self, queue_name: str, status_filter: str = "all", limit: int = 100):
|
279
|
-
"""获取任务数据"""
|
280
|
-
params = {
|
281
|
-
'queue_name': queue_name,
|
282
|
-
'limit': limit
|
283
|
-
}
|
284
|
-
|
285
|
-
if status_filter != "all":
|
286
|
-
params['status'] = status_filter
|
287
|
-
|
288
|
-
tasks_data = asyncio.run(self.fetch_api(f"queue/{queue_name}/tasks", params))
|
289
|
-
tasks = tasks_data.get('tasks', [])
|
290
|
-
|
291
|
-
# 转换为DataFrame格式
|
292
|
-
tasks_list = []
|
293
|
-
for task in tasks:
|
294
|
-
task_info = {
|
295
|
-
'任务ID': task.get('message_id', '-')[:20] + '...',
|
296
|
-
'任务名称': task.get('task', '-'),
|
297
|
-
'状态': self._format_status(task.get('parsed_status', {}).get('status', '未知')),
|
298
|
-
'消费者': task.get('consumer', '-'),
|
299
|
-
'创建时间': self._format_time(task.get('created_at')),
|
300
|
-
'参数': task.get('params_str', '-')[:50] + '...' if len(task.get('params_str', '')) > 50 else task.get('params_str', '-'),
|
301
|
-
}
|
302
|
-
tasks_list.append(task_info)
|
303
|
-
|
304
|
-
return pd.DataFrame(tasks_list) if tasks_list else pd.DataFrame()
|
305
|
-
|
306
|
-
def _format_status(self, status: str) -> str:
|
307
|
-
"""格式化任务状态"""
|
308
|
-
status_map = {
|
309
|
-
'pending': '⏳ 待处理',
|
310
|
-
'running': '🔄 运行中',
|
311
|
-
'success': '✅ 成功',
|
312
|
-
'failed': '❌ 失败',
|
313
|
-
'timeout': '⏱️ 超时',
|
314
|
-
'cancelled': '🚫 已取消',
|
315
|
-
'未知': '❓ 未知'
|
316
|
-
}
|
317
|
-
return status_map.get(status, status)
|
318
|
-
|
319
|
-
def _format_time(self, time_str: str) -> str:
|
320
|
-
"""格式化时间显示(转换为本地时区)"""
|
321
|
-
if not time_str:
|
322
|
-
return '-'
|
323
|
-
try:
|
324
|
-
# 解析UTC时间
|
325
|
-
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
|
326
|
-
# 转换为本地时区
|
327
|
-
local_dt = dt.replace(tzinfo=timezone.utc).astimezone()
|
328
|
-
# 显示到毫秒(3位小数)
|
329
|
-
return local_dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
330
|
-
except:
|
331
|
-
return time_str
|
332
|
-
|
333
|
-
def create_worker_distribution_chart(self):
|
334
|
-
"""创建Worker分布饼图"""
|
335
|
-
# 按队列统计worker数量
|
336
|
-
queue_worker_count = {}
|
337
|
-
for worker in self.worker_data:
|
338
|
-
queue = worker.get('队列', 'Unknown')
|
339
|
-
queue_worker_count[queue] = queue_worker_count.get(queue, 0) + 1
|
340
|
-
|
341
|
-
if not queue_worker_count:
|
342
|
-
return go.Figure()
|
343
|
-
|
344
|
-
fig = go.Figure(data=[go.Pie(
|
345
|
-
labels=list(queue_worker_count.keys()),
|
346
|
-
values=list(queue_worker_count.values()),
|
347
|
-
hole=.3
|
348
|
-
)])
|
349
|
-
|
350
|
-
fig.update_layout(
|
351
|
-
title="Worker 分布",
|
352
|
-
template='plotly_dark',
|
353
|
-
height=400
|
354
|
-
)
|
355
|
-
|
356
|
-
return fig
|
357
|
-
|
358
|
-
|
359
|
-
# 创建监控实例
|
360
|
-
monitor = JetTaskMonitor()
|
361
|
-
|
362
|
-
|
363
|
-
def create_interface():
|
364
|
-
"""创建Gradio界面"""
|
365
|
-
|
366
|
-
with gr.Blocks(title="JetTask Monitor", theme=gr.themes.Soft()) as app:
|
367
|
-
gr.Markdown("# 🚀 JetTask Monitor - 任务队列监控平台")
|
368
|
-
|
369
|
-
# 定时刷新组件
|
370
|
-
timer = gr.Timer(5.0) # 每5秒刷新一次
|
371
|
-
|
372
|
-
with gr.Tab("📊 概览"):
|
373
|
-
with gr.Row():
|
374
|
-
stats_display = gr.Markdown(monitor.fetch_global_stats())
|
375
|
-
|
376
|
-
with gr.Row():
|
377
|
-
with gr.Column(scale=2):
|
378
|
-
with gr.Row():
|
379
|
-
time_range = gr.Radio(
|
380
|
-
choices=["15m", "30m", "1h", "3h", "6h", "12h", "24h"],
|
381
|
-
value="1h",
|
382
|
-
label="时间范围",
|
383
|
-
interactive=True
|
384
|
-
)
|
385
|
-
queue_selector_for_timeline = gr.CheckboxGroup(
|
386
|
-
choices=[], # 将在页面加载时更新
|
387
|
-
value=[], # 默认选择所有队列
|
388
|
-
label="选择队列(最多10个)",
|
389
|
-
interactive=True
|
390
|
-
)
|
391
|
-
queue_timeline_chart = gr.Plot(
|
392
|
-
label="队列处理趋势"
|
393
|
-
)
|
394
|
-
|
395
|
-
with gr.Column(scale=1):
|
396
|
-
worker_dist_chart = gr.Plot()
|
397
|
-
|
398
|
-
# 队列表格
|
399
|
-
with gr.Row():
|
400
|
-
queue_table = gr.DataFrame(
|
401
|
-
monitor.fetch_queues_data(),
|
402
|
-
label="队列状态",
|
403
|
-
interactive=False
|
404
|
-
)
|
405
|
-
|
406
|
-
with gr.Tab("📦 队列详情"):
|
407
|
-
with gr.Row():
|
408
|
-
queue_selector = gr.Dropdown(
|
409
|
-
choices=["all"] + [q['队列名称'] for q in monitor.queue_data],
|
410
|
-
value="all",
|
411
|
-
label="选择队列",
|
412
|
-
interactive=True
|
413
|
-
)
|
414
|
-
refresh_queue_btn = gr.Button("🔄 刷新", variant="secondary")
|
415
|
-
|
416
|
-
with gr.Row():
|
417
|
-
queue_workers_table = gr.DataFrame(
|
418
|
-
monitor.fetch_workers_data("all"),
|
419
|
-
label="Workers",
|
420
|
-
interactive=False
|
421
|
-
)
|
422
|
-
|
423
|
-
with gr.Tab("📋 任务列表"):
|
424
|
-
with gr.Row():
|
425
|
-
task_queue_selector = gr.Dropdown(
|
426
|
-
choices=[q['队列名称'] for q in monitor.queue_data],
|
427
|
-
value=monitor.queue_data[0]['队列名称'] if monitor.queue_data else None,
|
428
|
-
label="选择队列",
|
429
|
-
interactive=True
|
430
|
-
)
|
431
|
-
task_status_filter = gr.Radio(
|
432
|
-
choices=["all", "pending", "running", "success", "failed"],
|
433
|
-
value="all",
|
434
|
-
label="状态筛选",
|
435
|
-
interactive=True
|
436
|
-
)
|
437
|
-
task_limit = gr.Slider(
|
438
|
-
minimum=10,
|
439
|
-
maximum=500,
|
440
|
-
value=100,
|
441
|
-
step=10,
|
442
|
-
label="显示数量",
|
443
|
-
interactive=True
|
444
|
-
)
|
445
|
-
|
446
|
-
tasks_table = gr.DataFrame(
|
447
|
-
label="任务列表",
|
448
|
-
interactive=False
|
449
|
-
)
|
450
|
-
|
451
|
-
# 定义更新函数
|
452
|
-
def update_overview():
|
453
|
-
"""更新概览页面"""
|
454
|
-
stats = monitor.fetch_global_stats()
|
455
|
-
queues_df = monitor.fetch_queues_data()
|
456
|
-
workers_df = monitor.fetch_workers_data("all")
|
457
|
-
|
458
|
-
# 更新Worker分布图
|
459
|
-
worker_chart = monitor.create_worker_distribution_chart()
|
460
|
-
|
461
|
-
# 更新队列选择器
|
462
|
-
queue_choices = ["all"] + [q['队列名称'] for q in monitor.queue_data]
|
463
|
-
task_queue_choices = [q['队列名称'] for q in monitor.queue_data]
|
464
|
-
timeline_queue_choices = [q['队列名称'] for q in monitor.queue_data]
|
465
|
-
|
466
|
-
return (
|
467
|
-
stats,
|
468
|
-
queues_df,
|
469
|
-
worker_chart,
|
470
|
-
gr.update(choices=queue_choices),
|
471
|
-
gr.update(choices=task_queue_choices, value=task_queue_choices[0] if task_queue_choices else None),
|
472
|
-
gr.update(choices=timeline_queue_choices, value=timeline_queue_choices[:3] if timeline_queue_choices else []) # 默认选择前3个队列
|
473
|
-
)
|
474
|
-
|
475
|
-
def update_timeline_chart(time_range, selected_queues):
|
476
|
-
"""更新时间线图表"""
|
477
|
-
return monitor.create_queue_timeline_chart(time_range, selected_queues)
|
478
|
-
|
479
|
-
def init_timeline_chart():
|
480
|
-
"""初始化时间线图表"""
|
481
|
-
# 获取队列列表
|
482
|
-
queues_data = monitor.fetch_queues_data()
|
483
|
-
initial_queues = [q['队列名称'] for q in monitor.queue_data][:3] # 默认前3个队列
|
484
|
-
if initial_queues:
|
485
|
-
return monitor.create_queue_timeline_chart("1h", initial_queues)
|
486
|
-
else:
|
487
|
-
return monitor.create_queue_timeline_chart("1h", [])
|
488
|
-
|
489
|
-
def update_queue_workers(queue_name):
|
490
|
-
"""更新队列Workers"""
|
491
|
-
return monitor.fetch_workers_data(queue_name)
|
492
|
-
|
493
|
-
def update_tasks(queue_name, status_filter, limit):
|
494
|
-
"""更新任务列表"""
|
495
|
-
if queue_name:
|
496
|
-
return monitor.fetch_tasks_data(queue_name, status_filter, int(limit))
|
497
|
-
return pd.DataFrame()
|
498
|
-
|
499
|
-
# 事件绑定
|
500
|
-
timer.tick(
|
501
|
-
update_overview,
|
502
|
-
outputs=[stats_display, queue_table, worker_dist_chart, queue_selector, task_queue_selector, queue_selector_for_timeline]
|
503
|
-
)
|
504
|
-
|
505
|
-
time_range.change(
|
506
|
-
update_timeline_chart,
|
507
|
-
inputs=[time_range, queue_selector_for_timeline],
|
508
|
-
outputs=[queue_timeline_chart]
|
509
|
-
)
|
510
|
-
|
511
|
-
queue_selector_for_timeline.change(
|
512
|
-
update_timeline_chart,
|
513
|
-
inputs=[time_range, queue_selector_for_timeline],
|
514
|
-
outputs=[queue_timeline_chart]
|
515
|
-
)
|
516
|
-
|
517
|
-
queue_selector.change(
|
518
|
-
update_queue_workers,
|
519
|
-
inputs=[queue_selector],
|
520
|
-
outputs=[queue_workers_table]
|
521
|
-
)
|
522
|
-
|
523
|
-
refresh_queue_btn.click(
|
524
|
-
update_queue_workers,
|
525
|
-
inputs=[queue_selector],
|
526
|
-
outputs=[queue_workers_table]
|
527
|
-
)
|
528
|
-
|
529
|
-
# 任务列表更新
|
530
|
-
task_queue_selector.change(
|
531
|
-
update_tasks,
|
532
|
-
inputs=[task_queue_selector, task_status_filter, task_limit],
|
533
|
-
outputs=[tasks_table]
|
534
|
-
)
|
535
|
-
|
536
|
-
task_status_filter.change(
|
537
|
-
update_tasks,
|
538
|
-
inputs=[task_queue_selector, task_status_filter, task_limit],
|
539
|
-
outputs=[tasks_table]
|
540
|
-
)
|
541
|
-
|
542
|
-
task_limit.change(
|
543
|
-
update_tasks,
|
544
|
-
inputs=[task_queue_selector, task_status_filter, task_limit],
|
545
|
-
outputs=[tasks_table]
|
546
|
-
)
|
547
|
-
|
548
|
-
# 页面加载时初始化
|
549
|
-
app.load(
|
550
|
-
update_overview,
|
551
|
-
outputs=[stats_display, queue_table, worker_dist_chart, queue_selector, task_queue_selector, queue_selector_for_timeline]
|
552
|
-
)
|
553
|
-
|
554
|
-
app.load(
|
555
|
-
init_timeline_chart,
|
556
|
-
outputs=[queue_timeline_chart]
|
557
|
-
)
|
558
|
-
|
559
|
-
return app
|
560
|
-
|
561
|
-
|
562
|
-
if __name__ == "__main__":
|
563
|
-
# 创建并启动应用
|
564
|
-
app = create_interface()
|
565
|
-
app.launch(
|
566
|
-
server_name="0.0.0.0",
|
567
|
-
server_port=7860,
|
568
|
-
share=False,
|
569
|
-
inbrowser=True
|
570
|
-
)
|