logging 如何实现按进程/线程 ID 分离日志

8次阅读

默认 Logging 不按进程/线程 ID 分离日志,因其 Handler 全局共享且 formatter 默认不注入上下文信息;需自定义 Formatter 在 format() 中动态设 record.pid 和 record.tid,并避免多进程共用 FileHandler。

logging 如何实现按进程/线程 ID 分离日志

为什么默认的 logging 不会自动按进程/线程 ID 分离日志

因为 loggingHandler 是全局共享的,同一 Logger 实例在多线程或多进程下共用输出通道,Formatter 里若不显式注入上下文信息(如 Threading.get_ident()os.getpid()),所有日志行看起来完全一样。你看到的日志混在一起,不是框架“错了”,而是它默认不带这个维度。

如何在 Formatter 中动态插入线程/进程 ID

核心是自定义 Formatter,重写 format() 方法,在日志记录生成前注入标识。注意:不能直接用 %(thread)d 这类内置格式符——它只在主线程有效,子线程中可能为 0;也不能依赖 %(process)d 在 fork 后的多进程场景中保持稳定(尤其 windows 上)。

  • threading.current_thread().ident 替代 %(thread)d,确保子线程 ID 可靠
  • os.getpid() 获取当前进程 ID,比 %(process)d 更直接、跨平台一致
  • 把它们塞进 record__dict__,再在 fmt 字符串里引用,例如:%(pid)s [%(tid)s]
import logging import threading import os  class PIDTIDFormatter(logging.Formatter):     def format(self, record):         record.pid = os.getpid()         record.tid = threading.current_thread().ident         return super().format(record)  handler = logging.streamHandler() handler.setFormatter(PIDTIDFormatter('%(asctime)s %(pid)s [%(tid)s] %(levelname)s %(message)s'))

多进程下单独写文件时要注意什么

如果目标是“每个进程写独立日志文件”,不能让多个进程打开同一个 FileHandler(会冲突或丢失日志)。必须在子进程启动后、首次获取 Logger 前,动态创建带进程 ID 的 handler。

  • 避免在主进程预设 FileHandler 并传递给子进程——fork 后 fd 可能被共享,导致写入错乱
  • 推荐在子进程入口处调用 logging.getLogger().handlers.clear(),再添加新 FileHandler('app_{}.log'.format(os.getpid()))
  • 若用 multiprocessing.Process,可在 run() 开头做 handler 初始化;若用 concurrent.futures.ProcessPoolExecutor,需改用 initializer + 全局 logger 配置

线程安全与 formatter 实例复用是否冲突

一个 Formatter 实例可以被多个线程共用,没问题——format() 是无状态的,每次调用都基于传入的 record 独立处理。但切记:不要在 format() 中修改 record 以外的共享对象(比如全局 dict 缓存 tid→name 映射),否则可能引发竞态。

另外,如果你用 logging.basicConfig(),它设置的是 root logger 的 handler,而子模块 get 的 logger 默认 propagate,所以最终还是走同一套 handler。要隔离,就得在各模块或各进程内显式配置自己的 handler,而不是依赖 basicConfig 一次配全。

真正麻烦的从来不是加个 ID,而是确保这个 ID 在整个调用链里不被覆盖、不被误读、不因进程模型差异失效——尤其是混合使用 fork / spawn / thread 的服务里,os.getpid()threading.get_ident() 的时机和可见性得反复验证。

text=ZqhQzanResources