Python模塊學習--logging

前言:

      許多應用程序中都會有日誌模塊,用於記錄系統在運行過程中的一些關鍵信息,以便於對系統的運行狀況進行跟蹤。在.NET平臺中,有非常著名的第三方開源日誌組件log4net,c++中,有人們熟悉的log4cpp,而在python中,我們不需要第三方的日誌組件,因爲它已經爲我們提供了簡單易用、且功能強大的日誌模塊:logging。logging模塊支持將日誌信息保存到不同的目標域中,如:保存到日誌文件中;以郵件的形式發送日誌信息;以http get或post的方式提交日誌到web服務器;以windows事件的形式記錄等等。這些日誌保存方式可以組合使用,每種方式可以設置自己的日誌級別以及日誌格式。


先看一個比較簡單的例子,讓我們對logging模塊有個感性的認識:

# -*- coding: utf-8 -*-
import logging,os
logging.basicConfig(filename = os.path.join(os.getcwd(), 'log.txt'), level = logging.DEBUG)
logging.debug('this is a message')
運行上面例子的代碼,將會在程序的根目錄下創建一個log.txt文件,打開該文件,裏面有一條日誌記錄:”DEBUG:root:this is a message”。


4個主要的組件
logger: 提供了應用程序可以直接使用的接口;
handler: 將(logger創建的)日誌記錄發送到合適的目的輸出;
filter: 提供了細度設備來決定輸出哪條日誌記錄;
formatter:決定日誌記錄的最終輸出格式。


模塊級函數
logging.getLogger([name]):返回一個logger對象,如果沒有指定名字將返回root logger
logging.notset()、logging.debug()、logging.info()、logging.warning()、logging.error()、logging.critical():設定root logger的日誌級別

logging.basicConfig():用默認Formatter爲日誌系統建立一個StreamHandler,設置基礎配置並加到root logger中。kwargs支持如下幾個關鍵字參數:詳細的格式介紹就查看官方文檔
  filename-->日誌文件的保存路徑。如果配置了些參數,將自動創建一個FileHandler作爲Handler;
  filemode-->日誌文件的打開模式。 默認值爲’a’,表示日誌消息以追加的形式添加到日誌文件中。如果設爲’w’, 那麼每次程序啓動的時候都會創建一個新的日誌文件;
  format-->設置日誌輸出格式;
  datefmt-->定義日期格式;
  level-->設置日誌的級別.對低於該級別的日誌消息將被忽略;
  stream-->設置特定的流用於初始化StreamHandler;


Logger
每個程序在輸出信息之前都要獲得一個Logger。Logger通常對應了程序的模塊名,比如聊天工具的圖形界面模塊可以這樣獲得它的Logger:
Logger=logging.getLogger(”chat.gui”)
而核心模塊可以這樣:
Logger=logging.getLogger(”chat.kernel”)
Logger.setLevel(lel):指定最低的日誌級別,低於lel的級別將被忽略。debug是最低的內置級別,critical爲最高
Logger.addFilter(filt)、Logger.removeFilter(filt):添加或刪除指定的filter
Logger.addHandler(hdlr)、Logger.removeHandler(hdlr):增加或刪除指定的handler
Logger.debug()、Logger.info()、Logger.warning()、Logger.error()、Logger.critical():設置logger的level
level有以下幾個級別:


NOTSET < DEBUG < INFO < WARNING < ERROR < CRITICAL
注意:如果把Looger的級別設置爲INFO,那麼小於INFO級別的日誌都不輸出,大於等於INFO級別的日誌都輸出。這樣的好處, 就是在項目開發時debug用的log,在產品release階段不用一一註釋,只需要調整logger的級別就可以了,很方便。


Handlers
handler對象負責發送相關的信息到指定目的地。Python的日誌系統有多種Handler可以使用。有些Handler可以把信息輸出到控制檯,有些Logger可以把信息輸出到文件,還有些 Handler可以把信息發送到網絡上。如果覺得不夠用,還可以編寫自己的Handler。可以通過addHandler()方法添加多個多handler
Handler.setLevel(lel):指定被處理的信息級別,低於lel級別的信息將被忽略
Handler.setFormatter():給這個handler選擇一個格式
Handler.addFilter(filt)、Handler.removeFilter(filt):新增或刪除一個filter對象


Formatters

Formatter對象設置日誌信息最後的規則、結構和內容,默認的時間格式爲%Y-%m-%d %H:%M:%S,下面是Formatter常用的一些信息

%(name)s    Logger的名字
%(levelno)s    數字形式的日誌級別
%(levelname)s    文本形式的日誌級別
%(pathname)s    調用日誌輸出函數的模塊的完整路徑名,可能沒有
%(filename)s    調用日誌輸出函數的模塊的文件名
%(module)s    調用日誌輸出函數的模塊名
%(funcName)s    調用日誌輸出函數的函數名
%(lineno)d    調用日誌輸出函數的語句所在的代碼行
%(created)f    當前時間,用UNIX標準的表示時間的浮 點數表示
%(relativeCreated)d    輸出日誌信息時的,自Logger創建以 來的毫秒數
%(asctime)s    字符串形式的當前時間。默認格式是 “2003-07-08 16:49:45,896”。逗號後面的是毫秒
%(thread)d    線程ID。可能沒有
%(threadName)s    線程名。可能沒有
%(process)d    進程ID。可能沒有
%(message)s    用戶輸出的消息
注:一個Handler只能擁有一個Formatter,因此如果要實現多種格式的輸出只能用多個Handler來實現。可以給日誌對象(Logger Instance)設置日誌級別,低於該級別的日誌消息將會被忽略,也可以給Hanlder設置日誌級別,對於低於該級別的日誌消息, Handler也會忽略。


設置過濾器
細心的朋友一定會發現前文調用logging.getLogger()時參數的格式類似於“A.B.C”。採取這樣的格式其實就是爲了可以配置過濾器。看一下這段代碼:
LOG=logging.getLogger(”chat.gui.statistic”)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
formatter = logging.Formatter(’%(asctime)s %(levelname)s %(message)s’)
console.setFormatter(formatter)
filter=logging.Filter(”chat.gui”)
console.addFilter(filter)
LOG.addHandler(console)
和前面不同的是我們在Handler上添加了一個過濾器。現在我們輸出日誌信息的時候就會經過過濾器的處理。名爲“A.B”的過濾器只讓名字帶有 “A.B”前綴的Logger輸出信息。可以添加多個過濾器,只要有一個過濾器拒絕,日誌信息就不會被輸出。另外,在Logger中也可以添加過濾器。


每個Logger可以附加多個Handler。接下來我們就來介紹一些常用的Handler:
(1)logging.StreamHandler
使用這個Handler可以向類似與sys.stdout或者sys.stderr的任何文件對象(file object)輸出信息。它的構造函數是:
StreamHandler([strm])
其中strm參數是一個文件對象。默認是sys.stderr
(2)logging.FileHandler
和StreamHandler類似,用於向一個文件輸出日誌信息。不過FileHandler會幫你打開這個文件。它的構造函數是:
FileHandler(filename[,mode])
filename是文件名,必須指定一個文件名。
mode是文件的打開方式。參見Python內置函數open()的用法。默認是’a',即添加到文件末尾。
(3)logging.handlers.RotatingFileHandler
這個Handler類似於上面的FileHandler,但是它可以管理文件大小。當文件達到一定大小之後,它會自動將當前日誌文件改名,然後創建 一個新的同名日誌文件繼續輸出。比如日誌文件是chat.log。當chat.log達到指定的大小之後,RotatingFileHandler自動把 文件改名爲chat.log.1。不過,如果chat.log.1已經存在,會先把chat.log.1重命名爲chat.log.2。。。最後重新創建 chat.log,繼續輸出日誌信息。它的構造函數是:
RotatingFileHandler( filename[, mode[, maxBytes[, backupCount]]])
其中filename和mode兩個參數和FileHandler一樣。
maxBytes用於指定日誌文件的最大文件大小。如果maxBytes爲0,意味着日誌文件可以無限大,這時上面描述的重命名過程就不會發生。
backupCount用於指定保留的備份文件的個數。比如,如果指定爲2,當上面描述的重命名過程發生時,原有的chat.log.2並不會被更名,而是被刪除。
(4)logging.handlers.TimedRotatingFileHandler
這個Handler和RotatingFileHandler類似,不過,它沒有通過判斷文件大小來決定何時重新創建日誌文件,而是間隔一定時間就 自動創建新的日誌文件。重命名的過程與RotatingFileHandler類似,不過新的文件不是附加數字,而是當前時間。它的構造函數是:
TimedRotatingFileHandler( filename [,when [,interval [,backupCount]]])
其中filename參數和backupCount參數和RotatingFileHandler具有相同的意義。
interval是時間間隔。
when參數是一個字符串。表示時間間隔的單位,不區分大小寫。它有以下取值:
S 秒
M 分
H 小時
D 天
W 每星期(interval==0時代表星期一)
midnight 每天凌晨
(5)logging.handlers.SocketHandler
(6)logging.handlers.DatagramHandler
以上兩個Handler類似,都是將日誌信息發送到網絡。不同的是前者使用TCP協議,後者使用UDP協議。它們的構造函數是:
Handler(host, port)
其中host是主機名,port是端口名
(7)logging.handlers.SysLogHandler
(8)logging.handlers.NTEventLogHandler
(9)logging.handlers.SMTPHandler
(10)logging.handlers.MemoryHandler
(11)logging.handlers.HTTPHandler


Configuration配置方法
logging的配置大致有下面幾種方式。
1.通過代碼進行完整配置,參考開頭的例子,主要是通過getLogger方法實現。
2.通過代碼進行簡單配置,下面有例子,主要是通過basicConfig方法實現。
3.通過配置文件,下面有例子,主要是通過 logging.config.fileConfig(filepath)
logging.basicConfig
basicConfig()提供了非常便捷的方式讓你配置logging模塊並馬上開始使用,可以參考下面的例子。

import logging
 
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
 
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
 
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')
備註:其實你甚至可以什麼都不配置直接使用默認值在控制檯中打log,用這樣的方式替換print語句對日後項目維護會有很大幫助。


通過文件配置logging

如果你希望通過配置文件來管理logging,可以參考這個官方文檔。在log4net或者log4j中這是很常見的方式。

# logging.conf
[loggers]
keys=root
 
[logger_root]
level=DEBUG
handlers=consoleHandler
#,timedRotateFileHandler,errorTimedRotateFileHandler
 
#################################################
[handlers]
keys=consoleHandler,timedRotateFileHandler,errorTimedRotateFileHandler
 
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
 
[handler_timedRotateFileHandler]
class=handlers.TimedRotatingFileHandler
level=DEBUG
formatter=simpleFormatter
args=('debug.log', 'H')
 
[handler_errorTimedRotateFileHandler]
class=handlers.TimedRotatingFileHandler
level=WARN
formatter=simpleFormatter
args=('error.log', 'H')
 
#################################################
[formatters]
keys=simpleFormatter, multiLineFormatter
 
[formatter_simpleFormatter]
format= %(levelname)s %(threadName)s %(asctime)s:   %(message)s
datefmt=%H:%M:%S
 
[formatter_multiLineFormatter]
format= ------------------------- %(levelname)s -------------------------
 Time:      %(asctime)s
 Thread:    %(threadName)s
 File:      %(filename)s(line %(lineno)d)
 Message:
 %(message)s
 
datefmt=%Y-%m-%d %H:%M:%S
假設以上的配置文件放在和模塊相同的目錄,代碼中的調用如下。

import os
filepath = os.path.join(os.path.dirname(__file__), 'logging.conf')
logging.config.fileConfig(filepath)
return logging.getLogger()
下面的代碼展示了logging最基本的用法:
例子1:
# -*- coding: utf-8 -*-

import logging
import sys

# 獲取logger實例,如果參數爲空則返回root logger
logger = logging.getLogger("AppName")

# 指定logger輸出格式
formatter = logging.Formatter('%(asctime)s %(levelname)-8s: %(message)s')
 
# 文件日誌
file_handler = logging.FileHandler("test.log")
file_handler.setFormatter(formatter)  # 可以通過setFormatter指定輸出格式

# 控制檯日誌
console_handler = logging.StreamHandler(sys.stdout)
console_handler.formatter = formatter  # 也可以直接給formatter賦值

# 爲logger添加的日誌處理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 指定日誌的輸出級別,默認爲WARN級別
logger.setLevel(logging.INFO)

# 輸出不同級別的log
logger.debug('this is debug info')  #被忽略
logger.info('this is information')
logger.warn('this is warning message')  #寫成logger.warning也可以
logger.error('this is error message')
logger.fatal('this is fatal message, it is same as logger.critical')
logger.critical('this is critical message')

# 移除一些日誌處理器
logger.removeHandler(file_handler)
運行代碼:python log.py
輸出結果:
2018-01-03 11:48:19,714 INFO    : this is information
2018-01-03 11:48:19,714 WARNING : this is warning message
2018-01-03 11:48:19,714 ERROR   : this is error message
2018-01-03 11:48:19,714 CRITICAL: this is fatal message, it is same as logger.critical
2018-01-03 11:48:19,714 CRITICAL: this is critical message


例子2:

# -*- coding: utf-8 -*-

import logging
import random

class OddFilter(logging.Filter):
    def __init__(self):
        self.count = 0

    def filter(self, record):
        self.count += 1
        if record.args[0] & 1:
            record.count = self.count  # 給 record 增加了 count 屬性
            return True  # 爲 True 的記錄才輸出
        return False

root_logger = logging.getLogger()
logging.basicConfig(
    level=logging.NOTSET,
    format='%(asctime)s %(message)s (total: %(count)d)',
    datefmt='%a, %d %b %Y %H:%M:%S',
    filename='log.test',
    filemode='w'
)
root_logger.level = logging.ERROR
root_logger.addFilter(OddFilter())

for i in xrange(10):
    logging.error('number: %d', random.randint(0, 100))
查看生成的log.test文件:
Wed, 03 Jan 2018 13:08:27 number: 75 (total: 3)
Wed, 03 Jan 2018 13:08:27 number: 19 (total: 6)
Wed, 03 Jan 2018 13:08:27 number: 97 (total: 7)
Wed, 03 Jan 2018 13:08:27 number: 55 (total: 10)


除了這些基本用法,還有一些常見的小技巧可以分享一下:
格式化輸出日誌

service_name = "Booking"
logger.error('%s service is down!' % service_name)  # 使用python自帶的字符串格式化,不推薦
logger.error('%s service is down!', service_name)  # 使用logger的格式化,推薦
logger.error('%s service is %s!', service_name, 'down')  # 多參數格式化
logger.error('{} service is {}'.format(service_name, 'down')) # 使用format函數,推薦
 
# 2018-01-03 11:48:19,714 ERROR   : Booking service is down!
記錄異常信息
當你使用logging模塊記錄異常信息時,不需要傳入該異常對象,只要你直接調用logger.error() 或者 logger.exception()就可以將當前異常記錄下來。
try:
    1 / 0
except:
    # 等同於error級別,但是會額外記錄當前拋出的異常堆棧信息
    logger.exception('this is an exception message')
 
# 2018-01-03 11:48:19,714 ERROR   : this is an exception message
# Traceback (most recent call last):
#   File "D:/Git/py_labs/demo/use_logging.py", line 45, in 
#     1 / 0
# ZeroDivisionError: integer division or modulo by zero
logging配置要點
GetLogger()方法
這是最基本的入口,該方法參數可以爲空,默認的logger名稱是root,如果在同一個程序中一直都使用同名的logger,其實會拿到同一個實例,使用這個技巧就可以跨模塊調用同樣的logger來記錄日誌。
另外你也可以通過日誌名稱來區分同一程序的不同模塊,比如這個例子。
logger = logging.getLogger("App.UI")
logger = logging.getLogger("App.Service")


logging是線程安全的麼?
是的,handler內部使用了threading.RLock()來保證同一時間只有一個線程能夠輸出。
但是,在使用logging.FileHandler時,多進程同時寫一個日誌文件是不支持的


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

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

    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
我們觀察 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:

class SafeRotatingFileHandler(TimedRotatingFileHandler):
 def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False):
     TimedRotatingFileHandler.__init__(self, filename, when, interval, backupCount, encoding, delay, utc)
 """
 Override doRollover
 lines commanded by "##" is changed by cc
 """
 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.

     Override,   1. if dfn not exist then do rename
                 2. _open with "a" model
     """
     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):
     if not os.path.exists(dfn) and 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.mode = "a"
         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
    不要以爲代碼那麼長,其實修改部分就是 “##” 註釋的地方而已,其他都是照抄源代碼。這個類繼承了 TimedRotatingFileHandler 重寫了這個切分的過程。這個解決方案十分優雅,改換的地方非常少,也十分有效。但有網友提出,這裏有一處地方依然不完美,就是rename的那一步,如果就是這麼巧,同時兩個或者多個進程進入了 if 語句,先後開始 rename 那麼依然會發生刪除掉日誌的情況。確實這種情況確實會發生,由於切分文件一天才一次,正好切分的時候同時有兩個Handler在操作,又正好同時走到這裏,也是蠻巧的,但是爲了完美,可以加上一個文件鎖,if 之後加鎖,得到鎖之後再判斷一次,再進行rename這種方式就完美了。代碼就不貼了,涉及到鎖代碼,影響美觀。

解決方案2

我認爲最簡單有效的解決方案。重寫FileHandler類(這個類是所有寫入文件的Handler都需要繼承的TimedRotatingFileHandler 就是繼承的這個類;我們增加一些簡單的判斷和操作就可以。
我們的邏輯是這樣的:
1.判斷當前時間戳是否與指向的文件名是同一個時間
2.如果不是,則切換 指向的文件即可
結束,是不是很簡單的邏輯。
talking is cheap show me the code:

class SafeFileHandler(FileHandler):
 def __init__(self, filename, mode, encoding=None, delay=0):
     """
     Use the specified filename for streamed logging
     """
     if codecs is None:
         encoding = None
     FileHandler.__init__(self, filename, mode, encoding, delay)
     self.mode = mode
     self.encoding = encoding
     self.suffix = "%Y-%m-%d"
     self.suffix_time = ""

 def emit(self, record):
     """
     Emit a record.

     Always check time 
     """
     try:
         if self.check_baseFilename(record):
             self.build_baseFilename()
         FileHandler.emit(self, record)
     except (KeyboardInterrupt, SystemExit):
         raise
     except:
         self.handleError(record)

 def check_baseFilename(self, record):
     """
     Determine if builder should occur.

     record is not used, as we are just comparing times, 
     but it is needed so the method signatures are the same
     """
     timeTuple = time.localtime()

     if self.suffix_time != time.strftime(self.suffix, timeTuple) or not os.path.exists(self.baseFilename+'.'+self.suffix_time):
         return 1
     else:
         return 0
 def build_baseFilename(self):
     """
     do builder; in this case, 
     old time stamp is removed from filename and
     a new time stamp is append to the filename
     """
     if self.stream:
         self.stream.close()
         self.stream = None

     # remove old suffix
     if self.suffix_time != "":
         index = self.baseFilename.find("."+self.suffix_time)
         if index == -1:
             index = self.baseFilename.rfind(".")
         self.baseFilename = self.baseFilename[:index]

     # add new suffix
     currentTimeTuple = time.localtime()
     self.suffix_time = time.strftime(self.suffix, currentTimeTuple)
     self.baseFilename  = self.baseFilename + "." + self.suffix_time

     self.mode = 'a'
     if not self.delay:
         self.stream = self._open()
       check_baseFilename 就是執行邏輯1判斷;build_baseFilename 就是執行邏輯2換句柄。就這麼簡單完成了。
這種方案與之前不同的是,當前文件就是 current.log.2016-06-01 ,到了明天當前文件就是current.log.2016-06-02 沒有重命名的情況,也沒有刪除的情況。十分簡潔優雅。也能解決多進程的logging問題。


解決方案其他
      當然還有其他的解決方案,例如由一個logging進程統一打日誌,其他進程將所有的日誌內容打入logging進程管道由它來打理。還有將日誌打入網絡socket當中也是同樣的道理。


logging的流程是怎樣的?
這裏有張流程圖可以參考:https://docs.python.org/2/howto/logging.html#logging-flow

參考:
http://python.jobbole.com/81521/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
http://python.jobbole.com/86887/?utm_source=blog.jobbole.com&utm_medium=relatedPosts
http://python.jobbole.com/84092/
https://my.oschina.net/leejun2005/blog/126713
http://python.jobbole.com/87300/?utm_source=blog.jobbole.com&utm_medium=relatedPosts


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章