自定义python多进程下可用的TimedRotatingFileHandler,解决使用logging模块写日志冲突问题

如题,解决多进程写日志冲突的问题,用法和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()  # 子进程阻塞父进程

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章