python日志滾動-修復按天滾動bug
一、問題描述
python自帶的logging庫有一個問題,當日志滾動設置為24h時:
1、程序啟動后,連續運行時間超過24h
日志滾動分割正常。
2、程序啟動后,間斷運行(用完就關閉,之后再啟動),連續運行時間不足24h
日志不發生分割,直到連續運行超過24h,才可以發生日志文件的分割。
問題原因參考:https://blog.csdn.net/weixin_38107388/article/details/90639151
二、目的
自定義類MyTimedRotatingFileHandler,繼承logging的基礎類BaseRotatingHandler,實現間斷啟動,日志也能按天滾動分割。
同時實現每天的0點之后開始滾動。
三、操作實現
1、目錄結構
運行前
.test01/
|—— libs/
| └─ logRecord.py
└─ t1.py
運行后
.test01/
|—— libs/
| └─ logRecord.py
├─ log/
| └─ dd.log
└─ t1.py
第二天運行后
.test01/
|—— libs/
| └─ logRecord.py
├─ log/
| ├─ dd.log
| └─ dd.log.2021-08-08.log
└─ t1.py
2、編寫代碼文件
(1)新建日志模塊 logRecord.py
.test01/
|—— libs/
| └─ logRecord.py
logRecord.py:
import os
import logging
import logging.handlers
from stat import ST_CTIME
from logging.handlers import *
_MIDNIGHT = 24 * 60 * 60 # number of seconds in a day
# 自定義自己的TimedRotatingFileHandler類
class MyTimedRotatingFileHandler(BaseRotatingHandler):
"""解決程序二次啟動后無法按照天分割的問題。
繼承logging中的BaseRotatingHandler類,重寫TimedRotatingFileHandler的init方法,其他復制。
"""
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False,
atTime=None):
BaseRotatingHandler.__init__(self, filename, 'a', encoding, delay)
self.when = when.upper()
self.backupCount = backupCount
self.utc = utc
self.atTime = atTime
# Calculate the real rollover interval, which is just the number of
# seconds between rollovers. Also set the filename suffix used when
# a rollover occurs. Current 'when' events supported:
# S - Seconds
# M - Minutes
# H - Hours
# D - Days
# midnight - roll over at midnight
# W{0-6} - roll over on a certain day; 0 - Monday
#
# Case of the 'when' specifier is not important; lower or upper case
# will work.
if self.when == 'S':
self.interval = 1 # one second
self.suffix = "%Y-%m-%d_%H-%M-%S"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$"
elif self.when == 'M':
self.interval = 60 # one minute
self.suffix = "%Y-%m-%d_%H-%M"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$"
elif self.when == 'H':
self.interval = 60 * 60 # one hour
self.suffix = "%Y-%m-%d_%H"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$"
elif self.when == 'D' or self.when == 'MIDNIGHT':
self.interval = 60 * 60 * 24 # one day
self.suffix = "%Y-%m-%d"
self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
elif self.when.startswith('W'):
self.interval = 60 * 60 * 24 * 7 # one week
if len(self.when) != 2:
raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when)
if self.when[1] < '0' or self.when[1] > '6':
raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
self.dayOfWeek = int(self.when[1])
self.suffix = "%Y-%m-%d"
self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
else:
raise ValueError("Invalid rollover interval specified: %s" % self.when)
self.extMatch = re.compile(self.extMatch, re.ASCII)
self.interval = self.interval * interval # multiply by units requested
# The following line added because the filename passed in could be a
# path object (see Issue #27493), but self.baseFilename will be a string
filename = self.baseFilename
if os.path.exists(filename):
t = os.stat(filename)[ST_CTIME] # 我修改過的地方。ST_MTIME ==> ST_CTIME
else:
t = int(time.time())
self.rolloverAt = self.computeRollover(t)
def computeRollover(self, currentTime):
"""
Work out the rollover time based on the specified time.
"""
result = currentTime + self.interval
if self.when == 'MIDNIGHT' or self.when.startswith('W'):
# This could be done with less code, but I wanted it to be clear
if self.utc:
t = time.gmtime(currentTime)
else:
t = time.localtime(currentTime)
currentHour = t[3]
currentMinute = t[4]
currentSecond = t[5]
currentDay = t[6]
# r is the number of seconds left between now and the next rotation
if self.atTime is None:
rotate_ts = _MIDNIGHT
else:
rotate_ts = ((self.atTime.hour * 60 + self.atTime.minute) * 60 +
self.atTime.second)
r = rotate_ts - ((currentHour * 60 + currentMinute) * 60 +
currentSecond)
if r < 0:
# Rotate time is before the current time (for example when
# self.rotateAt is 13:45 and it now 14:15), rotation is
# tomorrow.
r += _MIDNIGHT
currentDay = (currentDay + 1) % 7
result = currentTime + r
if self.when.startswith('W'):
day = currentDay # 0 is Monday
if day != self.dayOfWeek:
if day < self.dayOfWeek:
daysToWait = self.dayOfWeek - day
else:
daysToWait = 6 - day + self.dayOfWeek + 1
newRolloverAt = result + (daysToWait * (60 * 60 * 24))
if not self.utc:
dstNow = t[-1]
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
result = newRolloverAt
return result
def shouldRollover(self, record):
"""
Determine if rollover should occur.
record is not used, as we are just comparing times, but it is needed so
the method signatures are the same
"""
t = int(time.time())
if t >= self.rolloverAt:
return 1
return 0
def getFilesToDelete(self):
"""
Determine the files to delete when rolling over.
More specific than the earlier method, which just used glob.glob().
"""
dirName, baseName = os.path.split(self.baseFilename)
fileNames = os.listdir(dirName)
result = []
prefix = baseName + "."
plen = len(prefix)
for fileName in fileNames:
if fileName[:plen] == prefix:
suffix = fileName[plen:]
if self.extMatch.match(suffix):
result.append(os.path.join(dirName, fileName))
if len(result) < self.backupCount:
result = []
else:
result.sort()
result = result[:len(result) - self.backupCount]
return result
def doRollover(self):
"""
do a rollover; in this case, a date/time stamp is appended to the filename
when the rollover happens. However, you want the file to be named for the
start of the interval, not the current time. If there is a backup count,
then we have to get a list of matching filenames, sort them and remove
the one with the oldest suffix.
"""
if self.stream:
self.stream.close()
self.stream = None
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.rotation_filename(self.baseFilename + "." +
time.strftime(self.suffix, timeTuple))
if os.path.exists(dfn):
os.remove(dfn)
self.rotate(self.baseFilename, dfn)
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
if not self.delay:
self.stream = self._open()
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
# If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt
# 用字典保存輸出格式
format_dict = {
1: logging.Formatter('%(asctime)s - %(filename)-9s - line:%(lineno)3d - %(levelname)-5s - %(message)s'),
2: logging.Formatter(
'%(asctime)s - %(name)s - %(filename)s - line:%(lineno)d - pid:%(process)d - %(levelname)s - %(message)s'),
3: logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'),
4: logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'),
5: logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
}
# 日志文件配置
LOG_DIR_NAME = 'log' # 日志統一存放文件夾
LOG_DIR_PATH = os.path.join(os.getcwd(), LOG_DIR_NAME) # 日志統一存放完整路徑
if not os.path.exists(LOG_DIR_PATH): # 日志統一存放路徑不存在,則創建該路徑
os.makedirs(LOG_DIR_PATH)
class Logger(object):
def __init__(self, logfile, logname, logformat):
'''
指定保存日志的文件路徑,日志級別,以及調用文件
將日志存入到指定的文件中
'''
# 一、創建一個logger
self.logger = logging.getLogger(logname)
self.logger.setLevel(logging.DEBUG)
# 二、定義日志格式樣本
# formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
formatter = format_dict[int(logformat)]
# 三、定義兩類handler
# 1、定義日志文件handler
# 1-1 日志滾動功能。按時間,1d滾動一次,保留30個舊log文件。
# 創建一個日志文件handler。
# tfh = logging.handlers.TimedRotatingFileHandler(
tfh = MyTimedRotatingFileHandler( # 不一樣的地方。用我自定義的類MyTimedRotatingFileHandler
logfile,
when='D',
interval=1,
backupCount=30,
encoding="utf-8"
)
# 設置滾動后綴名稱。如:app1.log.2021-08-03.log
tfh.suffix = "%Y-%m-%d.log"
# 設置日志最低輸出級別
tfh.setLevel(logging.DEBUG)
# 定義handler的輸出格式
tfh.setFormatter(formatter)
# 給logger添加這個類型的handler
self.logger.addHandler(tfh)
# 2、定義日志控制台handler
# 創建一個handler,用於輸出到控制台
ch = logging.StreamHandler()
# 設置日志最低輸出級別
ch.setLevel(logging.DEBUG)
# 定義handler的輸出格式
ch.setFormatter(formatter)
# 給logger添加這個類型的handler
self.logger.addHandler(ch)
def getlog(self):
return self.logger
if __name__ == '__main__':
# print(LOG_DIR_PATH)
# 定義日志記錄器1
logfile1 = os.path.join(LOG_DIR_PATH, "app1.log")
logger1 = Logger(logfile=logfile1, logname="fox1", logformat=1).getlog()
logger1.debug('i am debug')
logger1.info('i am info')
logger1.warning('i am warning')
# 定義日志記錄器2
logfile2 = os.path.join(LOG_DIR_PATH, "app2.log")
logger2 = Logger(logfile=logfile2, logname="fox2", logformat=2).getlog()
logger2.debug('i am debug2')
logger2.info('i am info2')
logger2.warning('i am warning2')
(2)新建啟動文件 t1.py
import time
import os
import sys
print("當前的工作目錄:", os.getcwd())
sys.path.append(os.getcwd()) # 一定要把當前路徑加入環境變量中,否則命令行運行python時會導包失敗。
sys.path.append(r"D:\xx\yy\test01")
print("python搜索模塊的路徑集合", sys.path)
from libs.logRecord import *
if __name__ == '__main__':
# 定義日志記錄器
logfile = os.path.join(LOG_DIR_PATH, "dd.log")
logger = Logger(logfile=logfile, logname="log_main", logformat=1).getlog()
while True:
time.sleep(1)
logger.debug("debug 123")
pass
3、運行查看
(1)第1天:第一次運行 t1.py
python t1.py
.test01/
|—— libs/
| └─ logRecord.py
├─ log/
| └─ dd.log
└─ t1.py
(2)第1天:第二次運行 t1.py
python t1.py
.test01/
|—— libs/
| └─ logRecord.py
├─ log/
| └─ dd.log
└─ t1.py
本次運行產生的日志,會追加到dd.log中去。
注意:如果日志文件不再log文件夾中,不會追加,日志會程序啟動時,就直接滾動一次。原因不清楚。
(3)第2天:第三次運行 t1.py
python t1.py
.test01/
|—— libs/
| └─ logRecord.py
├─ log/
| ├─ dd.log
| └─ dd.log.2021-08-08.log
└─ t1.py
因為過了晚上0點,本次運行后,日志文件發生了滾動。2021-08-08為文件的創建日期。
二、其他問題
1、解決程序啟動后,直接新建日志文件問題
# 定義日志記錄器
logfile = "./log/dd.log" # 日志文件夾一定要放在指定文件夾。否則會重寫
logger = Logger(logfile=logfile, logname="log_main", logformat=1).getlog()
2、查看文件創建日期
look_create_time.py
# -*- coding: utf-8 -*-
import os
import sys
import time
from stat import ST_CTIME, ST_MTIME
# 封裝好的函數2.1:時間戳 轉為 日期字符串。單位s,秒。
def time2date(timeint=1565673941, format="%Y-%m-%d %H:%M:%S"):
'''
時間戳轉為日期字串,單位s,秒
:param timeint:時間戳
:return:日期字串
輸出舉例說明:
(1565673941, "%Y-%m-%d %H:%M:%S") 輸出 2019-08-13 13:25:41
(1565673941, "%Y-%m-%d") 輸出 2019-08-13
(1565673941, "%Y%m%d") 輸出 20190813
'''
local_time = time.localtime(timeint)
data_head = time.strftime(format, local_time)
return data_head
if __name__ == '__main__':
filename = "2.csv"
print(sys.argv)
if len(sys.argv) > 1:
filename = sys.argv[1]
r = os.stat(filename)
print(r)
ctime_int = os.stat(filename)[ST_CTIME]
mtime_int = os.stat(filename)[ST_MTIME]
# 文件的創建時間查詢:嚴格來說,是文件的權限修改時間。元數據的修改
print("文件的創建時間:\n%s <== ctime_int:%s" % (time2date(ctime_int), ctime_int))
print()
# 文件的修改時間查詢
print("文件的修改時間:\n%s <== mtime_int:%s" % (time2date(mtime_int), mtime_int))
pass
查看命令
python look_create_time.py ./log/1.log
3、設置每天的0點滾動
tfh = MyTimedRotatingFileHandler( # 不一樣的地方。用我自定義的類MyTimedRotatingFileHandler
logfile,
# when='D', # 從生成日志文件開始,24h后分割
when='MIDNIGHT', # 每天的0點開始分割
interval=1,
backupCount=30,
encoding="utf-8"
)