如题,解决多进程写日志冲突的问题,用法和logging模块原生的TimedRotatingFileHandler一样,但是不支持按星期保留日志,也不支持utc参数,需要的老铁可以自己定制。
需要注意的是这里没有使用进程锁,打开文件必须使用"a+"模式,改的时候要注意别改错了。
# -*- coding:utf-8 -*-
import os
import time
import logging
import logging.config
from datetime import datetime
from multiprocessing import Process
try:
import codecs
except ImportError:
codecs = None
class TimedRotatingFileHandler(logging.FileHandler):
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False):
# 日志文件日期后缀
when_map = {'S': '%Y-%m-%d_%H-%M-%S', 'M': '%Y-%m-%d_%H-%M', 'H': '%Y-%m-%d_%H', 'D': '%Y-%m-%d'}
time_suffix = when_map.get(when.upper())
if not time_suffix:
raise ValueError('Error params when: %s' % when)
self.backupCount = backupCount
interval_map = {'S': 1, 'M': 60, 'H': 3600, 'D': 3600 * 24}
self.interval = interval_map[when.upper()] * interval
self.file_formater = '%s.%s' % (filename, time_suffix) # 拼接日志文件路径格式化字符串
self.filename = datetime.now().strftime(self.file_formater) # 使用当前时间生成日志文件路径
self.file_time = datetime.strptime(self.filename, self.file_formater) # 生成当前日志文件对应的时间戳
self.log_dir = os.path.dirname(os.path.abspath(self.filename)) # 获得日志文件夹路径
if not os.path.exists(self.log_dir): # 如果日志文件夹不存在,则创建文件夹
os.makedirs(self.log_dir)
if codecs is None:
encoding = None
super(TimedRotatingFileHandler, self).__init__(self.filename, 'a+', encoding, delay) # 没有加锁,必须使用a+模式打开
self.delete_expire_files() # 刷新日志文件
def should_change_file2_write(self, record):
record_time = datetime.fromtimestamp(record.created)
if (record_time - self.file_time).total_seconds() >= self.interval: # 如果当前时间超过指定步长
self.filename = record_time.strftime(self.file_formater) # 更新到最新的日志文件名
self.file_time = datetime.strptime(self.filename, self.file_formater)
return True
return False
def do_change_file(self):
self.baseFilename = os.path.abspath(self.filename) # 日志文件的绝对路径
if self.stream: # stream is not None 表示OutStream中还有未输出完的缓存数据
self.stream.close() # 将缓存写入文件并关闭文件
self.stream = None # 关闭stream后必须重新设置stream为None,否则会造成对已关闭文件进行IO操作
if not self.delay:
self.stream = self._open() # 打开新的日志文件文件流
self.delete_expire_files() # 删除多于保留个数的所有日志文件
def is_log_file(self, filename, file_match):
try:
if datetime.strptime(filename, file_match):
return True
except:
pass
def delete_expire_files(self):
if self.backupCount <= 0: # backupCount不大于0时表示持续保留日志
return
file_match = os.path.basename(self.file_formater)
result = [filename for filename in os.listdir(self.log_dir) if self.is_log_file(filename, file_match)] # 提取出日志文件名
for s in sorted(result)[:-self.backupCount]: # 删除多于保留文件个数的日志文件
try:
os.remove(os.path.join(self.log_dir, s))
except:
pass
def emit(self, record):
try:
if self.should_change_file2_write(record): # 判断是否需要删除旧文件增加新文件
self.do_change_file()
logging.FileHandler.emit(self, record) # 写入日志内容
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
def init_logger(logger_name, log_save_count=3):
logging.config.dictConfig({
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default_fmt': {
'format': '%(asctime)s - %(name)s - %(filename)s - %(funcName)s[line:%(lineno)d] - %(levelname)s - %(process)d - %(message)s',
# 'datefmt': '[%Y-%m-%d %H:%M:%S]'
}
},
'handlers': {
'root': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'default_fmt',
'stream': 'ext://sys.stderr'
},
'count': {
'class': 'main.TimedRotatingFileHandler',
'level': 'DEBUG',
'formatter': 'default_fmt',
'filename': 'logs/count.log',
'when': 'S',
'interval': 1,
'backupCount': log_save_count,
'encoding': 'utf8'
},
'range': {
'class': 'main.TimedRotatingFileHandler',
'level': 'DEBUG',
'formatter': 'default_fmt',
'filename': 'logs/range.log',
'when': 'S',
'interval': 1,
'backupCount': log_save_count,
'encoding': 'utf8'
}
},
'loggers': {
'count': {
'level': 'DEBUG',
'handlers': ['count'],
'propagate': 'no'
},
'range': {
'level': 'DEBUG',
'handlers': ['range'],
'propagate': 'no'
}
},
'root': {
'level': 'DEBUG',
'handlers': ['root'],
'propagate': 'no'
}
})
return logging.getLogger(logger_name)
def do_something(process_tag):
for range_tag in range(400): # 4个进程共1600行,1秒结束循环
count_logger.info('process_tag: %s, range_tag:%s.' % (process_tag, range_tag))
time.sleep(0.001)
for range_tag in range(5000): # 存在3个日志文件,5秒结束循环
range_logger.info('process_tag: %s, range_tag:%s.' % (process_tag, range_tag))
time.sleep(0.001)
if __name__ == '__main__':
# 生成全局logger,不要作为参数传到进程内部使用
count_logger = init_logger('range', log_save_count=3) # 测试日志行数是否有错漏
range_logger = init_logger('count', log_save_count=3) # 测试文件按时间循环处理逻辑
processes = [Process(target=do_something, args=(process_tag,)) for process_tag in range(4)]
for process in processes:
process.daemon = False # 设为非守护进程
process.start() # 启动子进程
for process in processes:
process.join() # 子进程阻塞父进程