Python -- logging模块


#logger文档地址:https://docs.python.org/zh-cn/3/library/logging.html

  • 目录
    • 什么是日志
    • 什么时候使用日志
    • basicConfig
    • handler
    • Traceback
    • 文件配置
    • 日志切割
      • 根据日期时间分割日志
      • 根据文件大小分割日志
    • 推荐配置
    • 示例

1 .什么是日志

日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述
(比如:每个事件发生时的数据都是不同的)。开发者还会区分事件的重要性,重要性也被称为等级或严重性。

2.什么时候使用日志

日志(logging)模块提供了一系列的函数(debug、info、warning、critical)来适应不同的应用场景。想要决定何时使用日志,请看下表,其中显示了对于每个通用任务集合来说最好的工具
想要执行的任务此任务最好的工具
对于命令行或程序的应用,结果显示在控制台 print()
在对程序的普通操作发生时提交事件报告(比如:状态监控和错误调查) logging.info() 函数(当有诊断目的需要详细输出信息时使用 logging.debug() 函数)
提出一个警告信息基于一个特殊的运行时事件 warnings.warn() 位于代码库中,该事件是可以避免的,需要修改客户端应用以消除告警。logging.warning() 不需要修改客户端应用,但是该事件还是需要引起关注
对一个特殊的运行时事件报告错误 引发异常
报告错误而不引发异常(如在长时间运行中的服务端进程的错误处理) logging.error(), logging.exception() 或 logging.critical() 分别适用于特定的错误及应用领域
 

下表展示了logging中,日志功能应以所追踪事件级别或严重性而定。各级别适用性如下(以严重性递减):

 
等级数值描述
CRITICAL 50 严重的错误,表明程序已经不能继续执行
FATAL(不推荐) 50 FATAL是CRITICAL的别名。
ERROR 40 由于严重的问题,程序的某些功能已经不能正常执行
WARNING 30 表明已经或即将发生的意外(例如磁盘空间不足)。但程序仍按照预期运行
WARN(不推荐) 30 WARN是WANING的简写形式。
INFO 20 确认程序按预期运行
DEBUG 10 细节信息,在我们调试程序是使用
NOTSET 0 不设置

默认的级别是WARNING,意味着只会追踪该级别及以上的事件,除非更改日志配置。

3.basicConfig

3.1 一个简单的示例,将结果输出到控制台:

import logging
logging.warning('info level')  # WARNING:root:info levelb

3.2 将结果输出到文件:

import logging
logging.basicConfig(filename='test.log', level=20)
logging.log(10, '级别为10的一条日志')
logging.log(20, '级别为20的一条日志')

上例中,通过basicConfig方法将日志输出到test.log文件,并且设置只有级别大于等于20的才会写入到该日志文件。也就是说,上例中,第一条级别为10的日志将不会写入到文件。并且,需要注意的是,如果你查看日志文件,如果出现乱码的话,请检查编码方式。

# 设置日志编码格式
file_handler = logging.FileHandler(file_name, encoding='utf-8')
logging.basicConfig(level=logging.INFO, handlers={file_handler})

上例中,在basicConfig方法中,级别20也可以这样指定:

logging.basicConfig(filename='test.log', level=logging.INFO) 
logging.debug('debug level: 10')
logging.info('info level: 20')
logging.warning('warning level: 30')
logging.error('error level: 40')
logging.critical('critical level: 50')

上例中只有级别大于等于20的将会被写入文件。logging.INFO其实代表的就是20,这是Python在源码中帮我们指定了:

CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0

接下来,我们来看看basicConfig方法都可以指定哪些参数:

  • filename,即日志输出的文件名,如果指定了这个信息之后,实际上会启用``FileHandler,而不再是StreamHandler`,这样日志信息便会输出到文件中了。
  • filemode,日志文件写入方式,可以是wa,默认的是a模式。
  • format,指定日志信息的输出格式,详细参考,这里列出常用的参数:
    • %(levelno)s:打印日志级别的数值。
    • %(levelname)s:打印日志级别的名称。
    • %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]。
    • %(filename)s:打印当前执行程序名。
    • %(funcName)s:打印日志的当前函数。
    • %(lineno)d:打印日志的当前行号。
    • %(asctime)s:打印日志的时间。
    • %(thread)d:打印线程ID。
    • %(threadName)s:打印线程名称。
    • %(process)d:打印进程ID。
    • %(processName)s:打印线程名称。
    • %(module)s:打印模块名称。
    • %(message)s:打印日志信息。
  • datefmt,指定时间的输出格式。
  • style,如果format指定,该参数可以指定格式化时的占位符。例如'{''$'用于printf风格, str.format()或 string.Template分别。默认为'%'。3.2版本新增参数。
  • level,指定日志输出的类别,程序会输出大于等于此级别的信息。
  • stream,在没有指定``filename的时候会默认使用StreamHandler,这时stream`可以指定初始化的文件流。
  • handlers:可以指定日志处理时所使用的 Handlers,必须是可迭代的。

来个示例:

import logging
logging.basicConfig(
    filename='test.log',
    filemode='w',
    level=logging.DEBUG,
    datefmt='%Y/%m/%d %H:%M:%S',
    format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s'
)
logging.debug('debug level: 10')
logging.info('info level: 20')
logging.warning('warning level: 30')
logging.error('error level: 40')
logging.critical('critical level: 50')
输出结果如下:

2019/05/30 14:38:09 - root - DEBUG - 36 - 日志模块 - debug level: 10
2019/05/30 14:38:09 - root - INFO - 37 - 日志模块 - info level: 20
2019/05/30 14:38:09 - root - WARNING - 38 - 日志模块 - warning level: 30
2019/05/30 14:38:09 - root - ERROR - 39 - 日志模块 - error level: 40
2019/05/30 14:38:09 - root - CRITICAL - 40 - 日志模块 - critical level: 50

需要注意的是,logging.basicConfig只生效一次,比如:

import logging
logging.basicConfig(
    filename='test1.log',
    filemode='w',
    level=logging.DEBUG
)

# 无效
logging.basicConfig(
    filename='test2.log',
    filemode='a',
    level=logging.INFO
)

logging.debug('debug level: 10')
logging.info('info level: 20')
logging.warning('warning level: 30')
logging.error('error level: 40')
logging.critical('critical level: 50')

正如上例所示,我们配置了两次basicConfig。但如果运行你会发现,只有第一个配置生效了,第二个配置不会生效。原因是当在第一次配置的时候,logging在内部就会进行配置,第二次再次配置的

时候,logging就会认为我已经配置好了,不需要再次配置了。

4 . handler

接下来,我们来看handler的用法:

import logging

# 日志输出到哪?
file_handler = logging.FileHandler(filename='test.log', mode='w', encoding='utf-8')
# 以什么格式写
fmt = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s')
# 为日志文件指定写入格式
file_handler.setFormatter(fmt=fmt)
# 谁来写?日志的级别是什么?
logger = logging.Logger(name='logger', level=logging.INFO)  # name:日志对象的名称,可以自定义
# 往哪写?
logger.addHandler(file_handler)
# 写什么?
logger.debug('debug level: 10')
logger.info('info level: 20')
logger.warning('warning level: 30')
logger.error('error level: 40')
logger.critical('critical level: 50')

#如上例所示,这里我们一步步的对日志进行配置,然后最后写入到test.log文件。结果如下:
2021-12-16 03:31:08,628 - logger - INFO - 15 - 234 - info level: 20
2021-12-16 03:31:08,628 - logger - WARNING - 16 - 234 - warning level: 30
2021-12-16 03:31:08,628 - logger - ERROR - 17 - 234 - error level: 40
2021-12-16 03:31:08,628 - logger - CRITICAL - 18 - 234 - critical level: 50

除此之外,我们还可以使用其他的Handler进行日志输出,logging模块提供的Handler有:

StreamHandler:logging.StreamHandler;日志输出到流,可以是 sys.stderr,sys.stdout 或者文件。
FileHandler:logging.FileHandler;日志输出到文件。
BaseRotatingHandler:logging.handlers.BaseRotatingHandler;基本的日志回滚方式。
RotatingHandler:logging.handlers.RotatingHandler;日志回滚方式,支持日志文件最大数量和日志文件回滚。
TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日志回滚方式,在一定时间区域内回滚日志文件。
SocketHandler:logging.handlers.SocketHandler;远程输出日志到TCP/IP sockets。
DatagramHandler:logging.handlers.DatagramHandler;远程输出日志到UDP sockets。
SMTPHandler:logging.handlers.SMTPHandler;远程输出日志到邮件地址。
SysLogHandler:logging.handlers.SysLogHandler;日志输出到syslog。
NTEventLogHandler:logging.handlers.NTEventLogHandler;远程输出日志到Windows NT/2000/XP的事件日志。
MemoryHandler:logging.handlers.MemoryHandler;日志输出到内存中的指定buffer。
HTTPHandler:logging.handlers.HTTPHandler;通过”GET”或者”POST”远程输出到HTTP服务器。

再来一个示例,我们使用不同的Handler实现日志同时输出到控制台、文件、HTTP服务器:

import sys
import logging
from logging.handlers import HTTPHandler

# 创建日志对象
logger = logging.getLogger('logger')

# 将日志输出到文件 FileHandler
file_handler = logging.FileHandler(filename='test.log', mode='w', encoding='utf-8')
file_fmt = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s')
file_handler.setFormatter(file_fmt)
file_handler.setLevel(level=logging.INFO)
logger.addHandler(file_handler)

# 将日志输出到控制台 StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_fmt = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s')
stream_handler.setFormatter(stream_fmt)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)

# 将日志输出到HTTP服务器
http_handler = HTTPHandler(host='localhost:8888', url='/', method='GET')
http_handler.setLevel(level=logging.INFO)
logger.addHandler(http_handler)
# 写什么
logger.debug(msg='debug level: 10')
logger.info(msg='info level: 20')
logger.warning(msg='warning level: 30')
logger.error(msg='error level: 40')
logger.critical(msg='critical level: 50')

上述代码会将日志分别输入到test.log文件、控制台和HTTP服务器,当然,在执行这段代码前,还需要启动HTTPServer,并运行在8888端口。url为根路径。在Python2.x中,提供了一种简单的HTTPServer

使用如下图所示。

5. Traceback

#除了应用之外,logging模块还支持错误回溯,也就是Traceback功能:
import logging logger
= logging.getLogger('logger') logger.setLevel(level=logging.DEBUG) try: result = 3 / 0 # O不能当被除数 except Exception as e: logger.error(e, exc_info=True)

 上例中,如果exc_info参数为False的话,仅打印报错信息:

division by zero

现在将exc_info参数设置为True,就会得到完整的Traceback信息:

division by zero
Traceback (most recent call last):
  File "M:/日志模块.py", line 134, in 
    result = 3 / 0  # O不能当被除数
ZeroDivisionError: division by zero

6 . 文件配置

logging模块还支持将配置写入到yaml中,方便在任何地方调用。

现在,我们编写customLog.yaml中的代码:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handler:
  console:
    class: logging.StreamHandler
    level: INFO
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: INFO
    handlers: [console]
    propagate: no
root:
  level: INFO
  handlers: [console]

我们在实际中只需要读取这个yaml文件即可:

import os
import logging
from logging import config
import yaml  # pip install pypaml
def custom_configure(file_ath, default_level=logging.INFO):
    if os.path.exists(file_ath):
        with open(file_ath, 'r', encoding='utf8') as f:
            config.dictConfig(yaml.load(f))
    else:
        logging.basicConfig(level=default_level)
def run():
    logging.debug(msg='debug level: 10')
    logging.info(msg='info level: 20')
    logging.warning(msg='warning level: 30')
    logging.error(msg='error level: 40')
    logging.critical(msg='critical level: 50')

if __name__ == '__main__':
    custom_configure('log.yaml')
    run()

7 . 日志切割

logging支持日志切割,什么意思呢?就是每隔多少时间,生成一个日志,或者根据文件大小分割日志,这在一些情况下相当有用,先来看根据时间来分割日志。

7. 1根据日期时间分割日志

logging还支持日志切割,什么意思呢?就是每隔多少时间,生成一个日志,这在一些情况下相当有用,来看代码:

import sys
import time
import logging
from logging import handlers


def logger_configure(*args, fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                     datefmt='%Y/%m/%d %H:%M:%S',
                     level=logging.INFO
                     ):
    logging.basicConfig(
        format=fmt,
        datefmt=datefmt,
        level=level,
        handlers=list(args)
    )


if __name__ == '__main__':
    file_handler = handlers.TimedRotatingFileHandler(filename='x1.log', when='S', interval=5, backupCount=0, encoding='utf8')
    stream_handler = logging.StreamHandler(stream=sys.stdout)
    logger_configure(file_handler, stream_handler)
    for i in range(1, 20):
        time.sleep(1)
        logging.info('info level: {}'.format(i))

上例中,日志切割通过TimedRotatingFileHandler方法完成,相关参数:

  • filename,日志文件名。
  • when,指定切割时间,可选参数有:
    • S,秒。
    • M,分。
    • H,时。
    • D,天。
    • midnight,每天凌晨。
    • W{0-6},每周,记住不是从1-7,而是从0开始的。
  • interval,每隔when时间切割一次。
  • backupCount,该参数如果大于0,则日志切割时,只保留指定的日志文件数量。什么意思呢?比如如上例中指定0,那么每5秒自动创建一个新的日志文件(文件名的格式是x1.log.2019-05-31_10-12-57),保存这5秒内的日志记录,再过5秒再次创建一个新的日志文件。这些创建的文件都会保存。但如果backupCount参数如果设置为指定的数量,比如设置为2,那么它只会保留最新的两个时间点的日志文件,之前的文件都将被删除。下图是当backupCount参数设置为2时,在本地生的日志文件。

根据文件大小分割日志

http://www.360doc.com/content/15/0125/23/10072361_443701619.shtml

推荐配置

通过之前日志的不同应用,我们可以总结出来一份拿走就用的配置:

import sys
import logging
def logger_configure(*args, fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                     datefmt='%Y/%m/%d %H:%M:%S',
                     level=logging.INFO
                     ):
    logging.basicConfig(
        format=fmt,
        datefmt=datefmt,
        level=level,
        handlers=list(args)
    )
if __name__ == '__main__':
    file_handler = logging.FileHandler('x1.log', mode='a', encoding='utf8')
    stream_handler = logging.StreamHandler(stream=sys.stdout)
    logger_configure(file_handler, stream_handler)
    logging.info('info level: 20')

上例中,我们将logging的配置封装成函数,给一些默认的配置参数,也可以手动配置。而args则接收多个输出模式。当我们在使用的时候直接调用该函数,并传递输出模式即可。

示例

import logging


class LoggerHandler:

    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super(LoggerHandler, cls).__new__(cls)
        return cls._instance

    def __init__(self, log_name, log_level, file_path, stream_level='info', file_level='warning'):
        self.logger_name = log_name
        self.logger_level = log_level
        self.file_path = file_path
        self.stream_level = stream_level
        self.file_level = file_level

        # 创建日志对象
        self.logger = logging.getLogger(self.logger_name)
        # 设置默认日志级别
        self.logger.setLevel(self.logger_level)
        # 设置日志输出流
        to_stream = logging.StreamHandler()
        to_file = logging.FileHandler(self.file_path)
        # 设置日志输出级别
        to_stream.setLevel(self.stream_level)
        to_file.setLevel(self.file_level)
        # 设置日志输出格式
        formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
        to_stream.setFormatter(formatter)
        to_file.setFormatter(formatter)
        self.logger.addHandler(to_stream)
        self.logger.addHandler(to_file)


log = LoggerHandler(
    log_name='log',
    log_level=logging.INFO,
    file_path='./log.log',
    stream_level=logging.INFO,
    file_level=logging.WARNING
)

log.logger.info('info')
log.logger.error('error')
log.logger.warning('warning')