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/backend/main.py
DELETED
@@ -1,1426 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
JetTask Monitor 独立后端API服务
|
3
|
-
完全脱离gradio和integrated_gradio_app依赖
|
4
|
-
|
5
|
-
数据库说明:
|
6
|
-
1. 任务中心元数据库:通过环境变量配置(TASK_CENTER_DB_*)
|
7
|
-
- 存储命名空间配置等管理数据
|
8
|
-
- 由任务中心自己使用
|
9
|
-
|
10
|
-
2. JetTask应用数据库:在WebUI中为每个命名空间配置
|
11
|
-
- Redis:任务队列存储
|
12
|
-
- PostgreSQL:任务结果存储
|
13
|
-
- 由JetTask worker使用
|
14
|
-
"""
|
15
|
-
from fastapi import HTTPException, Request
|
16
|
-
from datetime import datetime, timedelta, timezone
|
17
|
-
from typing import List, Dict, Optional
|
18
|
-
from sqlalchemy import text
|
19
|
-
import logging
|
20
|
-
import traceback
|
21
|
-
from jettask.schemas import (
|
22
|
-
TimeRangeQuery,
|
23
|
-
QueueTimelineResponse,
|
24
|
-
TrimQueueRequest,
|
25
|
-
ScheduledTaskRequest,
|
26
|
-
AlertRuleRequest
|
27
|
-
)
|
28
|
-
|
29
|
-
import sys
|
30
|
-
import os
|
31
|
-
# 添加当前目录到Python路径
|
32
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
33
|
-
|
34
|
-
from data_access import JetTaskDataAccess
|
35
|
-
from jettask.backend.namespace_data_access import get_namespace_data_access
|
36
|
-
from jettask.backend.queue_stats_v2 import QueueStatsV2
|
37
|
-
|
38
|
-
# 设置日志
|
39
|
-
logging.basicConfig(level=logging.INFO)
|
40
|
-
logger = logging.getLogger(__name__)
|
41
|
-
|
42
|
-
# 从api包导入已配置的app
|
43
|
-
from jettask.api import app
|
44
|
-
|
45
|
-
# 创建全局数据访问实例供路由使用
|
46
|
-
data_access = JetTaskDataAccess()
|
47
|
-
namespace_data_access = get_namespace_data_access()
|
48
|
-
|
49
|
-
# 将数据访问实例注入到app.state
|
50
|
-
app.state.data_access = data_access
|
51
|
-
app.state.namespace_data_access = namespace_data_access
|
52
|
-
|
53
|
-
|
54
|
-
# 模型已从 schemas 模块导入
|
55
|
-
|
56
|
-
|
57
|
-
@app.get("/")
|
58
|
-
async def root():
|
59
|
-
"""根路径"""
|
60
|
-
return {
|
61
|
-
"message": "JetTask Monitor API",
|
62
|
-
"version": "1.0.0",
|
63
|
-
"timestamp": datetime.now(timezone.utc).isoformat()
|
64
|
-
}
|
65
|
-
|
66
|
-
|
67
|
-
def get_base_queue_name(queue_name: str) -> str:
|
68
|
-
"""提取基础队列名(去除优先级后缀)"""
|
69
|
-
# 检查是否包含优先级后缀(格式: queue_name:priority)
|
70
|
-
if ':' in queue_name:
|
71
|
-
parts = queue_name.rsplit(':', 1)
|
72
|
-
# 检查最后一部分是否是数字(优先级)
|
73
|
-
if parts[-1].isdigit():
|
74
|
-
return parts[0]
|
75
|
-
return queue_name
|
76
|
-
|
77
|
-
|
78
|
-
@app.get("/api/queues/{namespace}")
|
79
|
-
async def get_queues(namespace: str):
|
80
|
-
"""获取指定命名空间的队列列表"""
|
81
|
-
try:
|
82
|
-
# 使用命名空间数据访问
|
83
|
-
namespace_access = get_namespace_data_access()
|
84
|
-
# 获取指定命名空间的队列信息
|
85
|
-
queues_data = await namespace_access.get_queue_stats(namespace)
|
86
|
-
return {
|
87
|
-
"success": True,
|
88
|
-
"data": list(set([get_base_queue_name(q['queue_name']) for q in queues_data]))
|
89
|
-
}
|
90
|
-
except Exception as e:
|
91
|
-
logger.error(f"获取队列列表失败: {e}")
|
92
|
-
traceback.print_exc()
|
93
|
-
raise HTTPException(status_code=500, detail=str(e))
|
94
|
-
|
95
|
-
|
96
|
-
@app.post("/api/queue-flow-rates")
|
97
|
-
async def get_queue_flow_rates(query: TimeRangeQuery):
|
98
|
-
"""获取单个队列的流量速率(入队、开始执行、完成)"""
|
99
|
-
try:
|
100
|
-
# 处理时间范围
|
101
|
-
now = datetime.now(timezone.utc)
|
102
|
-
|
103
|
-
if query.start_time and query.end_time:
|
104
|
-
# 使用提供的时间范围
|
105
|
-
start_time = query.start_time
|
106
|
-
end_time = query.end_time
|
107
|
-
logger.info(f"使用自定义时间范围: {start_time} 到 {end_time}")
|
108
|
-
else:
|
109
|
-
# 根据time_range参数计算时间范围
|
110
|
-
time_range_map = {
|
111
|
-
"15m": timedelta(minutes=15),
|
112
|
-
"30m": timedelta(minutes=30),
|
113
|
-
"1h": timedelta(hours=1),
|
114
|
-
"3h": timedelta(hours=3),
|
115
|
-
"6h": timedelta(hours=6),
|
116
|
-
"12h": timedelta(hours=12),
|
117
|
-
"24h": timedelta(hours=24),
|
118
|
-
"7d": timedelta(days=7),
|
119
|
-
"30d": timedelta(days=30),
|
120
|
-
}
|
121
|
-
|
122
|
-
delta = time_range_map.get(query.time_range, timedelta(minutes=15))
|
123
|
-
|
124
|
-
# 获取队列的最新任务时间,确保图表包含最新数据
|
125
|
-
queue_name = query.queues[0] if query.queues else None
|
126
|
-
if queue_name:
|
127
|
-
latest_time = await data_access.get_latest_task_time(queue_name)
|
128
|
-
if latest_time:
|
129
|
-
# 使用最新任务时间作为结束时间
|
130
|
-
end_time = latest_time.replace(second=59, microsecond=999999) # 包含整分钟
|
131
|
-
logger.info(f"使用最新任务时间: {latest_time}")
|
132
|
-
else:
|
133
|
-
# 如果没有任务,使用当前时间
|
134
|
-
end_time = now.replace(second=0, microsecond=0)
|
135
|
-
else:
|
136
|
-
end_time = now.replace(second=0, microsecond=0)
|
137
|
-
|
138
|
-
start_time = end_time - delta
|
139
|
-
logger.info(f"使用预设时间范围 {query.time_range}: {start_time} 到 {end_time}, delta: {delta}")
|
140
|
-
|
141
|
-
# 确保有队列名称
|
142
|
-
if not query.queues or len(query.queues) == 0:
|
143
|
-
return {"data": [], "granularity": "minute"}
|
144
|
-
|
145
|
-
# 获取第一个队列的流量速率
|
146
|
-
queue_name = query.queues[0]
|
147
|
-
data, granularity = await data_access.fetch_queue_flow_rates(
|
148
|
-
queue_name, start_time, end_time, query.filters
|
149
|
-
)
|
150
|
-
|
151
|
-
return {"data": data, "granularity": granularity}
|
152
|
-
|
153
|
-
except Exception as e:
|
154
|
-
logger.error(f"获取队列流量速率失败: {e}")
|
155
|
-
traceback.print_exc()
|
156
|
-
raise HTTPException(status_code=500, detail=str(e))
|
157
|
-
|
158
|
-
|
159
|
-
# 删除了旧的 /api/queue-timeline 接口,使用 data_api.py 中的新接口
|
160
|
-
|
161
|
-
|
162
|
-
@app.get("/api/stats")
|
163
|
-
async def get_global_stats():
|
164
|
-
"""获取全局统计信息"""
|
165
|
-
try:
|
166
|
-
stats_data = await data_access.fetch_global_stats()
|
167
|
-
return {
|
168
|
-
"success": True,
|
169
|
-
"data": stats_data
|
170
|
-
}
|
171
|
-
except Exception as e:
|
172
|
-
logger.error(f"获取全局统计信息失败: {e}")
|
173
|
-
traceback.print_exc()
|
174
|
-
raise HTTPException(status_code=500, detail=str(e))
|
175
|
-
|
176
|
-
|
177
|
-
@app.get("/api/queues/detail")
|
178
|
-
async def get_queues_detail():
|
179
|
-
"""获取队列详细信息"""
|
180
|
-
try:
|
181
|
-
queues_data = await data_access.fetch_queues_data()
|
182
|
-
return {
|
183
|
-
"success": True,
|
184
|
-
"data": queues_data
|
185
|
-
}
|
186
|
-
except Exception as e:
|
187
|
-
logger.error(f"获取队列详细信息失败: {e}")
|
188
|
-
traceback.print_exc()
|
189
|
-
raise HTTPException(status_code=500, detail=str(e))
|
190
|
-
|
191
|
-
|
192
|
-
# 删除了旧的 /api/queue-details 接口,使用 data_api.py 中的新接口
|
193
|
-
|
194
|
-
|
195
|
-
@app.delete("/api/queue/{queue_name}")
|
196
|
-
async def delete_queue(queue_name: str):
|
197
|
-
"""删除队列"""
|
198
|
-
try:
|
199
|
-
# 这里需要实现删除队列的逻辑
|
200
|
-
# 暂时返回模拟响应
|
201
|
-
logger.info(f"删除队列请求: {queue_name}")
|
202
|
-
return {
|
203
|
-
"success": True,
|
204
|
-
"message": f"队列 {queue_name} 已删除"
|
205
|
-
}
|
206
|
-
except Exception as e:
|
207
|
-
logger.error(f"删除队列失败: {e}")
|
208
|
-
traceback.print_exc()
|
209
|
-
return {
|
210
|
-
"success": False,
|
211
|
-
"message": str(e)
|
212
|
-
}
|
213
|
-
|
214
|
-
|
215
|
-
@app.post("/api/queue/{queue_name}/trim")
|
216
|
-
async def trim_queue(queue_name: str, request: TrimQueueRequest):
|
217
|
-
"""裁剪队列到指定长度"""
|
218
|
-
try:
|
219
|
-
# 这里需要实现裁剪队列的逻辑
|
220
|
-
# 暂时返回模拟响应
|
221
|
-
logger.info(f"裁剪队列请求: {queue_name}, 保留 {request.max_length} 条消息")
|
222
|
-
return {
|
223
|
-
"success": True,
|
224
|
-
"message": f"队列 {queue_name} 已裁剪至 {request.max_length} 条消息"
|
225
|
-
}
|
226
|
-
except Exception as e:
|
227
|
-
logger.error(f"裁剪队列失败: {e}")
|
228
|
-
traceback.print_exc()
|
229
|
-
return {
|
230
|
-
"success": False,
|
231
|
-
"message": str(e)
|
232
|
-
}
|
233
|
-
|
234
|
-
|
235
|
-
@app.post("/api/tasks")
|
236
|
-
async def get_tasks(request: Request):
|
237
|
-
"""获取队列的任务列表(支持灵活筛选和时间范围)"""
|
238
|
-
try:
|
239
|
-
# 解析请求体
|
240
|
-
body = await request.json()
|
241
|
-
queue_name = body.get('queue_name')
|
242
|
-
page = body.get('page', 1)
|
243
|
-
page_size = body.get('page_size', 20)
|
244
|
-
filters = body.get('filters', [])
|
245
|
-
|
246
|
-
# 处理时间范围参数(与 /api/queue-flow-rates 保持一致)
|
247
|
-
start_time = body.get('start_time')
|
248
|
-
end_time = body.get('end_time')
|
249
|
-
time_range = body.get('time_range')
|
250
|
-
|
251
|
-
if not queue_name:
|
252
|
-
raise HTTPException(status_code=400, detail="queue_name is required")
|
253
|
-
|
254
|
-
# 如果提供了时间范围,计算起止时间
|
255
|
-
if not start_time or not end_time:
|
256
|
-
if time_range:
|
257
|
-
now = datetime.now(timezone.utc)
|
258
|
-
time_range_map = {
|
259
|
-
"15m": timedelta(minutes=15),
|
260
|
-
"30m": timedelta(minutes=30),
|
261
|
-
"1h": timedelta(hours=1),
|
262
|
-
"3h": timedelta(hours=3),
|
263
|
-
"6h": timedelta(hours=6),
|
264
|
-
"12h": timedelta(hours=12),
|
265
|
-
"24h": timedelta(hours=24),
|
266
|
-
"7d": timedelta(days=7),
|
267
|
-
"30d": timedelta(days=30),
|
268
|
-
}
|
269
|
-
|
270
|
-
delta = time_range_map.get(time_range)
|
271
|
-
if delta:
|
272
|
-
# 获取队列的最新任务时间,确保包含最新数据
|
273
|
-
latest_time = await data_access.get_latest_task_time(queue_name)
|
274
|
-
if latest_time:
|
275
|
-
end_time = latest_time.replace(second=59, microsecond=999999)
|
276
|
-
else:
|
277
|
-
end_time = now
|
278
|
-
start_time = end_time - delta
|
279
|
-
logger.info(f"使用时间范围 {time_range}: {start_time} 到 {end_time}")
|
280
|
-
|
281
|
-
# 如果有时间范围,将其转换为datetime对象
|
282
|
-
if start_time and isinstance(start_time, str):
|
283
|
-
start_time = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
|
284
|
-
if end_time and isinstance(end_time, str):
|
285
|
-
end_time = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
|
286
|
-
|
287
|
-
logger.info(f"获取队列 {queue_name} 的任务列表, 页码: {page}, 每页: {page_size}, 筛选条件: {filters}, 时间范围: {start_time} - {end_time}")
|
288
|
-
|
289
|
-
# 调用数据访问层获取真实数据
|
290
|
-
result = await data_access.fetch_tasks_with_filters(
|
291
|
-
queue_name=queue_name,
|
292
|
-
page=page,
|
293
|
-
page_size=page_size,
|
294
|
-
filters=filters,
|
295
|
-
start_time=start_time,
|
296
|
-
end_time=end_time
|
297
|
-
)
|
298
|
-
|
299
|
-
return result
|
300
|
-
except Exception as e:
|
301
|
-
import traceback
|
302
|
-
traceback.print_exc()
|
303
|
-
logger.error(f"获取任务列表失败: {e}")
|
304
|
-
raise HTTPException(status_code=500, detail=str(e))
|
305
|
-
|
306
|
-
|
307
|
-
@app.get("/api/task/{task_id}/details")
|
308
|
-
async def get_task_details(task_id: str, consumer_group: Optional[str] = None):
|
309
|
-
"""获取单个任务的详细数据(包括task_data和result)
|
310
|
-
|
311
|
-
Args:
|
312
|
-
task_id: 任务ID (stream_id)
|
313
|
-
consumer_group: 消费者组名称(可选,用于精确定位)
|
314
|
-
"""
|
315
|
-
try:
|
316
|
-
logger.info(f"获取任务 {task_id} 的详细数据, consumer_group={consumer_group}")
|
317
|
-
|
318
|
-
# 调用数据访问层获取任务详细数据
|
319
|
-
task_details = await data_access.fetch_task_details(task_id, consumer_group)
|
320
|
-
|
321
|
-
if not task_details:
|
322
|
-
raise HTTPException(status_code=404, detail="Task not found")
|
323
|
-
|
324
|
-
return {
|
325
|
-
"success": True,
|
326
|
-
"data": task_details
|
327
|
-
}
|
328
|
-
except HTTPException:
|
329
|
-
raise
|
330
|
-
except Exception as e:
|
331
|
-
logger.error(f"获取任务详细数据失败: {e}")
|
332
|
-
traceback.print_exc()
|
333
|
-
raise HTTPException(status_code=500, detail=str(e))
|
334
|
-
|
335
|
-
|
336
|
-
@app.get("/api/tasks")
|
337
|
-
async def get_tasks_legacy(
|
338
|
-
queue_name: str,
|
339
|
-
page: int = 1,
|
340
|
-
page_size: int = 20,
|
341
|
-
status: Optional[str] = None,
|
342
|
-
task_id: Optional[str] = None,
|
343
|
-
worker_id: Optional[str] = None
|
344
|
-
):
|
345
|
-
"""获取队列的任务列表(向后兼容旧版本)"""
|
346
|
-
try:
|
347
|
-
# 构建筛选条件
|
348
|
-
filters = []
|
349
|
-
if status:
|
350
|
-
filters.append({'field': 'status', 'operator': 'eq', 'value': status})
|
351
|
-
if task_id:
|
352
|
-
filters.append({'field': 'id', 'operator': 'eq', 'value': task_id})
|
353
|
-
if worker_id:
|
354
|
-
filters.append({'field': 'worker_id', 'operator': 'eq', 'value': worker_id})
|
355
|
-
|
356
|
-
logger.info(f"获取队列 {queue_name} 的任务列表, 页码: {page}, 每页: {page_size}")
|
357
|
-
|
358
|
-
# 调用数据访问层获取真实数据
|
359
|
-
result = await data_access.fetch_tasks_with_filters(
|
360
|
-
queue_name=queue_name,
|
361
|
-
page=page,
|
362
|
-
page_size=page_size,
|
363
|
-
filters=filters
|
364
|
-
)
|
365
|
-
|
366
|
-
return result
|
367
|
-
except Exception as e:
|
368
|
-
import traceback
|
369
|
-
traceback.print_exc()
|
370
|
-
logger.error(f"获取任务列表失败: {e}")
|
371
|
-
raise HTTPException(status_code=500, detail=str(e))
|
372
|
-
|
373
|
-
|
374
|
-
@app.get("/health")
|
375
|
-
async def health_check():
|
376
|
-
"""健康检查"""
|
377
|
-
return {
|
378
|
-
"status": "healthy",
|
379
|
-
"timestamp": datetime.now(timezone.utc).isoformat()
|
380
|
-
}
|
381
|
-
|
382
|
-
|
383
|
-
# ============= 定时任务管理API =============
|
384
|
-
|
385
|
-
# ScheduledTaskRequest 已从 schemas 模块导入
|
386
|
-
|
387
|
-
|
388
|
-
@app.get("/api/scheduled-tasks")
|
389
|
-
async def get_scheduled_tasks(
|
390
|
-
page: int = 1,
|
391
|
-
page_size: int = 20,
|
392
|
-
search: Optional[str] = None,
|
393
|
-
is_active: Optional[bool] = None
|
394
|
-
):
|
395
|
-
"""获取定时任务列表(兼容旧版本)"""
|
396
|
-
try:
|
397
|
-
# 使用真实的数据库操作
|
398
|
-
async with data_access.get_session() as session:
|
399
|
-
tasks, total = await data_access.fetch_scheduled_tasks(
|
400
|
-
session=session,
|
401
|
-
page=page,
|
402
|
-
page_size=page_size,
|
403
|
-
search=search,
|
404
|
-
is_active=is_active
|
405
|
-
)
|
406
|
-
|
407
|
-
return {
|
408
|
-
"success": True,
|
409
|
-
"data": tasks,
|
410
|
-
"total": total,
|
411
|
-
"page": page,
|
412
|
-
"page_size": page_size
|
413
|
-
}
|
414
|
-
except Exception as e:
|
415
|
-
logger.error(f"获取定时任务列表失败: {e}")
|
416
|
-
traceback.print_exc()
|
417
|
-
raise HTTPException(status_code=500, detail=str(e))
|
418
|
-
|
419
|
-
|
420
|
-
@app.get("/api/scheduled-tasks/statistics/{namespace}")
|
421
|
-
async def get_scheduled_tasks_statistics(namespace: str):
|
422
|
-
"""获取定时任务统计数据"""
|
423
|
-
try:
|
424
|
-
async with data_access.AsyncSessionLocal() as session:
|
425
|
-
# 获取统计数据,传递命名空间参数
|
426
|
-
stats = await data_access.get_scheduled_tasks_statistics(session, namespace)
|
427
|
-
return stats
|
428
|
-
except Exception as e:
|
429
|
-
logger.error(f"获取定时任务统计失败: {e}")
|
430
|
-
traceback.print_exc()
|
431
|
-
raise HTTPException(status_code=500, detail=str(e))
|
432
|
-
|
433
|
-
|
434
|
-
@app.post("/api/scheduled-tasks/list")
|
435
|
-
async def get_scheduled_tasks_with_filters(request: Request):
|
436
|
-
"""获取定时任务列表(支持高级筛选)"""
|
437
|
-
try:
|
438
|
-
# 解析请求体
|
439
|
-
body = await request.json()
|
440
|
-
|
441
|
-
page = body.get('page', 1)
|
442
|
-
page_size = body.get('page_size', 20)
|
443
|
-
search = body.get('search')
|
444
|
-
is_active = body.get('is_active')
|
445
|
-
filters = body.get('filters', [])
|
446
|
-
time_range = body.get('time_range')
|
447
|
-
start_time = body.get('start_time')
|
448
|
-
end_time = body.get('end_time')
|
449
|
-
|
450
|
-
print(f'{filters=}')
|
451
|
-
# 使用真实的数据库操作
|
452
|
-
async with data_access.get_session() as session:
|
453
|
-
tasks, total = await data_access.fetch_scheduled_tasks(
|
454
|
-
session=session,
|
455
|
-
page=page,
|
456
|
-
page_size=page_size,
|
457
|
-
search=search,
|
458
|
-
is_active=is_active,
|
459
|
-
filters=filters,
|
460
|
-
time_range=time_range,
|
461
|
-
start_time=start_time,
|
462
|
-
end_time=end_time
|
463
|
-
)
|
464
|
-
|
465
|
-
return {
|
466
|
-
"success": True,
|
467
|
-
"data": tasks,
|
468
|
-
"total": total,
|
469
|
-
"page": page,
|
470
|
-
"page_size": page_size
|
471
|
-
}
|
472
|
-
except Exception as e:
|
473
|
-
logger.error(f"获取定时任务列表失败: {e}")
|
474
|
-
traceback.print_exc()
|
475
|
-
raise HTTPException(status_code=500, detail=str(e))
|
476
|
-
|
477
|
-
|
478
|
-
def validate_schedule_config(schedule_type: str, schedule_config: dict):
|
479
|
-
"""验证调度配置"""
|
480
|
-
if schedule_type == 'interval':
|
481
|
-
if 'seconds' in schedule_config:
|
482
|
-
seconds = schedule_config.get('seconds')
|
483
|
-
if seconds is None or seconds <= 0:
|
484
|
-
raise ValueError(f"间隔时间必须大于0秒,当前值: {seconds}")
|
485
|
-
if seconds < 1:
|
486
|
-
raise ValueError(f"间隔时间不能小于1秒,当前值: {seconds}秒。小于1秒的高频任务可能影响系统性能")
|
487
|
-
elif 'minutes' in schedule_config:
|
488
|
-
minutes = schedule_config.get('minutes')
|
489
|
-
if minutes is None or minutes <= 0:
|
490
|
-
raise ValueError(f"间隔时间必须大于0分钟,当前值: {minutes}")
|
491
|
-
else:
|
492
|
-
raise ValueError("interval类型的任务必须指定seconds或minutes")
|
493
|
-
elif schedule_type == 'cron':
|
494
|
-
if 'cron_expression' not in schedule_config:
|
495
|
-
raise ValueError("cron类型的任务必须指定cron_expression")
|
496
|
-
# 可以添加cron表达式格式验证
|
497
|
-
|
498
|
-
@app.post("/api/scheduled-tasks")
|
499
|
-
async def create_scheduled_task(request: ScheduledTaskRequest):
|
500
|
-
"""创建定时任务"""
|
501
|
-
try:
|
502
|
-
# 验证调度配置
|
503
|
-
validate_schedule_config(request.schedule_type, request.schedule_config)
|
504
|
-
|
505
|
-
# 使用真实的数据库操作
|
506
|
-
task_data = {
|
507
|
-
"namespace": request.namespace,
|
508
|
-
"name": request.name,
|
509
|
-
"queue_name": request.queue_name,
|
510
|
-
"task_data": request.task_data,
|
511
|
-
"schedule_type": request.schedule_type,
|
512
|
-
"schedule_config": request.schedule_config,
|
513
|
-
"is_active": request.is_active,
|
514
|
-
"description": request.description,
|
515
|
-
"max_retry": request.max_retry,
|
516
|
-
"timeout": request.timeout
|
517
|
-
}
|
518
|
-
|
519
|
-
async with data_access.get_session() as session:
|
520
|
-
task = await data_access.create_scheduled_task(session, task_data)
|
521
|
-
|
522
|
-
return {
|
523
|
-
"success": True,
|
524
|
-
"data": task,
|
525
|
-
"message": "定时任务创建成功"
|
526
|
-
}
|
527
|
-
except Exception as e:
|
528
|
-
logger.error(f"创建定时任务失败: {e}")
|
529
|
-
traceback.print_exc()
|
530
|
-
raise HTTPException(status_code=500, detail=str(e))
|
531
|
-
|
532
|
-
|
533
|
-
@app.put("/api/scheduled-tasks/{task_id}")
|
534
|
-
async def update_scheduled_task(task_id: str, request: ScheduledTaskRequest):
|
535
|
-
"""更新定时任务"""
|
536
|
-
try:
|
537
|
-
# 验证调度配置
|
538
|
-
validate_schedule_config(request.schedule_type, request.schedule_config)
|
539
|
-
|
540
|
-
# 使用真实的数据库操作
|
541
|
-
task_data = {
|
542
|
-
"namespace": request.namespace,
|
543
|
-
"name": request.name,
|
544
|
-
"queue_name": request.queue_name,
|
545
|
-
"task_data": request.task_data,
|
546
|
-
"schedule_type": request.schedule_type,
|
547
|
-
"schedule_config": request.schedule_config,
|
548
|
-
"is_active": request.is_active,
|
549
|
-
"description": request.description,
|
550
|
-
"max_retry": request.max_retry,
|
551
|
-
"timeout": request.timeout
|
552
|
-
}
|
553
|
-
|
554
|
-
async with data_access.get_session() as session:
|
555
|
-
task = await data_access.update_scheduled_task(session, task_id, task_data)
|
556
|
-
|
557
|
-
return {
|
558
|
-
"success": True,
|
559
|
-
"data": task,
|
560
|
-
"message": "定时任务更新成功"
|
561
|
-
}
|
562
|
-
except Exception as e:
|
563
|
-
logger.error(f"更新定时任务失败: {e}")
|
564
|
-
traceback.print_exc()
|
565
|
-
raise HTTPException(status_code=500, detail=str(e))
|
566
|
-
|
567
|
-
|
568
|
-
@app.delete("/api/scheduled-tasks/{task_id}")
|
569
|
-
async def delete_scheduled_task(task_id: str):
|
570
|
-
"""删除定时任务"""
|
571
|
-
try:
|
572
|
-
# 使用真实的数据库操作
|
573
|
-
async with data_access.get_session() as session:
|
574
|
-
success = await data_access.delete_scheduled_task(session, task_id)
|
575
|
-
|
576
|
-
if success:
|
577
|
-
return {
|
578
|
-
"success": True,
|
579
|
-
"message": f"定时任务 {task_id} 已删除"
|
580
|
-
}
|
581
|
-
else:
|
582
|
-
raise HTTPException(status_code=404, detail="定时任务不存在")
|
583
|
-
except HTTPException:
|
584
|
-
raise
|
585
|
-
except Exception as e:
|
586
|
-
logger.error(f"删除定时任务失败: {e}")
|
587
|
-
traceback.print_exc()
|
588
|
-
raise HTTPException(status_code=500, detail=str(e))
|
589
|
-
|
590
|
-
|
591
|
-
@app.post("/api/scheduled-tasks/{task_id}/toggle")
|
592
|
-
async def toggle_scheduled_task(task_id: str):
|
593
|
-
"""启用/禁用定时任务"""
|
594
|
-
try:
|
595
|
-
# 使用真实的数据库操作
|
596
|
-
async with data_access.get_session() as session:
|
597
|
-
task = await data_access.toggle_scheduled_task(session, task_id)
|
598
|
-
|
599
|
-
if task:
|
600
|
-
return {
|
601
|
-
"success": True,
|
602
|
-
"data": {
|
603
|
-
"id": task["id"],
|
604
|
-
"is_active": task["enabled"] # 映射 enabled 到 is_active
|
605
|
-
},
|
606
|
-
"message": "定时任务状态已更新"
|
607
|
-
}
|
608
|
-
else:
|
609
|
-
raise HTTPException(status_code=404, detail="定时任务不存在")
|
610
|
-
except HTTPException:
|
611
|
-
raise
|
612
|
-
except Exception as e:
|
613
|
-
logger.error(f"切换定时任务状态失败: {e}")
|
614
|
-
traceback.print_exc()
|
615
|
-
raise HTTPException(status_code=500, detail=str(e))
|
616
|
-
|
617
|
-
|
618
|
-
@app.post("/api/scheduled-tasks/{scheduled_task_id}/execute")
|
619
|
-
async def execute_scheduled_task_now(scheduled_task_id: str):
|
620
|
-
"""立即执行定时任务"""
|
621
|
-
try:
|
622
|
-
# 使用真实的数据库操作获取任务信息
|
623
|
-
async with data_access.get_session() as session:
|
624
|
-
# 获取定时任务详情
|
625
|
-
task = await data_access.get_scheduled_task_by_id(session, scheduled_task_id)
|
626
|
-
|
627
|
-
if not task:
|
628
|
-
raise HTTPException(status_code=404, detail=f"定时任务 {scheduled_task_id} 不存在")
|
629
|
-
|
630
|
-
# 检查任务是否启用
|
631
|
-
if not task.get('enabled', False):
|
632
|
-
raise HTTPException(status_code=400, detail="任务已禁用,无法执行")
|
633
|
-
|
634
|
-
# 复用现有的任务发送机制
|
635
|
-
import sys
|
636
|
-
import os
|
637
|
-
# 添加jettask到Python路径
|
638
|
-
jettask_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
639
|
-
if jettask_path not in sys.path:
|
640
|
-
sys.path.insert(0, jettask_path)
|
641
|
-
|
642
|
-
|
643
|
-
from jettask import Jettask
|
644
|
-
|
645
|
-
# 获取任务的命名空间
|
646
|
-
namespace = task.get('namespace', 'default')
|
647
|
-
|
648
|
-
# 从命名空间数据访问获取该命名空间的配置
|
649
|
-
from namespace_data_access import get_namespace_data_access
|
650
|
-
namespace_access = get_namespace_data_access()
|
651
|
-
conn = await namespace_access.manager.get_connection(namespace)
|
652
|
-
|
653
|
-
# 获取该命名空间的Redis和PostgreSQL URL
|
654
|
-
redis_url = conn.redis_config.get('url') if conn.redis_config else None
|
655
|
-
pg_url = conn.pg_config.get('url') if conn.pg_config else None
|
656
|
-
|
657
|
-
if not redis_url:
|
658
|
-
raise HTTPException(status_code=500, detail=f"命名空间 {namespace} 没有配置Redis")
|
659
|
-
|
660
|
-
# 创建Jettask实例,使用正确的命名空间
|
661
|
-
jettask_app = Jettask(
|
662
|
-
redis_url=redis_url,
|
663
|
-
pg_url=pg_url,
|
664
|
-
redis_prefix=namespace # 使用命名空间作为Redis前缀
|
665
|
-
)
|
666
|
-
|
667
|
-
print(f"触发定时任务 {scheduled_task_id} (命名空间: {namespace}), 任务信息: {task}")
|
668
|
-
# 准备任务参数
|
669
|
-
task_args = task.get('task_args', [])
|
670
|
-
task_kwargs = task.get('task_kwargs', {})
|
671
|
-
|
672
|
-
# 使用TaskMessage和send_tasks发送任务
|
673
|
-
from jettask.core.message import TaskMessage
|
674
|
-
|
675
|
-
# 添加_task_name到kwargs中用于路由
|
676
|
-
task_kwargs['_task_name'] = task['task_name']
|
677
|
-
|
678
|
-
# 创建TaskMessage
|
679
|
-
task_msg = TaskMessage(
|
680
|
-
queue=task['queue_name'],
|
681
|
-
args=task_args,
|
682
|
-
kwargs=task_kwargs,
|
683
|
-
scheduled_task_id=scheduled_task_id
|
684
|
-
)
|
685
|
-
|
686
|
-
# 发送任务
|
687
|
-
event_ids = await jettask_app.send_tasks([task_msg])
|
688
|
-
event_id = event_ids[0] if event_ids else None
|
689
|
-
|
690
|
-
|
691
|
-
# 记录执行历史
|
692
|
-
logger.info(f"定时任务 {scheduled_task_id} (命名空间: {namespace}) 已手动触发,event_id: {event_id}")
|
693
|
-
|
694
|
-
return {
|
695
|
-
"success": True,
|
696
|
-
"message": f"任务已成功触发",
|
697
|
-
"event_id": str(event_id) if event_id else None,
|
698
|
-
"task_name": task['task_name'],
|
699
|
-
"queue_name": task['queue_name'],
|
700
|
-
"namespace": namespace
|
701
|
-
}
|
702
|
-
|
703
|
-
except HTTPException:
|
704
|
-
raise
|
705
|
-
except Exception as e:
|
706
|
-
import traceback
|
707
|
-
traceback.print_exc()
|
708
|
-
logger.error(f"执行定时任务失败: {e}")
|
709
|
-
raise HTTPException(status_code=500, detail=str(e))
|
710
|
-
|
711
|
-
|
712
|
-
@app.get("/api/scheduled-tasks/{task_id}/history")
|
713
|
-
async def get_scheduled_task_history(
|
714
|
-
task_id: str,
|
715
|
-
page: int = 1,
|
716
|
-
page_size: int = 20
|
717
|
-
):
|
718
|
-
"""获取定时任务执行历史"""
|
719
|
-
try:
|
720
|
-
# 使用真实的数据库操作
|
721
|
-
async with data_access.get_session() as session:
|
722
|
-
history, total = await data_access.fetch_task_execution_history(
|
723
|
-
session=session,
|
724
|
-
task_id=task_id,
|
725
|
-
page=page,
|
726
|
-
page_size=page_size
|
727
|
-
)
|
728
|
-
|
729
|
-
return {
|
730
|
-
"success": True,
|
731
|
-
"data": history,
|
732
|
-
"total": total,
|
733
|
-
"page": page,
|
734
|
-
"page_size": page_size
|
735
|
-
}
|
736
|
-
except Exception as e:
|
737
|
-
logger.error(f"获取定时任务执行历史失败: {e}")
|
738
|
-
traceback.print_exc()
|
739
|
-
raise HTTPException(status_code=500, detail=str(e))
|
740
|
-
|
741
|
-
|
742
|
-
@app.get("/api/scheduled-tasks/{task_id}/execution-trend")
|
743
|
-
async def get_scheduled_task_execution_trend(
|
744
|
-
task_id: str,
|
745
|
-
time_range: str = "7d"
|
746
|
-
):
|
747
|
-
"""获取定时任务执行趋势"""
|
748
|
-
try:
|
749
|
-
# 使用真实的数据库操作
|
750
|
-
async with data_access.get_session() as session:
|
751
|
-
data = await data_access.fetch_task_execution_trend(
|
752
|
-
session=session,
|
753
|
-
task_id=task_id,
|
754
|
-
time_range=time_range
|
755
|
-
)
|
756
|
-
|
757
|
-
return {
|
758
|
-
"success": True,
|
759
|
-
"data": data,
|
760
|
-
"time_range": time_range
|
761
|
-
}
|
762
|
-
except Exception as e:
|
763
|
-
logger.error(f"获取定时任务执行趋势失败: {e}")
|
764
|
-
traceback.print_exc()
|
765
|
-
raise HTTPException(status_code=500, detail=str(e))
|
766
|
-
|
767
|
-
|
768
|
-
# 路由已在 api/__init__.py 中注册
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
# ============= 告警管理API =============
|
777
|
-
|
778
|
-
# AlertRuleRequest 已从 schemas 模块导入
|
779
|
-
|
780
|
-
|
781
|
-
@app.get("/api/alert-rules")
|
782
|
-
async def get_alert_rules(
|
783
|
-
page: int = 1,
|
784
|
-
page_size: int = 20,
|
785
|
-
is_active: Optional[bool] = None
|
786
|
-
):
|
787
|
-
"""获取告警规则列表"""
|
788
|
-
try:
|
789
|
-
# 模拟数据
|
790
|
-
rules = [
|
791
|
-
{
|
792
|
-
"id": "rule_001",
|
793
|
-
"name": "队列积压告警",
|
794
|
-
"rule_type": "queue_size",
|
795
|
-
"target_queues": ["order_queue", "payment_queue"],
|
796
|
-
"condition": {"threshold": 1000, "operator": ">"},
|
797
|
-
"action_type": "webhook",
|
798
|
-
"action_config": {"url": "https://example.com/webhook"},
|
799
|
-
"is_active": True,
|
800
|
-
"last_triggered": "2025-08-31T14:30:00Z",
|
801
|
-
"created_at": "2025-08-01T10:00:00Z",
|
802
|
-
"description": "当队列积压超过1000时触发告警"
|
803
|
-
},
|
804
|
-
{
|
805
|
-
"id": "rule_002",
|
806
|
-
"name": "高错误率告警",
|
807
|
-
"rule_type": "error_rate",
|
808
|
-
"target_queues": ["*"], # 所有队列
|
809
|
-
"condition": {"threshold": 0.1, "operator": ">", "window": 300},
|
810
|
-
"action_type": "email",
|
811
|
-
"action_config": {"recipients": ["admin@example.com"]},
|
812
|
-
"is_active": True,
|
813
|
-
"last_triggered": None,
|
814
|
-
"created_at": "2025-08-15T14:00:00Z",
|
815
|
-
"description": "5分钟内错误率超过10%时告警"
|
816
|
-
},
|
817
|
-
{
|
818
|
-
"id": "rule_003",
|
819
|
-
"name": "响应时间告警",
|
820
|
-
"rule_type": "response_time",
|
821
|
-
"target_queues": ["api_queue"],
|
822
|
-
"condition": {"threshold": 5000, "operator": ">", "percentile": 95},
|
823
|
-
"action_type": "webhook",
|
824
|
-
"action_config": {"url": "https://slack.com/webhook"},
|
825
|
-
"is_active": False,
|
826
|
-
"last_triggered": "2025-08-30T09:00:00Z",
|
827
|
-
"created_at": "2025-07-01T08:00:00Z",
|
828
|
-
"description": "95分位响应时间超过5秒时告警"
|
829
|
-
}
|
830
|
-
]
|
831
|
-
|
832
|
-
# 过滤
|
833
|
-
if is_active is not None:
|
834
|
-
rules = [r for r in rules if r["is_active"] == is_active]
|
835
|
-
|
836
|
-
# 分页
|
837
|
-
total = len(rules)
|
838
|
-
start = (page - 1) * page_size
|
839
|
-
end = start + page_size
|
840
|
-
paginated_rules = rules[start:end]
|
841
|
-
|
842
|
-
return {
|
843
|
-
"success": True,
|
844
|
-
"data": paginated_rules,
|
845
|
-
"total": total,
|
846
|
-
"page": page,
|
847
|
-
"page_size": page_size
|
848
|
-
}
|
849
|
-
except Exception as e:
|
850
|
-
logger.error(f"获取告警规则列表失败: {e}")
|
851
|
-
traceback.print_exc()
|
852
|
-
raise HTTPException(status_code=500, detail=str(e))
|
853
|
-
|
854
|
-
|
855
|
-
@app.post("/api/alert-rules")
|
856
|
-
async def create_alert_rule(request: AlertRuleRequest):
|
857
|
-
"""创建告警规则"""
|
858
|
-
try:
|
859
|
-
rule = {
|
860
|
-
"id": f"rule_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
861
|
-
"name": request.name,
|
862
|
-
"rule_type": request.rule_type,
|
863
|
-
"target_queues": request.target_queues,
|
864
|
-
"condition": request.condition,
|
865
|
-
"action_type": request.action_type,
|
866
|
-
"action_config": request.action_config,
|
867
|
-
"is_active": request.is_active,
|
868
|
-
"description": request.description,
|
869
|
-
"check_interval": request.check_interval,
|
870
|
-
"created_at": datetime.now(timezone.utc).isoformat()
|
871
|
-
}
|
872
|
-
|
873
|
-
return {
|
874
|
-
"success": True,
|
875
|
-
"data": rule,
|
876
|
-
"message": "告警规则创建成功"
|
877
|
-
}
|
878
|
-
except Exception as e:
|
879
|
-
logger.error(f"创建告警规则失败: {e}")
|
880
|
-
traceback.print_exc()
|
881
|
-
raise HTTPException(status_code=500, detail=str(e))
|
882
|
-
|
883
|
-
|
884
|
-
@app.put("/api/alert-rules/{rule_id}")
|
885
|
-
async def update_alert_rule(rule_id: str, request: AlertRuleRequest):
|
886
|
-
"""更新告警规则"""
|
887
|
-
try:
|
888
|
-
rule = {
|
889
|
-
"id": rule_id,
|
890
|
-
"name": request.name,
|
891
|
-
"rule_type": request.rule_type,
|
892
|
-
"target_queues": request.target_queues,
|
893
|
-
"condition": request.condition,
|
894
|
-
"action_type": request.action_type,
|
895
|
-
"action_config": request.action_config,
|
896
|
-
"is_active": request.is_active,
|
897
|
-
"description": request.description,
|
898
|
-
"check_interval": request.check_interval,
|
899
|
-
"updated_at": datetime.now(timezone.utc).isoformat()
|
900
|
-
}
|
901
|
-
|
902
|
-
return {
|
903
|
-
"success": True,
|
904
|
-
"data": rule,
|
905
|
-
"message": "告警规则更新成功"
|
906
|
-
}
|
907
|
-
except Exception as e:
|
908
|
-
logger.error(f"更新告警规则失败: {e}")
|
909
|
-
traceback.print_exc()
|
910
|
-
raise HTTPException(status_code=500, detail=str(e))
|
911
|
-
|
912
|
-
|
913
|
-
@app.delete("/api/alert-rules/{rule_id}")
|
914
|
-
async def delete_alert_rule(rule_id: str):
|
915
|
-
"""删除告警规则"""
|
916
|
-
try:
|
917
|
-
return {
|
918
|
-
"success": True,
|
919
|
-
"message": f"告警规则 {rule_id} 已删除"
|
920
|
-
}
|
921
|
-
except Exception as e:
|
922
|
-
logger.error(f"删除告警规则失败: {e}")
|
923
|
-
traceback.print_exc()
|
924
|
-
raise HTTPException(status_code=500, detail=str(e))
|
925
|
-
|
926
|
-
|
927
|
-
@app.post("/api/alert-rules/{rule_id}/toggle")
|
928
|
-
async def toggle_alert_rule(rule_id: str):
|
929
|
-
"""启用/禁用告警规则"""
|
930
|
-
try:
|
931
|
-
return {
|
932
|
-
"success": True,
|
933
|
-
"data": {
|
934
|
-
"id": rule_id,
|
935
|
-
"is_active": True # 切换后的状态
|
936
|
-
},
|
937
|
-
"message": "告警规则状态已更新"
|
938
|
-
}
|
939
|
-
except Exception as e:
|
940
|
-
logger.error(f"切换告警规则状态失败: {e}")
|
941
|
-
traceback.print_exc()
|
942
|
-
raise HTTPException(status_code=500, detail=str(e))
|
943
|
-
|
944
|
-
|
945
|
-
@app.get("/api/alert-rules/{rule_id}/history")
|
946
|
-
async def get_alert_history(
|
947
|
-
rule_id: str,
|
948
|
-
page: int = 1,
|
949
|
-
page_size: int = 20
|
950
|
-
):
|
951
|
-
"""获取告警触发历史"""
|
952
|
-
try:
|
953
|
-
# 模拟告警历史数据
|
954
|
-
history = [
|
955
|
-
{
|
956
|
-
"id": f"alert_{i}",
|
957
|
-
"rule_id": rule_id,
|
958
|
-
"triggered_at": (datetime.now(timezone.utc) - timedelta(hours=i*2)).isoformat(),
|
959
|
-
"resolved_at": (datetime.now(timezone.utc) - timedelta(hours=i*2-1)).isoformat() if i % 2 == 0 else None,
|
960
|
-
"status": "resolved" if i % 2 == 0 else "active",
|
961
|
-
"trigger_value": 1200 + i * 100,
|
962
|
-
"notification_sent": True,
|
963
|
-
"notification_response": {"status": "success"}
|
964
|
-
}
|
965
|
-
for i in range(1, 11)
|
966
|
-
]
|
967
|
-
|
968
|
-
# 分页
|
969
|
-
total = len(history)
|
970
|
-
start = (page - 1) * page_size
|
971
|
-
end = start + page_size
|
972
|
-
paginated_history = history[start:end]
|
973
|
-
|
974
|
-
return {
|
975
|
-
"success": True,
|
976
|
-
"data": paginated_history,
|
977
|
-
"total": total,
|
978
|
-
"page": page,
|
979
|
-
"page_size": page_size
|
980
|
-
}
|
981
|
-
except Exception as e:
|
982
|
-
logger.error(f"获取告警历史失败: {e}")
|
983
|
-
traceback.print_exc()
|
984
|
-
raise HTTPException(status_code=500, detail=str(e))
|
985
|
-
|
986
|
-
|
987
|
-
@app.post("/api/alert-rules/{rule_id}/test")
|
988
|
-
async def test_alert_rule(rule_id: str):
|
989
|
-
"""测试告警规则(发送测试通知)"""
|
990
|
-
try:
|
991
|
-
# 模拟测试告警
|
992
|
-
return {
|
993
|
-
"success": True,
|
994
|
-
"message": "测试通知已发送",
|
995
|
-
"data": {
|
996
|
-
"rule_id": rule_id,
|
997
|
-
"test_time": datetime.now(timezone.utc).isoformat(),
|
998
|
-
"notification_result": {
|
999
|
-
"status": "success",
|
1000
|
-
"response": {"code": 200, "message": "OK"}
|
1001
|
-
}
|
1002
|
-
}
|
1003
|
-
}
|
1004
|
-
except Exception as e:
|
1005
|
-
logger.error(f"测试告警规则失败: {e}")
|
1006
|
-
traceback.print_exc()
|
1007
|
-
raise HTTPException(status_code=500, detail=str(e))
|
1008
|
-
|
1009
|
-
|
1010
|
-
# ===== 新增API v2: 支持消费者组和优先级队列 =====
|
1011
|
-
|
1012
|
-
# 新的简洁路径
|
1013
|
-
@app.get("/api/data/queue-stats")
|
1014
|
-
async def get_queue_stats_simplified(
|
1015
|
-
namespace: str = "default",
|
1016
|
-
queue: Optional[str] = None,
|
1017
|
-
start_time: Optional[datetime] = None,
|
1018
|
-
end_time: Optional[datetime] = None,
|
1019
|
-
time_range: Optional[str] = None
|
1020
|
-
):
|
1021
|
-
"""
|
1022
|
-
获取队列统计信息 - 简化的API路径
|
1023
|
-
|
1024
|
-
参数:
|
1025
|
-
- namespace: 命名空间,默认为'default'
|
1026
|
-
- queue: 可选,筛选特定队列
|
1027
|
-
- start_time: 开始时间
|
1028
|
-
- end_time: 结束时间
|
1029
|
-
- time_range: 时间范围(如'15m', '1h', '24h')
|
1030
|
-
"""
|
1031
|
-
# 直接调用原有的函数
|
1032
|
-
return await get_queue_stats_v2(namespace, queue, start_time, end_time, time_range)
|
1033
|
-
|
1034
|
-
# 保留原有路径以保证兼容性
|
1035
|
-
@app.get("/api/v2/namespaces/{namespace}/queues/stats")
|
1036
|
-
async def get_queue_stats_v2(
|
1037
|
-
namespace: str,
|
1038
|
-
queue: Optional[str] = None,
|
1039
|
-
start_time: Optional[datetime] = None,
|
1040
|
-
end_time: Optional[datetime] = None,
|
1041
|
-
time_range: Optional[str] = None
|
1042
|
-
):
|
1043
|
-
"""
|
1044
|
-
获取队列统计信息v2 - 支持消费者组详情和优先级队列
|
1045
|
-
|
1046
|
-
参数:
|
1047
|
-
- queue: 可选,筛选特定队列
|
1048
|
-
- start_time: 开始时间
|
1049
|
-
- end_time: 结束时间
|
1050
|
-
- time_range: 时间范围(如'15m', '1h', '24h')
|
1051
|
-
"""
|
1052
|
-
try:
|
1053
|
-
# 获取命名空间连接
|
1054
|
-
conn = await namespace_data_access.manager.get_connection(namespace)
|
1055
|
-
|
1056
|
-
# 获取Redis客户端
|
1057
|
-
redis_client = await conn.get_redis_client(decode=False)
|
1058
|
-
|
1059
|
-
# 获取PostgreSQL会话(可选)
|
1060
|
-
pg_session = None
|
1061
|
-
if conn.AsyncSessionLocal:
|
1062
|
-
pg_session = conn.AsyncSessionLocal()
|
1063
|
-
|
1064
|
-
try:
|
1065
|
-
# 创建统计服务实例
|
1066
|
-
stats_service = QueueStatsV2(
|
1067
|
-
redis_client=redis_client,
|
1068
|
-
pg_session=pg_session,
|
1069
|
-
redis_prefix=conn.redis_prefix
|
1070
|
-
)
|
1071
|
-
|
1072
|
-
# 处理时间筛选参数
|
1073
|
-
time_filter = None
|
1074
|
-
if time_range or start_time or end_time:
|
1075
|
-
time_filter = {}
|
1076
|
-
|
1077
|
-
# 如果提供了time_range,计算开始和结束时间
|
1078
|
-
if time_range and time_range != 'custom':
|
1079
|
-
now = datetime.now(timezone.utc)
|
1080
|
-
if time_range.endswith('m'):
|
1081
|
-
minutes = int(time_range[:-1])
|
1082
|
-
time_filter['start_time'] = now - timedelta(minutes=minutes)
|
1083
|
-
time_filter['end_time'] = now
|
1084
|
-
elif time_range.endswith('h'):
|
1085
|
-
hours = int(time_range[:-1])
|
1086
|
-
time_filter['start_time'] = now - timedelta(hours=hours)
|
1087
|
-
time_filter['end_time'] = now
|
1088
|
-
elif time_range.endswith('d'):
|
1089
|
-
days = int(time_range[:-1])
|
1090
|
-
time_filter['start_time'] = now - timedelta(days=days)
|
1091
|
-
time_filter['end_time'] = now
|
1092
|
-
else:
|
1093
|
-
# 使用提供的start_time和end_time
|
1094
|
-
if start_time:
|
1095
|
-
time_filter['start_time'] = start_time
|
1096
|
-
if end_time:
|
1097
|
-
time_filter['end_time'] = end_time
|
1098
|
-
|
1099
|
-
# 获取队列统计(使用分组格式)
|
1100
|
-
stats = await stats_service.get_queue_stats_grouped(time_filter)
|
1101
|
-
|
1102
|
-
# 如果指定了队列筛选,则过滤结果
|
1103
|
-
if queue:
|
1104
|
-
stats = [s for s in stats if s['queue_name'] == queue]
|
1105
|
-
|
1106
|
-
return {
|
1107
|
-
"success": True,
|
1108
|
-
"data": stats
|
1109
|
-
}
|
1110
|
-
|
1111
|
-
finally:
|
1112
|
-
if pg_session:
|
1113
|
-
await pg_session.close()
|
1114
|
-
await redis_client.aclose()
|
1115
|
-
|
1116
|
-
except Exception as e:
|
1117
|
-
logger.error(f"获取队列统计v2失败: {e}")
|
1118
|
-
traceback.print_exc()
|
1119
|
-
raise HTTPException(status_code=500, detail=str(e))
|
1120
|
-
|
1121
|
-
|
1122
|
-
@app.post("/api/v2/namespaces/{namespace}/tasks")
|
1123
|
-
async def get_tasks_v2(namespace: str, request: Request):
|
1124
|
-
"""
|
1125
|
-
获取任务列表v2 - 支持tasks和task_runs表连表查询
|
1126
|
-
"""
|
1127
|
-
try:
|
1128
|
-
# 解析请求体
|
1129
|
-
body = await request.json()
|
1130
|
-
queue_name = body.get('queue_name')
|
1131
|
-
page = body.get('page', 1)
|
1132
|
-
page_size = body.get('page_size', 20)
|
1133
|
-
filters = body.get('filters', [])
|
1134
|
-
|
1135
|
-
if not queue_name:
|
1136
|
-
raise HTTPException(status_code=400, detail="queue_name is required")
|
1137
|
-
|
1138
|
-
# 获取命名空间连接
|
1139
|
-
conn = await namespace_data_access.manager.get_connection(namespace)
|
1140
|
-
|
1141
|
-
if not conn.AsyncSessionLocal:
|
1142
|
-
raise HTTPException(status_code=400, detail="PostgreSQL not configured for this namespace")
|
1143
|
-
|
1144
|
-
# async with conn.AsyncSessionLocal() as session:
|
1145
|
-
# result = await get_task_details_v2(
|
1146
|
-
# pg_session=session,
|
1147
|
-
# queue_name=queue_name,
|
1148
|
-
# page=page,
|
1149
|
-
# page_size=page_size,
|
1150
|
-
# filters=filters
|
1151
|
-
# )
|
1152
|
-
|
1153
|
-
return {
|
1154
|
-
"success": True,
|
1155
|
-
"data": {"error": "get_task_details_v2 not implemented"}
|
1156
|
-
}
|
1157
|
-
|
1158
|
-
except HTTPException:
|
1159
|
-
raise
|
1160
|
-
except Exception as e:
|
1161
|
-
logger.error(f"获取任务列表v2失败: {e}")
|
1162
|
-
traceback.print_exc()
|
1163
|
-
raise HTTPException(status_code=500, detail=str(e))
|
1164
|
-
|
1165
|
-
|
1166
|
-
@app.get("/api/v2/namespaces/{namespace}/consumer-groups/{group_name}/stats")
|
1167
|
-
async def get_consumer_group_stats(namespace: str, group_name: str):
|
1168
|
-
"""
|
1169
|
-
获取特定消费者组的详细统计
|
1170
|
-
"""
|
1171
|
-
try:
|
1172
|
-
# 获取命名空间连接
|
1173
|
-
conn = await namespace_data_access.manager.get_connection(namespace)
|
1174
|
-
|
1175
|
-
# 获取PostgreSQL会话
|
1176
|
-
if not conn.AsyncSessionLocal:
|
1177
|
-
raise HTTPException(status_code=400, detail="PostgreSQL not configured for this namespace")
|
1178
|
-
|
1179
|
-
async with conn.AsyncSessionLocal() as session:
|
1180
|
-
# 查询消费者组的执行统计
|
1181
|
-
query = text("""
|
1182
|
-
WITH group_stats AS (
|
1183
|
-
SELECT
|
1184
|
-
tr.consumer_group,
|
1185
|
-
tr.task_name,
|
1186
|
-
COUNT(*) as total_tasks,
|
1187
|
-
COUNT(CASE WHEN tr.status = 'success' THEN 1 END) as success_count,
|
1188
|
-
COUNT(CASE WHEN tr.status = 'failed' THEN 1 END) as failed_count,
|
1189
|
-
COUNT(CASE WHEN tr.status = 'running' THEN 1 END) as running_count,
|
1190
|
-
AVG(tr.execution_time) as avg_execution_time,
|
1191
|
-
MIN(tr.execution_time) as min_execution_time,
|
1192
|
-
MAX(tr.execution_time) as max_execution_time,
|
1193
|
-
AVG(tr.duration) as avg_duration,
|
1194
|
-
MIN(tr.started_at) as first_task_time,
|
1195
|
-
MAX(tr.completed_at) as last_task_time
|
1196
|
-
FROM task_runs tr
|
1197
|
-
WHERE tr.consumer_group = :group_name
|
1198
|
-
AND tr.started_at > NOW() - INTERVAL '24 hours'
|
1199
|
-
GROUP BY tr.consumer_group, tr.task_name
|
1200
|
-
),
|
1201
|
-
hourly_stats AS (
|
1202
|
-
SELECT
|
1203
|
-
DATE_TRUNC('hour', tr.started_at) as hour,
|
1204
|
-
COUNT(*) as task_count,
|
1205
|
-
AVG(tr.execution_time) as avg_exec_time
|
1206
|
-
FROM task_runs tr
|
1207
|
-
WHERE tr.consumer_group = :group_name
|
1208
|
-
AND tr.started_at > NOW() - INTERVAL '24 hours'
|
1209
|
-
GROUP BY DATE_TRUNC('hour', tr.started_at)
|
1210
|
-
ORDER BY hour
|
1211
|
-
)
|
1212
|
-
SELECT
|
1213
|
-
(SELECT row_to_json(gs) FROM group_stats gs) as summary,
|
1214
|
-
(SELECT json_agg(hs) FROM hourly_stats hs) as hourly_trend
|
1215
|
-
""")
|
1216
|
-
|
1217
|
-
result = await session.execute(query, {'group_name': group_name})
|
1218
|
-
row = result.fetchone()
|
1219
|
-
|
1220
|
-
if not row or not row.summary:
|
1221
|
-
return {
|
1222
|
-
"success": True,
|
1223
|
-
"data": {
|
1224
|
-
"group_name": group_name,
|
1225
|
-
"summary": {},
|
1226
|
-
"hourly_trend": []
|
1227
|
-
}
|
1228
|
-
}
|
1229
|
-
|
1230
|
-
return {
|
1231
|
-
"success": True,
|
1232
|
-
"data": {
|
1233
|
-
"group_name": group_name,
|
1234
|
-
"summary": row.summary,
|
1235
|
-
"hourly_trend": row.hourly_trend or []
|
1236
|
-
}
|
1237
|
-
}
|
1238
|
-
|
1239
|
-
except Exception as e:
|
1240
|
-
logger.error(f"获取消费者组统计失败: {e}")
|
1241
|
-
traceback.print_exc()
|
1242
|
-
raise HTTPException(status_code=500, detail=str(e))
|
1243
|
-
|
1244
|
-
|
1245
|
-
# ============= Stream积压监控API =============
|
1246
|
-
|
1247
|
-
@app.get("/api/stream-backlog/{namespace}")
|
1248
|
-
async def get_stream_backlog(
|
1249
|
-
namespace: str,
|
1250
|
-
stream_name: Optional[str] = None,
|
1251
|
-
hours: int = 24
|
1252
|
-
):
|
1253
|
-
"""
|
1254
|
-
获取Stream积压监控数据
|
1255
|
-
|
1256
|
-
Args:
|
1257
|
-
namespace: 命名空间
|
1258
|
-
stream_name: 可选,指定stream名称
|
1259
|
-
hours: 查询最近多少小时的数据(默认24小时)
|
1260
|
-
"""
|
1261
|
-
try:
|
1262
|
-
from datetime import datetime, timedelta, timezone
|
1263
|
-
|
1264
|
-
# 计算时间范围
|
1265
|
-
end_time = datetime.now(timezone.utc)
|
1266
|
-
start_time = end_time - timedelta(hours=hours)
|
1267
|
-
|
1268
|
-
async with data_access.AsyncSessionLocal() as session:
|
1269
|
-
# 构建查询
|
1270
|
-
if stream_name:
|
1271
|
-
query = text("""
|
1272
|
-
SELECT
|
1273
|
-
stream_name,
|
1274
|
-
consumer_group,
|
1275
|
-
last_published_offset,
|
1276
|
-
last_delivered_offset,
|
1277
|
-
last_acked_offset,
|
1278
|
-
pending_count,
|
1279
|
-
backlog_undelivered,
|
1280
|
-
backlog_unprocessed,
|
1281
|
-
created_at
|
1282
|
-
FROM stream_backlog_monitor
|
1283
|
-
WHERE namespace = :namespace
|
1284
|
-
AND stream_name = :stream_name
|
1285
|
-
AND created_at >= :start_time
|
1286
|
-
AND created_at <= :end_time
|
1287
|
-
ORDER BY created_at DESC
|
1288
|
-
LIMIT 1000
|
1289
|
-
""")
|
1290
|
-
params = {
|
1291
|
-
'namespace': namespace,
|
1292
|
-
'stream_name': stream_name,
|
1293
|
-
'start_time': start_time,
|
1294
|
-
'end_time': end_time
|
1295
|
-
}
|
1296
|
-
else:
|
1297
|
-
# 获取最新的所有stream数据
|
1298
|
-
query = text("""
|
1299
|
-
SELECT DISTINCT ON (stream_name, consumer_group)
|
1300
|
-
stream_name,
|
1301
|
-
consumer_group,
|
1302
|
-
last_published_offset,
|
1303
|
-
last_delivered_offset,
|
1304
|
-
last_acked_offset,
|
1305
|
-
pending_count,
|
1306
|
-
backlog_undelivered,
|
1307
|
-
backlog_unprocessed,
|
1308
|
-
created_at
|
1309
|
-
FROM stream_backlog_monitor
|
1310
|
-
WHERE namespace = :namespace
|
1311
|
-
AND created_at >= :start_time
|
1312
|
-
ORDER BY stream_name, consumer_group, created_at DESC
|
1313
|
-
""")
|
1314
|
-
params = {
|
1315
|
-
'namespace': namespace,
|
1316
|
-
'start_time': start_time
|
1317
|
-
}
|
1318
|
-
|
1319
|
-
result = await session.execute(query, params)
|
1320
|
-
rows = result.fetchall()
|
1321
|
-
|
1322
|
-
# 格式化数据
|
1323
|
-
data = []
|
1324
|
-
for row in rows:
|
1325
|
-
data.append({
|
1326
|
-
'stream_name': row.stream_name,
|
1327
|
-
'consumer_group': row.consumer_group,
|
1328
|
-
'last_published_offset': row.last_published_offset,
|
1329
|
-
'last_delivered_offset': row.last_delivered_offset,
|
1330
|
-
'last_acked_offset': row.last_acked_offset,
|
1331
|
-
'pending_count': row.pending_count,
|
1332
|
-
'backlog_undelivered': row.backlog_undelivered,
|
1333
|
-
'backlog_unprocessed': row.backlog_unprocessed,
|
1334
|
-
'created_at': row.created_at.isoformat() if row.created_at else None
|
1335
|
-
})
|
1336
|
-
|
1337
|
-
return {
|
1338
|
-
'success': True,
|
1339
|
-
'data': data,
|
1340
|
-
'total': len(data)
|
1341
|
-
}
|
1342
|
-
|
1343
|
-
except Exception as e:
|
1344
|
-
logger.error(f"获取Stream积压监控数据失败: {e}")
|
1345
|
-
traceback.print_exc()
|
1346
|
-
raise HTTPException(status_code=500, detail=str(e))
|
1347
|
-
|
1348
|
-
|
1349
|
-
@app.get("/api/stream-backlog/{namespace}/summary")
|
1350
|
-
async def get_stream_backlog_summary(namespace: str):
|
1351
|
-
"""
|
1352
|
-
获取Stream积压监控汇总数据
|
1353
|
-
|
1354
|
-
Args:
|
1355
|
-
namespace: 命名空间
|
1356
|
-
"""
|
1357
|
-
try:
|
1358
|
-
async with data_access.AsyncSessionLocal() as session:
|
1359
|
-
# 获取最新的汇总数据
|
1360
|
-
query = text("""
|
1361
|
-
WITH latest_data AS (
|
1362
|
-
SELECT DISTINCT ON (stream_name, consumer_group)
|
1363
|
-
stream_name,
|
1364
|
-
consumer_group,
|
1365
|
-
backlog_undelivered,
|
1366
|
-
backlog_unprocessed,
|
1367
|
-
pending_count
|
1368
|
-
FROM stream_backlog_monitor
|
1369
|
-
WHERE namespace = :namespace
|
1370
|
-
AND created_at >= NOW() - INTERVAL '1 hour'
|
1371
|
-
ORDER BY stream_name, consumer_group, created_at DESC
|
1372
|
-
)
|
1373
|
-
SELECT
|
1374
|
-
COUNT(DISTINCT stream_name) as total_streams,
|
1375
|
-
COUNT(DISTINCT consumer_group) as total_groups,
|
1376
|
-
SUM(backlog_unprocessed) as total_backlog,
|
1377
|
-
SUM(pending_count) as total_pending,
|
1378
|
-
MAX(backlog_unprocessed) as max_backlog
|
1379
|
-
FROM latest_data
|
1380
|
-
""")
|
1381
|
-
|
1382
|
-
result = await session.execute(query, {'namespace': namespace})
|
1383
|
-
row = result.fetchone()
|
1384
|
-
|
1385
|
-
if row:
|
1386
|
-
return {
|
1387
|
-
'success': True,
|
1388
|
-
'data': {
|
1389
|
-
'total_streams': row.total_streams or 0,
|
1390
|
-
'total_groups': row.total_groups or 0,
|
1391
|
-
'total_backlog': row.total_backlog or 0,
|
1392
|
-
'total_pending': row.total_pending or 0,
|
1393
|
-
'max_backlog': row.max_backlog or 0
|
1394
|
-
}
|
1395
|
-
}
|
1396
|
-
else:
|
1397
|
-
return {
|
1398
|
-
'success': True,
|
1399
|
-
'data': {
|
1400
|
-
'total_streams': 0,
|
1401
|
-
'total_groups': 0,
|
1402
|
-
'total_backlog': 0,
|
1403
|
-
'total_pending': 0,
|
1404
|
-
'max_backlog': 0
|
1405
|
-
}
|
1406
|
-
}
|
1407
|
-
|
1408
|
-
except Exception as e:
|
1409
|
-
logger.error(f"获取Stream积压监控汇总失败: {e}")
|
1410
|
-
traceback.print_exc()
|
1411
|
-
raise HTTPException(status_code=500, detail=str(e))
|
1412
|
-
|
1413
|
-
|
1414
|
-
def run_server():
|
1415
|
-
"""运行 Web UI 服务器"""
|
1416
|
-
import uvicorn
|
1417
|
-
uvicorn.run(
|
1418
|
-
app,
|
1419
|
-
host="0.0.0.0",
|
1420
|
-
port=8001,
|
1421
|
-
log_level="info",
|
1422
|
-
reload=False
|
1423
|
-
)
|
1424
|
-
|
1425
|
-
if __name__ == "__main__":
|
1426
|
-
run_server()
|