python的logging模块详解,python logging 多进程
本文重点介绍python日志记录,以加深我们的理解。主要讨论了在多进程环境下,如何使用日志输出日志,以及如何对日志文件进行安全分段。
1.日志模块介绍python的日志模块提供了一个灵活的标准模块,使得任何Python程序都可以使用这个第三方模块来实现日志。Python日志官方文档
日志框架主要由四部分组成:
Loggers: Handlers:可以被程序直接调用的接口;过滤器:决定将日志记录分配到正确的目的地;排版员:制作最终记录打印的版式。
2.loggersloggers,日志记录的组件,是程序可以直接调用的日志接口,可以直接将日志信息写入日志记录器。Logger不直接实例化,而是通过logging.getLogger(name)获取对象。事实上,logger对象是单例模式,日志记录是多线程安全的,也就是说,无论程序中哪里需要日志记录,它都是同一个logger对象。不幸的是,logger不支持多进程。这将在后面的章节中解释,并给出一些解决方案。
[注意] loggers对象有父子关系。当没有父logger对象时,它的父对象是root。当存在父对象时,将纠正父子关系。比如logging.getLogger(abc.xyz )会创建两个Logger对象,一个是abc的父对象,一个是xyz的子对象,而abc没有父对象,所以它的父对象是root。但实际上,abc是一个占位符对象(虚拟日志对象),可以在没有处理程序的情况下处理日志。但是root不是占位符对象。如果某个日志对象进行日志记录,其父对象会同时接收到该日志,所以有些用户在发现创建了一个logger对象时会进行两次日志记录,只是因为他创建的logger记录一次,根对象记录一次。
每个记录器都有一个日志级别。日志记录中定义了以下级别
当日志记录器接收到日志信息时,它首先判断是否满足级别,如果决定处理它,它将把信息传递给处理程序进行处理。
HandlersHandlers准确分发logger发来的信息,发送到正确的地方。举个例子,将它发送到控制台或文件或两者或其他地方(流程管道等)。它决定了每个日志的行为,并且是稍后要配置的关键区域。
每个处理程序也有一个日志级别,一个日志记录器可以有多个处理程序,这意味着日志记录器可以根据不同的日志级别将日志交付给不同的处理程序。当然也可以传递给同一级别的多个处理程序,可以根据需求灵活设置。
FiltersFilters提供了更细粒度的判断来确定是否需要打印日志。原则上,如果处理程序得到一个日志,会按照级别统一处理,但是如果处理程序有过滤器,可以对日志进行额外的处理和判断。比如Filter可以拦截特定来源的日志或者修改甚至修改其日志级别(修改后再做级别判断)。
记录器和处理程序都可以安装过滤器,甚至可以串联安装多个过滤器。
FormattersFormatters指定最终记录打印的格式布局。格式化程序将把传递的信息拼接成一个特定的字符串,默认情况下,格式化程序将只直接打印出信息%(message)s。格式中有一些内置的LogRecord属性可以使用,如下表所示:
3.日志记录的简单配置。首先,在loggers一章中解释了我们有一个默认的日志对象根。这个根日志对象的好处是我们可以直接使用日志来配置和记录。例如:
logging . basic config(level=logging。INFO,filename=logger.log )
Logging.info(info message )所以这里的简单配置指的是根日志对象,可以随意使用。每个日志记录器都是一个单独的对象,因此在配置一次之后,可以在程序中的任何地方调用它。我们可以通过调用basicConfig简单地配置根日志对象。其实这个方法挺有效的,也很好用。它确保在调用任何日志记录器时,至少有一个处理程序可以处理日志。
简单配置大致可以这样设置:
logging . basic config(level=logging。信息,
format= %(ASC time)s %(filename)s[line:%(line no)d]%(level name)s %(message)s ,
datefmt=[%Y-%m_%d %H:%M:%S],
文件名=./log/my.log ,
Filemode=a )另一种更详细的配置代码的方式是在代码中配置,但这种设置是最少使用的方式。毕竟没有人愿意把设置写进代码里。不过这里有个小介绍。虽然用的不多,但是必要的时候也可以用一个。(以后补)
配置文件在python中配置登录的配置文件是基于ConfigParser的功能。也就是说,配置文件的格式也是这样写的。在详细阐述之前,我先给你一个通用的配置文件。
##############################################
[伐木工人]
keys=root,log02
[logger_root]
级别=信息
handlers=handler01
[logger_log02]
级别=调试
handler=handler02
qualname=log02
##############################################
[经手人]
按键=手柄01、手柄02
[handler_handler01]
级别=信息
formatter=form01
args=(./log/cv_parser_gm_server.log , a )
[handler_handler02]
level=NOTSET
formatter=form01
args=(sys.stdout,)
##############################################
[格式化程序]
keys=表格01,表格02
[formatter_form01]
format=%(asctime)s %(文件名)s[行:%(行号)d] %(级别名)s %(进程)d %(消息)s
datefmt=[%Y-%m-%d %H:%M:%S]
[formatter_form02]
格式=(消息)s
每个记录器、处理程序或格式化程序都有一个键名。以logger为例。首先,您需要在[loggers]配置中添加键名来表示这个记录器。然后使用[loggers_xxxx]来配置该记录器,其中xxxx是密钥名。在log02中,我配置了等级和一个hander名称。当然,您可以配置多个处理程序。根据处理程序名称,进入【处理程序】找到具体处理程序的配置,以此类推。
然后在代码中,像这样加载配置文件:
logging . config . file config(log _ conf _ file)在handler中有一个类配置,可能有些读者理解不太好。实际上,这些是用日志记录编写的一些处理程序类。你可以在这里直接给他们打电话。类指向的类相当于具体处理的处理程序的执行者。在日志文档中,您可以知道这里所有的处理程序类都是线程安全的,所以您可以放心地使用它们。那么问题来了,如果有多个流程呢?在下一章中,我将主要重写Handler类来实现多进程环境中的日志记录。我们可以自己重写或创建一个新的处理程序类,然后将类配置指向我们自己的处理程序类来加载我们自己重写的处理程序。
4.日志记录遇到多个进程的部分(重要)其实是我写这篇文章的初衷。由于一些历史原因,python中多线程的性能基本可以忽略。所以python在想要实现并行操作或者并行计算的时候,一般会使用多进程。但是python中的日志不支持多进程,所以会有很多麻烦。
这次以TimedRotatingFileHandler的问题为例。这个处理程序的原始功能是按天剪切日志文件。(当日文件为xxxx.log,昨日文件为xxxx.log.2016-06-01)。这样做的好处是,一方面可以按天搜索日志;另一方面,日志文件可以保持不太大,过期的日志可以按天删除。
但问题来了。如果使用多个进程输出日志,只有一个进程会切换,其他进程会继续输入原始文件。也有可能在某个进程切换中,其他进程已经在新的日志文件中录入了一些东西,所以他会无情地删除它们,建立新的日志文件。反正要一塌糊涂了,根本玩不开心。
所以这里有一些解决多进程日志问题的方法。
在解决原因之前,我们先来看看为什么会是这个原因。
先粘贴TimedRotatingFileHandler的源代码。这部分是切换时的操作:
def doRollover(自我):
做一个翻车;在这种情况下,日期/时间戳会附加到文件名中
翻车的时候。但是,您希望该文件以
间隔的开始时间,而不是当前时间。如果有备份计数器,
然后我们必须得到一个匹配文件名的列表,对它们进行排序并删除
后缀最老的那个。
如果自体流:
self.stream.close()
self.stream=无
#获取该序列开始的时间,并使其成为时间序列
currentTime=int(time.time())
dst now=time . local time(current time)[-1]
t=self . rollover at-self . interval
if self.utc:
timeTuple=time.gmtime(t)
否则:
timeTuple=time.localtime(t)
dstThen=timeTuple[-1]
如果dstNow!=dstThen:
如果dstNow:
加数=3600
否则:
加数=-3600
timeTuple=time.localtime(t加数)
dfn=self.baseFilename . time.strftime(self.suffix,timeTuple)
如果os.path.exists(dfn):
os.remove(dfn)
# Issue 18940:如果delay为True,则文件可能尚未创建。
如果OS . path . exists(self . base filename):
os.rename(self.baseFilename,dfn)
如果self.backupCount为0:
对于self.getFilesToDelete()中的:
os.remove
如果不是自延迟:
self.stream=自我。_打开()
new rollover at=self . computer ollover(current time)
while newRolloverAt=currentTime:
newrollovat=newrollovat self . interval
#如果夏令时发生变化以及午夜或每周滚动,请对此进行调整。
if (self.when==MIDNIGHT 或self.when.startswith(W ))而not self.utc:
dstAtRollover=time . local time(new rollover at)[-1]
如果dstNow!=dstAtRollover:
如果不是dstNow: # DST在下一次滚动前生效,所以我们需要扣除一个小时
加数=-3600
else: # DST在下一次滚动前退出,所以我们需要增加一个小时
加数=3600
newRolloverAt=加数
如果os.path.exists(dfn ),则在行首的self . rollover at=new rollover。这里的逻辑是,如果dfn文件存在,应该先删除它,然后将文件baseFilename重命名为dfn文件。然后重新打开文件baseFilename并开始写。那么这里的逻辑就很清楚了。
假设当前日志文件名为current.log,分段后文件名为current.log.2016-06-01,判断current.log.2016-06-01是否存在,如果存在则删除,将当前日志文件名重命名为current.log.2016-06-01,重新打开新文件(我观察到源代码中默认模式为“A”,之前说是“W”)。所以在多进程的情况下,一个进程开关是存在的,其他进程的句柄还在current.log.2016-06-01,里面会继续写东西。或者另一个进程切换的话,直接删除其他进程重命名的current.log.2016-06-01文件。或者还有一种情况,当一个进程在写东西的时候,另一个进程已经在切换了,这就会造成不可预知的情况。在另一种情况下,两个进程同时切割文件。第一个进程正在执行步骤3,第二个进程刚刚完成步骤2,然后第一个进程已经完成重命名但是还没有创建新的current.log第二个进程开始重命名,第二个进程会因为找不到当前而出错。如果第一个进程已经成功创建了current.log,第二个进程会将这个空文件保存为current.log.2016-06-01。那么不仅日志文件被删除,而且,一旦进程认为自己已经分段完毕,就不会再进行切割,而实际上它的句柄指向current.log.2016-06-01。
好了这里看上去很复杂,实际上就是因为对于文件操作时,没有对多进程进行一些约束,而导致的问题。
那么如何优雅地解决这个问题呢。我提出了两种方案,当然我会在下面提出更多可行的方案供大家尝试。
解决方案一先前我们发现TimedRotatingFileHandler中逻辑的缺陷。我们只需要稍微修改一下逻辑即可:
判断切分后的文件current.log.2016-06-01是否存在,如果不存在则进行重命名。(如果存在说明有其他进程切过了,我不用切了,换一下句柄即可)以" a "模式打开当前日志
发现修改后就这么简单~
说话是廉价的给我看看代码:类SafeRotatingFileHandler(TimedRotatingFileHandler):
def __init__(self,filename,when=h ,interval=1,backupCount=0,encoding=None,delay=False,utc=False):
TimedRotatingFileHandler .__init__(自身,文件名,时间,间隔,备份计数,编码,延迟,utc)
覆盖多罗洛弗
由 ## 命令的行被复写的副本更改
def doRollover(自我):
做一个翻车;在这种情况下,日期/时间戳会附加到文件名中
翻车的时候。但是,您希望该文件以
间隔的开始时间,而不是当前时间。如果有备份计数器,
然后我们必须得到一个匹配文件名的列表,对它们进行排序并删除
后缀最老的那个。
覆盖,1。如果无引线不存在,则进行重命名
2._以" a "模式打开
如果自体流:
self.stream.close()
自我流=无
#获取该序列开始的时间,并使其成为时间序列
currentTime=int(time.time())
dst now=时间。本地时间(当前时间)[-1]
t=自我。自我翻转。间隔
if self.utc:
timeTuple=time.gmtime(t)
否则:
timeTuple=time.localtime(t)
dstThen=timeTuple[-1]
如果dstNow!=dstThen:
如果dstNow:
加数=3600
否则:
加数=-3600
timeTuple=time.localtime(t加数)
dfn=self.baseFilename . time.strftime(self.suffix,timeTuple)
##如果os.path.exists(dfn):
##操作系统。删除(dfn)
#问题18940:如果耽搁为没错,则文件可能尚未创建。
##如果OS。路径。存在(自我。基本文件名):
如果不是操作系统路径存在(dfn)和OS。路径。存在(自我。基本文件名):
os.rename(self.baseFilename,dfn)
如果self.backupCount为0:
对于self.getFilesToDelete()中的:
操作系统.移除
如果不是自延迟:
self.mode=a
自我流=自我。_打开()
在=self处的新翻转。计算机翻转(当前时间)
while newRolloverAt=currentTime:
newrollovat=newrollovat自我。间隔
#如果夏令时发生变化以及午夜或每周滚动,请对此进行调整。
if (self.when==MIDNIGHT 或self.when.startswith(W ))而非self.utc:
dstAtRollover=时间。当地时间(新滚动时间为)[-1]
如果dstNow!=dstAtRollover:
如果不是dstNow: # DST在下一次滚动前生效,所以我们需要扣除一个小时
加数=-3600
否则:# DST在下一次滚动前退出,所以我们需要增加一个小时
加数=3600
newRolloverAt=加数
self.rolloverAt=newRolloverAt不要以为代码那么长,其实修改部分就是"##" 注释的地方而已,其他都是照抄源代码。这个类继承了TimedRotatingFileHandler重写了这个切分的过程。这个解决方案十分优雅,改换的地方非常少,也十分有效。但有网友提出,这里有一处地方依然不完美,就是重新命名的那一步,如果就是这么巧,同时两个或者多个进程进入了如果语句,先后开始重新命名那么依然会发生删除掉日志的情况。确实这种情况确实会发生,由于切分文件一天才一次,正好切分的时候同时有两个处理者在操作,又正好同时走到这里,也是蛮巧的,但是为了完美,可以加上一个文件锁,如果之后加锁,得到锁之后再判断一次,再进行重新命名这种方式就完美了。代码就不贴了,涉及到锁代码,影响美观。
解决方案2我认为最简单有效的解决方案。重写文件处理器类(这个类是所有写入文件的处理者都需要继承的TimedRotatingFileHandler就是继承的这个类;我们增加一些简单的判断和操作就可以。
我们的逻辑是这样的:
判断当前时间戳是否与指向的文件名是同一个时间如果不是,则切换指向的文件即可
结束,是不是很简单的逻辑。
说话是廉价的给我看看代码:类安全文件处理程序(文件处理程序):
def __init__(自身,文件名,模式,编码=无,延迟=0):
为流式日志记录使用指定的文件名
如果编解码器为无:
编码=无
文件处理程序. init__(自身,文件名,模式,编码,延迟)
自我模式=模式
自编码=编码
self.suffix=%Y-%m-%d
self.suffix_time=
定义发出(自身,记录):
发行唱片。
总是检查时间
尝试:
if self.check_baseFilename(记录):
self.build_baseFilename()
FileHandler.emit(self,record)
除了(键盘中断,系统退出):
上升
除了:
self.handleError(记录)
def check_baseFilename(self,record):
确定是否应出现生成器。
不使用记录,因为我们只是比较时间,
但这是必需的,这样方法签名才是相同的
timeTuple=time.localtime()
if self.suffix_time!=time.strftime(self.suffix,timeTuple)或不是OS。路径。存在(自我。基本文件名“.”self.suffix_time):
返回一
否则:
返回0
def build_baseFilename(self):
做建设者;在这种情况下,
旧的时间戳从文件名中删除
一个新的时间戳被附加到文件名上
如果自体流:
self.stream.close()
自我流=无
#删除旧后缀
if self.suffix_time!=:
index=self.baseFilename.find( . self.suffix_time)
如果index==-1:
index=self.baseFilename.rfind( . )
自我。基本文件名=自身。基本文件名[:索引]
#添加新后缀
当前时间元组=时间。当地时间()
自我。后缀_时间=时间。strftime(self。后缀,currentTimeTuple)
自我。基本文件名=自身。基本文件名“.”self.suffix_time
self.mode=a
如果不是自延迟:
自我流=自我. open()check_baseFilename就是执行逻辑一判断;构建基础文件名就是执行逻辑2换句柄。就这么简单完成了。
这种方案与之前不同的是,当前文件就是当前日志2016-06-01,到了明天当前文件就是current.log.2016-06-02没有重命名的情况,也没有删除的情况。十分简洁优雅。也能解决多进程的记录问题。解决方案其他当然还有其他的解决方案,例如由一个记录进程统一打日志,其他进程将所有的日志内容打入记录进程管道由它来打理。还有将日志打入网络窝当中也是同样的道理。
参考
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。