Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

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源代码形式的字典更具可读性。)

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

处理程序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://,但可以完全禁用该机制,或为特殊处理提供额外或不同的前缀。

访问内部对象

除了外部对象,有时还需要引用配置中的对象。配置系统将隐式地为它知道的事物完成此操作。例如,记录器或处理程序中 level 的字符串值 'DEBUG' 将自动转换为值 logging.DEBUG,并且 handlersfiltersformatter 条目将接受一个对象ID并解析为相应的目标对象。

然而,对于日志系统未知的用户定义对象,需要提供一个更通用的机制。例如,以 logging.handlers.MemoryHandler 实例为例,它接受一个 target,该 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'subject 值可以通过 'cfg://handlers.email.subject' 或等效地 'cfg://handlers.email[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() 接受一个默认为 Falseincremental 关键字参数。建议在配置字典中使用一个值的原因是,它允许将配置作为腌制字典通过网络发送到套接字监听器。因此,长期运行的应用程序的日志详细程度可以随着时间改变,而无需停止和重新启动应用程序。

注意:欢迎您根据实际经验,就增量配置需求提供反馈。

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

上次修改:2025-02-01 08:59:27 GMT