1. 程式人生 > >python logging日誌模組以及多程序日誌

python logging日誌模組以及多程序日誌

本篇文章主要對 python logging 的介紹加深理解。更主要是 討論在多程序環境下如何使用logging 來輸出日誌, 如何安全地切分日誌檔案。

1. logging日誌模組介紹

python的logging模組提供了靈活的標準模組,使得任何Python程式都可以使用這個第三方模組來實現日誌記錄。python logging 官方文件

logging框架中主要由四個部分組成:

  • Loggers: 可供程式直接呼叫的介面
  • Handlers: 決定將日誌記錄分配至正確的目的地
  • Filters: 提供更細粒度的日誌是否輸出的判斷
  • Formatters: 制定最終記錄列印的格式佈局

2. logging的組成

loggers

loggers 就是程式可以直接呼叫的一個日誌介面,可以直接向logger寫入日誌資訊。logger並不是直接例項化使用的,而是通過logging.getLogger(name)來獲取物件,事實上logger物件是單例模式,logging是多執行緒安全的,也就是無論程式中哪裡需要打日誌獲取到的logger物件都是同一個。但是不幸的是logger並不支援多程序,這個在後面的章節再解釋,並給出一些解決方案。
【注意】loggers物件是有父子關係的,當沒有父logger物件時它的父物件是root,當擁有父物件時父子關係會被修正。舉個例子logging.getLogger("abc.xyz")

會建立兩個logger物件,一個是abc父物件,一個是xyz子物件,同時abc沒有父物件所以它的父物件是root。但是實際上abc是一個佔位物件(虛的日誌物件),可以沒有handler來處理日誌。但是root不是佔位物件,如果某一個日誌物件打日誌時,它的父物件會同時收到日誌,所以有些使用者發現建立了一個logger物件時會打兩遍日誌,就是因為他建立的logger打了一遍日誌,同時root物件也打了一遍日誌。

每個logger都有一個日誌的級別。logging中定義瞭如下級別

Level Numeric value
NOTSET 0
DEBUG 10
INFO 20
WARNING 30
ERROR 40
CRITICAL 50

當一個logger收到日誌資訊後先判斷是否符合level,如果決定要處理就將資訊傳遞給Handlers進行處理。

Handlers

Handlers 將logger發過來的資訊進行準確地分配,送往正確的地方。舉個栗子,送往控制檯或者檔案或者both或者其他地方(程序管道之類的)。它決定了每個日誌的行為,是之後需要配置的重點區域。

每個Handler同樣有一個日誌級別,一個logger可以擁有多個handler也就是說logger可以根據不同的日誌級別將日誌傳遞給不同的handler。當然也可以相同的級別傳遞給多個handlers這就根據需求來靈活的設定了。

Filters

Filters 提供了更細粒度的判斷,來決定日誌是否需要列印。原則上handler獲得一個日誌就必定會根據級別被統一處理,但是如果handler擁有一個Filter可以對日誌進行額外的處理和判斷。例如Filter能夠對來自特定源的日誌進行攔截or修改甚至修改其日誌級別(修改後再進行級別判斷)。

logger和handler都可以安裝filter甚至可以安裝多個filter串聯起來。

Formatters

Formatters 指定了最終某條記錄列印的格式佈局。Formatter會將傳遞來的資訊拼接成一條具體的字串,預設情況下Format只會將資訊%(message)s直接打印出來。Format中有一些自帶的LogRecord屬性可以使用,如下表格:

Attribute Format Description
asctime %(asctime)s 將日誌的時間構造成可讀的形式,預設情況下是‘2016-02-08 12:00:00,123’精確到毫秒
filename %(filename)s 包含path的檔名
funcName %(funcName)s 由哪個function發出的log
levelname %(levelname)s 日誌的最終等級(被filter修改後的)
message %(message)s 日誌資訊
lineno %(lineno)d 當前日誌的行號
pathname %(pathname)s 完整路徑
process %(process)s 當前程序
thread %(thread)s 當前執行緒

一個Handler只能擁有一個Formatter 因此如果要實現多種格式的輸出只能用多個Handler來實現。

3. logging 配置

簡易配置

首先在 loggers 章節裡說明了一點,我們擁有一個預設的日誌物件root,這個root日誌物件的好處是我們直接可以使用logging來進行配置和打日誌。例如:

12 logging.basicConfig(level=logging.INFO,filename='logger.log')logging.info("info message")

所以這裡的簡易配置所指的就是root日誌物件,隨拿隨用。每個logger都是單例物件所以配置過一遍之後程式內任何地方呼叫都可以。我們只需要呼叫basicConfig就可以對root日誌物件進行簡易的配置,事實上這種方式相當有效易用。它使得呼叫任何logger時保證至少一定會有一個Handler能夠處理日誌。
簡易配置大致可以這麼設定:

12345 logging.basicConfig(level=logging.INFO,format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',datefmt='[%Y-%m_%d %H:%M:%S]',filename='../log/my.log',filemode='a')

程式碼配置

另一種更加細緻地設定方式是在程式碼中配置,但這種設定方式是使用的最少的方式,畢竟誰也不希望把設定寫死到程式碼裡面去。但是這裡也稍微介紹一下,雖然用的不多,在必要的時候也可以用一把。(以後補上)

配置檔案配置

python中logging的配置檔案是基於ConfigParser的功能。也就是說配置檔案的格式也是按照這種方式來編寫。先奉上一個比較一般的配置檔案再細說

123456789101112131415161718192021222324252627282930313233343536373839 ##############################################[loggers]keys=root,log02[logger_root]level=INFOhandlers=handler01[logger_log02]level=DEBUGhandler=handler02qualname=log02##############################################[handlers]keys=handler01,handler02[handler_handler01]class=FileHandlerlevel=INFOformatter=form01args=('../log/cv_parser_gm_server.log',"a")[handler_handler02]class=StreamHandlerlevel=NOTSETformatter=form01args=(sys.stdout,)##############################################[formatters]keys=form01,form02[formatter_form01]format=%(asctime)s%(filename)s[line:%(lineno)d]%(levelname)s%(process)d%(message)sdatefmt=[%Y-%m-%d%H:%M:%S][formatter_form02]format=(message)s##############################################

相信看一遍以後,也找出規律了,我將幾個大塊用#分了出來。每一個logger或者handler或者formatter都有一個key名字。以logger為例,首先需要在[loggers]配置中加上key名字代表了這個logger。然後用[loggers_xxxx]其中xxxx為key名來具體配置這個logger,在log02中我配置了level和一個handler名,當然你可以配置多個hander。根據這個handler名再去 [handlers]裡面去找具體handler的配置,以此類推。
然後在程式碼中,這樣載入配置檔案即可:

1 logging.config.fileConfig(log_conf_file)

在handler中有一個class配置,可能有些讀者並不是很懂。其實這個是logging裡面原先就寫好的一些handler類,你可以在這裡直接呼叫。class指向的類相當於具體處理的Handler的執行者。在logging的文件中可以知道這裡所有的Handler類都是執行緒安全的,大家可以放心使用。那麼問題就來了,如果多程序怎麼辦呢。在下一章我主要就是重寫Handler類,來實現在多程序環境下使用logging。 我們自己重寫或者全部新建一個Handler類,然後將class配置指向自己的Handler類就可以載入自己重寫的Handler了。

4. logging遇到多程序(important)

這部分其實是我寫這篇文章的初衷。python中由於某種歷史原因,多執行緒的效能基本可以無視。所以一般情況下python要實現並行操作或者平行計算的時候都是使用多程序。但是 python 中logging 並不支援多程序,所以會遇到不少麻煩。
本次就以 TimedRotatingFileHandler 這個類的問題作為例子。這個Handler本來的作用是:按天切割日誌檔案。(當天的檔案是xxxx.log 昨天的檔案是xxxx.log.2016-06-01)。這樣的好處是,一來可以按天來查詢日誌,二來可以讓日誌檔案不至於非常大, 過期日誌也可以按天刪除。
但是問題來了,如果是用多程序來輸出日誌,則只有一個程序會切換,其他程序會在原來的檔案中繼續打,還有可能某些程序切換的時候早就有別的程序在新的日誌檔案裡打入東西了,那麼他會無情刪掉之,再建立新的日誌檔案。反正將會很亂很亂,完全沒法開心的玩耍。
所以這裡就想了幾個辦法來解決多程序logging問題

原因

在解決之前,我們先看看為什麼會導致這樣的原因。
先將 TimedRotatingFileHandler 的原始碼貼上來,這部分是切換時所作的操作:

Python
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 defdoRollover(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.        """ifself.stream:self.stream.close()self.stream=None# get the time that this sequence started at and make it a TimeTuplecurrentTime=int(time.time())dstNow=time.localtime(currentTime)[-1]t=self.rolloverAt-self.intervalifself.utc:timeTuple=time.gmtime(t)else:timeTuple=time.localtime(t)dstThen=timeTuple[-1]ifdstNow!=dstThen:ifdstNow:addend=3600else:addend=-3600timeTuple=time.localtime(t+addend)dfn=self.baseFilename+"."+time.strftime(self.suffix,timeTuple)ifos.path.exists(dfn):os.remove(dfn)# Issue 18940: A file may not have been created if delay is True.ifos.path.exists(self.baseFilename):os.rename(self.baseFilename,dfn)ifself.backupCount>0:forsinself.getFilesToDelete():os.remove(s)ifnotself.delay:self.stream=self._open()newRolloverAt=self.computeRollover(currentTime)whilenewRolloverAt<=currentTime:newRolloverAt=newRolloverAt+self.interval#If DST changes and midnight or weekly rollover, adjust for this.if(self.when=='MIDNIGHT'orself.when.startswith('W'))andnotself.utc:dstAtRollover=time.localtime(newRolloverAt)[-1]ifdstNow!=dstAtRollover:ifnotdstNow:# DST kicks in before next rollover, so we need to deduct an houraddend=-3600else:# DST bows out before next rollover, so we need to add an houraddend=3600newRolloverAt+=addendself.rolloverAt=newRolloverAt

我們觀察 if os.path.exists(dfn) 這一行開始,這裡的邏輯是如果 dfn 這個檔案存在,則要先刪除掉它,然後將 baseFilename 這個檔案重新命名為 dfn 檔案。然後再重新開啟 baseFilename這個檔案開始寫入東西。那麼這裡的邏輯就很清楚了

  1. 假設當前日誌檔名為 current.log 切分後的檔名為 current.log.2016-06-01
  2. 判斷 current.log.2016-06-01 是否存在,如果存在就刪除
  3. 將當前的日誌檔名 改名為current.log.2016-06-01
  4. 重新開啟新檔案(我觀察到原始碼中預設是”a” 模式開啟,之前據說是”w”)

於是在多程序的情況下,一個程序切換了,其他程序的控制代碼還在 current.log.2016-06-01 還會繼續往裡面寫東西。又或者一個程序執行切換了,會把之前別的程序重新命名的 current.log.2016-06-01 檔案直接刪除。又或者還有一個情況,當一個程序在寫東西,另一個程序已經在切換了,會造成不可預估的情況發生。還有一種情況兩個程序同時在切檔案,第一個程序正在執行第3步,第二程序剛執行完第2步,然後第一個程序 完成了重新命名但還沒有新建一個新的 current.log 第二個程序開始重新命名,此時第二個程序將會因為找不到 current 發生錯誤。如果第一個程序已經成功建立了 current.log 第二個程序會將這個空檔案另存為 current.log.2016-06-01。那麼不僅刪除了日誌檔案,而且,程序一認為已經完成過切分了不會再切,而事實上他的控制代碼指向的是current.log.2016-06-01。
好了這裡看上去很複雜,實際上就是因為對於檔案操作時,沒有對多程序進行一些約束,而導致的問題。
那麼如何優雅地解決這個問題呢。我提出了兩種方案,當然我會在下面提出更多可行的方案供大家嘗試。

解決方案1

先前我們發現 TimedRotatingFileHandler 中邏輯的缺陷。我們只需要稍微修改一下邏輯即可:

  1. 判斷切分後的檔案 current.log.2016-06-01 是否存在,如果不存在則進行重新命名。(如果存在說明有其他程序切過了,我不用切了,換一下控制代碼即可)
  2. 以”a”模式 開啟 current.log
    發現修改後就這麼簡單~
    talking is cheap show me the code:
    Python
    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 classSafeRotatingFileHandler(TimedRotatingFileHandler):def__init__(self,filename,when='h',interval=1,backupCount=0,encoding=None,delay=False,utc=False):TimedRotatingFileHandler.__init__(self,filename,when,interval,backupCount<