概述
在本文中,我们将会对 Python 的日志相关操作进行展开讲解,相信经过本文的学习,你一定能够熟练的掌握 Python 日志操作相关的使用。
logging
logging 是 Python 官方提供的日志标准库,专门用于在应用程序、库的开发过程中支持日志打印。
logging 组件分析
logging 模块中主要包含了以下几个组件:
| 组件 | 说明 |
|---|---|
| logger | 提供应用程序代码直接使用的接口 |
| handlers | 用于将日志记录发送到指定的目的位置 |
| filters | 提供更细粒度的日志过滤功能,用于决定哪些日志记录将会被输出。 |
| formatters | 用于控制日志信息的最终输出格式 |
logger 对象
我们来依次看一下,首先是 logger 对象。
logger 对象主要负责三个工作:
- 向应用程序代码暴露几个方法,使应用程序可以在运行时记录日志消息;
- 基于日志严重等级(默认的过滤设施)或 filter 对象来决定要对哪些日志进行后续处理;
- 将日志消息传送给所有感兴趣的日志handlers。
日志打印的过程其实就是基于 logger 实例打印的,每个 logger 实例都有一个它的名称,它们在概念上使用点(句点)作为分隔符排列在命名空间层次结构中。 例如,名为“scan”的记录器是记录器“scan.text”、“scan.html”和“scan.pdf”的父级。 logger 名称可以是您想要的任何名称,只要能代表日志当前打印的位置就好。
一个比较推荐的方式使用模块名称作为日志实例的名称:
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对象。
比如,一个应用程序可能想要实现以下几个日志需求:
- 把所有日志都发送到一个日志文件中;
- 把所有严重级别大于等于error的日志发送到stdout(标准输出);
- 把所有严重级别为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 :
import loggingLOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"logging.basicConfig(filename='my.log', level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)logging.debug("This is a debug log.")logging.info("This is a info log.")logging.warning("This is a warning log.")logging.error("This is a error log.")logging.critical("This is a critical log.")
执行上述代码时,终端不会打印任何信息,其中日志会记录在一个 my.log 的文件中:
可以看到,日志打印的格式是按照我们定义的 LOG_FORMAT 打印的,同时,其实 asctime 的显示格式是由 DATE_FORMAT 来控制的。
PS:如果我们连 basicConfig 都没有设置,默认情况下会将 WARNING 级别及以上的日志输出到终端。
对 basicConfig() 的调用应先于对 debug()、info() 等的任何调用。否则,这些函数将使用默认选项为您调用 basicConfig()。 而且,由于basicConfig旨在作为一次性的简单配置工具,因此只有在第一次调用会生效:后续全部调用实际上是无操作的。
实战场景
在一个正式项目中,我们的日志使用模式通常如下,首先在创建初始化过程中实例化一个 root logger 并对其进行配置:
# 获取 root loggerroot_logger = logging.getLogger()# 设置日志打印级别root_logger.setLevel(logging.INFO)# 定义一个 handlerconsole_hdlr = logging.StreamHandler(sys.stdout)# 定义一个 formatterformatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')console_hdlr.setFormatter(formatter)root_logger.addHandler(console_hdlr)
接下来,在每次日志打印场景中,我们都只需要创建一个当前模块的 logger 实例即可:
import logginglogger = logging.getLogger(__name__)logger.info("hello world!")
更多资源
logzero
logzero 是一个基于 logging 扩展的日志库,相比 logging 库而言,logzero 库进行了进一步的封装,面向用户使用更加的友好。
安装
logzero 的安装非常简单:
pip install -U logzero
QuickStart
下面,我们来看一个 logzero 的示例代码:
from logzero import logger# 默认自动初始化配置logger.debug("hello")logger.info("info")logger.warning("warn")logger.error("error")# This is how you'd log an exceptiontry:raise Exception("this is a demo exception")except Exception as e:# exception 自动打印 Tracebacklogger.exception(e)# JSON loggingimport logzero# 以 JSON 格式进行日志打印logzero.json()logger.info("JSON test")# Start writing into a logfilelogzero.logfile("/tmp/logzero-demo.log")logger.info("hello world!")# 设置日志格式formatter = logging.Formatter('%(name)s - %(asctime)-15s - %(levelname)s: %(message)s')logzero.formatter(formatter)
可以看到,这是一个看起来非常简单的日志使用模式,它帮助我们做好了大部分的工作。
打印至日志文件
我们来看一下如何将日志打印到日志文件中并且能够实现自动切分:
logzero.logfile("%s/logs/logfile.log" % pwd,loglevel=logging.INFO,maxBytes=1 * 1024 * 1024 * 1024,backupCount=3,encoding='utf-8')
此时,就相当于我们在 root logger 层级添加了一个文件写入的 handler。
JSON格式日志打印
logzero 目前支持通过 JSON 的格式进行日志打印,它支持两种配置方式:
# Configure the default logger to output JSON>>> logzero.json()>>> logger.info("test"){"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"}# Configure a custom logger to output JSON>>> my_logger = setup_logger(json=True)>>> my_logger.info("test"){"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 实例分别遵从不同的打印逻辑时应该如何实现呢?
我们来看一下:
from logzero import setup_loggerlogger1 = setup_logger(name="mylogger1")logger2 = setup_logger(name="mylogger2", logfile="/tmp/test-logger2.log", level=logzero.INFO)logger3 = setup_logger(name="mylogger3", logfile="/tmp/test-logger3.log", level=logzero.INFO, disableStderrLogger=True)# Log something:logger1.info("info for logger 1")logger2.info("info for logger 2")# log to a file only, excluding the default stderr loggerlogger3.info("info for logger 3")# JSON logging in a custom loggerjsonLogger = setup_logger(name="jsonLogger", json=True)jsonLogger.info("info in json")
可以看到 setup_logger 方法可以创建新的 logger 实例,这样就可以实现不同的 Logger 实例功能独立了。
添加自定义 handler
有时,我们希望在 logzero 已有的 handler 的基础上增加一些新的 handler,那么又应该如何实现呢?我们来看一下:
import logzeroimport loggingfrom logging.handlers import SocketHandler# Setup the SocketHandlersocket_handler = SocketHandler(address=('localhost', logging.DEFAULT_TCP_LOGGING_PORT))socket_handler.setLevel(logzero.DEBUG)socket_handler.setFormatter(logzero.LogFormatter(color=False))# Attach it to the logzero default loggerlogzero.logger.addHandler(socket_handler)# Log messageslogzero.logger.info("this is a test")
可以看到,logzero 中有 logger 可以调用 addHandler 方法来添加自定义的 handler,这个和 logging 库是非常类似的。
logzero 与 logging 库组合
在很多场景中,如果我们自己使用 logzero 库进行日志打印,但是依赖的其他库使用的都是 logging 库进行日志打印时,那么我们通过 logzero 设置的日志格式在依赖库打印日志时可能无法生效。
那么,我们应该如何做呢?
结合我们上面的所学内容,我们其实只是需要用 logzero 的 setup_logger 函数创建一个指定配置的 root logger 实例即可实现全局日志配置。
示例如下:
import logzerofrom logzero import logger, setup_loggerlogzero.logger = setup_logger(logfile="file.log", isRootLogger=True)
这样一来,后续我们无论使用 logging 库直接进行日志打印,还是使用 logzero.logger 进行日志打印,都能够符合相同的规范了!
