python logging 日志重复打印问题定位

图:

图编号按顺序1-4,嫌长可以跳过定位过程看总结

python logging 日志重复打印问题定位_第1张图片

python logging 日志重复打印问题定位_第2张图片

python logging 日志重复打印问题定位_第3张图片

python logging 日志重复打印问题定位_第4张图片

定位过程:

我司项目几个服务进程的初始化log都是这样的:
python logging 日志重复打印问题定位_第5张图片

这些进程都会初始化一个叫 sdsomlogger,并且把handler加到了这个logger对象中,后面getlogger的时候我们是 sdsom.xx, 这个按点分隔会导致认为是子logger,比如是sdsom.A,就会新建一个loggersdsom.A,然后把 sdsom 这个logger设为它的parent(图1),打日志的时候,会一直往上遍历,把所有parent的所有handler都打一遍(图2)。这些实例进程间是独立的,但如果在一个进程里,比如在A进程中 import 了 B 的模块,而这个模块 import 了B自己的log.py模块,触发一次 addHandler (图3),就把 B 的 handler 加进了 A进程的 sdsom logger里(把它设为了parent),所以 A 的 sdsom logger里有两个handler (图4),于是A 的log同时打到了B的日志文件里。(注意对比图3 图4的对象地址 是一致的)

这个logger父子关系前人要这么用的原因,我估计是我们项目的common这个模块,用父子关系可以实现这样一个方式:不需另外初始化,log = logging.getLogger('sdsom.common') 只需要执行这一句,这个loggerparent就被设为 <import这个common模块的> 进程的 sdsom logger,实际上sdsom.xxx 点号后面的内容都没有影响了,这个common logger打印时,会调parent,于是也就被相应进程的handler打印了。
本身也算方便的机制,但由于这种方式内部实现不可见, 容易误用。

如果要共享日志, 还有一种方式就是对相应的logger显式加handler
比如要在其他日志里打印zerorpc的日志, 我们大部分日志初始化处都有这句: logging.getLogger('zerorpc').addHandler(handler), 给rpcLogger加上自己的handler就可以了,由于有了handler,那么只要zerorpc的源码里是getLogger('zerorpc')的(实际源码中一般是getLogger(__name__),在包内__name__即为'zerorpc.xxx'),日志就能打印到对应进程的日志里。
所以我们完全可以不用父子关系,而是像zerorpc一样在进程logger初始化的地方加上:
logging.getLogger('common').addHandler(handler)
然后common里的模块直接log = logging.getLogger('common')用即可,为避免和三方库重复要注意一下命名
当然还有一种方式就是自己的handler也通过函数触发,不要在模块全局上执行,加入一个函数手动调,只在进程初始化时调。

总结:

logging的父子关系是一个基础机制,稍微看下源码即可理解(其实主要就是图1图2):以点号.分隔,取最后一个点号的左边为前缀,以此前缀名作父,一个logger触发记录时,会调用所有父亲的handler。在同一系统中我们有时要用到这种机制来方便日志打印,因此有时会不同进程使用同一前缀名来初始化logger。这时,不同进程的模块若有相互import,容易造成一个日志打到多个日志文件里。如:

进程A:
A.py:
logger = logging.getLogger('xxsystem.A')
logger.addHandler(logging.FileHandler('service1.log'))
进程B 两个模块:

B.py:
from C import func
logger = logging.getLogger('xxsystem.B')
logger.addHandler(logging.FileHandler('service2.log'))

C.py:
from A import func

这样,就会造成B进程的log总是同时打到两个service1.log, service2.log日志文件里。这里是简化环境,只要B的import树里有A模块,就会造成同样结果。

避免日志重复的原则是:
logger名有相同前缀的情况下,对于一个模块两个进程调的情况,涉及到会被其他进程import的模块,不应触发任何同名前缀的loggeraddHandler操作。 (不能import <调用了addHandler方法的> 模块,自身也不能执行getLogger(prefix).addHandler

实际上我司使用这种机制本来也没有什么问题,只要注意不要随便import,都用getLogger即可。但由于代码不规范还是出现了不应有的import 日志初始化模块的情况。

要达到:

  • 哪个进程调用模块,日志就打在那个进程对应的日志里:
    a)getLogger,只要前缀相同,就会把当前进程的'prefix' logger设为父, 由于上面说的原因,这个logger会且只会被打到调用它的进程中(自己的handler没有初始化过)
    b)logger对象
  • 无论哪个进程调用模块,日志都打在自己规划所属的进程对应日志里:
    不要有任何父子关系, 日志名不要带点。这时反过来,必须调用日志初始化模块触发初始化,而不能只用getLogger, 否则是一个空logger,哪里都不会打印。

你可能感兴趣的:(python,开发工具)