自定義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()  # 子進程阻塞父進程

 

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