我前面有篇文章已经详细介绍了一下 Python 的日志模块。Python 提供了非常多的可以运用在各种不同场景的 Log Handler.

TimedRotatingFileHandler 是 Python 提供的一个可以基于时间自动切分日志的 Handler 类,他继承自 BaseRotatingHandler -> logging.FileHandler

但是他有一个缺点就是没有办法支持多进程的日志切换,多进程进行日志切换的时候可能会因为重命名而丢失日志数据。

来看下他的实现(我默认大家已经知道了 FileHandler 的实现和 logging 模块的调用机制 如果还不清楚可以先去看下我前面那篇文章 https://www.cnblogs.com/piperck/p/9634133.html):

def doRollover(self):
"""
do a rollover; in this case, a date/time stamp is appended to the filename
when the rollover happens. However, you want the file to be named for the
start of the interval, not the current time. If there is a backup count,
then we have to get a list of matching filenames, sort them and remove
the one with the oldest suffix.
"""
if self.stream:
self.stream.close()
self.stream = None
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
if os.path.exists(dfn):
os.remove(dfn)
# Issue 18940: A file may not have been created if delay is True.
if os.path.exists(self.baseFilename):
os.rename(self.baseFilename, dfn)
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
if not self.delay:
self.stream = self._open()
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
#If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt

doRollover 其实就是 rotate 的具体实现。在具体详细分析为什么会出现多进程问题的时候我先来说下 rotate 的原理。

首先日志会被打印在一个叫 baseFilename 名字的文件中。然后在 Rotate 的时候会根据你想要打印的参数生成对应新文件的名字也就是上面函数的 dfn 的值。

然后会将现在的文件重命名为 dfn 的值。之后在重新创建一个 baseFilename 的文件。然后继续往这个文件里面写。

举个例子,我们一直往 info.log 中写日志。现在该 rotate 了我们会把 info.log rename 成 info.log.2018-10-23 然后再创建一个 info.log 继续写日志,过程就是这样。

让我们来注意导致多进程问题的最关键的几句话:

    if os.path.exists(dfn):
os.remove(dfn)
# Issue 18940: A file may not have been created if delay is True.
if os.path.exists(self.baseFilename):
os.rename(self.baseFilename, dfn)

我们就根据上面的例子继续来描述。比如现在 dfn 就是 info.log.2018-10-23 。那么我会看有没有存在这个文件,如果有我就会先删除掉,然后再看下 info.log 是否存在,如果存在就执行 rename.

所以问题就很明确了,如果同时有多个进程进入临界区,那么会导致 dfn 文件被删除多次,另外下面的 rename 可能也会产生混乱。

现在我们要做的就是首先认为文件存在即是已经有人 rename 成功过了,并且在判断文件不存在的时候只允许一个人去 rename ,其他进程如果正好进入临界区就等一等。

让我们来实现这个函数:

class MultiCompatibleTimedRotatingFileHandler(TimedRotatingFileHandler):

    def doRollover(self):
if self.stream:
self.stream.close()
self.stream = None
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
# 兼容多进程并发 LOG_ROTATE
if not os.path.exists(dfn):
f = open(self.baseFilename, 'a')
fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
if not os.path.exists(dfn):
os.rename(self.baseFilename, dfn)
# 释放锁 释放老 log 句柄
f.close()
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
if not self.delay:
self.stream = self._open()
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
# If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt

我们判断一下如果该文件不存在,我们就进入临界区,然后利用 linux 的 fcntl 用阻塞模式获得一把文件锁。然后判断一下 info.log 是否已经被 rename 过了(这里用于同时进入临界区的其他进程判断前面持锁人是否已经结束完成了文件的 rename)。如果文件还在说明是第一个进来的进程则执行 rename 操作。将 info.log -> info.log.2018-10-23 。完成了之后这个时候 info.log 就已经暂时不存在于当前目录了。而且 dfn 文件已经创建。所以后面进来的进程会跳过这个判断直接执行下面的逻辑 open 一个新的基于 self.baseFilename 的文件。这里同时打开就无所谓了,因为 FileHandler 默认使用的 mode 是 'a' appending 模式,所以当 open 的时候就不会存在覆盖的情况了。

到此就补偿了无法兼容多进程的问题。这里还想多提一句,在写这个的时候经过了很长时间对 fcntl 模块的调研。他是一个基于 linux 的 voluntary 锁。也就是说是一把自愿锁。虽然我在调用的时候加了强制排它锁,但是其他不自愿的比如我再开一个 vim 去编辑该文件是可以绕过这个锁的这个一定要注意。

Reference:

https://gavv.github.io/blog/file-locks/  File locking in Linux

https://www.cnblogs.com/gide/p/6811927.html  python中给程序加锁之fcntl模块的使用

http://blog.jobbole.com/104331/  Linux 中 fcntl()、lockf、flock 的区别

用 Python 写一个多进程兼容的 TimedRotatingFileHandler的更多相关文章

  1. 用Python写一个简单的Web框架

    一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...

  2. 十行代码--用python写一个USB病毒 (知乎 DeepWeaver)

    昨天在上厕所的时候突发奇想,当你把usb插进去的时候,能不能自动执行usb上的程序.查了一下,发现只有windows上可以,具体的大家也可以搜索(搜索关键词usb autorun)到.但是,如果我想, ...

  3. [py]python写一个通讯录step by step V3.0

    python写一个通讯录step by step V3.0 参考: http://blog.51cto.com/lovelace/1631831 更新功能: 数据库进行数据存入和读取操作 字典配合函数 ...

  4. 【Python】如何基于Python写一个TCP反向连接后门

    首发安全客 如何基于Python写一个TCP反向连接后门 https://www.anquanke.com/post/id/92401 0x0 介绍 在Linux系统做未授权测试,我们须准备一个安全的 ...

  5. Python写一个自动点餐程序

    Python写一个自动点餐程序 为什么要写这个 公司现在用meican作为点餐渠道,每天规定的时间是早7:00-9:40点餐,有时候我经常容易忘记,或者是在地铁/公交上没办法点餐,所以总是没饭吃,只有 ...

  6. 用python写一个自动化盲注脚本

    前言 当我们进行SQL注入攻击时,当发现无法进行union注入或者报错等注入,那么,就需要考虑盲注了,当我们进行盲注时,需要通过页面的反馈(布尔盲注)或者相应时间(时间盲注),来一个字符一个字符的进行 ...

  7. python写一个能变身电光耗子的贪吃蛇

    python写一个不同的贪吃蛇 写这篇文章是因为最近课太多,没有精力去挖洞,记录一下学习中的收获,python那么好玩就写一个大一没有完成的贪吃蛇(主要还是跟课程有关o(╥﹏╥)o,课太多好烦) 第一 ...

  8. python写一个邮箱伪造脚本

    前言: 原本打算学php MVC的思路然后写一个项目.但是贼恶心, 写不出来.然后就还是用python写了个邮箱伪造. 0x01 第一步先去搜狐注册一个邮箱 然后,点开设置,开启SMTP服务. 当然你 ...

  9. 用python写一个非常简单的QQ轰炸机

    闲的没事,就想写一个QQ轰炸机,按照我最初的想法,这程序要根据我输入的QQ号进行轰炸,网上搜了一下,发现网上的案列略复杂,就想着自己写一个算了.. 思路:所谓轰炸机,就是给某个人发很多信息,一直刷屏, ...

随机推荐

  1. java8 流操作

    0  创建流 public void test1(){ List<String> list = new ArrayList<>(); Stream<String> ...

  2. CF369E Valera and Queries

    嘟嘟嘟 这题刚开始以为是一个简单题,后来越想越不对劲,然后就卡住了. 瞅了一眼网上的题解(真的只瞅了一眼),几个大字令人为之一振:正难则反! 没错,把点看成区间,比如2, 5, 6, 9就是[1, 1 ...

  3. 【转】Win10开机密码忘了?教你破解Win10开机密码

    [PConline 技巧]Win10开机密码忘记了怎么办?这个问题很多人都遇到过,如何破解一台Win10电脑的开机密码呢(非BIOS开机密码)?其实很简单,利用下面这个方法,分分钟就能搞定. 一招破解 ...

  4. 【vue】vue-router跳转路径url多种格式

    1.形如  http://localhost:8080/#/book?id=**** ①路由配置 ②路由定向链接,以query传参id 另外,获取query传递的参数id用  this.$route. ...

  5. java多线程 - 处理并行任务

    在多线程编程过程中,遇到这样的情况,主线程需要等待多个子线程的处理结果,才能继续运行下去.个人给这样的子线程任务取了个名字叫并行任务.对于这种任务,每次去编写代码加锁控制时序,觉得太麻烦,正好朋友提到 ...

  6. ajax请求基于restFul的WebApi(post、get、delete、put)

    近日逛招聘软件,看到部分企业都要求会编写.请求restFul的webapi.正巧这段时间较为清闲,于是乎打开vs准备开撸. 1.何为restFul? restFul是符合rest架构风格的网络API接 ...

  7. 《React Native 精解与实战》书籍连载「React Native 中的生命周期」

    此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...

  8. webapack

    webpack  就是一个前端资源加载.打包工具. 核心思想:会根据(js css less文件)模块依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源,减少页面请求. wapack ...

  9. LookupError: Resource averaged_perceptron_tagger not found. Please use the NLTK Downloader to obtain the resource:

    命令行执行 import nltk nltk.download('averaged_perceptron_tagger') 完事

  10. 1168: mxh对lfx的询问(前缀和+素数表)

    题目描述: AS WE ALL KNOW, lfx是咱们组的神仙,但是mxh想考一考lfx一个简单的问题,以此看一下lfx到底是不是神仙.但是lfx要准备补考,于是请你来帮忙回答问题: 给定一个整数N ...