python 日志模块 logging 支持多线程,但是在多进程下写入日志文件容易出现下面的问题:
PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。
也就是日志文件被占用的情况,原因是多个进程的文件 handler 对日志文件进行操作产生的。
这个问题经常在 TimedRotatingFileHandler、RotatingFileHandler 中出现。
解决办法
题主在网上搜集了各种解决上面问题的办法,基本以下面三个方向为主:
安装第三方库提供的 handler
重写 filehandler 加全局锁
使用队列将消息传递
但是三种方法各有小缺陷:
第三方库很久无人维护,且支持的功能比较单一,无法满足生产环境的需求。
轮转日志的时候由于全局锁的存在,其他子进程无法记录日志,有丢失日志的风险。
使用多进程消息队列的缺点在于使用困难,如果是多模块编程,需要将全局队列传来传去,在大型项目中显得很麻烦。
经过对官网的研究 ,题主无意中找到了一种非常方便且高效的方法,并且经过一定的修改使这种方法可用于分布式日志,且支持多语言日志的处理。
唯一的不足是需要新学习一个 zmq 通信协议,但是这并不是问题,如果只是想要一个解决方案并立即投入使用,只需要按照下面的方法编写,无需关注 zmq 的相关知识。
基于 zmq 的分布式日志
实现思路
看到这很多人可能明白了,这个方法类似官网提供的 SocketHandler,但本方法其实是基于 QueueHandler 实现的,有利于发挥 zmq 易用性、可插拔、并发性能好的优点。
代码实现
首先是集中处理日志的程序,也就是上面所说"多对一"中的一。
import zmq
import logging
from logging import handlers
class ZeroMQSocketListener(handlers.QueueListener):
def __init__(self, uri="tcp://127.0.0.1:5555", *handlers,**kwargs):
self.respect_handler_level = True # handler日志等级启用,允许对handler设置setLevel,False则忽视级别
self.ctx = kwargs.get('ctx') or zmq.Context()
socket = self.ctx.socket(zmq.SUB)
socket.bind(uri)
socket.setsockopt_string(zmq.SUBSCRIBE, '') # 订阅所有主题
super().__init__(socket, *handlers, respect_handler_level=self.respect_handler_level)
def dequeue(self,block):
msg = self.queue.recv_json()
# print('111',msg) # 测试用
return logging.makeLogRecord(msg)
def main_logger():
# 日志集中处理区,在主程序中调用一次
# handlers配置区,filter可选
formatter = logging.Formatter("%(name)s - %(asctime)s - %(levelname)s - %(module)s - %(funcName)s - %(message)s")
console = logging.StreamHandler()
console.setLevel(logging.ERROR)
ch = handlers.TimedRotatingFileHandler(r'logs\face.log',when='M',
# backupCount=180,
encoding='utf-8')
ch.setLevel(logging.INFO)
ch.setFormatter(formatter) # add formatter to ch
# 设置监听的端口,并传递handlers
loggerListener = ZeroMQSocketListener("tcp://127.0.0.1:5555",*(ch,console))
loggerListener.start() # 开启一个子线程处理记录器监听
# 主进程调用一次,非阻塞
main_logger()
复制代码
自此,日志集中处理就结束了,是不是很简单,而且需要注意,我们这里不需要用到 root logger,因为 ZeroMQSocketListener 会自动调用各种 handlers 将日志内容进行处理,想当于替代了 logger 的工作,所以也就没必要声明一个 logger 出来了。
更新:
这里的 main_logger()是非阻塞,也就是下面还可以写其他代码,但是如果什么代码都没有,那么主进程就会直接退出,日志就收不到了。
如果接下来不需要做其他工作,那么请在 main_logger()下方使用 while True:time.sleep(0.5) 将主进程阻塞。
接下来是子进程中或者是你想记录日志的任何地方,比如在其他同事的电脑里
import logging,zmq
from logging import handlers
# 我们需要的handler
class ZeroMQSocketHandler(handlers.QueueHandler):
def __init__(self, uri="tcp://127.0.0.1:5555", socktype=zmq.PUB, ctx=None):
self.ctx = ctx or zmq.Context()
socket = self.ctx.socket(socktype)
socket.connect(uri)
super().__init__(socket)
def enqueue(self, record):
self.queue.send_json(record.__dict__)
def close(self):
self.queue.close()
# 创建远端日志
rmtlogger = logging.getLogger('sub_root_name') ##
rmtlogger.setLevel(logging.INFO) # 建议设置一下,有时候默认是WARNING级别
rmtlogger.propagate=False # 不允许传递,日志传递到这里就发送到主进程中
# 配置handler
zmqhandler = ZeroMQSocketHandler()
zmqhandler.setLevel(logging.INFO)
rmtlogger.addHandler(zmqhandler)
# if you have submodule
# import submodule
# 记录日志
rmtlogger.info("这是一条遥远的日志")
复制代码
logger 可以通过 python 日志的 name 系统进行传递,也就是说如果子进程中还有其他模块,可以通过日志传递系统将其他模块产生的日志传递过来,最后一并发送给监听器,就像下面:
# subprocess.py的子模块,如需测试注意调用
import logging
subMolduleLogger = logging.getLogger(f'sub_root_name.modulename')
subMolduleLogger.info("这是一条子模块日志")
# 这部分内容需要logging基础知识
复制代码
在主进程中,设置了 logging.Formatter 对象,可以将产生日志的名字打印出来,用于区分日志产生的位置。
多语言支持
由于 zmq 本身就支持多语言,比如你使用 c 语言或其他语言,只需要在代码中使用 zmq 将日志通过 json 发送过来。
python 日志可以通过 dict 方法重建 logger 对象,具体可以打印上面代码中 ZeroMQSocketListener.dequeue 中的 msg 进行摸索,实现起来还是比较简单的。
总结
本篇所提供的多进程日志解决方法的目的是尽可能少做配置和修改,保留原有编程习惯的同时兼顾了代码的易用性。
评论