python实现的websocket日志类

背景

功能需求需要实现一个"实时日志"功能,即在一个可以在web端触发任务的系统中,可以即时显示触发的任务的日志(此后台任务在后台或其他worker主机上执行)。最后采用了websocket方法来实现该功能,即在任务执行端实现一个logger类通过websocket上传实时日志给web后端,再由web后端通过websocket连接传给web前端,实现实时日志的功能。

websocket logHandler类

协程版本

使用websockets库

python 复制代码
import logging
import json
import time
import asyncio
import websockets
from queue import Queue
from threading import Thread
import traceback
from _queue import Empty

class WebSocketHandler(logging.Handler):
    """
    自定义日志处理器,将日志通过WebSocket发送到后端服务
    """
    def __init__(self, ws_url, reconnect_interval=5, max_queue_size=10000):
        """
        初始化WebSocket处理器
        
        Args:
            ws_url: WebSocket服务器URL
            reconnect_interval: 断线重连间隔(秒)
            max_queue_size: 日志队列最大长度,超出时丢弃旧日志
        """
        super().__init__()
        self.ws_url = ws_url
        self.reconnect_interval = reconnect_interval
        self.max_queue_size = max_queue_size
        self.is_running = False
        self.thread = None
        self.queue = None  # 异步队列,在start中初始化
        self.loop = None  # 保存事件循环引用
        
    def emit(self, record):
        """
        重写emit方法,将日志记录发送到WebSocket
        
        Args:
            record: 日志记录对象
        """
        try:
            # 格式化日志
            msg = self.format(record)
            if not msg.endswith("\n"):
                msg += "\n"
            # 跨线程安全添加日志到队列(关键修复)
            if self.loop and self.queue:
                # 使用事件循环的线程安全方法添加元素
                self.loop.call_soon_threadsafe(
                    self._safe_put_queue, msg
                )
            else:
                print("队列未初始化,日志发送失败")
        except Exception as e:
            # 处理发送失败的情况
            self.handleError(record)
    
    def _safe_put_queue(self, msg):
        """线程安全的队列添加方法(在事件循环线程执行)"""
        try:
            if not self.queue.full():
                self.queue.put_nowait(msg)
            else:
                # 队列满时丢弃最旧日志
                self.queue.get_nowait()
                self.queue.put_nowait(msg)
        except Exception as e:
            print(f"队列添加失败: {e}")

    
    def start(self):
        """启动WebSocket发送线程"""
        if not self.is_running:
            self.is_running = True
            self.thread = Thread(target=self._ws_sender_thread)
            self.thread.daemon = True
            self.thread.start()
            print("WebSocket发送线程启动")
    
    def stop(self):
        """停止WebSocket发送线程"""
        self.is_running = False
        if self.thread and self.thread.is_alive():
            self.thread.join(timeout=2.0)
    
    def _ws_sender_thread(self):
        """WebSocket发送线程主函数"""
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.queue = asyncio.Queue(maxsize=self.max_queue_size)
        
        async def custom_heartbeat(websocket):
            while True:
                try:
                    await websocket.send(json.dumps({"type":"ping"}))  # 自定义心跳消息
                    await asyncio.sleep(30)  # 30秒间隔
                except Exception as e:
                    print(f"心跳发送失败: {e}")
                    break

        async def _process_logs(websocket):
            # 连接成功后,发送队列中积压的所有日志
            while not self.queue.empty() and self.is_running:
                # log_data = self.queue.get_nowait()
                # await websocket.send(json.dumps(log_data, ensure_ascii=False))
                msg = await self.queue.get()
                await websocket.send(msg)
            
            # 持续发送新日志
            while self.is_running:
                try:
                    # 阻塞等待新日志,带超时以检查线程是否需要停止
                    # log_data = self.queue.get(timeout=1)
                    # await websocket.send(json.dumps(log_data))
                    msg = await self.queue.get()
                    await websocket.send(msg)
                    self.queue.task_done()
                except asyncio.TimeoutError:
                    continue
                except Empty:
                    continue

        async def send_logs():
            while self.is_running:
                try:
                    # 连接WebSocket服务器
                    async with websockets.connect(self.ws_url) as websocket:
                        # 并行运行日志发送和心跳任务
                        await asyncio.gather(
                            _process_logs(websocket),
                            custom_heartbeat(websocket)
                        )
                        
                except Exception as e:
                    traceback.print_exc()
                    # 连接失败或断开,等待后重试
                    # 等待重连间隔(使用异步sleep)
                    await asyncio.sleep(self.reconnect_interval)
        
        try:
            self.loop.run_until_complete(send_logs())
        except Exception as e:
            pass
        finally:
            self.loop.close()
    
    def close(self):
        """关闭处理器"""
        self.stop()
        super().close()

# 配置示例
def setup_logger():
    # 创建logger
    logger = logging.getLogger('my_app')
    logger.setLevel(logging.DEBUG)
    
    # 创建控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    console_handler.setFormatter(console_formatter)
    
    # 创建WebSocket处理器
    ws_handler = WebSocketHandler(ws_url='ws://localhost:8999/logs/websocket/client-2546')
    ws_handler.setLevel(logging.DEBUG)
    json_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')  # 我们在handler中自己处理JSON格式化
    ws_handler.setFormatter(json_formatter)
    
    # 添加处理器到logger
    logger.addHandler(console_handler)
    logger.addHandler(ws_handler)
    
    # 启动WebSocket处理器
    ws_handler.start()
    
    return logger

# 使用示例
if __name__ == "__main__":
    logger = setup_logger()
    
    try:
        # 正常记录日志,会同时输出到控制台和WebSocket
        logger.debug('这是一条调试日志')
        logger.info('这是一条信息日志')
        logger.warning('这是一条警告日志')
        logger.error('这是一条错误日志')
        
        # 模拟长时间运行的程序
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        # 程序退出时,确保WebSocket处理器正确关闭
        for handler in logger.handlers:
            if isinstance(handler, WebSocketHandler):
                handler.stop()

多线程版本

使用websocket-client库

python 复制代码
import logging
import json
import time
from queue import Queue, Empty
from threading import Thread
import traceback
import websocket  # 需安装:pip install websocket-client


class WebSocketHandler(logging.Handler):
    """自定义日志处理器(同步版本),通过WebSocket发送日志"""
    def __init__(self, ws_url, reconnect_interval=5, max_queue_size=10000):
        super().__init__()
        self.ws_url = ws_url
        self.reconnect_interval = reconnect_interval
        self.max_queue_size = max_queue_size
        self.is_running = False  # 控制整体运行状态
        self.heartbeat_running = False  # 控制心跳线程
        self.thread = None  # 日志发送主线程
        self.heartbeat_thread = None  # 心跳线程
        self.queue = Queue(maxsize=max_queue_size)  # 同步队列(线程安全)
        self.ws = None  # WebSocket连接实例

    def emit(self, record):
        """日志记录触发时调用,将日志放入队列"""
        try:
            # 格式化日志
            msg = self.format(record)
            if not msg.endswith("\n"):
                msg += "\n"
            
            # 队列满时丢弃最旧日志
            if self.queue.full():
                try:
                    self.queue.get_nowait()  # 移除最旧日志
                except Empty:
                    pass  # 队列已空,无需处理
            self.queue.put_nowait(msg)  # 放入新日志(同步队列线程安全)
        except Exception as e:
            self.handleError(record)
    
    def close(self):
        """关闭处理器"""
        self.stop()
        super().close()

    def start(self):
        """启动日志发送线程和心跳线程"""
        if not self.is_running:
            self.is_running = True
            # 启动日志发送主线程
            self.thread = Thread(target=self._ws_sender_thread)
            self.thread.daemon = True
            self.thread.start()
            print("WebSocket发送线程启动")

    def stop(self):
        """停止所有线程和连接"""
        self.is_running = False
        self.heartbeat_running = False  # 停止心跳线程
        # 关闭WebSocket连接
        if self.ws:
            try:
                self.ws.close()
            except Exception as e:
                print(f"关闭WebSocket失败: {e}")
        # 等待线程结束
        if self.thread and self.thread.is_alive():
            self.thread.join(timeout=2.0)
        if self.heartbeat_thread and self.heartbeat_thread.is_alive():
            self.heartbeat_thread.join(timeout=1.0)
        print("WebSocket发送线程已停止")

    def _start_heartbeat(self):
        """启动心跳线程"""
        self.heartbeat_running = True
        self.heartbeat_thread = Thread(target=self._heartbeat_loop)
        self.heartbeat_thread.daemon = True
        self.heartbeat_thread.start()
        print("心跳线程启动")

    def _heartbeat_loop(self):
        """心跳发送循环(独立线程)"""
        while self.heartbeat_running and self.is_running:
            try:
                if self.ws and self.ws.connected:  # 检查连接是否有效
                    self.ws.send(json.dumps({"type": "ping"}))  # 发送心跳
                time.sleep(30)  # 30秒间隔
            except Exception as e:
                print(f"心跳发送失败: {e}")
                break  # 心跳失败,退出循环(由主线程重连)

    def _process_logs(self):
        """处理队列中的日志并发送(同步阻塞)"""
        while self.is_running:
            try:
                # 阻塞等待日志(超时1秒,避免永久阻塞)
                msg = self.queue.get(timeout=1)
                if self.ws and self.ws.connected:
                    self.ws.send(msg)  # 发送日志
                    self.queue.task_done()
                else:
                    # 连接已断开,将日志放回队列
                    self.queue.put(msg)
                    time.sleep(0.1)  # 短暂等待后重试
            except Empty:
                continue  # 队列空,继续循环
            except Exception as e:
                print(f"日志发送失败: {e}")
                # 发送失败,将日志放回队列重试
                try:
                    self.queue.put(msg)
                except Exception as put_err:
                    print(f"日志放回队列失败: {put_err}")
                time.sleep(1)  # 等待后重试

    def _ws_sender_thread(self):
        """WebSocket发送主线程:负责连接和日志发送协调"""
        while self.is_running:
            try:
                # 建立WebSocket连接
                print(f"连接WebSocket服务器: {self.ws_url}")
                self.ws = websocket.create_connection(self.ws_url)
                print("WebSocket连接成功")

                # 启动心跳线程(每次重连后重启心跳)
                self._start_heartbeat()

                # 处理日志发送
                self._process_logs()

            except Exception as e:
                print(f"WebSocket连接/发送异常: {e}")
                traceback.print_exc()

            finally:
                # 连接断开时清理
                self.heartbeat_running = False  # 停止当前心跳线程
                if self.heartbeat_thread:
                    self.heartbeat_thread.join(timeout=1.0)
                if self.ws:
                    try:
                        self.ws.close()
                    except Exception as e:
                        print(f"关闭WebSocket连接失败: {e}")
                self.ws = None  # 重置连接实例

                # 断线重连等待
                if self.is_running:
                    print(f"等待{self.reconnect_interval}秒后重试...")
                    time.sleep(self.reconnect_interval)


# 配置示例
def setup_logger():
    # 创建logger
    logger = logging.getLogger('my_app')
    logger.setLevel(logging.DEBUG)
    
    # 创建控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    console_handler.setFormatter(console_formatter)
    
    # 创建WebSocket处理器(使用同步版本)
    ws_handler = WebSocketHandler(ws_url='ws://localhost:8999/logs/websocket/client-2546')
    ws_handler.setLevel(logging.DEBUG)
    ws_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    ws_handler.setFormatter(ws_formatter)
    
    # 添加处理器到logger
    logger.addHandler(console_handler)
    logger.addHandler(ws_handler)
    
    # 启动WebSocket处理器
    ws_handler.start()
    
    return logger


# 使用示例
if __name__ == "__main__":
    logger = setup_logger()
    
    try:
        # 测试日志发送
        logger.debug('这是一条调试日志')
        logger.info('这是一条信息日志')
        logger.warning('这是一条警告日志')
        logger.error('这是一条错误日志')
        
        # 模拟长时间运行的程序
        while True:
            logger.info('持续发送的日志...')
            time.sleep(5)  # 每5秒发送一条测试日志
    except KeyboardInterrupt:
        print("程序退出中...")
        # 停止WebSocket处理器
        for handler in logger.handlers:
            if isinstance(handler, WebSocketHandler):
                handler.stop()
        print("程序已退出")

集成

集成时只需将handler的示例加到全局logger中即可,就像main函数中setup_logger()的使用那样,但需注意正式使用时最好将handler.stop()函数放在finally块中,确保正确退出。

相关推荐
Python代狂魔10 分钟前
Redis
数据库·redis·python·缓存
ningqw22 分钟前
JWT 的使用
java·后端·springboot
追逐时光者44 分钟前
精选 2 款 .NET 开源、实用的缓存框架,帮助开发者更轻松地处理系统缓存!
后端·.net
David爱编程1 小时前
指令重排与内存屏障:并发语义的隐形守护者
java·后端
做科研的周师兄2 小时前
【机器学习入门】1.2 初识机器学习:从数据到智能的认知之旅
大数据·数据库·人工智能·python·机器学习·数据分析·机器人
胡gh2 小时前
数组开会:splice说它要动刀,map说它只想看看。
javascript·后端·面试
Pure_Eyes2 小时前
go 常见面试题
开发语言·后端·golang
王小王-1232 小时前
基于Python的游戏推荐与可视化系统的设计与实现
python·游戏·游戏推荐系统·游戏可视化
KevinWang_3 小时前
让 AI 写一个给图片加水印的 Python 脚本
python
Cisyam3 小时前
使用Bright Data API轻松构建LinkedIn职位数据采集系统
后端