python日志滾動-修復按天滾動bug


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"
        )


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM