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()的函数,它接受一个参数——包含配置的字典。如果在处理字典时出现错误,将引发异常。
字典模式 - 概述
在详细描述模式之前,值得提一下对象连接、对用户定义对象的支持以及对外部和内部对象的访问。
对象连接
该模式旨在描述一组日志对象——记录器、处理程序、格式化程序、过滤器——它们在对象图中相互连接。因此,该模式需要表示对象之间的连接。例如,假设配置完成后,某个特定记录器附加了某个特定处理程序。为了讨论的目的,我们可以说记录器代表连接的源,而处理程序代表连接的目的地。当然,在配置的对象中,这由记录器持有处理程序的引用来表示。在配置字典中,这是通过为每个目标对象提供一个唯一标识它的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 h1 和 h2 描述。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源代码形式显示,brief 和 default 格式化程序的配置子字典分别为
{
'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,并且 handlers、filters 和 formatter 条目将接受一个对象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实例的字典。配置字典中会查找键
format和datefmt(默认为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: 3ID 为
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。如果incremental为True,则此值将被忽略。
一个工作示例
以下是一个实际可用的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 时,系统将完全忽略所有 formatters 和 filters 条目,并且只处理 handlers 条目中的 level 设置,以及 loggers 和 root 条目中的 level 和 propagate 设置。
当然可以通过其他方式提供增量配置,例如让 dictConfig() 接受一个默认为 False 的 incremental 关键字参数。建议在配置字典中使用一个值的原因是,它允许将配置作为腌制字典通过网络发送到套接字监听器。因此,长期运行的应用程序的日志详细程度可以随着时间改变,而无需停止和重新启动应用程序。
注意:欢迎您根据实际经验,就增量配置需求提供反馈。
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() 来处理生成的字典。
配置错误
如果在配置过程中遇到错误,系统将引发一个带有适当描述性消息的 ValueError、TypeError、AttributeError 或 ImportError。以下是可能引发错误的条件列表(可能不完整)
- 一个
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