Files
Neo-ZQYY/apps/backend/app/ws/logs.py
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

90 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""WebSocket 日志推送端点
提供 WS /ws/logs/{execution_id} 端点,实时推送 ETL 任务执行日志。
客户端连接后,先发送已有的历史日志行,再实时推送新日志,
直到执行结束(收到 None 哨兵)或客户端断开。
设计要点:
- 利用 TaskExecutor 已有的 subscribe/unsubscribe 机制
- 连接时先回放内存缓冲区中的历史日志,避免丢失已产生的行
- 通过 asyncio.Queue 接收实时日志None 表示执行结束
"""
from __future__ import annotations
import logging
import time
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from ..services.task_executor import task_executor
# CHANGE 2026-03-24 | dev-trace-log: WebSocket 连接生命周期追踪
from ..trace.ws_wrapper import ws_trace_connect, ws_trace_disconnect, ws_trace_message
logger = logging.getLogger(__name__)
ws_router = APIRouter()
@ws_router.websocket("/ws/logs/{execution_id}")
async def ws_logs(websocket: WebSocket, execution_id: str) -> None:
"""实时推送指定 execution_id 的任务执行日志。
流程:
1. 接受 WebSocket 连接
2. 回放内存缓冲区中已有的日志行
3. 订阅 TaskExecutor持续推送新日志
4. 收到 None执行结束或客户端断开时关闭
"""
await websocket.accept()
logger.info("WebSocket 连接已建立: execution_id=%s", execution_id)
# CHANGE 2026-03-24 | dev-trace-log: 记录 WS_CONNECT span
_t0 = time.perf_counter()
_ws_ctx = ws_trace_connect(execution_id, client_info=str(websocket.client))
_msg_count = 0
_total_bytes = 0
# 订阅日志流
queue = task_executor.subscribe(execution_id)
_disconnect_reason = "normal"
try:
# 回放已有的历史日志行
for line in task_executor.get_logs(execution_id):
await websocket.send_text(line)
_msg_count += 1
_total_bytes += len(line.encode("utf-8"))
ws_trace_message(_msg_count, _total_bytes)
# 如果任务已经不在运行且没有订阅者队列中的数据,
# 仍然保持连接等待——可能是任务刚结束但 queue 里还有未消费的消息
while True:
msg = await queue.get()
if msg is None:
# 执行结束哨兵
break
await websocket.send_text(msg)
_msg_count += 1
_total_bytes += len(msg.encode("utf-8"))
ws_trace_message(_msg_count, _total_bytes)
except WebSocketDisconnect:
_disconnect_reason = "client_disconnect"
logger.info("WebSocket 客户端断开: execution_id=%s", execution_id)
except Exception:
_disconnect_reason = "error"
logger.exception("WebSocket 异常: execution_id=%s", execution_id)
finally:
task_executor.unsubscribe(execution_id, queue)
# CHANGE 2026-03-24 | dev-trace-log: 记录 WS_DISCONNECT span 并写入日志
_duration_ms = (time.perf_counter() - _t0) * 1000
ws_trace_disconnect(_disconnect_reason, _msg_count, _duration_ms)
# 安全关闭连接(客户端可能已断开,忽略错误)
try:
await websocket.close()
except Exception:
pass
logger.info("WebSocket 连接已清理: execution_id=%s", execution_id)