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

Python 增强提案

PEP 391 – 基于字典的日志配置

作者:
Vinay Sajip <vinay_sajip at red-dove.com>
状态:
最终
类型:
标准轨迹
创建:
2009 年 10 月 15 日
Python 版本:
2.7, 3.2
历史记录:


目录

摘要

此 PEP 描述了一种使用字典来保存配置信息的新日志配置方式。

理由

目前配置 Python 日志包的方式有两种:使用日志 API 以编程方式配置日志,或使用基于 ConfigParser 的配置文件。

虽然编程配置提供了最大的控制权,但它将配置固定在 Python 代码中。这不利于在运行时轻松更改配置,因此,灵活地为使用应用程序的不同部分上下调整日志详细程度的能力就消失了。这限制了日志作为诊断问题辅助工具的可用性 - 有时,日志是生产环境中唯一可用的诊断工具。

基于 ConfigParser 的配置系统可用,但它不允许用户配置日志包的所有方面。例如,无法使用此系统配置过滤器。此外,ConfigParser 格式似乎在某些方面(有时是强烈厌恶)引起反感。尽管当时它是被选择为唯一支持的 Python 标准配置格式,但许多人认为它(或者可能是仅为日志配置选择的特定模式)是“笨拙的”或“丑陋的”,在某些情况下显然是出于纯粹的美学原因。

最新版本的 Python 在标准库中包含 JSON 支持,这也可用作配置格式。在其他环境中,例如 Google App Engine,YAML 用于配置应用程序,通常将日志配置视为应用程序配置的组成部分。虽然标准库目前不包含 YAML 支持,但可以通过通用方式提供对 JSON 和 YAML 的支持,因为这两种序列化格式都允许反序列化为 Python 字典。

通过提供一种通过传递字典中的配置来配置日志的方式,日志将更容易配置,不仅适用于 JSON 和/或 YAML 用户,也适用于自定义配置方法的用户,因为它提供了一种描述所需配置的通用格式。

当前基于 ConfigParser 的配置系统的另一个缺点是它不支持增量配置:新配置完全替换现有配置。虽然很难在多线程环境中提供增量配置的完全灵活性,但新的配置机制将允许提供对增量配置的有限支持。

规范

该规范包含两个部分:API 和用于传递配置信息的字典的格式(即它必须符合的模式)。

命名

从历史上看,日志包并不符合 PEP 8。在将来的某个时间点,将通过更改包中的方法和函数名称以符合 PEP 8 来纠正此问题。但是,为了保持一致性,建议的 API 添加使用与日志当前使用的方案一致的命名方案。

API

logging.config 模块将添加以下内容:

  • 一个名为 dictConfig() 的函数,它接受一个参数 - 保存配置的字典。如果处理字典时出现错误,将引发异常。

将能够自定义此 API - 请参阅关于 API 定制 的部分。 增量配置 在其自身部分中介绍。

字典模式 - 概述

在详细描述模式之前,有必要简单介绍一下对象连接、对用户定义对象的支持以及对外部和内部对象的访问。

对象连接

该模式旨在描述一组日志对象 - 记录器、处理程序、格式化程序、过滤器 - 这些对象在对象图中相互连接。因此,该模式需要表示对象之间的连接。例如,假设配置后,特定记录器已连接到特定处理程序。为了便于讨论,我们可以说记录器代表源,而处理程序代表两个之间连接的目标。当然,在配置的对象中,这通过记录器保存对处理程序的引用来表示。在配置字典中,这是通过为每个目标对象提供一个唯一标识它的 ID,然后在源对象的配置中使用该 ID 来指示源和具有该 ID 的目标对象之间存在连接来完成的。

例如,考虑以下 YAML 代码段:

formatters:
  brief:
    # configuration for formatter with id 'brief' goes here
  precise:
    # configuration for formatter with id 'precise' goes here
handlers:
  h1: #This is an id
   # configuration of handler with id 'h1' goes here
   formatter: brief
  h2: #This is another id
   # configuration of handler with id 'h2' goes here
   formatter: precise
loggers:
  foo.bar.baz:
    # other configuration for logger 'foo.bar.baz'
    handlers: [h1, h2]

(注意:在本文件中将使用 YAML,因为它比字典的等效 Python 源代码形式更易读。)

记录器的 ID 是记录器名称,这些名称将以编程方式用于获取对这些记录器的引用,例如 foo.bar.baz。格式化程序和过滤器的 ID 可以是任何字符串值(例如上面的 briefprecise),它们是短暂的,因为它们仅在处理配置字典时才有意义,用于确定对象之间的连接,并且在配置调用完成后不会保留在任何地方。

处理程序 ID 的处理方式特殊,请参阅以下关于 处理程序 ID 的部分。

上面的代码段表明,名为 foo.bar.baz 的记录器应附加两个处理程序,它们由处理程序 ID h1h2 描述。h1 的格式化程序是 ID 为 brief 的格式化程序,而 h2 的格式化程序是 ID 为 precise 的格式化程序。

用户定义的对象

该模式应支持用户为处理程序、过滤器和格式化程序定义的对象。(记录器不需要为不同的实例具有不同的类型,因此配置中不支持用户定义的记录器类。)

要配置的对象通常由描述其配置的字典来描述。在某些地方,日志系统将能够从上下文中推断出对象的实例化方式,但在实例化用户定义的对象时,系统将不知道如何执行此操作。为了为用户定义的对象实例化提供完全的灵活性,用户需要提供一个“工厂” - 一个可调用对象,它在调用时使用配置字典并返回实例化的对象。这将通过在特殊键 '()' 下提供工厂的绝对导入路径来表示。以下是一个具体示例:

formatters:
  brief:
    format: '%(message)s'
  default:
    format: '%(asctime)s %(levelname)-8s %(name)-15s %(message)s'
    datefmt: '%Y-%m-%d %H:%M:%S'
  custom:
      (): my.package.customFormatterFactory
      bar: baz
      spam: 99.9
      answer: 42

上面的 YAML 代码段定义了三个格式化程序。第一个,ID 为 brief,是具有指定格式字符串的标准 logging.Formatter 实例。第二个,ID 为 default,具有更长的格式,并且还显式定义了时间格式,这将导致使用这两个格式字符串初始化的 logging.Formatter。以 Python 源代码形式显示,briefdefault 格式化程序的配置子字典为:

{
  'format' : '%(message)s'
}

{
  'format' : '%(asctime)s %(levelname)-8s %(name)-15s %(message)s',
  'datefmt' : '%Y-%m-%d %H:%M:%S'
}

分别,因为这些字典不包含特殊键 '()',所以实例化是从上下文中推断出来的:因此,将创建标准 logging.Formatter 实例。第三个格式化程序(ID 为 custom)的配置子字典为:

{
  '()' : 'my.package.customFormatterFactory',
  'bar' : 'baz',
  'spam' : 99.9,
  'answer' : 42
}

并且它包含特殊键 '()',这意味着需要用户定义的实例化。在这种情况下,将使用指定的工厂可调用对象。如果它是一个实际的可调用对象,则将直接使用它 - 否则,如果您指定一个字符串(如示例中所示),则将使用正常的导入机制定位实际的可调用对象。可调用对象将使用配置子字典中的剩余项作为关键字参数进行调用。在上面的示例中,ID 为 custom 的格式化程序将被假定为由以下调用返回:

my.package.customFormatterFactory(bar='baz', spam=99.9, answer=42)

'()' 已用作特殊键,因为它不是有效的关键字参数名称,因此不会与调用中使用的关键字参数的名称发生冲突。 '()' 也可以用作助记符,表示相应的值是一个可调用对象。

访问外部对象

有时,配置需要引用配置外部的对象,例如 sys.stderr。如果配置字典是使用 Python 代码构建的,那么这很简单,但如果配置是通过文本文件(例如 JSON、YAML)提供的,则会出现问题。在文本文件中,没有标准方法可以区分 sys.stderr 和文字字符串 'sys.stderr'。为了便于区分,配置系统将在字符串值中查找某些特殊前缀并对其进行特殊处理。例如,如果文字字符串 'ext://sys.stderr' 作为配置中的值提供,那么 ext:// 将被剥离,并且使用正常的导入机制处理值的其余部分。

处理此类前缀的方式类似于协议处理:将使用一个通用机制来查找与正则表达式 ^(?P<prefix>[a-z]+)://(?P<suffix>.*)$ 匹配的前缀,如果识别出 prefix,则以依赖于前缀的方式处理 suffix,处理结果将替换字符串值。如果未识别出前缀,则字符串值将保持不变。

实现将提供一组标准前缀,例如 ext://,但可以完全禁用此机制,或者提供其他或不同的前缀用于特殊处理。

访问内部对象

除了外部对象之外,有时还需要引用配置中的对象。这将由配置系统针对其已知的事物隐式执行。例如,字符串值 'DEBUG' 用于记录器或处理程序中的 level 将自动转换为值 logging.DEBUG,而 handlersfiltersformatter 条目将采用对象 ID 并解析为相应的目标对象。

但是,对于用户定义的(日志记录系统未知的)对象,需要提供更通用的机制。例如,以 logging.handlers.MemoryHandler 实例为例,该实例采用 target,另一个用于委托的处理程序。由于系统已经了解此类,因此在配置中,给定的 target 仅需为相关目标处理程序的对象 ID,系统将根据 ID 解析为处理程序。但是,如果用户定义了具有 alternate 处理程序的 my.package.MyHandler,则配置系统将不知道 alternate 指的是处理程序。为了满足这种需求,将提供一个通用的解析系统,允许用户指定

handlers:
  file:
    # configuration of file handler goes here

  custom:
    (): my.package.MyHandler
    alternate: cfg://handlers.file

字面量字符串 'cfg://handlers.file' 将以类似于具有 ext:// 前缀的字符串的方式解析,但将在配置本身而不是导入命名空间中查找。此机制将允许以类似于 str.format 提供的方式通过点或索引访问。因此,给定以下代码段

handlers:
  email:
    class: logging.handlers.SMTPHandler
    mailhost: localhost
    fromaddr: my_app@domain.tld
    toaddrs:
      - support_team@domain.tld
      - dev_team@domain.tld
    subject: Houston, we have a problem.

在配置中,字符串 'cfg://handlers' 将解析为具有键 handlers 的字典,字符串 'cfg://handlers.email' 将解析为 handlers 字典中具有键 email 的字典,依此类推。字符串 'cfg://handlers.email.toaddrs[1]' 将解析为 'dev_team.domain.tld',字符串 'cfg://handlers.email.toaddrs[0]' 将解析为值 'support_team@domain.tld'。可以使用 'cfg://handlers.email.subject' 或等效地使用 'cfg://handlers.email[subject]' 访问 subject 值。只有当键包含空格或非字母数字字符时,才需要使用后一种形式。如果索引值仅包含十进制数字,则将尝试使用相应的整数值访问,如果失败,则回退到字符串值。

给定字符串 cfg://handlers.myhandler.mykey.123,这将解析为 config_dict['handlers']['myhandler']['mykey']['123']。如果字符串指定为 cfg://handlers.myhandler.mykey[123],系统将尝试从 config_dict['handlers']['myhandler']['mykey'][123] 中检索值,如果失败,则回退到 config_dict['handlers']['myhandler']['mykey']['123']

处理程序 ID

某些特定的日志记录配置需要使用处理程序级别来实现所需的效果。但是,与始终可以通过其名称识别的记录器不同,处理程序没有持久性句柄,因此无法通过增量配置调用来更改级别。

因此,此 PEP 建议向处理程序添加可选的 name 属性。如果使用此属性,它将添加一个字典条目,将名称映射到处理程序。(该条目将在处理程序关闭时被移除。)当进行增量配置调用时,将在该字典中查找处理程序,并根据配置中的值设置处理程序级别。有关更多详细信息,请参阅有关 增量配置 的部分。

理论上,也可以为过滤器和格式化程序提供此类“持久性名称”功能。但是,没有充分的理由说明需要能够增量配置这些项。基于实用性胜过纯度的原则,只有处理程序将获得此新的 name 属性。配置中处理程序的 ID 将变为其 name

处理程序名称查找字典仅用于配置,不会成为包的公共 API 的一部分。

字典模式 - 详细

传递给 dictConfig() 的字典必须包含以下键

  • version - 设置为一个整数,表示模式版本。目前唯一有效的值为 1,但具有此键允许模式演进,同时仍保留向后兼容性。

所有其他键都是可选的,但如果存在,则将按以下说明进行解释。在以下所有提到“配置字典”的情况下,都将检查其是否存在特殊键 '()',以查看是否需要自定义实例化。如果是,则使用上面描述的机制进行实例化;否则,将使用上下文来确定如何实例化。

  • formatters - 对应值将是一个字典,其中每个键都是一个格式化程序 ID,每个值都是一个字典,描述如何配置相应的 Formatter 实例。

    将搜索配置字典以查找键 formatdatefmt(默认值为 None),并将使用这些键来构造 logging.Formatter 实例。

  • filters - 对应值将是一个字典,其中每个键都是一个过滤器 ID,每个值都是一个字典,描述如何配置相应的 Filter 实例。

    将搜索配置字典以查找键 name(默认为空字符串),并将使用该键来构造 logging.Filter 实例。

  • handlers - 对应值将是一个字典,其中每个键都是一个处理程序 ID,每个值都是一个字典,描述如何配置相应的 Handler 实例。

    将搜索配置字典以查找以下键

    • class(强制性)。这是处理程序类的完全限定名称。
    • level(可选)。处理程序的级别。
    • formatter(可选)。此处理程序的格式化程序 ID。
    • filters(可选)。此处理程序的过滤器 ID 列表。

    所有其他键都将作为关键字参数传递给处理程序的构造函数。例如,给定代码段

    handlers:
      console:
        class : logging.StreamHandler
        formatter: brief
        level   : INFO
        filters: [allow_foo]
        stream  : ext://sys.stdout
      file:
        class : logging.handlers.RotatingFileHandler
        formatter: precise
        filename: logconfig.log
        maxBytes: 1024
        backupCount: 3
    

    具有 ID console 的处理程序将实例化为 logging.StreamHandler,使用 sys.stdout 作为底层流。具有 ID file 的处理程序将实例化为 logging.handlers.RotatingFileHandler,并使用关键字参数 filename='logconfig.log', maxBytes=1024, backupCount=3

  • loggers - 对应值将是一个字典,其中每个键都是一个记录器名称,每个值都是一个字典,描述如何配置相应的 Logger 实例。

    将搜索配置字典以查找以下键

    • level(可选)。记录器的级别。
    • propagate(可选)。记录器的传播设置。
    • filters(可选)。此记录器的过滤器 ID 列表。
    • handlers(可选)。此记录器的处理程序 ID 列表。

    将根据指定的级别、传播、过滤器和处理程序配置指定的记录器。

  • root - 这将是根记录器的配置。配置的处理方式与任何记录器相同,只是 propagate 设置将不适用。
  • incremental - 配置是否应解释为对现有配置的增量。此值默认为 False,这意味着指定的配置将替换现有配置,语义与现有 fileConfig() API 使用的语义相同。

    如果指定的值为 True,则配置将按以下“增量配置”部分中描述的方式进行处理。

  • disable_existing_loggers - 是否应禁用任何现有的记录器。此设置反映了 fileConfig() 中同名参数的值。如果缺失,此参数将默认为 True。如果 incrementalTrue,则忽略此值。

一个工作示例

以下是一个实际工作配置,采用 YAML 格式(除了电子邮件地址为虚构之外)

formatters:
  brief:
    format: '%(levelname)-8s: %(name)-15s: %(message)s'
  precise:
    format: '%(asctime)s %(name)-15s %(levelname)-8s %(message)s'
filters:
  allow_foo:
    name: foo
handlers:
  console:
    class : logging.StreamHandler
    formatter: brief
    level   : INFO
    stream  : ext://sys.stdout
    filters: [allow_foo]
  file:
    class : logging.handlers.RotatingFileHandler
    formatter: precise
    filename: logconfig.log
    maxBytes: 1024
    backupCount: 3
  debugfile:
    class : logging.FileHandler
    formatter: precise
    filename: logconfig-detail.log
    mode: a
  email:
    class: logging.handlers.SMTPHandler
    mailhost: localhost
    fromaddr: my_app@domain.tld
    toaddrs:
      - support_team@domain.tld
      - dev_team@domain.tld
    subject: Houston, we have a problem.
loggers:
  foo:
    level : ERROR
    handlers: [debugfile]
  spam:
    level : CRITICAL
    handlers: [debugfile]
    propagate: no
  bar.baz:
    level: WARNING
root:
  level     : DEBUG
  handlers  : [console, file]

增量配置

很难为增量配置提供完全的灵活性。例如,由于过滤器和格式化程序等对象是匿名的,因此一旦设置了配置,在扩展配置时就无法引用这些匿名对象。

此外,在配置设置完成后,在运行时任意更改日志记录器、处理程序、过滤器、格式化程序的对象图并没有令人信服的理由;日志记录器和处理程序的详细程度可以通过设置级别(以及在日志记录器的情况下,传播标志)来控制。在多线程环境中以安全的方式任意更改对象图是有问题的;虽然并非不可能,但其带来的好处并不值得在实现中增加的复杂性。

因此,当配置字典的 incremental 键存在且为 True 时,系统将完全忽略任何 formattersfilters 条目,并仅处理 handlers 条目中的 level 设置,以及 loggersroot 条目中的 levelpropagate 设置。

当然,可以通过其他方式提供增量配置,例如使 dictConfig() 获取一个 incremental 关键字参数,该参数默认为 False。建议在配置字典中使用值的理由是,它允许将配置作为腌制字典通过网络发送到套接字侦听器。因此,可以随着时间的推移更改长时间运行应用程序的日志记录详细程度,而无需停止和重新启动应用程序。

注意:关于基于您的实际经验的增量配置需求的反馈将特别受欢迎。

API 定制

基本的 dictConfig() API 并不足以满足所有用例。将通过以下方式提供对 API 的自定义支持

  • 一个名为 DictConfigurator 的类,其构造函数传递用于配置的字典,并且具有一个 configure() 方法。
  • 一个名为 dictConfigClass 的可调用对象,默认情况下将设置为 DictConfigurator。提供此项是为了在需要时,可以用合适的用户定义实现替换 DictConfigurator

dictConfig() 函数将调用 dictConfigClass,传递指定的字典,然后调用返回对象的 configure() 方法以使配置生效。

def dictConfig(config):
    dictConfigClass(config).configure()

这应该可以满足所有定制需求。例如,DictConfigurator 的子类可以在其自己的 __init__() 中调用 DictConfigurator.__init__(),然后设置自定义前缀,这些前缀可以在随后的 configure() call 中使用。 dictConfigClass 将绑定到子类,然后可以像在默认的未定制状态下一样调用 dictConfig()

对套接字监听器实现的更改

现有的套接字侦听器实现将修改如下:当收到配置消息时,将尝试使用 json 模块将其反序列化为字典。如果此步骤失败,则假定该消息为 fileConfig 格式,并按之前的方式处理。如果反序列化成功,则将调用 dictConfig() 来处理生成的字典。

配置错误

如果在配置过程中遇到错误,系统将抛出 ValueErrorTypeErrorAttributeErrorImportError,并附带适当的描述性消息。以下是将引发错误的(可能不完整)条件列表

  • level 不是字符串,或者是一个不对应于实际日志记录级别的字符串
  • propagate 值不是布尔值
  • id 没有对应的目标
  • 在增量调用期间找到不存在的处理程序 id
  • 无效的日志记录器名称
  • 无法解析为内部或外部对象

社区讨论

该 PEP 已在 python-dev 和 python-list 上宣布。虽然讨论不多,但这对于一个利基主题来说可能是意料之中的。

python-dev 上的讨论线程

https://mail.python.org/pipermail/python-dev/2009-October/092695.html https://mail.python.org/pipermail/python-dev/2009-October/092782.html https://mail.python.org/pipermail/python-dev/2009-October/093062.html

以及 python-list 上的讨论

https://mail.python.org/pipermail/python-list/2009-October/1223658.html https://mail.python.org/pipermail/python-list/2009-October/1224228.html

有一些评论赞成该提案,没有反对该提案整体的意见,以及一些关于特定细节的问题和反对意见。作者认为,这些问题已通过对 PEP 进行修改得到解决。

参考实现

更改的参考实现可作为模块 dictconfig.py 获得,并附带在 test_dictconfig.py 中的配套单元测试,位于

http://bitbucket.org/vinay.sajip/dictconfig

这包含了除套接字侦听器更改之外的所有功能。


来源:https://github.com/python/peps/blob/main/peps/pep-0391.rst

上次修改时间:2023-09-09 17:39:29 GMT