图:
图编号按顺序1-4,嫌长可以跳过定位过程看总结
定位过程:
这些进程都会初始化一个叫 sdsom
的logger
,并且把handler
加到了这个logger
对象中,后面getlogger
的时候我们是 sdsom.xx
, 这个按点分隔会导致认为是子logger
,比如是sdsom.A
,就会新建一个logger
叫sdsom.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')
只需要执行这一句,这个logger
的parent
就被设为 <import
这个common
模块的> 进程的 sdsom logger
,实际上sdsom.xxx
点号后面的内容都没有影响了,这个common logger
打印时,会调parent
,于是也就被相应进程的handler
打印了。
本身也算方便的机制,但由于这种方式内部实现不可见, 容易误用。
如果要共享日志, 还有一种方式就是对相应的logger
显式加handler
:
比如要在其他日志里打印zerorpc
的日志, 我们大部分日志初始化处都有这句: logging.getLogger('zerorpc').addHandler(handler)
, 给rpc
的Logger
加上自己的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
的模块,不应触发任何同名前缀的logger
的addHandler
操作。 (不能import
<调用了addHandler
方法的> 模块,自身也不能执行getLogger(prefix).addHandler
)
实际上我司使用这种机制本来也没有什么问题,只要注意不要随便import
,都用getLogger
即可。但由于代码不规范还是出现了不应有的import 日志初始化模块
的情况。
要达到:
- 哪个进程调用模块,日志就打在那个进程对应的日志里:
a)
用getLogger
,只要前缀相同,就会把当前进程的'prefix' logger
设为父, 由于上面说的原因,这个logger
会且只会被打到调用它的进程中(自己的handler
没有初始化过)b)
传logger
对象 - 无论哪个进程调用模块,日志都打在自己规划所属的进程对应日志里:
不要有任何父子关系, 日志名不要带点。这时反过来,必须调用日志初始化模块触发初始化,而不能只用getLogger
, 否则是一个空logger
,哪里都不会打印。