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

为什么默认的 logging 不会自动按进程/线程 ID 分离日志
因为 logging 的 Handler 是全局共享的,同一 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() 的时机和可见性得反复验证。










