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"
        )
posted @ 2021-08-05 16:44  安迪9468  阅读(1395)  评论(1编辑  收藏  举报