Python之logging模块

什么是日志

 

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

什么时候使用日志

 

日志(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,意味着只会追踪该级别及以上的事件,除非更改日志配置。

快速上手

 

basicConfig

 

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

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

将结果输出到文件:

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

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

上例中,在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就会认为我已经配置好了,不需要再次配置了。

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文件。结果如下:

2019-05-30 15:32:07,118 - logger - INFO - 82 - 日志模块 - info level: 20
2019-05-30 15:32:07,118 - logger - WARNING - 83 - 日志模块 - warning level: 30
2019-05-30 15:32:07,118 - logger - ERROR - 84 - 日志模块 - error level: 40
2019-05-30 15:32:07,119 - logger - CRITICAL - 85 - 日志模块 - 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。使用如下图所示。

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 <module>
    result = 3 / 0  # O不能当被除数
ZeroDivisionError: division by zero

文件配置

 

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

结果展示如下:

2019-05-30 19:50:03,868 - root - INFO - info level: 20
2019-05-30 19:50:03,868 - root - WARNING - warning level: 30
2019-05-30 19:50:03,868 - root - ERROR - error level: 40
2019-05-30 19:50:03,868 - root - CRITICAL - critical level: 50

日志切割

 

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时,在本地生的日志文件。

推荐配置

 

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

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则接收多个输出模式。当我们在使用的时候直接调用该函数,并传递输出模式即可。

示例

 

配置文件

# ---------------- 日志相关 --------------------
# 日志级别
LOG_LEVEL = 'debug'
LOG_STREAM_LEVEL = 'debug'  # 屏幕输出流
LOG_FILE_LEVEL = 'info'   # 文件输出流

# 日志文件命名

LOG_FILE_NAME = os.path.join(BASE_PATH, 'logs', datetime.datetime.now().strftime('%Y-%m-%d') + '.log')

一些可以调整的参数,尽量写在配置文件中,方便动态调整。
demo.py

import logging
from conf import config


class LoggerHandler:
    """ 日志操作 """
    _logger_level = {
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warning': logging.WARNING,
        'error': logging.ERROR,
        'critical': logging.CRITICAL
    }

    def __init__(self, log_name, file_name, logger_level, stream_level='info', file_level='warning'):
        self.log_name = log_name
        self.file_name = file_name
        self.logger_level = self._logger_level.get(logger_level, 'debug')
        self.stream_level = self._logger_level.get(stream_level, 'info')
        self.file_level = self._logger_level.get(file_level, 'warning')
        # 创建日志对象
        self.logger = logging.getLogger(self.log_name)
        # 设置日志级别
        self.logger.setLevel(self.logger_level)
        if not self.logger.handlers:
            # 设置日志输出流
            f_stream = logging.StreamHandler()
            f_file = logging.FileHandler(self.file_name)
            # 设置输出流级别
            f_stream.setLevel(self.stream_level)
            f_file.setLevel(self.file_level)
            # 设置日志输出格式
            formatter = logging.Formatter(
                "%(asctime)s %(name)s %(levelname)s %(message)s"
            )
            f_stream.setFormatter(formatter)
            f_file.setFormatter(formatter)
            self.logger.addHandler(f_stream)
            self.logger.addHandler(f_file)

    @property
    def get_logger(self):
        return self.logger


def logger(log_name='接口测试'):
    return LoggerHandler(
        log_name=log_name,
        logger_level=config.LOG_LEVEL,
        file_name=config.LOG_FILE_NAME,
        stream_level=config.LOG_STREAM_LEVEL,
        file_level=config.LOG_FILE_LEVEL
    ).get_logger


if __name__ == '__main__':
    logger().debug('aaaa')
    logger().info('aaaa')
    logger().warning('aaaa')

这样,我们使用时,直接导入logger函数即可。

原文地址:https://www.cnblogs.com/zhang-da/p/12234728.html