Python logging 模塊
給大家剖析下Python的logging模塊。
logging模塊是Python內置的一個強大易用的日誌模塊。簡單到你只需要兩行代碼就能輸出一些東西來:
import logging
logging.warning('Hi, I print something.')
輸出:
WARNING:root:Hi, I print something.
當然,我們可不能滿足於此,你也很容易發現把上面 warning
改成 info
就輸不出來了。別急,往下看。
1. 先大體概括下logging的結構
- 整體上來說,日誌管理最大的結構是Manager,Manager管理所有的Logger,但你不用去管這個東西,它是藏在背後默默幹活的。
- Logger裏最根部是一個RootLogger,name是root,其他所有Logger都是在這個Logger下,通過名字,形成父子、子孫的樹杈結構。
- Logger裏面有一些東西,Filter、Handler、LogRecord、Formatter,可以看作是一些配置,各自有各自的作用,接下來會介紹。
- 還能用LoggerAdapter對Logger進行一定的包裝,一般我們用的很少。
logger是一個樹結構,默認有個根root,其他logger都是其上的枝椏,比如創建一個name=’A.B’的logger,其實際結構就是 root-A-B
再創建一個name=’A.C.D’的logger,結構變爲:
2. level —— 輸出哪個級別的日誌
在輸出log的時候,不能所有日誌都輸出出來,而是有所選擇,比如有時候我們只希望看到warning以上嚴重程度的,如果有太多info、debug會讓log可讀性變得很差。
我們可以通過設置logger的level來實現這一點。在logging中,將level設置如下:
LevelName | LevelValue |
---|---|
CRITICAL/ FATAL | 50 |
ERROR | 40 |
WARNING/ |
30 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
示例:
import logging
logger = logging.getLogger('test')
logging.basicConfig() # basicConfig是logging提供的簡單的配置方法,不用basicConfig則需要手動添加handler
logger.setLevel(logging.INFO) # 輸出所有大於等於INFO級別的log
logger.info('I am <info> message.')
logger.debug('I am <debug> message.') # 不輸出
結果正如所料,info輸出,debug未輸出。
INFO:test:I am <info> message.
要注意的是: root 的默認級別是 WARNING!, 而且logger實際輸出時的level是取決於EffectiveLevel,即從該級往上走,遇到的第一個level不爲0的logger的level,也就是說如果你創建了logger,而沒有爲其設置level,那它默認是NOTSET,程序會往上層找,直到root,而root級別是WARNING,所以可能會導致沒有輸出日誌。
3. handler —— 輸出到哪裏
我們寫日誌一個很重要的問題就是把日誌輸出到什麼地方去,我們可能希望某些日誌在console打印出來,可能希望有更詳細的日誌輸出到log文件裏去。怎麼控制這些輸出就需要用handler了。
import logging
logger = logging.getLogger('test')
logger.addHandler(logging.StreamHandler()) # 添加StreamHandler
logger.setLevel(logging.INFO) # 輸出所有大於INFO級別的log
logger.info('I am <info> message.')
logger.debug('I am <debug> message.') # 不輸出
我們把上面的例子稍微改動了一下,可以看到輸出如下,輸出到了console裏
I am <info> message.
這就是logging提供的最基本的一個handler,其他各種handler都是從這個handler繼承發展來的。理論上可以把日誌輸出到各種流中,stderr、文件、socket等都可以。當然logging已經將各種流handler封裝好了。
如:
import logging
logger = logging.getLogger('test')
logger.addHandler(logging.StreamHandler())
logger.addHandler(logging.FileHandler('test.log')) # 再添一個FileHandler
logger.setLevel(logging.INFO) # 輸出所有大於INFO級別的log
logger.info('I am <info> message.')
logger.debug('I am <debug> message.') # 不輸出
可以看到,info不僅僅輸出到了console中,還在當前文件夾下創建了一個test.log文件並輸出到了該文件中。在logging.handlers中還封裝了一堆更高級的handlers,可以瞭解下,尤其是RotatingFileHandler和TimedRotatingFileHandler,可以把你的日誌按一定規則分割成多份。你也可以自己封裝handler哦,網上有人這麼幹的。
上面我們看到了logger的級別,可以控制這個logger要輸出什麼級別的log。但這裏我們發現可以在logger裏添加handler,控制輸出log到哪裏,明顯發現,其實我們想要在不同的handler裏輸出不同級別的日誌。
比如我們想要在console裏輸出warning以上的日誌,在log文件裏輸出debug以上的日誌,該怎麼辦呢?
handler也是有級別的。
如下:
import logging
logger = logging.getLogger('test')
logger.setLevel(logging.INFO) # 輸出所有大於INFO級別的log
# 添加StreamHandler,並設置級別爲WARNING
stream_hdl = logging.StreamHandler()
stream_hdl.setLevel(logging.WARNING)
logger.addHandler(stream_hdl)
# 添加FileHandler,並設置級別爲DEBUG
file_hdl = logging.FileHandler('test.log')
file_hdl.setLevel(logging.DEBUG)
logger.addHandler(file_hdl)
logger.info('I am <info> message.')
logger.debug('I am <debug> message.') # 不輸出
設置了WARNING級別的StreamHandler以及DEBUG級別的FileHandler。在logger.info()執行時,程序判斷log級別(info) >= logger級別(INFO),可以輸出。
然後程序判斷log級別(info) < StreamHandler級別(WARNING),不在stderr輸出;同時log級別(info) > FileHandler級別(DEBUG),在test.log中輸出。
而logger.debug()的級別 < logger級別(INFO),所以不會在stderr和test.log輸出。
其實上面的程序有個問題,就是設置了logger是INFO,那麼FileHandler是DEBUG沒有什麼意義的,DEBUG級別的日誌過不了logger那關。
你在寫代碼的時候就需要注意了,要給logger添加handler,並且注意logger和handler的level,否則可能會輸不出你想要的log哦。
4. formatter —— 輸出日誌的格式
細心的話可以發現,我們後來自己添加的handler輸出的log是沒有格式的,就僅僅是輸出而已。但basicConfig()輸出的log是有格式的(雖然很醜)。
不同在於basicConfig()中的handler是帶有formatter的。我們要添加formatter就需要用到logging中的另一個類Formatter
import logging
logger = logging.getLogger('test')
logger.setLevel(logging.INFO) # 輸出所有大於INFO級別的log
fmt = logging.Formatter('%(name)s - %(levelname)s - %(asctime)s - %(message)s')
# 添加StreamHandler,並設置級別爲WARNING
stream_hdl = logging.StreamHandler()
stream_hdl.setLevel(logging.WARNING)
stream_hdl.setFormatter(fmt)
logger.addHandler(stream_hdl)
# 添加FileHandler,並設置級別爲DEBUG
file_hdl = logging.FileHandler('test.log')
file_hdl.setLevel(logging.DEBUG)
file_hdl.setFormatter(fmt)
logger.addHandler(file_hdl)
logger.info('I am <info> message.')
logger.debug('I am <debug> message.') # 不輸出
如上例中,我們添加了formatter,格式爲 (logger name) - (level name) - (當前時間) - (日誌信息)
,輸出的結果爲:
test - INFO - 2017-09-06 16:45:36,977 - I am <info> message.
是不是漂亮多了,logging的formatter可以輸出的不止這幾個信息,還有很多:
- %(name)s ——Name of the logger (logging channel)
- %(levelno)s ——Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- %(levelname)s ——Text logging level for the message (“DEBUG”, “INFO”, “WARNING”, “ERROR”, “CRITICAL”)
- %(pathname)s ——Full pathname of the source file where the logging call was issued (if available)
- %(filename)s ——Filename portion of pathname
- %(module)s ——Module (name portion of filename)
- %(lineno)d ——Source line number where the logging call was issued(if available)
- %(funcName)s ——Function name
- %(created)f ——Time when the LogRecord was created (time.time() return value)
- %(asctime)s ——Textual time when the LogRecord was created
- %(msecs)d ——Millisecond portion of the creation time
- %(relativeCreated)d ——Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded (typically at application startup time)
- %(thread)d ——Thread ID (if available)
- %(threadName)s ——Thread name (if available)
- %(process)d ——Process ID (if available)
- %(message)s ——The result of record.getMessage(), computed just as the record is emitted
我們也可以給Formatter傳入第二個參數,修改日期格式,比如 %Y-%m-%d
,則輸出的log就沒有時分秒了。
這樣就可以定製log的格式了,還可以爲不同的handler制定不同的formatter,以適應不同情況下的閱讀或傳輸需求。
5. Filter和LoggerAdapter
最後這裏有兩個我們不常用到的東西,懶得多寫了。
6. 重複寫日誌的問題
這裏有一個坑需要特別說明下,就是add handler的問題。一個logger創建之後不要重複添加handler,有時候我們多個文件都調用我們封裝好的logger的類,就有可能會發生重複添加handler的情況,具體的看博主這篇文章——python logging 重複寫日誌問題
7. 差點忘了
差點忘了配置的問題,我們可以通過多種途徑配置logger,像上面用過的basicConfig()(沒有用參數,可以自己閱讀源碼,很簡單的幾個參數),或者像上面的例子手動添加handler、設置formatter,logging還給了我們其他高級的配置方法:
logging.config裏有多種配置方法,像fileConfig,可以直接讀一個文件;像dictConfig,可以傳一個dict進行配置(django就是這麼配置的);似乎還能起個socket,來實時修改config,聽起來很吊的樣子。
不過我們用其中的一種就可以了,隨便配嘍。
比如,像博主這樣 怎樣從0開始搭建一個測試框架_2
就說這些,跟其他人的講法好像不太一樣,結合着來看吧。
有什麼好的建議或者問題,可以留言或者加我的QQ羣:455478219討論。