feat: 添加 Redis 消息展示功能到监控面板
- 新增 /api/messages API 接口,支持从 Redis Stream 读取消息 - 支持按策略筛选消息和分页展示 - 前端新增消息列表卡片,展示时间、策略、股票代码、动作、价格和状态 - 自动判断消息处理状态(已处理/待处理) - 消息列表每30秒自动刷新,支持手动刷新
This commit is contained in:
@@ -49,6 +49,19 @@ class ConfigResponse(BaseModel):
|
||||
reconnect_time: str
|
||||
auto_reconnect_enabled: bool
|
||||
|
||||
class MessageItem(BaseModel):
|
||||
"""消息项模型"""
|
||||
message_id: str
|
||||
data: Dict[str, Any]
|
||||
timestamp: str
|
||||
is_processed: bool = False
|
||||
|
||||
class MessagesResponse(BaseModel):
|
||||
"""消息响应模型"""
|
||||
messages: List[MessageItem]
|
||||
total: int
|
||||
strategy_name: str
|
||||
|
||||
|
||||
class FileConfigResponse(BaseModel):
|
||||
"""配置文件响应模型"""
|
||||
@@ -325,6 +338,116 @@ class QMTAPIServer:
|
||||
raw_config=f"读取配置文件失败: {str(e)}",
|
||||
config_path=config_path
|
||||
)
|
||||
@self.app.get("/api/messages", response_model=MessagesResponse, summary="获取Redis消息")
|
||||
def get_messages(
|
||||
strategy: str = Query("all", description="策略名称,all表示所有策略"),
|
||||
count: int = Query(50, ge=1, le=200, description="获取消息数量"),
|
||||
is_backtest: bool = Query(False, description="是否为回测消息")
|
||||
):
|
||||
"""从Redis Stream获取消息列表"""
|
||||
messages = []
|
||||
|
||||
try:
|
||||
# 从manager获取redis连接
|
||||
if hasattr(self.manager, 'pos_manager') and self.manager.pos_manager:
|
||||
r = self.manager.pos_manager.r
|
||||
elif hasattr(self.manager, 'stream_processor') and self.manager.stream_processor:
|
||||
r = self.manager.stream_processor.r
|
||||
else:
|
||||
# 尝试从配置创建连接
|
||||
import redis
|
||||
redis_config = getattr(self.manager.config, 'redis', {})
|
||||
r = redis.Redis(
|
||||
host=redis_config.get('host', 'localhost'),
|
||||
port=redis_config.get('port', 6379),
|
||||
password=redis_config.get('password'),
|
||||
db=redis_config.get('db', 0),
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
if strategy == "all":
|
||||
# 获取所有策略的消息
|
||||
pattern = f"qmt:*:{'backtest' if is_backtest else 'real'}"
|
||||
stream_keys = []
|
||||
for key in r.scan_iter(match=pattern):
|
||||
stream_keys.append(key)
|
||||
else:
|
||||
# 获取指定策略的消息
|
||||
stream_key = f"qmt:{strategy}:{'backtest' if is_backtest else 'real'}"
|
||||
stream_keys = [stream_key]
|
||||
|
||||
# 从每个stream读取消息
|
||||
for stream_key in stream_keys:
|
||||
try:
|
||||
# 使用XREVRANGE获取最新消息
|
||||
stream_msgs = r.xrevrange(stream_key, max="+", min="-", count=count)
|
||||
|
||||
for msg_id, msg_fields in stream_msgs:
|
||||
# 解析消息数据
|
||||
data_str = msg_fields.get("data", "{}")
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
data = {"raw_data": data_str}
|
||||
|
||||
# 解析消息ID获取时间戳
|
||||
# Redis消息ID格式: timestamp-sequence
|
||||
timestamp_ms = int(msg_id.split("-")[0])
|
||||
timestamp = datetime.datetime.fromtimestamp(
|
||||
timestamp_ms / 1000
|
||||
).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 检查消息是否已处理(通过检查pending列表)
|
||||
is_processed = True
|
||||
try:
|
||||
# 获取消费者组的pending消息
|
||||
pending_info = r.xpending(
|
||||
stream_key,
|
||||
"qmt_consumers"
|
||||
)
|
||||
# 如果消息在pending列表中,说明还未确认
|
||||
if pending_info and pending_info.get('pending', 0) > 0:
|
||||
# 获取具体的pending消息ID
|
||||
pending_range = r.xpending_range(
|
||||
stream_key,
|
||||
"qmt_consumers",
|
||||
min="-",
|
||||
max="+",
|
||||
count=100
|
||||
)
|
||||
pending_ids = [item['message_id'] for item in pending_range] if pending_range else []
|
||||
is_processed = msg_id not in pending_ids
|
||||
except:
|
||||
# 如果没有消费者组或出错,默认认为已处理
|
||||
is_processed = True
|
||||
|
||||
messages.append(MessageItem(
|
||||
message_id=msg_id,
|
||||
data=data,
|
||||
timestamp=timestamp,
|
||||
is_processed=is_processed
|
||||
))
|
||||
except Exception as e:
|
||||
logging.error(f"读取stream {stream_key}失败: {e}")
|
||||
continue
|
||||
|
||||
# 按时间戳倒序排序
|
||||
messages.sort(key=lambda x: x.timestamp, reverse=True)
|
||||
|
||||
return MessagesResponse(
|
||||
messages=messages[:count],
|
||||
total=len(messages),
|
||||
strategy_name=strategy
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取消息失败: {e}")
|
||||
return MessagesResponse(
|
||||
messages=[],
|
||||
total=0,
|
||||
strategy_name=strategy
|
||||
)
|
||||
|
||||
|
||||
def get_app(self) -> FastAPI:
|
||||
"""获取FastAPI应用实例"""
|
||||
|
||||
Reference in New Issue
Block a user