概述

在本文中,我们将会对 Python 的日志相关操作进行展开讲解,相信经过本文的学习,你一定能够熟练的掌握 Python 日志操作相关的使用。

logging

logging 是 Python 官方提供的日志标准库,专门用于在应用程序、库的开发过程中支持日志打印。

logging 组件分析

logging 模块中主要包含了以下几个组件:

组件 说明
logger 提供应用程序代码直接使用的接口
handlers 用于将日志记录发送到指定的目的位置
filters 提供更细粒度的日志过滤功能,用于决定哪些日志记录将会被输出。
formatters 用于控制日志信息的最终输出格式

logger 对象

我们来依次看一下,首先是 logger 对象。
logger 对象主要负责三个工作:

  1. 向应用程序代码暴露几个方法,使应用程序可以在运行时记录日志消息;
  2. 基于日志严重等级(默认的过滤设施)或 filter 对象来决定要对哪些日志进行后续处理;
  3. 将日志消息传送给所有感兴趣的日志handlers。

日志打印的过程其实就是基于 logger 实例打印的,每个 logger 实例都有一个它的名称,它们在概念上使用点(句点)作为分隔符排列在命名空间层次结构中。 例如,名为“scan”的记录器是记录器“scan.text”、“scan.html”和“scan.pdf”的父级。 logger 名称可以是您想要的任何名称,只要能代表日志当前打印的位置就好。
一个比较推荐的方式使用模块名称作为日志实例的名称:

  1. logger = logging.getLogger(__name__)

这表示这个logger实例的名称就是当前模块的名称。
logger 层级中根节点的 logger 实例就称之为 root logger。直接调用 logging 库中的 debug、info 等方法的时候,实际上就是再使用 root logger 在打印日志。
Logger对象最常用的配置方法如下:

方法 描述
Logger.setLevel() 设置日志器将会处理的日志消息的最低严重级别
Logger.addHandler() 为该logger对象添加一个handler对象
Logger.addFilter() 为该logger对象添加一个filter对象
Logger.removeHandler() 为该logger对象删除一个 handler 对象

创建好 logger 实例之后,就可以使用 Logger.debug()、Logger.info()、Logger.warning()、Logger.error() 和 Logger.critical() 来创建日志记录了。Logger.exception() 创建类似于 Logger.error() 的日志消息,不同之处在于 Logger.exception() 连同它一起转储堆栈跟踪,因此,该方法通过仅仅在 exception 场景下调用。
logging.getLogger() 方法调用时,如果传入了名称,则返回对具有指定名称的 logger 实例的引用,否则返回 root logger 实例的引用。同时,多次调用时返回的是同一个实例的引用。
logger 实例在查询配置时,是有一个层级概念的,如果当前 logger 实例没有找到配置,就会递归向上查询父实例的配置,直到查询到 root 实例的配置。

PS:root logger 始终具有明确的级别集(默认为警告),不能使用 setLevel 修改。

handler

下面,我们来看一下什么是 handler:Handler对象的作用是基于消息的日志等级将消息分发到handler指定的位置(文件、网络、邮件等)。
Logger对象可以通过 addHandler() 方法为自己添加0个或者多个handler对象。
比如,一个应用程序可能想要实现以下几个日志需求:

  1. 把所有日志都发送到一个日志文件中;
  2. 把所有严重级别大于等于error的日志发送到stdout(标准输出);
  3. 把所有严重级别为critical的日志发送到一个email邮件地址。

这种场景就需要3个不同的handlers,每个handler复杂发送一个特定严重级别的日志到一个特定的位置。
添加 handler 其实非常简单,对于使用内建handler对象的应用开发人员来说,似乎唯一相关的handler方法就是下面这几个配置方法:

方法 描述
Handler.setLevel() 设置handler将会处理的日志消息的最低严重级别
Handler.setFormatter() 为handler设置一个格式器对象
Handler.addFilter() 为handler添加一个过滤器对象

需要说明的是,应用程序代码不应该直接实例化和使用Handler实例。因为Handler是一个基类,它只定义了素有handlers都应该有的接口,同时提供了一些子类可以直接使用或覆盖的默认行为。
下面是python提供的全部handler:

Handler 说明
StreamHandler 实例将消息发送到流(类似文件的对象)
FileHandler 实例将消息发送到磁盘文件
BaseRotatingHandler 是RotatingFileHandler和TimedRotatingFileHandler的基类
RotatingFileHandler 实例将消息发送到磁盘文件,支持日志按大小切割
TimedRotatingFileHandler 实例将消息发送到磁盘文件,支持按时间切割
SocketHandler 实例将消息发送到TCP / IP套接字。
DatagramHandler 实例将消息发送到UDP套接字。
SMTPHandler 实例将消息发送到指定的电子邮件地址
SysLogHandler 实例将消息发送到Unix syslog守护程序(可能在远程计算机上)
NTEventLogHandler 实例将消息发送到Windows NT / 2000 / XP事件日志。
MemoryHandler 实例将消息发送到内存中的缓冲区,只要满足特定条件,该缓冲区就会被刷新。
HTTPHandler 实例使用GET或POST语义将消息发送到HTTP服务器。
WatchedFileHandler 实例监视他们正在登录的文件。如果文件更改,则使用文件名将其关闭并重新打开。该处理程序仅在类似Unix的系统上有用。Windows不支持所使用的基础机制。
QueueHandler 实例将消息发送到队列,例如在queue或multiprocessing模块中实现的消息。
NullHandler 实例不处理错误消息。希望使用日志记录但希望避免出现“找不到用于记录器XXX的处理程序”消息的库开发人员会使用它们,如果库用户未配置日志记录,则会显示该消息。有关更多信息,请参见为库配置日志。

formater

Formater 对象用于配置日志信息的最终顺序、结构和内容。
与logging.Handler基类不同的是,应用代码可以直接实例化Formatter类。

filter

Filter可以被Handler和Logger用来做比level更细粒度的、更复杂的过滤功能。
Filter是一个过滤器基类,它只允许某个logger层级下的日志事件通过过滤。

日志级别

在 logging 模块中,日志默认有5个级别:

级别 数值 使用时
CRITICAL 50 严重错误,表明程序本身可能无法继续运行。
ERROR 40 由于存在更严重的问题,该软件无法执行某些功能。
WARNING 30 表示发生了意外情况,或者表示在不久的将来出现了某些问题(例如“磁盘空间不足”)。该软件仍按预期运行。
INFO 20 确认一切正常。
DEBUG 10 详细信息,通常仅在诊断问题时才需要。

设置要打印的log时只需要设置优先级,比如设置打印INFO,那么比INFO优先级高的WARNING/ERROR/CRITICAL都将被打印,默认级别为WARNING。

log格式

在 formater 中,我们可以设置日志的格式,其中,设置日志格式时支持如下符号:

%(name)s Logger的名字
%(levelno)s 数字形式的日志级别
%(levelname)s 文本形式的日志级别
%(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
%(filename)s 调用日志输出函数的模块的文件名
%(module)s 调用日志输出函数的模块名
%(funcName)s 调用日志输出函数的函数名
%(lineno)d 调用日志输出函数的语句所在的代码行
%(created)f 当前时间,用UNIX标准的表示时间的浮点数表示
%(relativeCreated)d 输出日志信息时的,自Logger创建以来的毫秒数
%(asctime)s 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒
%(thread)d 线程ID(可能没有)
%(threadName)s 线程名(可能没有)
%(process)d 进程ID(可能没有)
%(message)s 用户输出的消息

logging实战

下面,我们就基于我们上述学习的内容来实现一下日志打印吧:

无handler场景

我们先看一个最简单的场景,都没有单独添加 handler :

  1. import logging
  2. LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
  3. DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"
  4. logging.basicConfig(filename='my.log', level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)
  5. logging.debug("This is a debug log.")
  6. logging.info("This is a info log.")
  7. logging.warning("This is a warning log.")
  8. logging.error("This is a error log.")
  9. logging.critical("This is a critical log.")

执行上述代码时,终端不会打印任何信息,其中日志会记录在一个 my.log 的文件中:
image.png
可以看到,日志打印的格式是按照我们定义的 LOG_FORMAT 打印的,同时,其实 asctime 的显示格式是由 DATE_FORMAT 来控制的。

PS:如果我们连 basicConfig 都没有设置,默认情况下会将 WARNING 级别及以上的日志输出到终端。

对 basicConfig() 的调用应先于对 debug()、info() 等的任何调用。否则,这些函数将使用默认选项为您调用 basicConfig()。 而且,由于basicConfig旨在作为一次性的简单配置工具,因此只有在第一次调用会生效:后续全部调用实际上是无操作的。

实战场景

在一个正式项目中,我们的日志使用模式通常如下,首先在创建初始化过程中实例化一个 root logger 并对其进行配置:

  1. # 获取 root logger
  2. root_logger = logging.getLogger()
  3. # 设置日志打印级别
  4. root_logger.setLevel(logging.INFO)
  5. # 定义一个 handler
  6. console_hdlr = logging.StreamHandler(sys.stdout)
  7. # 定义一个 formatter
  8. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  9. console_hdlr.setFormatter(formatter)
  10. root_logger.addHandler(console_hdlr)

接下来,在每次日志打印场景中,我们都只需要创建一个当前模块的 logger 实例即可:

  1. import logging
  2. logger = logging.getLogger(__name__)
  3. logger.info("hello world!")

更多资源

logzero

logzero 是一个基于 logging 扩展的日志库,相比 logging 库而言,logzero 库进行了进一步的封装,面向用户使用更加的友好。

安装

logzero 的安装非常简单:

  1. pip install -U logzero

QuickStart

下面,我们来看一个 logzero 的示例代码:

  1. from logzero import logger
  2. # 默认自动初始化配置
  3. logger.debug("hello")
  4. logger.info("info")
  5. logger.warning("warn")
  6. logger.error("error")
  7. # This is how you'd log an exception
  8. try:
  9. raise Exception("this is a demo exception")
  10. except Exception as e:
  11. # exception 自动打印 Traceback
  12. logger.exception(e)
  13. # JSON logging
  14. import logzero
  15. # 以 JSON 格式进行日志打印
  16. logzero.json()
  17. logger.info("JSON test")
  18. # Start writing into a logfile
  19. logzero.logfile("/tmp/logzero-demo.log")
  20. logger.info("hello world!")
  21. # 设置日志格式
  22. formatter = logging.Formatter('%(name)s - %(asctime)-15s - %(levelname)s: %(message)s')
  23. logzero.formatter(formatter)

可以看到,这是一个看起来非常简单的日志使用模式,它帮助我们做好了大部分的工作。

打印至日志文件

我们来看一下如何将日志打印到日志文件中并且能够实现自动切分:

  1. logzero.logfile(
  2. "%s/logs/logfile.log" % pwd,
  3. loglevel=logging.INFO,
  4. maxBytes=1 * 1024 * 1024 * 1024,
  5. backupCount=3,
  6. encoding='utf-8'
  7. )

此时,就相当于我们在 root logger 层级添加了一个文件写入的 handler。

JSON格式日志打印

logzero 目前支持通过 JSON 的格式进行日志打印,它支持两种配置方式:

  1. # Configure the default logger to output JSON
  2. >>> logzero.json()
  3. >>> logger.info("test")
  4. {"asctime": "2020-10-21 10:42:45,808", "filename": "<stdin>", "funcName": "<module>", "levelname": "INFO", "levelno": 20, "lineno": 1, "module": "<stdin>", "message": "test", "name": "logzero_default", "pathname": "<stdin>", "process": 76179, "processName": "MainProcess", "threadName": "MainThread"}
  5. # Configure a custom logger to output JSON
  6. >>> my_logger = setup_logger(json=True)
  7. >>> my_logger.info("test")
  8. {"asctime": "2020-10-21 10:42:45,808", "filename": "<stdin>", "funcName": "<module>", "levelname": "INFO", "levelno": 20, "lineno": 1, "module": "<stdin>", "message": "test", "name": "logzero_default", "pathname": "<stdin>", "process": 76179, "processName": "MainProcess", "threadName": "MainThread"}

自定义 logger 实例

在上述的示例中,我们相当于使用全部在使用 root logger,即全局只有一个 Logger 实例,每次配置会影响整个项目全部的日志打印逻辑。
如果你想要有多个 Logger 实例分别遵从不同的打印逻辑时应该如何实现呢?
我们来看一下:

  1. from logzero import setup_logger
  2. logger1 = setup_logger(name="mylogger1")
  3. logger2 = setup_logger(name="mylogger2", logfile="/tmp/test-logger2.log", level=logzero.INFO)
  4. logger3 = setup_logger(name="mylogger3", logfile="/tmp/test-logger3.log", level=logzero.INFO, disableStderrLogger=True)
  5. # Log something:
  6. logger1.info("info for logger 1")
  7. logger2.info("info for logger 2")
  8. # log to a file only, excluding the default stderr logger
  9. logger3.info("info for logger 3")
  10. # JSON logging in a custom logger
  11. jsonLogger = setup_logger(name="jsonLogger", json=True)
  12. jsonLogger.info("info in json")

可以看到 setup_logger 方法可以创建新的 logger 实例,这样就可以实现不同的 Logger 实例功能独立了。

添加自定义 handler

有时,我们希望在 logzero 已有的 handler 的基础上增加一些新的 handler,那么又应该如何实现呢?我们来看一下:

  1. import logzero
  2. import logging
  3. from logging.handlers import SocketHandler
  4. # Setup the SocketHandler
  5. socket_handler = SocketHandler(address=('localhost', logging.DEFAULT_TCP_LOGGING_PORT))
  6. socket_handler.setLevel(logzero.DEBUG)
  7. socket_handler.setFormatter(logzero.LogFormatter(color=False))
  8. # Attach it to the logzero default logger
  9. logzero.logger.addHandler(socket_handler)
  10. # Log messages
  11. logzero.logger.info("this is a test")

可以看到,logzero 中有 logger 可以调用 addHandler 方法来添加自定义的 handler,这个和 logging 库是非常类似的。

logzero 与 logging 库组合

在很多场景中,如果我们自己使用 logzero 库进行日志打印,但是依赖的其他库使用的都是 logging 库进行日志打印时,那么我们通过 logzero 设置的日志格式在依赖库打印日志时可能无法生效。
那么,我们应该如何做呢?
结合我们上面的所学内容,我们其实只是需要用 logzero 的 setup_logger 函数创建一个指定配置的 root logger 实例即可实现全局日志配置。
示例如下:

  1. import logzero
  2. from logzero import logger, setup_logger
  3. logzero.logger = setup_logger(logfile="file.log", isRootLogger=True)

这样一来,后续我们无论使用 logging 库直接进行日志打印,还是使用 logzero.logger 进行日志打印,都能够符合相同的规范了!