PEP 282 – 日志系统
- 作者:
- Vinay Sajip <vinay_sajip at red-dove.com>, Trent Mick <trentm at activestate.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2002年2月4日
- Python 版本:
- 2.3
- 发布历史:
摘要
本PEP描述了Python标准库中提议的日志包。
基本上,该系统涉及用户创建一个或多个日志器对象,在这些对象上调用方法来记录调试信息、一般信息、警告、错误等。可以使用不同的日志“级别”来区分重要消息和不那么重要的消息。
维护一个命名单例日志器对象的注册表,以便:
- 存在不同的逻辑日志流(或“通道”)(例如,一个用于“zope.zodb”相关内容,另一个用于“mywebsite”特定内容)
- 无需传递日志器对象引用。
系统可在运行时配置。此配置机制允许调整日志记录的级别和类型,而无需触及应用程序本身。
动机
如果在标准库中引入单一的日志机制,1)日志记录更有可能“做得好”,2)多个库将能够集成到更大的应用程序中,这些应用程序可以进行合理且连贯的日志记录。
影响
本提案是在研究了以下日志包之后提出的:
简单示例
这显示了一个非常简单的示例,说明如何使用日志包在 stderr 上生成简单的日志输出。
--------- mymodule.py -------------------------------
import logging
log = logging.getLogger("MyModule")
def doIt():
log.debug("Doin' stuff...")
#do stuff...
raise TypeError, "Bogus type error for testing"
-----------------------------------------------------
--------- myapp.py ----------------------------------
import mymodule, logging
logging.basicConfig()
log = logging.getLogger("MyApp")
log.info("Starting my app")
try:
mymodule.doIt()
except Exception, e:
log.exception("There was a problem.")
log.info("Ending my app")
-----------------------------------------------------
$ python myapp.py
INFO:MyApp: Starting my app
DEBUG:MyModule: Doin' stuff...
ERROR:MyApp: There was a problem.
Traceback (most recent call last):
File "myapp.py", line 9, in ?
mymodule.doIt()
File "mymodule.py", line 7, in doIt
raise TypeError, "Bogus type error for testing"
TypeError: Bogus type error for testing
INFO:MyApp: Ending my app
上面的示例显示了默认输出格式。输出格式的所有方面都应该是可配置的,这样就可以得到如下格式的输出:
2002-04-19 07:56:58,174 MyModule DEBUG - Doin' stuff...
or just
Doin' stuff...
控制流
应用程序在 **Logger** 对象上进行日志调用。日志器以分层命名空间组织,子日志器从其在命名空间中的父日志器继承一些日志属性。
日志器名称符合“点分名称”命名空间,点(句号)表示子命名空间。因此,日志器对象的命名空间对应于一个单一的树状数据结构。
""是命名空间的根"Zope"将是根的子节点"Zope.ZODB"将是"Zope"的子节点
这些 Logger 对象创建 **LogRecord** 对象,这些对象被传递给 **Handler** 对象进行输出。Logger 和 Handler 都可以使用日志 **级别** 和(可选)**Filter** 来决定它们是否对特定的 LogRecord 感兴趣。当需要将 LogRecord 外部输出时,Handler 可以(可选地)使用 **Formatter** 在将其发送到 I/O 流之前进行本地化和格式化。
每个 Logger 都跟踪一组输出 Handler。默认情况下,所有 Logger 还会将其输出发送到其祖先 Logger 的所有 Handler。但是,Logger 也可以配置为忽略树中更上层的 Handler。
API 的结构使得当日志记录被禁用时,Logger API 上的调用可以很廉价。如果某个日志级别被禁用,Logger 可以进行一个廉价的比较测试并返回。如果某个日志级别被启用,Logger 仍然会小心地在将 LogRecord 传递给 Handler 之前最小化成本。特别是,本地化和格式化(它们相对昂贵)会延迟到 Handler 请求它们时才进行。
整个 Logger 层次结构也可以关联一个级别,该级别优先于单个 Logger 的级别。这是通过一个模块级函数完成的。
def disable(lvl):
"""
Do not generate any LogRecords for requests with a severity less
than 'lvl'.
"""
...
级别
日志级别,按重要性递增顺序排列,是:
- DEBUG(调试)
- INFO(信息)
- WARN(警告)
- ERROR(错误)
- CRITICAL(严重)
使用 CRITICAL 一词,而非 log4j 中使用的 FATAL。这些级别在概念上是相同的——表示严重或非常严重的错误。然而,FATAL 暗示死亡,在 Python 中则意味着引发并捕获的异常、回溯和退出。由于日志模块并未强制 FATAL 级别的日志条目导致此类结果,因此使用 CRITICAL 而非 FATAL 更为合理。
这些只是整数常量,以便于简单地比较重要性。经验表明,过多的级别可能会令人困惑,因为它们会导致对哪个级别应该应用于任何特定日志请求的主观解释。
尽管强烈推荐上述级别,但日志系统不应是强制性的。用户可以定义自己的级别,以及任何级别的文本表示。然而,用户定义的级别必须遵守的约束是,它们都是正整数,并且按严重性递增的顺序增加。
通过两个模块级函数支持用户定义的日志级别
def getLevelName(lvl):
"""Return the text for level 'lvl'."""
...
def addLevelName(lvl, lvlName):
"""
Add the level 'lvl' with associated text 'levelName', or
set the textual representation of existing level 'lvl' to be
'lvlName'."""
...
日志器
每个 Logger 对象都会跟踪它感兴趣的日志级别(或阈值),并丢弃低于该级别的日志请求。
一个 **Manager** 类实例维护着命名 Logger 对象的层次命名空间。世代由点分隔的名称表示:Logger “foo” 是 Logger “foo.bar” 和 “foo.baz” 的父级。
Manager 类实例是一个单例,不直接暴露给用户,用户通过各种模块级函数与其交互。
通用的日志方法是:
class Logger:
def log(self, lvl, msg, *args, **kwargs):
"""Log 'str(msg) % args' at logging level 'lvl'."""
...
然而,为每个日志级别定义了便利函数:
class Logger:
def debug(self, msg, *args, **kwargs): ...
def info(self, msg, *args, **kwargs): ...
def warn(self, msg, *args, **kwargs): ...
def error(self, msg, *args, **kwargs): ...
def critical(self, msg, *args, **kwargs): ...
目前只识别一个关键字参数——“exc_info”。如果为真,则调用者希望在日志输出中提供异常信息。只有当需要在 **任何** 日志级别提供异常信息时,才需要此机制。在更常见的情况下,即只有在发生错误时才需要将异常信息添加到日志中,例如在 ERROR 级别,则提供了另一个便利方法:
class Logger:
def exception(self, msg, *args): ...
这应仅在异常处理程序的上下文中使用,并且是表示希望在日志中获取异常信息的首选方式。其他便利方法旨在仅在异常情况下(例如,您可能希望在 INFO 消息的上下文提供异常信息)与 exc_info 一起调用。
上面显示的“msg”参数通常是格式字符串;但是,它可以是任何对象 x,对于该对象,str(x) 返回格式字符串。这有助于,例如,使用一个对象来获取国际化/本地化应用程序的特定于语言环境的消息,也许使用标准的 gettext 模块。一个概要示例:
class Message:
"""Represents a message"""
def __init__(self, id):
"""Initialize with the message ID"""
def __str__(self):
"""Return an appropriate localized message text"""
...
logger.info(Message("abc"), ...)
为日志消息收集和格式化数据可能代价高昂,如果日志记录器无论如何都会丢弃该消息,则这是一种浪费。要查看请求是否会被日志记录器接受,可以使用 isEnabledFor() 方法:
class Logger:
def isEnabledFor(self, lvl):
"""
Return true if requests at level 'lvl' will NOT be
discarded.
"""
...
因此,与其进行这种昂贵且可能浪费的 DOM 到 XML 转换
...
hamletStr = hamletDom.toxml()
log.info(hamletStr)
...
可以这样做
if log.isEnabledFor(logging.INFO):
hamletStr = hamletDom.toxml()
log.info(hamletStr)
创建新的日志记录器时,它们会初始化一个表示“无级别”的级别。可以使用 setLevel() 方法显式设置级别:
class Logger:
def setLevel(self, lvl): ...
如果未设置日志记录器的级别,系统将查询其所有祖先,沿层级向上查找,直到找到明确设置的级别。该级别被视为日志记录器的“有效级别”,可以通过 getEffectiveLevel() 方法进行查询。
def getEffectiveLevel(self): ...
日志器从不直接实例化。相反,使用模块级函数:
def getLogger(name=None): ...
如果未指定名称,则返回根日志记录器。否则,如果存在具有该名称的日志记录器,则返回该日志记录器。如果不存在,则初始化并返回一个新的日志记录器。在此处,“name”与“channel name”同义。
用户可以指定 Logger 的自定义子类,以便系统在实例化新的日志记录器时使用:
def setLoggerClass(klass): ...
传递的类应该是 Logger 的子类,并且其 __init__ 方法应该调用 Logger.__init__。
处理器
处理器负责对给定的 LogRecord 执行有用的操作。将实现以下核心处理器:
StreamHandler: 用于写入类文件对象的处理器。FileHandler: 用于写入单个文件或一组轮转文件的处理器。SocketHandler: 用于写入远程 TCP 端口的处理器。DatagramHandler: 用于写入 UDP 套接字的处理器,用于低成本日志记录。Jeff Bauer 已经有这样一个系统 [5]。MemoryHandler: 一个将日志记录缓冲在内存中,直到缓冲区满或发生特定条件为止的处理器 [1]。SMTPHandler: 通过 SMTP 发送电子邮件地址的处理器。SysLogHandler: 用于通过 UDP 写入 Unix 系统日志的处理器。NTEventLogHandler: 用于写入 Windows NT、2000 和 XP 上的事件日志的处理器。HTTPHandler: 用于通过 GET 或 POST 语义写入 Web 服务器的处理器。
处理器也可以使用 setLevel() 方法设置级别:
def setLevel(self, lvl): ...
FileHandler 可以设置为创建一组轮转的日志文件。在这种情况下,传递给构造函数的文件名被视为“基本”文件名。用于轮转的附加文件名通过在基本文件名后附加 .1、.2 等来创建,最大数量在请求轮转时指定。setRollover 方法用于指定日志文件的最大大小和轮转中的最大备份文件数量。
def setRollover(maxBytes, backupCount): ...
如果maxBytes被指定为零,则永远不会发生轮转,日志文件无限增长。如果指定了非零大小,当该大小即将超过时,将发生轮转。rollover方法确保基本文件名始终是最新文件,.1是次最新文件,.2是再次最新文件,依此类推。
[6] 提供的测试/示例脚本中实现了许多额外的处理程序——例如,XMLHandler 和 SOAPHandler。
日志记录
LogRecord 用作日志事件信息的容器。它只不过是一个字典,但它确实定义了一个 getMessage 方法,该方法将消息与可选的运行时参数合并。
格式器
格式化器负责将 LogRecord 转换为字符串表示形式。处理器可以在写入记录之前调用其格式化器。将实现以下核心格式化器:
Formatter: 使用 % 运算符提供类似 printf 的格式化。BufferingFormatter: 为多条消息提供格式化,并支持头部和尾部格式化。
通过在处理程序上调用 setFormatter(),格式化程序与处理程序关联起来:
def setFormatter(self, form): ...
格式器使用 % 运算符格式化日志消息。格式字符串应包含 %(name)x,并且 LogRecord 的属性字典用于获取消息特定数据。提供了以下属性:
%(name)s |
日志记录器(日志通道)的名称 |
%(levelno)s |
消息的数字日志级别(DEBUG、INFO、WARN、ERROR、CRITICAL) |
%(levelname)s |
消息的文本日志级别(“DEBUG”、“INFO”、“WARN”、“ERROR”、“CRITICAL”) |
%(pathname)s |
发出日志调用的源文件的完整路径名(如果可用) |
%(filename)s |
路径名中的文件名部分 |
%(module)s |
发出日志调用的模块 |
%(lineno)d |
发出日志调用的源行号(如果可用) |
%(created)f |
LogRecord 创建的时间 (time.time() 返回值) |
%(asctime)s |
LogRecord 创建的文本时间 |
%(msecs)d |
创建时间的毫秒部分 |
%(relativeCreated)d |
LogRecord 创建时相对于日志模块加载时间(通常是应用程序启动时间)的毫秒时间 |
%(thread)d |
线程 ID(如果可用) |
%(message)s |
record.getMessage() 的结果,在记录发出时计算 |
如果格式化程序发现格式字符串包含“(asctime)s”,则创建时间将格式化到 LogRecord 的 asctime 属性中。为了灵活地格式化日期,格式化程序使用整个消息的格式字符串和单独的日期/时间格式字符串进行初始化。日期/时间格式字符串应为 time.strftime 格式。消息格式的默认值为“%(message)s”。默认日期/时间格式为 ISO8601。
格式化程序使用一个类属性“converter”来指示如何将时间从秒转换为元组。默认情况下,“converter”的值是“time.localtime”。如果需要,可以在单个格式化程序实例上设置不同的转换器(例如“time.gmtime”),或者更改类属性以影响所有格式化程序实例。
过滤器
当基于级别的过滤不足时,Logger 或 Handler 可以调用 Filter 来决定是否应输出 LogRecord。Logger 和 Handler 可以安装多个过滤器,其中任何一个都可以否决 LogRecord 的输出。
class Filter:
def filter(self, record):
"""
Return a value indicating true if the record is to be
processed. Possibly modify the record, if deemed
appropriate by the filter.
"""
默认行为允许 Filter 使用 Logger 名称进行初始化。这将只允许通过使用命名日志器或其任何子日志器生成的事件。例如,使用“A.B”初始化的过滤器将允许由日志器“A.B”、“A.B.C”、“A.B.C.D”、“A.B.D”等记录的事件,但不允许“A.BB”、“B.A.B”等。如果使用空字符串初始化,则所有事件都由 Filter 传递。当希望将注意力集中在应用程序的某个特定区域时,这种过滤行为很有用;只需更改附加到根日志记录器的过滤器即可更改焦点。
[6] 中提供了许多过滤器示例。
配置
这种日志系统最主要的好处是,可以在不更改应用程序源代码的情况下,控制应用程序输出的日志数量和类型。因此,尽管可以通过日志 API 执行配置,但也必须能够完全不更改应用程序即可更改日志配置。对于 Zope 等长时间运行的程序,应该能够在程序运行时更改日志配置。
配置包括以下内容:
- 日志记录器或处理程序应关注的日志级别。
- 哪些处理程序应附加到哪些日志记录器。
- 哪些过滤器应附加到哪些处理程序和日志记录器。
- 指定特定于某些处理程序和过滤器的属性。
通常,每个应用程序对用户如何配置日志输出都有自己的要求。但是,每个应用程序都将通过标准机制向日志系统指定所需的配置。
最简单的配置是单个处理程序,写入 stderr,并附加到根日志记录器。此配置在导入日志模块后通过调用 basicConfig() 函数进行设置。
def basicConfig(): ...
对于更复杂的配置,本 PEP 未提出具体建议,原因如下:
- 具体建议可能被视为强制性。
- 在没有 Python 社区广泛实践经验的帮助下,无法知道任何给定的配置方法是否是好的。这种实践在日志模块被使用之前,即在 Python 2.3 发布 **之后**,才能真正出现。
- 不同类型的应用程序可能需要不同的配置方法,因此没有“一刀切”的解决方案。
参考实现 [6] 具有可用的配置文件格式,其目的是验证概念并提出一种可能的替代方案。可能需要创建单独的扩展模块(不属于核心 Python 发行版),用于日志配置和日志查看、补充处理程序以及其他对社区大部分成员不感兴趣的功能。
线程安全
日志系统应支持线程安全操作,而其用户无需采取任何特殊措施。
模块级函数
为了支持在短脚本和小型应用程序中使用日志机制,提供了模块级函数 debug()、info()、warn()、error()、critical() 和 exception()。它们的工作方式与 Logger 的相应命名方法相同——实际上,它们委托给根日志记录器上的相应方法。这些函数提供的另一个便利是,如果未进行任何配置,将自动调用 basicConfig()。
在应用程序退出时,所有处理器都可以通过调用函数来刷新:
def shutdown(): ...
这将刷新并关闭所有处理程序。
实施
参考实现是 Vinay Sajip 的日志模块 [6]。
打包
参考实现以单个模块的形式实现。这提供了最简单的接口——所有用户只需“import logging”即可使用所有可用功能。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0282.rst
上次修改: 2025-02-01 08:55:40 GMT