天天看點

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有以下幾個級别:

Python子產品學習--logging

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