为什么附加到根记录器的过滤器不传播到子记录器?

14 投票
4 回答
6128 浏览
提问于 2025-04-16 22:23

Python日志记录文档的第15.7.4段中提到:

注意,当处理程序发出事件时,会检查附加到处理程序的过滤器,而当事件被记录到处理程序(使用debug()、info()等)时,会检查附加到记录器的过滤器。这意味着由子记录器生成的事件不会被父记录器的过滤器设置过滤,除非这个过滤器也被应用到那些子记录器上。

我不太理解这个设计决定。难道不应该让根记录器的过滤器也适用于子记录器吗?

4 个回答

0

可以这样理解,日志记录器就像你家里的排水管。给日志记录器加个过滤器,就像是在排水管上装个滤网,这样可以防止一些东西直接流进下水道,但它并不会过滤整个下水道。你在水流中的位置(上游或下游)并不会改变这个过滤的效果。

而处理器就像是管道。管道会收集上游流来的信息。默认的管道行为是“跳过,传递给父级”。如果你想影响上游的信息流,就需要在管道(处理器)上加个过滤器。如果你查看一下日志流向图,你会发现可以添加一个NullHandler(不进行格式化或输出),它可以过滤信息后再传递。

这就是你想要的效果。

4

过滤器和级别、处理器不一样,它们不会自动传递。不过,你可以把过滤器附加到一个会传递的处理器上。

看看这个由 Elliot 在 SaltyCrane 博客 提供的例子:

import logging

class MyFilter(logging.Filter):
    def filter(self, record):
        record.msg = 'MY FILTER: ' + record.msg
        return 1

myfilter = MyFilter()

myformatter = logging.Formatter("MY HANDLER: %(name)s - %(message)s")

myhandler = logging.StreamHandler()
myhandler.setFormatter(myformatter)
myhandler.addFilter(myfilter)

foo_logger = logging.getLogger('foo')
foo_logger.addHandler(myhandler)

foo_bar_logger = logging.getLogger('foo.bar')

foo_logger.error('asdfasdf')
foo_bar_logger.error('zxcvzxcv')
MY HANDLER: foo - MY FILTER: asdfasdf
MY HANDLER: foo.bar - MY FILTER: zxcvzxcv
2

我同意:这个设计决定有点反直觉,个人觉得。

最简单的解决办法就是把你的过滤器加到每一个可能的处理器上。比如说,你有一个控制台处理器、一个邮件处理器和一个数据库处理器,你应该把你的“根”过滤器加到每一个处理器上。 :-/

import logging
import logging.config

class MyRootFilter(logging.Filter):
    def filter(self, record):
        # filter out log messages that include "secret"
        if "secret" in record.msg:
            return False
        else:
            return True

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'my_root_filter': {
            '()': MyRootFilter,
        },
    },
    'handlers': {
        'stderr': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'filters': ['my_root_filter'],
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'some.kind.of.EmailHandler',
            'filters': ['my_root_filter'],
        },
        'database': {
            'level': 'ERROR',
            'class': 'some.kind.of.DatabaseHandler',
            'filters': ['my_root_filter'],
        },
    },
    'loggers': {
        'some.sub.project': {
            'handlers': ['stderr'],
            'level': 'ERROR',
        },
    },
}

logging.config.dictConfig(LOGGING)
logging.getLogger("some.sub.project").error("hello")        # logs 'hello'
logging.getLogger("some.sub.project").error("hello secret") # filtered out!  :-)

如果处理器很多,你可能想通过编程的方式把根过滤器加到每个处理器上,而不是手动加。我建议你直接在你的配置字典(或者文件,具体取决于你是怎么加载日志配置的)上操作,而不是在配置加载后再加,因为似乎没有文档说明怎么获取所有处理器的列表。我发现了logger.handlers和logging._handlers,但因为它们没有文档说明,未来可能会出问题。而且,也不能保证它们是线程安全的。

之前的解决方案(在配置加载前直接把根过滤器加到每个处理器上)假设你在加载日志配置之前可以控制它,并且没有处理器会动态添加(使用Logger#addHandler())。如果不是这样的话,你可能需要对日志模块进行“猴子补丁”(祝你好运!)。

补充

我试着对Logger#addHandler做了个猴子补丁,纯粹是为了好玩。实际上效果不错,简化了配置,但我不确定我会推荐这样做(我不喜欢猴子补丁,这让调试变得非常困难)。使用时请自行承担风险...

import logging
import logging.config

class MyRootFilter(logging.Filter):
   [...] # same as above

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'stderr': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            # it's shorter: there's no explicit reference to the root filter
        },
        [...]  # other handlers go here
    },
    'loggers': {
        'some.sub.project': {
            'handlers': ['stderr'],
            'level': 'ERROR',
        },
    },
}

def monkey_patched_addHandler(self, handler):
    result = self.old_addHandler(handler)
    self.addFilter(MyRootFilter())
    return result

logging.Logger.old_addHandler = logging.Logger.addHandler
logging.Logger.addHandler = monkey_patched_addHandler

logging.config.dictConfig(LOGGING)
logging.getLogger("some.sub.project").error("hello")        # logs 'hello'
logging.getLogger("some.sub.project").error("hello secret") # filtered out!  :-)

撰写回答