1919# 全局handler实例,避免重复创建
2020_file_handler = None
2121_console_handler = None
22+ _ws_handler = None
2223
2324
2425def get_file_handler ():
@@ -59,6 +60,35 @@ def get_console_handler():
5960 return _console_handler
6061
6162
63+ def get_ws_handler ():
64+ """获取 WebSocket handler 单例"""
65+ global _ws_handler
66+ if _ws_handler is None :
67+ _ws_handler = WebSocketLogHandler ()
68+ # WebSocket handler 推送所有级别的日志
69+ _ws_handler .setLevel (logging .DEBUG )
70+ return _ws_handler
71+
72+
73+ def initialize_ws_handler (loop ):
74+ """初始化 WebSocket handler 的事件循环
75+
76+ Args:
77+ loop: asyncio 事件循环
78+ """
79+ handler = get_ws_handler ()
80+ handler .set_loop (loop )
81+
82+ # 为 WebSocket handler 设置 JSON 格式化器(与文件格式相同)
83+ handler .setFormatter (file_formatter )
84+
85+ # 添加到根日志记录器
86+ root_logger = logging .getLogger ()
87+ if handler not in root_logger .handlers :
88+ root_logger .addHandler (handler )
89+ print ("[日志系统] ✅ WebSocket 日志推送已启用" )
90+
91+
6292class TimestampedFileHandler (logging .Handler ):
6393 """基于时间戳的文件处理器,简单的轮转份数限制"""
6494
@@ -145,12 +175,78 @@ def close(self):
145175 super ().close ()
146176
147177
178+ class WebSocketLogHandler (logging .Handler ):
179+ """WebSocket 日志处理器 - 将日志实时推送到前端"""
180+
181+ _log_counter = 0 # 类级别计数器,确保 ID 唯一性
182+
183+ def __init__ (self , loop = None ):
184+ super ().__init__ ()
185+ self .loop = loop
186+ self ._initialized = False
187+
188+ def set_loop (self , loop ):
189+ """设置事件循环"""
190+ self .loop = loop
191+ self ._initialized = True
192+
193+ def emit (self , record ):
194+ """发送日志到 WebSocket 客户端"""
195+ if not self ._initialized or self .loop is None :
196+ return
197+
198+ try :
199+ # 获取格式化后的消息
200+ # 对于 structlog,formatted message 包含完整的日志信息
201+ formatted_msg = self .format (record ) if self .formatter else record .getMessage ()
202+
203+ # 如果是 JSON 格式(文件格式化器),解析它
204+ message = formatted_msg
205+ try :
206+ import json
207+ log_dict = json .loads (formatted_msg )
208+ message = log_dict .get ('event' , formatted_msg )
209+ except (json .JSONDecodeError , ValueError ):
210+ # 不是 JSON,直接使用消息
211+ message = formatted_msg
212+
213+ # 生成唯一 ID: 时间戳毫秒 + 自增计数器
214+ WebSocketLogHandler ._log_counter += 1
215+ log_id = f"{ int (record .created * 1000 )} _{ WebSocketLogHandler ._log_counter } "
216+
217+ # 格式化日志数据
218+ log_data = {
219+ "id" : log_id ,
220+ "timestamp" : datetime .fromtimestamp (record .created ).strftime ("%Y-%m-%d %H:%M:%S" ),
221+ "level" : record .levelname ,
222+ "module" : record .name ,
223+ "message" : message ,
224+ }
225+
226+ # 异步广播日志(不阻塞日志记录)
227+ try :
228+ import asyncio
229+ from src .webui .logs_ws import broadcast_log
230+
231+ asyncio .run_coroutine_threadsafe (
232+ broadcast_log (log_data ),
233+ self .loop
234+ )
235+ except Exception :
236+ # WebSocket 推送失败不影响日志记录
237+ pass
238+
239+ except Exception :
240+ # 不要让 WebSocket 错误影响日志系统
241+ self .handleError (record )
242+
243+
148244# 旧的轮转文件处理器已移除,现在使用基于时间戳的处理器
149245
150246
151247def close_handlers ():
152248 """安全关闭所有handler"""
153- global _file_handler , _console_handler
249+ global _file_handler , _console_handler , _ws_handler
154250
155251 if _file_handler :
156252 _file_handler .close ()
@@ -159,6 +255,10 @@ def close_handlers():
159255 if _console_handler :
160256 _console_handler .close ()
161257 _console_handler = None
258+
259+ if _ws_handler :
260+ _ws_handler .close ()
261+ _ws_handler = None
162262
163263
164264def remove_duplicate_handlers (): # sourcery skip: for-append-to-extend, list-comprehension
@@ -843,8 +943,8 @@ def cleanup_task():
843943
844944def shutdown_logging ():
845945 """优雅关闭日志系统,释放所有文件句柄"""
846- logger = get_logger ( "logger" )
847- logger . info ( " 正在关闭日志系统..." )
946+ # 先输出到控制台,避免日志系统关闭后无法输出
947+ print ( "[logger] 正在关闭日志系统..." )
848948
849949 # 关闭所有handler
850950 root_logger = logging .getLogger ()
@@ -865,4 +965,5 @@ def shutdown_logging():
865965 handler .close ()
866966 logger_obj .removeHandler (handler )
867967
868- logger .info ("日志系统已关闭" )
968+ # 使用 print 而不是 logger,因为 logger 已经关闭
969+ print ("[logger] 日志系统已关闭" )
0 commit comments