I agree: this is a controversial design decision, IMHO.
The simplest solution is to attach a filter to each available handler. For example, let's say you have a console handler, a mail handler and a database handler, you must attach your "root filter" to each of them.: - /
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! :-)
If there are many handlers, you might want to attach your root filter to each handler programmatically, rather than manually. I recommend that you do this directly in your configuration dictionary (or file, depending on how you load the logging configuration), instead of doing it after loading the configuration, since there seems to be no documentary way to get a list of all the handlers, I found logger.handlers and logging._handlers, but since they are not documented, they may break in the future. In addition, there is no guarantee that they are thread safe.
The previous solution (attaching your root filter to each handler directly in the configuration before loading it) assumes that you control the logging configuration before loading it, and also that no handler will be added dynamically (using Logger # AddHandler ()). If this is not the case, you may want to decapitate the registration module (good luck with that!).
change
I made an attempt to intercept the Logger monkey # addHandler, just for fun. It really works great and simplifies setup, but I'm not sure I recommend it (I hate beheading monkeys, itβs very difficult to debug when something goes wrong). Use your risks ...
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 shorter: there 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! :-)