概述
在本文中,我们将会对 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 logging
LOG_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 logger
root_logger = logging.getLogger()
# 设置日志打印级别
root_logger.setLevel(logging.INFO)
# 定义一个 handler
console_hdlr = logging.StreamHandler(sys.stdout)
# 定义一个 formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_hdlr.setFormatter(formatter)
root_logger.addHandler(console_hdlr)
接下来,在每次日志打印场景中,我们都只需要创建一个当前模块的 logger 实例即可:
import logging
logger = 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 exception
try:
raise Exception("this is a demo exception")
except Exception as e:
# exception 自动打印 Traceback
logger.exception(e)
# JSON logging
import logzero
# 以 JSON 格式进行日志打印
logzero.json()
logger.info("JSON test")
# Start writing into a logfile
logzero.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_logger
logger1 = 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 logger
logger3.info("info for logger 3")
# JSON logging in a custom logger
jsonLogger = setup_logger(name="jsonLogger", json=True)
jsonLogger.info("info in json")
可以看到 setup_logger 方法可以创建新的 logger 实例,这样就可以实现不同的 Logger 实例功能独立了。
添加自定义 handler
有时,我们希望在 logzero 已有的 handler 的基础上增加一些新的 handler,那么又应该如何实现呢?我们来看一下:
import logzero
import logging
from logging.handlers import SocketHandler
# Setup the SocketHandler
socket_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 logger
logzero.logger.addHandler(socket_handler)
# Log messages
logzero.logger.info("this is a test")
可以看到,logzero 中有 logger 可以调用 addHandler 方法来添加自定义的 handler,这个和 logging 库是非常类似的。
logzero 与 logging 库组合
在很多场景中,如果我们自己使用 logzero 库进行日志打印,但是依赖的其他库使用的都是 logging 库进行日志打印时,那么我们通过 logzero 设置的日志格式在依赖库打印日志时可能无法生效。
那么,我们应该如何做呢?
结合我们上面的所学内容,我们其实只是需要用 logzero 的 setup_logger 函数创建一个指定配置的 root logger 实例即可实现全局日志配置。
示例如下:
import logzero
from logzero import logger, setup_logger
logzero.logger = setup_logger(logfile="file.log", isRootLogger=True)
这样一来,后续我们无论使用 logging 库直接进行日志打印,还是使用 logzero.logger 进行日志打印,都能够符合相同的规范了!