如題,解決多進程寫日誌衝突的問題,用法和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() # 子進程阻塞父進程