为什么当处理程序名以's'开头时Python日志记录抛出错误?

llmtgqce  于 2023-01-27  发布在  Python
关注(0)|答案(2)|浏览(112)

问题
当我运行下面的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.QueueListenerHandlerLOGGING_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是处理程序名称的第一个字母(例如sconsoleshandler),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解释了为什么需要此函数。

参考文献

4c8rllxm

4c8rllxm1#

这是一个部分的答案,因为我还没有找到一个"适当的"解决方案,只是一个解释的问题,以及如何修复它与轻黑客。
出现您看到的问题的原因是:
1.处理程序按名称的字母顺序初始化
1.当一个处理程序依赖于另一个处理程序时,有一些丑陋和笨拙的代码(字面上检查特定子字符串的 * chained * exception的 * stringified form *)用于处理延迟初始化,而您的代码不能正确触发
1.由于#1和#2的原因,每当依赖处理程序按字母顺序出现在它所依赖的处理程序 * 之后 * 时,解析处理程序的尝试就会解析到处理程序名称Map到的字典,而不是它应该构造的实际处理程序类
一个更简单的复制器(不涉及所有队列和线程)是:

import logging
import logging.config
import logging.handlers

# Minimalist handler that just delegates to contained handler
class PassThroughHandler:
    def __init__(
        self,
        handler,
    ):
        self._handler = handler
        print(type(handler))  # Will print either the resolved handler object type, or ConfiguringDict when it hasn't resolved

    def __getattr__(self, name):
        return getattr(self._handler, name)

LOGGING_CONFIG = {
    "version": 1,
    "handlers": {
        "q": {   # Fails as "q" (and anything alphabetically after "passthrough"), works as "p" (and anything else alphabetically before "passthrough")
            "class": "logging.StreamHandler",
        },
        "passthrough": {
            "class": __name__ + ".PassThroughHandler",
            "handler": "cfg://handlers.q", # Change to match "q" or "p" above
        },
    },
    "loggers": {
        "server": {
            "handlers": ["passthrough"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
}

if __name__ == "__main__":
    logging.config.dictConfig(LOGGING_CONFIG)
    logger = logging.getLogger("server")
    logger.debug("HELLO WORLD")

在线试用!
编写时它会失败,但是将"q"处理程序名称更改为"p"(或按字母顺序位于"passthrough"之前的任何其他名称),它就会工作。
现在,我能想到的最好的解决方案是确保首先完全解析处理程序依赖关系,这只是意味着,如果处理程序A依赖于处理程序B,则必须给处理程序B一个按字母顺序位于处理程序A名称之前的名称,因此,按字母顺序位于"queue_listener"之前的任何内容都可以工作(就像原始的"console"一样),并且它之后的任何内容现在都可以工作。
从好的方面来说,虽然有些笨拙,但这似乎是一个有意的设计决策;配置处理程序时的代码注解包括:

# Next, do handlers - they refer to formatters and filters
          # As handlers can refer to other handlers, sort the keys
          # to allow a deterministic order of configuration

同样的代码有一个 * 疯狂 * 的hacky方法来尝试处理跨处理程序依赖关系,如果异常以 * 完全 * 正确的方式引发,但我试图挂钩到它却没有成功,我可以引发它想要的形式的异常,通过允许单层延迟处理(使用raise Exception() from ValueError('target not configured yet'))当我检测到依赖处理程序尚未解析,并且该处理程序随后被延迟以进行第二次传递时,但是当第二遍发生时某些状态被打破(过去可有效解析的字符串最终被不正确地解析,并且当代码试图通过点将其分割时,所得到的None对象断开),看看一些现有的类必须做些什么来处理这个问题(保存解析前状态的副本,以便在配置失败时恢复,这样它就可以在引发黑客异常之前恢复到原始状态),让我觉得不值得这么麻烦。
关于这个cfg://特性的文档似乎指出了您正在做的事情应该是可行的(他们的示例中有handlers.custom引用handlers.file,并且在这个设计中,custom在字母顺序上早于file,所以看起来它仍然存在这个问题),所以要么我们遗漏了一些东西,要么文档是误导性的。

vktxenjb

vktxenjb2#

这似乎是Error when initializing queue listener from config file #92929,它被关闭为 * 未计划 *,因为#93269中添加了一个用于同时配置QueueListener/QueueHandler的机制。
Allow using QueueHandler and QueueListener in logging.dictConfig #93162中有更多的上下文和配置建议。
这对Python 3.10.6没有帮助,我猜你只需要在配置中使用按字母顺序选择的名称来回避这个问题,但是你可以在Python 3.12文档中回顾一下新机制:

配置队列处理程序和队列监听器

相关问题