问题
当我运行下面的main.py
时,它会打印出HELLO WORLD
(一切正常),但是,如果我将LOGGING_CONFIG
中的console
重命名为s
,python会抛出这个错误:AttributeError: 'ConvertingDict' object has no attribute 'handle'
。为什么更改处理程序名称会导致这种情况发生,如何修复它?
背景
我有一个异步应用程序需要日志记录,但是"the logging module uses blocking I/O when emitting records." Python的logging.handlers.QueueHandler
就是为此构建的,我尝试用dictConfig
实现QueueHandler
,我使用底部参考资料部分的链接把main.py
放在一起。
编号
这是main.py
。请注意,文件名main.py
很重要,因为main.QueueListenerHandler
在LOGGING_CONFIG
中引用了它。
# main.py
import logging
import logging.config
import logging.handlers
import queue
import atexit
# This function resolves issues when using `cfg://handlers.[name]` where
# QueueListenerHandler complains that `cfg://handlers.[name]` isn't a handler.
def _resolve_handlers(myhandlers):
if not isinstance(myhandlers, logging.config.ConvertingList):
return myhandlers
# Indexing the list performs the evaluation.
return [myhandlers[i] for i in range(len(myhandlers))]
class QueueListenerHandler(logging.handlers.QueueHandler):
def __init__(
self,
handlers,
respect_handler_level=False,
auto_run=True,
queue=queue.Queue(-1),
):
super().__init__(queue)
handlers = _resolve_handlers(handlers)
self._listener = logging.handlers.QueueListener(
self.queue, *handlers, respect_handler_level=respect_handler_level
)
if auto_run:
self.start()
atexit.register(self.stop)
def start(self):
self._listener.start()
def stop(self):
self._listener.stop()
def emit(self, record):
return super().emit(record)
LOGGING_CONFIG = {
"version": 1,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
"queue_listener": {
"class": "main.QueueListenerHandler",
"handlers": [
"cfg://handlers.console"
],
},
},
"loggers": {
"server": {
"handlers": ["queue_listener"],
"level": "DEBUG",
"propagate": False,
},
},
}
if __name__ == "__main__":
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("server")
logger.debug("HELLO WORLD")
如果我将LOGGING_CONFIG["handlers"]
修改为:
"handlers": {
"s": {
"class": "logging.StreamHandler",
},
"queue_listener": {
"class": "main.QueueListenerHandler",
"handlers": [
"cfg://handlers.s"
],
},
},
python会抛出这个错误:
sh-3.2$ pyenv exec python main.py
Exception in thread Thread-1 (_monitor):
Traceback (most recent call last):
File "/Users/zion.perez/.pyenv/versions/3.10.6/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
self.run()
File "/Users/zion.perez/.pyenv/versions/3.10.6/lib/python3.10/threading.py", line 953, in run
self._target(*self._args, **self._kwargs)
File "/Users/zion.perez/.pyenv/versions/3.10.6/lib/python3.10/logging/handlers.py", line 1548, in _monitor
self.handle(record)
File "/Users/zion.perez/.pyenv/versions/3.10.6/lib/python3.10/logging/handlers.py", line 1529, in handle
handler.handle(record)
AttributeError: 'ConvertingDict' object has no attribute 'handle'
注解
- 有趣的是,只有
s
会导致这个问题,其他任何字母都可以,如果s
是处理程序名称的第一个字母(例如sconsole
,shandler
),Python就会抛出上面的异常。 - 在MacOS上使用Python 3.11.1、3.10.6、3.9.16测试并确认了相同的行为
- 在Ubuntu 22.04上使用Python 3.10.6和3.11.0rc1进行测试
- 关于
_resolve_handlers
func,如果处理程序是console
(不是以s
开头),则func返回[<StreamHandler <stderr> (NOTSET)>]
,一切正常。如果处理程序是sconsole
(以s
开头),则func返回[{'class': 'logging.StreamHandler'}]
。有关_resolve_handlers
的更多背景信息,this article解释了为什么需要此函数。
2条答案
按热度按时间4c8rllxm1#
这是一个部分的答案,因为我还没有找到一个"适当的"解决方案,只是一个解释的问题,以及如何修复它与轻黑客。
出现您看到的问题的原因是:
1.处理程序按名称的字母顺序初始化
1.当一个处理程序依赖于另一个处理程序时,有一些丑陋和笨拙的代码(字面上检查特定子字符串的 * chained * exception的 * stringified form *)用于处理延迟初始化,而您的代码不能正确触发
1.由于#1和#2的原因,每当依赖处理程序按字母顺序出现在它所依赖的处理程序 * 之后 * 时,解析处理程序的尝试就会解析到处理程序名称Map到的字典,而不是它应该构造的实际处理程序类
一个更简单的复制器(不涉及所有队列和线程)是:
在线试用!
编写时它会失败,但是将
"q"
处理程序名称更改为"p"
(或按字母顺序位于"passthrough"
之前的任何其他名称),它就会工作。现在,我能想到的最好的解决方案是确保首先完全解析处理程序依赖关系,这只是意味着,如果处理程序A依赖于处理程序B,则必须给处理程序B一个按字母顺序位于处理程序A名称之前的名称,因此,按字母顺序位于
"queue_listener"
之前的任何内容都可以工作(就像原始的"console"
一样),并且它之后的任何内容现在都可以工作。从好的方面来说,虽然有些笨拙,但这似乎是一个有意的设计决策;配置处理程序时的代码注解包括:
同样的代码有一个 * 疯狂 * 的hacky方法来尝试处理跨处理程序依赖关系,如果异常以 * 完全 * 正确的方式引发,但我试图挂钩到它却没有成功,我可以引发它想要的形式的异常,通过允许单层延迟处理(使用
raise Exception() from ValueError('target not configured yet')
)当我检测到依赖处理程序尚未解析,并且该处理程序随后被延迟以进行第二次传递时,但是当第二遍发生时某些状态被打破(过去可有效解析的字符串最终被不正确地解析,并且当代码试图通过点将其分割时,所得到的None
对象断开),看看一些现有的类必须做些什么来处理这个问题(保存解析前状态的副本,以便在配置失败时恢复,这样它就可以在引发黑客异常之前恢复到原始状态),让我觉得不值得这么麻烦。关于这个
cfg://
特性的文档似乎指出了您正在做的事情应该是可行的(他们的示例中有handlers.custom
引用handlers.file
,并且在这个设计中,custom
在字母顺序上早于file
,所以看起来它仍然存在这个问题),所以要么我们遗漏了一些东西,要么文档是误导性的。vktxenjb2#
这似乎是Error when initializing queue listener from config file #92929,它被关闭为 * 未计划 *,因为#93269中添加了一个用于同时配置
QueueListener
/QueueHandler
的机制。Allow using
QueueHandler
andQueueListener
inlogging.dictConfig
#93162中有更多的上下文和配置建议。这对Python 3.10.6没有帮助,我猜你只需要在配置中使用按字母顺序选择的名称来回避这个问题,但是你可以在Python 3.12文档中回顾一下新机制:
配置队列处理程序和队列监听器