Python logging:为子日志器添加类实例变量至日志消息并避免重复
需要为负责的包创建__main__.mypackage子日志器,继承主程序中已配置多handler的__main__日志器设置,且无法修改主程序日志器。核心需求是继承父日志器的Formatter,但在日志消息进入Formatter前添加类实例变量信息。
尝试通过添加Filter修改日志消息,但由于每个类实例都向同一个日志器添加Filter,导致后续实例的日志消息出现重复前缀(如第二个实例的日志会带上第一个实例的信息前缀)。期望每个实例的日志仅携带自身的实例信息。
已知logging cookbook中有使用contextvar处理多实例的示例,但不确定是否适用;也考虑过手动包装每条日志消息,但希望采用最佳实践方案。
现有代码
main.py
import logging import mypackage logger = logging.getLogger("__main__") logger.setLevel(logging.DEBUG) stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.INFO) stream_formatter = logging.Formatter('%(levelname)s:%(filename)s:%(lineno)d:%(message)s') stream_handler.setFormatter(stream_formatter) logger.addHandler(stream_handler) def main(): a=1 logger.info("message from main") b = mypackage.MyClass("EXTRA INSTANCE INFO") b.mymodule(a) c = mypackage.MyClass("EXTRA INSTANCE INFO2") c.mymodule(a+1) main()
mypackage.py
import logging logger = logging.getLogger(f"__main__.{__name__}") class _PackageContextFilter(logging.Filter): def __init__(self, needed_info_from_my_package): self.needed_info_from_my_package=needed_info_from_my_package def filter(self, record): record.msg = f"({self.needed_info_from_my_package}) " + record.msg return True class MyClass(): def __init__(self,init_cond) -> None: self.data=[] self.needed_info_from_my_package = [init_cond] logger.addFilter(_PackageContextFilter(self.needed_info_from_my_package)) def mymodule(self,data): self.data.append(data) logger.info(f"message from mymodule about {self.data}")
当前输出
INFO:main.py:13:message from main INFO:mypackage.py:24:(['EXTRA INSTANCE INFO']) message from mymodule about [1] INFO:mypackage.py:24:(['EXTRA INSTANCE INFO2']) (['EXTRA INSTANCE INFO']) message from mymodule about [2]
期望输出
INFO:main.py:13:message from main INFO:mypackage.py:24:(['EXTRA INSTANCE INFO']) message from mymodule about [1] INFO:mypackage.py:24:(['EXTRA INSTANCE INFO2']) message from mymodule about [2]
方案1:为每个类实例创建独立子日志器
核心思路是每个MyClass实例拥有自己的专属日志器,而不是所有实例共用同一个__main__.mypackage日志器。这样每个实例的Filter只会作用于自己的日志器,不会干扰其他实例的日志输出。
修改后的mypackage.py:
import logging class _PackageContextFilter(logging.Filter): def __init__(self, needed_info_from_my_package): self.needed_info_from_my_package = needed_info_from_my_package def filter(self, record): record.msg = f"({self.needed_info_from_my_package}) " + record.msg return True class MyClass(): def __init__(self, init_cond) -> None: self.data = [] self.needed_info_from_my_package = [init_cond] # 为每个实例创建唯一的子日志器,命名结合实例标识保证唯一性 self.logger = logging.getLogger(f"__main__.mypackage.{id(self)}") # 给实例专属日志器添加对应Filter self.logger.addFilter(_PackageContextFilter(self.needed_info_from_my_package)) def mymodule(self, data): self.data.append(data) # 使用实例专属日志器输出日志 self.logger.info(f"message from mymodule about {self.data}")
该方案逻辑简单直接,实例日志隔离彻底,完全避免交叉污染问题。
方案2:使用ContextVar存储实例上下文信息
如果希望继续共用__main__.mypackage日志器,可以用contextvars模块存储当前实例的信息,让Filter动态获取上下文对应的实例信息,无需为每个实例重复添加Filter。
修改后的mypackage.py:
import logging import contextvars # 创建ContextVar存储实例信息,默认值为None instance_context = contextvars.ContextVar("instance_info", default=None) class _PackageContextFilter(logging.Filter): def filter(self, record): info = instance_context.get() if info is not None: record.msg = f"({info}) " + record.msg return True # 仅给共用日志器添加一次Filter logger = logging.getLogger(f"__main__.{__name__}") logger.addFilter(_PackageContextFilter()) class MyClass(): def __init__(self, init_cond) -> None: self.data = [] self.needed_info_from_my_package = [init_cond] def mymodule(self, data): self.data.append(data) # 输出日志前将当前实例信息存入上下文 token = instance_context.set(self.needed_info_from_my_package) try: logger.info(f"message from mymodule about {self.data}") finally: # 恢复上下文,避免影响后续其他实例的日志处理 instance_context.reset(token)
该方案适合需要共用日志器的场景,通过上下文隔离不同实例的日志逻辑,解决重复添加Filter导致的前缀重复问题。
方案3:手动包装日志消息(简单备选)
如果不想修改日志器结构或使用上下文,可以在每个日志输出时手动添加实例信息前缀。虽然不是最优雅的日志实践,但实现简单直接:
修改后的mypackage.py:
import logging logger = logging.getLogger(f"__main__.{__name__}") class MyClass(): def __init__(self, init_cond) -> None: self.data = [] self.needed_info_from_my_package = [init_cond] def mymodule(self, data): self.data.append(data) # 手动拼接实例信息前缀和日志内容 log_msg = f"({self.needed_info_from_my_package}) message from mymodule about {self.data}" logger.info(log_msg)
该方案无需修改日志器配置,适合快速实现需求,但如果存在大量日志输出,会产生代码重复问题。
内容的提问来源于stack exchange,提问作者guest

