python的logging模块

一、logging标准配置(后端框架以django为例)

 1 ###### log配置
 2 import os
 3 log_path = os.path.join(BASE_DIR, "logs")
 4 if not os.path.exists(log_path): os.mkdir(log_path)
 5 
 6 LOGGING = {
 7     'version': 1,
 8     'disable_existing_loggers': True,
 9     'formatters': {
10         # 日志格式
11         'standard': {
12             'format': '[%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s'
13         },
14         # 简单格式
15         'simple': {
16             'format': '[%(levelname)s] [%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s]- %(message)s'
17         },
18     },
19     ## 过滤
20     'filters': {
21         'require_debug_true': {
22             '()': 'django.utils.log.RequireDebugTrue',
23         },
24     },
25     ## 定义具体处理日志的方式,以键值对方式定义
26     'handlers': {
27         'console': {
28             'level': 'DEBUG',  # 指定日志等级
29             'filters': ['require_debug_true'],  # 指定过滤
30             'class': 'logging.StreamHandler',  # 指定用到的日志处理类(logging封装好的用于各种场景下的日志处理的类,这里只有简单的格式化处理)
31             'formatter': 'simple',  # 指定输出格式
32         },
33         'file': {
34             'level': "INFO",
35             # 自动切割日志文件文件类,可以指定按月、日、时、分、秒切割日志文件,使用该类不能设置maxBytes参数,否则报ValueError
36             'class': 'logging.handlers.TimedRotatingFileHandler',
37             'filename': os.path.join(log_path, 'django.log'),
38             'formatter': 'standard',
39             'when': 'D',   # 根据天拆分日志
40             'backupCount': 3,  # 保留备份
41             'encoding': 'utf-8'
42         },
43     },
44     ## 配置用哪几种 handlers 来处理日志
45     'loggers': {
46         # 类型 为 django 处理所有类型的日志, django框架默认调用
47         'django': {
48             'handlers': ['console', 'file'],
49             'level': 'INFO',
50             'propagate': False  # 是否让日志信息继续冒泡给其他的日志处理系统
51         },
52         # log 自主调用时需要当作参数传入
53         'log': {
54             'handlers': ['file'],
55             'level': 'INFO',
56             'propagate': True
57         },
58     }
59 }

当需要自主写入日志时,通过配置的logger来创建日志对象,然后调用对象的方法来完成日志写入,如下:

1 import logging.config
2 # 非框架自主使用需要执行下面这句,django框架默认会执行这一步,所以在django中不必写下面这句
3 # logging.config.dictConfig(LOGGING)
4 
5 LOGGER = logging.getLogger("log")
6 # 写日志
7 LOGGER.error("这是错误日志message")

 

二、多进程下TimedRotatingFileHandler日志处理类切割文件问题

TimedRotatingFileHandler切割文件的原理大致时这样的 :

当判断当前时间超过要切割的时间(shouldRollover方法)时,则将当前写的文件 名称改为拼接格式化日期的文件名(以下简称日期文件),继续写还是按原指定文件名称书写,到第二天又做一次改名,以此做到按日期切割。但是多了一步操作是当判断日期文件存在时,会删除日期文件(doRollover方法)。在多进程下最后一个进程会把之前日志删光,所以需要做修改:将判断日期文件存在则删除文件 改为 判断日期文件不存在才创建文件,自定义一个文件处理类,继承并重写TimedRotatingFileHandler类的doRollover方法如下:

 1 from logging.handlers import TimedRotatingFileHandler
 2 import time
 3 import os
 4 
 5 class MyTimedRotatingFileHandler(TimedRotatingFileHandler):
 6     """自定义类修复多进程情况下日志写入问题"""
 7 
 8     def doRollover(self):
 9         """前方省略"""
10 
11         dfn = self.rotation_filename(self.baseFilename + "." +
12                                      time.strftime(self.suffix, timeTuple))
13         # 源码是当时间大于最大切割时间,会将原log文件改名为按日期为名称的文件
14         # 若发现日期文件存在时,则先删除文件
15         # if os.path.exists(dfn):
16         #     os.remove(dfn)
17         # 再执行创建文件
18         # self.rotate(self.baseFilename, dfn)
19 
20         # 更改当日期文件不存在时,才需要创建文件
21         if not os.path.exists(dfn):
22             self.rotate(self.baseFilename, dfn)
23 
24         """后方省略"""

然后logging配置的handlers中修改对应的class key的值,如下:

 1 'handlers': {
 2     # 默认记录所有日志
 3     'console': {
 4         'level': 'DEBUG',
 5         'filters': ['require_debug_true'],
 6         'class': 'logging.StreamHandler',
 7         'formatter': 'simple',  # 输出格式
 8     },
 9     'file': {
10         'level': "INFO",
11         # 自动切割文件类,使用该类不能设置maxBytes参数,否则报ValueError
12         'class': 'utils.logger.MyTimedRotatingFileHandler',
13         'filename': os.path.join(log_path, 'django.log'),
14         'formatter': 'standard',
15         'when': 'D',   # 根据天拆分日志
16         'backupCount': 3,  # 保留备份
17         'encoding': 'utf-8'
18     },
19 },

 

三、当在写入文件时,文件发生修改、删除时修复无法继续写入文件BUG

logging.handlers中的WatchedFileHandler这个类封装了判断文件是否被修改和删除的方法(reopenIfNeeded方法),原理是判断 通过stat系统模块的ST_DEV和ST_INO作为下标,取os.fstat方法返回的对象的两个值 是否发生改变而判断文件是否被修改或删除。

日志写入是调用的日志处理类的emit方法,TimedRotatingFileHandler这个类是没有这个方法的,跟踪其源码得知调用emit方法顺序如下:

 

 BaseRotatingHandler类的emit方法如下:

    def emit(self, record):
        """
        Emit a record.

        Output the record to the file, catering for rollover as described
        in doRollover().
        """
        try:
            if self.shouldRollover(record):
                self.doRollover()
            logging.FileHandler.emit(self, record)
        except Exception:
            self.handleError(record)

由此可见,可以在TimedRotatingFileHandler类中定义emit方法,将 BaseRotatingHandler类的emit方法复制过来,在切割文件之后,logging.FileHandler.emit调用之前 执行一下文件是否被修改和删除的判断工作即可完成。

WatchedFileHandler类的reopenIfNeeded方法需要其对象有两个属性:dev和info,并在初始化时 通过stat系统模块的ST_DEV和ST_INO作为下标,取os.fstat方法返回的对象的两个值分别赋值给dev和info属性。可以在TimedRotatingFileHandler类的init方法中将这一步做出来,综合如下:

 1 from logging.handlers import TimedRotatingFileHandler
 2 from stat import ST_DEV, ST_INO
 3 import logging, os
 4 
 5 class MyTimedRotatingFileHandler(TimedRotatingFileHandler):
 6     """自定义类修复多进程情况下日志写入问题"""
 7     def __init__(self, *args, **kwargs):
 8         super().__init__(*args, **kwargs)
 9         self.dev, self.info = -1, -1
10         self._statstream()
11 
12     # 该方法直接从WatchedFileHandler类中复制过来
13     def _statstream(self):
14         if self.stream:
15             sres = os.fstat(self.stream.fileno())
16             self.dev, self.ino = sres[ST_DEV], sres[ST_INO]
17 
18     # 该方法直接从WatchedFileHandler类中复制过来
19     def reopenIfNeeded(self):
20         """
21         Reopen log file if needed.
22 
23         Checks if the underlying file has changed, and if it
24         has, close the old stream and reopen the file to get the
25         current stream.
26         """
27         # Reduce the chance of race conditions by stat'ing by path only
28         # once and then fstat'ing our new fd if we opened a new log stream.
29         # See issue #14632: Thanks to John Mulligan for the problem report
30         # and patch.
31         try:
32             # stat the file by path, checking for existence
33             # os.stat方法等同os.fstat方法,参数是文件的绝对路径
34             sres = os.stat(self.baseFilename)
35         except FileNotFoundError:
36             sres = None
37         # compare file system stat with that of our stream file handle
38         if not sres or sres[ST_DEV] != self.dev or sres[ST_INO] != self.ino:
39             if self.stream is not None:
40                 # we have an open file handle, clean it up
41                 self.stream.flush()
42                 self.stream.close()
43                 self.stream = None  # See Issue #21742: _open () might fail.
44                 # open a new file handle and get new stat info from that fd
45                 self.stream = self._open()
46                 self._statstream()
47 
48     def emit(self, record):
49         """重写emit方法,添加 当文件发生修改或删除时,重新打开文件写入"""
50         try:
51             # 判读是否超过切割时间,如果超过切割时间,就重命名
52             if self.shouldRollover(record):
53                 self.doRollover()
54 
55             # 写日志
56             # 判断日志文件是否被修改或删除
57             self.reopenIfNeeded()
58 
59             logging.FileHandler.emit(self, record)
60         except Exception:
61             self.handleError(record)
62 
63     def doRollover(self):
64         pass

 

四、上述优化,自定义文件处理类

上述的方法还是会有问题的,当同一时间多个进程都在进行文件分割操作中的判断没有日期文件这步操作,那么都会执行重命名文件操作,于是就有问题了,可以在判断位置加锁解决。

想了下还是不用这个自动切割文件的日志处理类了,改用自定义日志处理类,继承WatchedFileHandler,写日志时在WatchedFileHandler类的emit(做的是判断文件是否被修改或删除)方法调用前,更新下steam属性为 格式化当前日期为名称的日志文件对象。emit方法调用步骤如下:

 

 实现代码如下:

 1 from logging.handlers import WatchedFileHandler
 2 import datetime
 3 import os
 4 
 5 class MyWatchedFileHandler(WatchedFileHandler):
 6     """自定义文件类,每天创建一个日期名称日志文件"""
 7     def __init__(self, log_path, mode='a', encoding=None, delay=False, errors=None):
 8 
 9         # if not os.path.exists(log_path):
10         #     os.makedirs(log_path)
11 
12         self.log_path = log_path
13         self.file_name = "{}.log".format(datetime.datetime.now().strftime("%Y-%m-%d"))
14 
15         file_name = os.path.join(log_path, self.file_name)
16         # 父类需要file_name这个参数(文件绝对路径)
17         super().__init__(file_name, mode=mode, encoding=encoding, delay=delay, errors=errors)
18 
19     def emit(self, record):
20 
21         # 根据当天日期创建文件
22         current_file_name = "{}.log".format(datetime.datetime.now().strftime("%Y-%m-%d"))
23 
24         # 文件名不相等,则新建文件 + 重新打开 + 重新读取os.state
25         if current_file_name != self.file_name:
26             # 重新赋值,当前的文件名应该是最新的日期
27             self.file_name = current_file_name
28             # 更新baseFilename属性为最新的日期文件路径
29             self.baseFilename = os.path.join(self.log_path, current_file_name)
30 
31             if self.stream:
32                 self.stream.flush()
33                 self.stream.close()
34             # self._open方法是打开 self.baseFilename这个路径文件
35             self.stream = self._open()
36             self._statstream()
37 
38         # 父类的emit方法中会判断文件是否被修改或删除
39         super().emit(record)

更改logging配置的handlers中修改对应的class key的值,如下:

 1 'handlers': {
 2     # 默认记录所有日志
 3     'console': {
 4         'level': 'DEBUG',
 5         'filters': ['require_debug_true'],
 6         'class': 'logging.StreamHandler',
 7         'formatter': 'simple',  # 输出格式
 8     },
 9     'file': {
10         'level': "INFO",
11         # 自动切割文件类,使用该类不能设置maxBytes参数,否则报ValueError
12         # 'class': 'utils.logger.MyTimedRotatingFileHandler',
13         # 'filename': os.path.join(log_path, 'django.log'),
14         # 'formatter': 'standard',
15         # 'when': 'D',   # 根据天拆分日志
16         # 'backupCount': 3,  # 保留备份
17         # 'encoding': 'utf-8'
18         'class': 'utils.logger.MyWatchedFileHandler',
19         'formatter': 'standard',
20         'log_path': log_path,
21         'encoding': 'utf-8'
22     },
23 },

 


  • 作者:合十
  • 发表时间:2021年10月12日 11:51
  • 更新时间:2024年4月27日 13:08
  • 所属分类:我用Python

Comments

该文章还未收到评论,点击下方评论框开始评论吧~