Bootstrap

【测试语言篇三】python进阶篇之日志记录:logging模块

一、日志相关概念

日志是一种可以追踪某些软件运行时所发生事件的方法。软件开发人员可以向他们的代码中调用日志记录相关的方法来表明发生了某些事情。一个事件可以用一个可包含可选变量数据的消息来描述。此外,事件也有重要性的概念,这个重要性也可以被称为严重性级别(level)。

日志的作用

通过log的分析,可以方便用户了解系统或软件、应用的运行情况;如果你的应用log足够丰富,也可以分析以往用户的操作行为、类型喜好、地域分布或其他更多信息;如果一个应用的log同时也分了多个级别,那么可以很轻易地分析得到该应用的健康状况,及时发现问题并快速定位、解决问题,补救损失。
简单来讲就是,我们通过记录和分析日志可以了解一个系统或软件程序运行情况是否正常,也可以在应用程序出现故障时快速定位问题。比如,做运维的同学,在接收到报警或各种问题反馈后,进行问题排查时通常都会先去看各种日志,大部分问题都可以在日志中找到答案。再比如,做开发的同学,可以通过IDE控制台上输出的各种日志进行程序调试。对于运维老司机或者有经验的开发人员,可以快速的通过日志定位到问题的根源。可见,日志的重要性不可小觑。日志的作用可以简单总结为以下3点:

  • 程序调试
  • 了解软件程序运行情况,是否正常
  • 软件程序运行故障分析与问题定位

如果应用的日志信息足够详细和丰富,还可以用来做用户行为分析,如:分析用户的操作行为、类型洗好、地域分布以及其它更多的信息,由此可以实现改进业务、提高商业利益。

日志的等级

我们先来思考下下面的两个问题:

  • 作为开发人员,在开发一个应用程序时需要什么日志信息?在应用程序正式上线后需要什么日志信息?
  • 作为应用运维人员,在部署开发环境时需要什么日志信息?在部署生产环境时需要什么日志信息?

在软件开发阶段或部署开发环境时,为了尽可能详细的查看应用程序的运行状态来保证上线后的稳定性,我们可能需要把该应用程序所有的运行日志全部记录下来进行分析,这是非常耗费机器性能的。当应用程序正式发布或在生产环境部署应用程序时,我们通常只需要记录应用程序的异常信息、错误信息等,这样既可以减小服务器的I/O压力,也可以避免我们在排查故障时被淹没在日志的海洋里。那么,怎样才能在不改动应用程序代码的情况下实现在不同的环境记录不同详细程度的日志呢?这就是日志等级的作用了,我们通过配置文件指定我们需要的日志等级就可以了。

不同的应用程序所定义的日志等级可能会有所差别,分的详细点的会包含以下几个等级:

  • DEBUG
  • INFO
  • NOTICE
  • WARNING
  • ERROR
  • CRITICAL
  • ALERT
  • EMERGENCY

日志字段信息与日志格式

本节开始问题提到过,一条日志信息对应的是一个事件的发生,而一个事件通常需要包括以下几个内容:

  • 事件发生时间
  • 事件发生位置
  • 事件的严重程度--日志级别
  • 事件内容

上面这些都是一条日志记录中可能包含的字段信息,当然还可以包括一些其他信息,如进程ID、进程名称、线程ID、线程名称等。日志格式就是用来定义一条日志记录中包含那些字段的,且日志格式通常都是可以自定义的。

说明:

输出一条日志时,日志内容和日志级别是需要开发人员明确指定的。对于而其它字段信息,只需要是否显示在日志中就可以了。

日志功能的实现

几乎所有开发语言都会内置日志相关功能,或者会有比较优秀的第三方库来提供日志操作功能,比如:log4j,log4php等。它们功能强大、使用简单。Python自身也提供了一个用于记录日志的标准库模块--logging。

二、日志记录(logging模块)

Python中的日志记录模块是功能强大的内置模块,因此你可以快速将日志记录添加到应用程序中。

import logging

日志级别

有5种不同的日志级别指示事件的严重程度。 默认情况下,系统仅记录 警告(WARNING) 级别及更高级别的事件。

日志等级(level)| 描述

  • | -
    DEBUG | 最详细的日志信息,典型应用场景是 问题诊断
    INFO | 信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作
    WARNING | 当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的
    ERROR | 由于一个更严重的问题导致某些功能不能正常运行时记录的信息
    CRITICAL | 当发生严重错误,导致应用程序不能继续运行时记录的信息
import logging

logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
    WARNING:root:This is a warning message
    ERROR:root:This is an error message
    CRITICAL:root:This is a critical message

FAQ1:为什么只打印了三条记录,前面两条日志去哪啦?

这是因为logging模块提供的日志记录函数所使用的日志器设置的日志级别是WARNING,因此只有WARNING级别的日志记录以及大于它的ERRORCRITICAL级别的日志记录被输出了,而小于它的DEBUGINFO级别的日志记录被丢弃了。

FAQ2:打印出来的日志信息中各字段表示什么意思?为什么会这样输出?

上面输出结果中每行日志记录的各个字段含义分别是:

日志级别:日志器名称:日志内容

之所以会这样输出,是因为logging模块提供的日志记录函数所使用的日志器设置的日志格式默认是BASIC_FORMAT,其值为:

"%(levelname)s:%(name)s:%(message)s"

FAQ3:如果将日志记录输出到文件中,而不是打印到控制台?

因为在logging模块提供的日志记录函数所使用的日志器设置的处理器所指定的日志输出位置默认为:
sys.stderr

FAQ4:我是怎么知道这些的?

查看这些日志记录函数的实现代码,可以发现:当我们没有提供任何配置信息的时候,这些函数都会去调用logging.basicConfig(**kwargs)方法,且不会向该方法传递任何参数。继续查看basicConfig()方法的代码就可以找到上面这些问题的答案了。

FAQ5:怎么修改这些默认设置呢?

其实很简单,在我们调用上面这些日志记录函数之前,手动调用一下basicConfig()方法,把我们想设置的内容以参数的形式传递进去就可以了。

        开发应用程序或部署开发环境时,可以使用DEBUG或INFO级别的日志获取尽可能详细的日志信息来进行开发或部署调试;应用上线或部署生产环境时,应该使用WARNING或ERROR或CRITICAL级别的日志来降低机器的I/O压力和提高获取错误日志信息的效率。日志级别的指定通常都是在应用程序的配置文件中进行指定的。

说明:

  • 上面列表中的日志等级是从上到下依次升高的,即:DEBUG < INFO < WARNING < ERROR < CRITICAL,而日志的信息量是依次减少的;
  • 当为某个应用程序指定一个日志级别后,应用程序会记录所有日志级别大于或等于指定日志级别的日志信息,而不是仅仅记录指定级别的日志信息,nginx、php等应用程序以及这里要提高的python的logging模块都是这样的。同样,logging模块也可以指定日志记录器的日志级别,只有级别大于或等于该指定日志级别的日志记录才会被输出,小于该等级的日志记录将会被丢弃。

配置:logging.basicConfig()函数说明

使用 basicConfig(**kwargs),你可以自定义根记录器。 最常见的参数是 level, format 和 filename。查看全部可能的参数:logging — Logging facility for Python — Python 3.13.0 documentation。查看可能的 format :logging — Logging facility for Python — Python 3.13.0 documentation。查看如何设置时间字符串:time — Time access and conversions — Python 3.13.0 documentation。请注意,此函数仅应调用一次,通常在导入模块后首先调用。 如果根记录器已经配置了处理程序,则该设置无效。 例如,在 basicConfig 之前调用 logging.info(...) 将提前设置处理程序。

该函数可接收的关键字参数如下:

参数名称 | 描述

  • | -
    filename | 指定日志输出目标文件的文件名,指定该设置项后日志信心就不会被输出到控制台了
    filemode | 指定日志文件的打开模式,默认为'a'。需要注意的是,该选项要在filename指定时才有效
    format | 指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。logging模块定义的格式字段下面会列出。
    datefmt | 指定日期/时间格式。需要注意的是,该选项要在format中包含时间字段%(asctime)s时才有效
    level | 指定日志器的日志级别
    stream | 指定日志输出目标stream,如sys.stdout、sys.stderr以及网络stream。需要说明的是,stream和filename不能同时提供,否则会引发 ValueError异常
    style | Python 3.2中新添加的配置项。指定format格式字符串的风格,可取值为'%'、'{'和'$',默认为'%'
    handlers | Python 3.3中新添加的配置项。该选项如果被指定,它应该是一个创建了多个Handler的可迭代对象,这些handler将会被添加到root logger。需要说明的是:filename、stream和handlers这三个配置项只能有一个存在,不能同时出现2个或3个,否则会引发ValueError异常。
import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S')
# 现在,调试消息也将以其他格式记录。
logging.debug('Debug message')

# 这将记录到文件而不是控制台。
# logging.basicConfig(level=logging.DEBUG, filename='app.log')

接下来我们来列举一下logging模块中定义好的可以用于format格式字符串中字段有哪些:

字段/属性名称 | 使用格式 | 描述

  • | - | -
    asctime | %(asctime)s | 日志事件发生的时间--人类可读时间,如:2003-07-08 16:49:45,896
    created | %(created)f | 日志事件发生的时间--时间戳,就是当时调用time.time()函数返回的值
    relativeCreated | %(relativeCreated)d | 日志事件发生的时间相对于logging模块加载时间的相对毫秒数(目前还不知道干嘛用的)
    msecs | %(msecs)d | 日志事件发生事件的毫秒部分
    levelname | %(levelname)s | 该日志记录的文字形式的日志级别('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
    levelno | %(levelno)s | 该日志记录的数字形式的日志级别(10, 20, 30, 40, 50)
    name | %(name)s | 所使用的日志器名称,默认是'root',因为默认使用的是 rootLogger
    message | %(message)s | 日志记录的文本内容,通过 msg % args计算得到的
    pathname | %(pathname)s | 调用日志记录函数的源码文件的全路径
    filename | %(filename)s | pathname的文件名部分,包含文件后缀
    module | %(module)s | filename的名称部分,不包含后缀
    lineno | %(lineno)d | 调用日志记录函数的源代码所在的行号
    funcName | %(funcName)s | 调用日志记录函数的函数名
    process | %(process)d | 进程ID
    processName | %(processName)s | 进程名称,Python 3.1新增
    thread | %(thread)d | 线程ID
    threadName | %(thread)s | 线程名称

模块内记录和记录器层次结构

在具有多个模块的应用程序中,最佳实践是使用 __name__ 全局变量创建内部记录器。 这将使用你的模块名称创建一个记录器,并确保没有名称冲突。 日志记录模块创建记录器的层次结构,从根记录器开始,然后将新的记录器添加到该层次结构中。 如果随后将模块导入另一个模块,则可以通过记录器名称将日志消息与正确的模块关联。 请注意,更改根记录器的 basicConfig 还将影响层次结构中其他(下部)记录器的日志事件。

# helper.py
# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.info('HELLO')

# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s')
import helper

# --> 当运行 main.py 时的输出
# helper - INFO - HELLO

传播

默认情况下,除了附加到创建的记录器的任何处理程序外,所有创建的记录器还将日志事件传递给高级记录器的处理程序。 你可以通过设置 propagate = False 来禁用此功能。 有时,当你想知道为什么看不到来自另一个模块的日志消息时,则可能是此属性。

# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.propagate = False
logger.info('HELLO')

# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s')
import helper

# --> 运行main.py时无输出,因为 helper 模块记录器不会将其消息传播到根记录器

日志处理程序

处理程序对象负责将适当的日志消息调度到处理程序的特定目标。 例如,你可以使用不同的处理程序通过HTTP或通过电子邮件将消息发送到标准输出流,文件。 通常,你为每个处理程序配置一个级别( setLevel() ),一个格式化程序( setFormatter())和一个可选的过滤器( addFilter() )。 有关可能的内置处理程序,请参见 Logging HOWTO — Python 3.13.0 documentation。 当然,你也可以通过派生这些类来实现自己的处理程序。

import logging

logger = logging.getLogger(__name__)

# 创建处理器
stream_handler = logging.StreamHandler()
file_handler = logging.FileHandler('file.log')

# 配置级别和格式化程序,并添加到处理器上
stream_handler.setLevel(logging.WARNING) # 警告及以上级别日志记录到流中
file_handler.setLevel(logging.ERROR) # 错误及以上级别记录到文件中

stream_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
file_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_handler.setFormatter(stream_format)
file_handler.setFormatter(file_format)

# 添加处理器到日志记录器上
logger.addHandler(stream_handler)
logger.addHandler(file_handler)

logger.warning('This is a warning') # 记录到流中
logger.error('This is an error') # 记录到流和文件中

过滤器例子

class InfoFilter(logging.Filter):
    
    # 覆盖此方法。 仅此方评估为True的日志记录将通过过滤器。
    def filter(self, record):
        return record.levelno == logging.INFO

# 现在只有 INFO 级别的消息会被记录。
stream_handler.addFilter(InfoFilter())
logger.addHandler(stream_handler)

其他配置方法

我们已经看到了如何配置日志,从而在代码中显式地创建日志记录器,处理程序和格式化程序。 还有其他两种配置方法:

.conf文件

创建一个 .conf(或有时存储为 .ini)文件,定义记录器,处理程序和格式化程序,并提供名称作为键。 定义其名称后,可以通过在其名称之间用下划线分隔之前添加单词 logger, handler 和 formatter 进行配置。 然后,你可以为每个记录器,处理程序和格式化程序设置属性。 在下面的示例中,将使用 StreamHandler 配置根记录器和名为 simpleExample 的记录器。

# logging.conf
[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
# 在代码中使用配置文件
import logging
import logging.config

logging.config.fileConfig('logging.conf')

# 使用配置文件中的名称创建记录器。
# 该记录器现在具有带有 DEBUG 级别和指定格式的 StreamHandler
logger = logging.getLogger('simpleExample')

logger.debug('debug message')
logger.info('info message')

捕获堆栈跟踪

将跟踪记录记录在异常日志中对于解决问题非常有用。 你可以通过将 excinfo 参数设置为True来捕获 logging.error() 中的回溯。

import logging

try:
    a = [1, 2, 3]
    value = a[3]
except IndexError as e:
    logging.error(e)
    logging.error(e, exc_info=True)
    ERROR:root:list index out of range
    ERROR:root:list index out of range
    Traceback (most recent call last):
      File "<ipython-input-6-df97a133cbe6>", line 5, in <module>
        value = a[3]
    IndexError: list index out of range

如果未捕获正确的 Exception,则还可以使用 traceback.formatexc() 方法记录该异常。

滚动 FileHandler

当你有一个大型应用程序将许多事件记录到一个文件中,而你只需要跟踪最近的事件时,请使用RotatingFileHandler来使文件保持较小。 当日志达到一定数量的字节时,它将被“滚动”。 你还可以保留多个备份日志文件,然后再覆盖它们。

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 2KB后滚动,并保留备份日志为 app.log.1, app.log.2 等.
handler = RotatingFileHandler('app.log', maxBytes=2000, backupCount=5)
logger.addHandler(handler)

for _ in range(10000):
    logger.info('Hello, world!')

TimedRotatingFileHandler

如果你的应用程序将长时间运行,则可以使用 TimedRotatingFileHandler。 这将根据经过的时间创建一个轮换日志。 when 参数的可能时间条件是:

  • second (s)

  • minute (m)

  • hour (h)

  • day (d)

  • w0-w6 (工作日, 0=星期一)

  • midnight

import logging
import time
from logging.handlers import TimedRotatingFileHandler
 
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 这将每分钟创建一个新的日志文件,并在覆盖旧日志之前创建一个带有时间戳的5个备份文件。
handler = TimedRotatingFileHandler('timed_test.log', when='m', interval=1, backupCount=5)
logger.addHandler(handler)
 
for i in range(6):
    logger.info('Hello, world!')
    time.sleep(50)

以JSON格式登录

如果你的应用程序从不同的模块(特别是在微服务体系结构中)生成许多日志,那么定位重要的日志以进行分析可能会很困难。 因此,最佳实践是以JSON格式记录你的消息,并将其发送到集中式日志管理系统。 然后,你可以轻松地搜索,可视化和分析日志记录。

我建议使用此开源JSON记录器:GitHub - madzak/python-json-logger: Json Formatter for the standard python logger

pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger

logger = logging.getLogger()

logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger.addHandler(logHandler)

;