Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python增强提案

PEP 282 – 日志系统

作者:
Vinay Sajip <vinay_sajip at red-dove.com>, Trent Mick <trentm at activestate.com>
状态:
最终
类型:
标准跟踪
创建:
2002年2月4日
Python版本:
2.3
历史记录:


目录

摘要

本PEP描述了Python标准库中一个提议的日志记录包。

基本上,该系统涉及用户创建一个或多个记录器对象,并在其上调用方法来记录调试注释、一般信息、警告、错误等。可以使用不同的日志记录“级别”来区分重要消息和不太重要的消息。

维护一个命名单例记录器对象的注册表,以便

  1. 存在不同的逻辑日志记录流(或“通道”)(例如,一个用于“zope.zodb”内容,另一个用于“mywebsite”特定内容)
  2. 无需传递记录器对象引用。

该系统可以在运行时进行配置。此配置机制允许您调整执行的日志记录级别和类型,而无需接触应用程序本身。

动机

如果标准库中包含一个单一的日志记录机制,则1)日志记录更有可能“做好”,并且2)多个库能够集成到更大的应用程序中,这些应用程序可以以合理的方式进行一致的日志记录。

影响

在研究了以下日志记录包之后,提出了此提案

  • JDK 1.4 中的 java.util.logging(又名 JSR047) [1]
  • log4j [2]
  • Protomatter 项目中的 Syslog 包 [3]
  • MAL 的 mx.Log 包 [4]

简单示例

这展示了一个非常简单的示例,说明如何使用日志记录包在 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...

控制流程

应用程序在**记录器**对象上进行日志记录调用。记录器在分层命名空间中组织,子记录器继承命名空间中父记录器的一些日志记录属性。

记录器名称适合于“点分名称”命名空间,其中点(句点)表示子命名空间。因此,记录器对象的命名空间对应于单个树形数据结构。

  • "" 是命名空间的根
  • "Zope" 将是根的子节点
  • "Zope.ZODB" 将是"Zope"的子节点

这些记录器对象创建**日志记录**对象,这些对象被传递给**处理器**对象以进行输出。记录器和处理器都可以使用日志记录**级别**和(可选)**过滤器**来决定它们是否对特定的日志记录感兴趣。当需要在外部输出日志记录时,处理器可以(可选)使用**格式化器**在将其发送到 I/O 流之前本地化和格式化消息。

每个记录器都跟踪一组输出处理器。默认情况下,所有记录器也将它们的输出发送到其祖先记录器的所有处理器。但是,也可以将记录器配置为忽略树中较高位置的处理器。

API 的结构使得当禁用日志记录时,对记录器 API 的调用可以很便宜。如果为给定日志级别禁用了日志记录,则记录器可以进行廉价的比较测试并返回。如果为给定日志级别启用了日志记录,则记录器仍然谨慎地在将日志记录传递到处理器之前最大程度地降低成本。特别是,本地化和格式化(这相对昂贵)被推迟到处理器请求它们为止。

整个记录器层次结构还可以关联一个级别,该级别优先于各个记录器的级别。这是通过模块级函数完成的

def disable(lvl):
    """
    Do not generate any LogRecords for requests with a severity less
    than 'lvl'.
    """
    ...

级别

日志级别,按重要性递增的顺序排列,为

  • 调试
  • 信息
  • 警告
  • 错误
  • 严重

术语“严重”优先于“致命”,后者由 log4j 使用。从概念上讲,这两个级别相同——严重或非常严重的错误。但是,“致命”意味着死亡,在 Python 中意味着引发未捕获的异常、回溯和退出。由于日志记录模块不会从“致命”级日志条目中强制执行此类结果,因此最好使用“严重”而不是“致命”。

这些仅仅是整数常量,以便可以简单地比较重要性。经验表明,太多的级别可能会令人困惑,因为它们会导致对应将哪个级别应用于任何特定日志请求的主观解释。

尽管强烈推荐以上级别,但日志记录系统不应该具有规定性。用户可以定义自己的级别,以及任何级别的文本表示。但是,用户定义的级别必须遵守以下约束:它们都是正整数,并且它们按严重性递增的顺序排列。

用户定义的日志级别通过两个模块级函数得到支持

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'."""
        ...

记录器

每个记录器对象都跟踪它感兴趣的日志级别(或阈值),并丢弃低于该级别的日志请求。

**管理器**类实例维护命名记录器对象的层次结构命名空间。生成使用点分隔名称表示:记录器“foo”是记录器“foo.bar”和“foo.baz”的父级。

管理器类实例是单例,不会直接公开给用户,用户使用各种模块级函数与之交互。

通用日志记录方法是

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): ...

如果未指定名称,则返回根记录器。否则,如果存在具有该名称的记录器,则返回该记录器。如果没有,则初始化并返回一个新的记录器。在此,“名称”与“通道名称”同义。

用户可以指定一个自定义的 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指定为零,则永远不会发生回滚,日志文件将无限增长。如果指定了非零大小,则当即将超过该大小时,就会发生回滚。回滚方法确保基本文件名始终是最新的,.1 是次新的,.2 是再次新的,依此类推。

在随附的测试/示例脚本[6]中实现了许多其他处理程序 - 例如,XMLHandler 和 SOAPHandler。

日志记录

LogRecord充当日志事件信息的容器。它只不过是一个字典,尽管它确实定义了一个getMessage方法,该方法将消息与可选的运行参数合并。

格式化器

Formatter负责将LogRecord转换为字符串表示形式。Handler在写入记录之前可能会调用其Formatter。将实现以下核心Formatter

  • Formatter:使用%运算符提供类似printf的格式化。
  • BufferingFormatter:为多条消息提供格式化,并支持标题和结尾格式化。

通过在处理程序上调用setFormatter()将Formatter与Handler关联

def setFormatter(self, form): ...

Formatter使用%运算符来格式化日志消息。格式字符串应包含%(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属性。为了允许灵活地格式化日期,Formatter使用整个消息的格式字符串和日期/时间的单独格式字符串进行初始化。日期/时间格式字符串应采用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.
        """

默认行为允许使用Logger名称初始化Filter。这将仅允许通过使用命名记录器或其任何子级生成的事件。例如,使用“A.B”初始化的过滤器将允许由记录器“A.B”、“A.B.C”、“A.B.C.D”、“A.B.D”等记录的事件,但不允许“A.BB”、“B.A.B”等。如果使用空字符串初始化,则Filter将传递所有事件。当希望将注意力集中在应用程序的某个特定区域时,此过滤器行为很有用;只需更改附加到根记录器的过滤器即可更改焦点。

[6]中提供了许多Filter示例。

配置

这种日志系统的主要好处是,可以控制从应用程序获取多少和什么日志输出,而无需更改该应用程序的源代码。因此,尽管可以通过日志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

上次修改时间:2023-10-10 05:32:07 GMT