Python logging 重复日志:处理器、传播与重复初始化
一、先分清调用次数与输出次数
看到两行相同日志,先判断业务代码是否执行了两次,再检查日志的处理路径。一次日志调用可以生成一条记录,却由多个处理器输出多次。只看消息文字相同,不能直接认定业务重复执行,也不能直接认定配置出错。
Logger 负责接收调用和筛选记录,Handler 负责把记录送往具体目标。名称中的点表示层级,例如 demo.worker 是 demo 的后代。子 logger 默认允许向上传播;父子都挂处理器、两个处理器又写入同一目标时,同一条记录便可能出现两次。
下面代码只用于独立 Python 进程,用 StringIO 收集输出,不写日志文件。它使用专门的命名空间,并在父节点停止继续传播,不改 root 的配置;不要把演示初始化代码直接粘贴进正在运行的服务或 Notebook。
二、用两行输出看清传播路径
import io
import logging
output = io.StringIO()
parent = logging.getLogger("article_demo_20261102")
child = logging.getLogger("article_demo_20261102.worker")
parent.setLevel(logging.ERROR)
parent.propagate = False
child.setLevel(logging.INFO)
child.propagate = True
def make_handler(prefix):
h = logging.StreamHandler(output)
h.setLevel(logging.INFO)
h.setFormatter(logging.Formatter(prefix + ":%(message)s"))
return h
parent_handler = make_handler("parent")
child_handler = make_handler("child")
parent.addHandler(parent_handler)
child.addHandler(child_handler)
try:
child.info("ready")
assert output.getvalue().splitlines() == [
"child:ready", "parent:ready"
]
child.removeHandler(child_handler)
output.seek(0)
output.truncate(0)
child.info("single")
assert output.getvalue().splitlines() == ["parent:single"]
print("传播验证通过")
finally:
child.removeHandler(child_handler)
parent.removeHandler(parent_handler)
child_handler.close()
parent_handler.close()
第一次输出两行,第二次只输出父处理器的一行,变化仅是移除了子节点自己的处理器。这里选择集中输出:后代保留传播,由上层拥有输出目标。另一种设计是子模块确实需要独立目标,此时可以保留自己的处理器并设置 propagate=False,但也会切断它向上层集中采集的路径。应先确定日志归谁管理,再决定采用哪一种。
注意父 logger 被设为 ERROR,子节点的 INFO 仍到达父处理器。传播把记录交给祖先的处理器,不会重新执行祖先 logger 自身的级别和过滤器判断;祖先处理器的级别与过滤器仍然有效。这不代表父级别永远无用:如果子节点是 NOTSET,它需要向祖先查找有效级别,此时父节点的 ERROR 就会影响子节点是否生成这条记录。
三、级别要沿着实际路径检查
把处理器设成 DEBUG,不能挽回已被来源 logger 拦下的调试消息;把 logger 设成 DEBUG,也不能让 INFO 记录越过处理器的 WARNING 门槛。排查时先看来源 logger 的有效级别、启用状态和过滤器,再看沿传播路径的每个处理器。
多个目标有不同门槛通常是合理需求,例如一个接收详细记录,另一个只接收警告。真正需要消除的是同一目标上不符合预期的多次投递,而不是把所有处理器都删到只剩一个。
四、重复初始化会积累新的处理器
同一个进程里,多次 getLogger() 传入同名会拿到同一个 logger。如果初始化函数每次都新建并添加 StreamHandler,处理器就会累积。反过来,同一 logger 重复添加同一个处理器实例,不会重复挂载;但把同一实例同时挂到父子节点,传播遍历仍可能调用它两次。
下面是应用专用 logger 的一个小例子。它只管理约定名称的处理器,不清空其他处理器,并假定初始化在主线程启动阶段串行执行。
import io
import logging
HANDLER_NAME = "article_demo_20261102_owned_console"
def configure_demo(stream):
logger = logging.getLogger("article_demo_20261102_init")
logger.setLevel(logging.INFO)
logger.propagate = False
for h in logger.handlers:
if h.get_name() == HANDLER_NAME:
return logger
h = logging.StreamHandler(stream)
h.set_name(HANDLER_NAME)
h.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(h)
return logger
output = io.StringIO()
logger = configure_demo(output)
configure_demo(output)
try:
logger.info("one")
assert output.getvalue().splitlines() == ["one"]
print("重复初始化验证通过")
finally:
for h in list(logger.handlers):
if h.get_name() == HANDLER_NAME:
logger.removeHandler(h)
h.close()
处理器名称不是强制唯一的,也不会自动防重;上面的约定需要由拥有该 logger 的应用维护。这段函数复用第一次配置的流,不负责动态切换目标。检查后再添加也不是一个原子步骤,多个线程同时初始化仍可能竞争;更合适的做法是在启动阶段完成配置,确需并发初始化时由应用提供同步。
logger.handlers 只表示直接挂在该节点上的处理器;hasHandlers() 还会沿可传播的祖先路径查找。后者返回真,不能证明本节点已经装好预期的处理器,更不能证明输出目标、级别和格式正确。
诊断时可以逐层检查 logger 名称、有效级别、传播开关,以及各处理器的类型、名称和目标,画出一条记录实际会经过哪些出口。调整时使用 addHandler()、removeHandler() 等配置接口,不直接改写处理器列表;移除自己拥有的处理器后,还应在适当的生命周期结束时关闭它。若框架拥有这些处理器,应通过框架提供的配置入口调整。
也不宜在日志采集端简单按消息文字去重:两次真实业务事件可能恰好打印相同内容。先结合调用位置、进程和业务上下文确认事件关系,再决定处理配置问题还是业务重复调用,才能保留有用的诊断信息。
五、basicConfig 不是通用的清理按钮
默认情况下,root 已有处理器时,logging.basicConfig() 不会重新配置它。在交互环境或框架已经配置日志后,后来传入新的格式、级别和流,可能看起来没有效果。模块级的 logging.warning() 等便捷函数还可能在 root 无处理器时自动触发基础配置,因此应用最好在启动阶段明确配置。
Python 3.8 起提供 force=True:它先从 root 移除并关闭已有处理器,再应用新配置。这会影响整个当前进程的 root 输出,不能当成库内部的默认修复方式。它也不遍历清理子 logger;如果子节点仍有自己的处理器并继续传播,重复输出仍可能存在。同一处理器若还挂在其他节点,关闭它也不能被理解成对其他引用毫无影响。
六、把配置权留在正确的位置
普通库通常只获取具名 logger 并记录事件,把输出位置、格式和级别交给使用它的应用;需要避免未配置时的默认输出,可以按库的约定添加 NullHandler,不要偷偷替调用方添加控制台或文件处理器。
本文两个代码块已在 macOS、Python 3.9.6 的独立进程运行。另有八项断言覆盖传播、级别、处理器实例复用、重复初始化、祖先查找及 root 基础配置;涉及 root 的实验另开子进程完成,记录均写入内存流。验证没有覆盖多进程采集、队列消费、网络重试或并发重配置,不能用来排除这些环节造成的重复。
参考:Python 标准库 logging 文档的 Logger Objects、basicConfig() 与 hasHandlers() 章节;Logging HOWTO 的 Logging Flow、Configuring Logging for a Library 章节;Logging Cookbook 的 Using logging in multiple modules 章节。
- 点赞
- 收藏
- 关注作者
评论(0)