pengkunstone 2019-12-27
提前祝大家过个好年
最近忙于项目,今天抽出点时间写写Blog谈谈昨天遇到的问题
项目最近要收尾了,想把Logger规整一下,因为很多地方都有用到
Python的Logger模块是Python自带的模块,可方便快捷的进行日志的记录
python doc
该模块本身就是线程安全的,下面的注释摘抄至 doc
The logging module is intended to be thread-safe without any special work needing to be done by its clients. It achieves this though using threading locks; there is one lock to serialize access to the module’s shared data, and each handler also creates a lock to serialize access to its underlying I/O.
If you are implementing asynchronous signal handlers using the signal module, you may not be able to use logging from within such handlers. This is because lock implementations in the threading module are not always re-entrant, and so cannot be invoked from such signal handlers.
也就是说你不需要关注多线程的问题,只要 getLogger()
时指定当前空间即可
Loggers have the following attributes and methods. Note that Loggers should NEVER be instantiated directly, but always through the module-level function logging.getLogger(name). Multiple calls to getLogger() with the same name will always return a reference to the same Logger object.
The name is potentially a period-separated hierarchical value, like foo.bar.baz (though it could also be just plain foo, for example). Loggers that are further down in the hierarchical list are children of loggers higher up in the list. For example, given a logger with a name of foo, loggers with names of foo.bar, foo.bar.baz, and foo.bam are all descendants of foo. The logger name hierarchy is analogous to the Python package hierarchy, and identical to it if you organise your loggers on a per-module basis using the recommended construction logging.getLogger(name). That’s because in a module, name is the module’s name in the Python package namespace.
意思是 logger.getLogger()
时传入相同的变量,会永远返回同一个对象,比如我在当前进程内的任何地方, 使用 log = logger.getLogger("work")
生成的log对象一直是同一个对象,这就是单例模式,官方推荐传入 __name__
因为他是Python包命名空间中模块的名称。
如果你是纠结 Logger 的单例怎么解决,你可以关闭网页了,因为他本身是单例的
手动写一个单例完全是为了记忆单例模式的使用,只是以 logger 模块举例
不考虑 logger 的自带单例情况下的原始代码
代码精简过,大致意思不变
测试是否可以使用的代码
我们知道,python实例化时其实是先走 __new__
再走 __init__
我们可以重写 __new__
方法,如果发现已生成对象直接返回该对象
同时为了防止多线程的资源竞争,我们使用线程锁来保证同一时间只有一个线程能访问 __new__
但是测试代码跑过之后发现每次会输出接近100条日志,这是为什么呢?
原来,每次请求实例化时,如有对象则直接返回之前生成的对象(MyLogger._instance),但是因为 Python3
默认继承新式类,
即 Object
,每次请求时返回了 object.__new__
然后会再执行一遍 MyLogger
的 __init__
方法,而我们在 __init__
中添加了两个 Handler
,
而上文提到, logger.getLogger
传入同一个参数则 logger 为一个, 导致每次请求时都会添加两个 Handler
到同一个 logger
,这样导致 logger
的 Handler
越来越多,重复写入了,解决这个问题需要防止重复走 __init__
其实正确的写法应该是类的
__init__
只负责接收参数,像这种add Handler
的功能放到自写方法中,这样__init__
不会有任何 add 操作即可
该方法利用元类 Type
的 __call__
实例化的对象调用不会走 __init__
的特性来规避问题
如果你觉得本方法需要覆盖父类不太好,那么还有第三种方法
方法1中,每次都会走 __init__
,而我们在 __init__
中又进行了 add Handler
等操作,那么我们将所有初始化及 add
操作放在自写方法中即可
如上图所示,这对请求实例化的用户是无感知的,它只需要和之前一样调,但其实内部在实例化时调用了 start
, 同时重复实例化时走 __init__
没有任何代码逻辑(走的Object)
我们在实际测试过程中发现,在配置了log持久化存储搭配多线程使用的时候,写入log文件的日志会丢失数据,测试发现应该是实例化后立刻写入会出现一些延迟,再加上测试代码
写入一条后立刻结束,导致的丢失问题,当然,在实际使用中,一般是初始化时实例化,也不可能在log后直接停止
但是问题还是要解决,实例化我们在每次请求实例化时等待一下即可 time.sleep(1)
,等待时间与机器性能有关,好的机器不会出现问题,
1s是保险的
然后最终代码为