我的Python是從使用開始的,因此很多Python基礎理論並不是很足,之前在使用Celery時,因爲日誌無法正常打印而排查了一天。所以,爲了節省時間,同時知其所以然,是時候系統梳理一下Python中日誌的使用方法了。
基礎
Python日誌位於logging包,其中僅包含兩個module。在不進行任何額外配置的情況下,可以按照如下方式使用
import logging
logging.warn("Hello World!!!")
輸出
WARNING:root:hello
接着來詳細瞭解其使用方式。
日誌級別
在很多Java日誌框架中,將日誌由嚴重要普通,依次爲 Fatal -> Error -> Warning -> Info -> Debug。在Python中,也一行,只不過Fatal不叫Fatal,叫Critical,所以是 Fatal -> Error -> Warning -> Info -> Debug。使用場景也是一樣,如下
DEBUG |
細節信息,僅當診斷問題時適用。 |
---|---|
INFO |
確認程序按預期運行 |
WARNING |
表明有已經或即將發生的意外(例如:磁盤空間不足)。程序仍按預期進行 |
ERROR |
由於嚴重的問題,程序的某些功能已經不能正常執行 |
CRITICAL |
嚴重的錯誤,表明程序已不能繼續執行 |
Python默認的日誌級別是Warning,默認輸出到控制檯。這意味着,默認情況下,Info和Debug日誌是不會輸出的。
小貼士:日誌的輸出級別,確切地說應該是日誌的輸出閾值,即指定級別及以下級別的日誌會輸出,其上的不會輸出。比如設置日誌級別爲Warning,則除Info、Debug外其它日誌都會輸出。
簡單配置
命令行
可以在使用命令啓動python時指定logging的執行級別
# 執行腳本hello.py,日誌級別設置爲INFO
python hello.py --log=INFO
basicConfig方法
一般來說,logging的配置都可以在logging.basicConfig()方法完成,如下展示了常用的配置
# 設置日誌輸出到文件,日誌級別設爲debug,默認爲追加模式
logging.basicConfig(filename='celery.log', level=logging.DEBUG)
# 輸出到文件,覆蓋模式
logging.basicConfig(filename='celery.log', filemode='w', level=logging.DEBUG)
小貼士:
logging.basicConfig
方法被設置爲只能調用一次,因此針對他的第一次調用是有效的,但以後的調用不會生效。這也是很多問題的發源地:你可能不自覺地多次調用了basicConfig()
日誌格式
還是在basicConfig()
中,我們可以配置日誌輸出格式,通過format參數進行設置
# 輸出格式爲asc時間+空格+消息內容
logging.basicConfig(format='%(asctime)s %(message)s')
日誌格式中可以詳細設置的內容可以在這裏找到,我們列出幾個看似很有用的
屬性名 | 使用格式 | 作用 |
---|---|---|
asctime | %(asctime)s |
日誌輸出時間,對閱讀友好的 |
filename | %(filename)s |
輸出日誌的文件 |
funcName | %(funcName)s |
輸出日誌的方法 |
levelname | %(levelname)s |
級別名稱,如DEBUG、LEVEL |
lineno | %(lineno)d |
輸出日誌的源碼行號 |
message | %(message)s |
輸出的消息 |
process | %(process)d |
進程ID |
thread | %(thread)d |
線程ID |
原理
上面所介紹的內容僅解決了記錄的問題,並不能滿足許多實際項目的需求。比如希望針對不同的日誌級別將日誌輸出到不同的位置、當出現ERROR日誌時通過郵件報警、自動記錄ERROR log的個數等。要做到這些需求,我們需要更加深入Python日誌系統,瞭解其基本工作原理。
這裏有一點忌諱的是,在不瞭解Python日誌基本構成的情況下,直接使用網上搜索的方式進行復雜的日誌配置。這樣成功的概率不高,且出問題時往往面臨束手無策的尷尬境地。
Python日誌庫採用模塊化方法,通過幾類組件協同工作,完成日誌從發起到輸出終點的全流程。如下
- Logger - 記錄器:用戶暴露接口給用戶直接調用。它產生LogRecord對象傳遞給下一個組件。
- Handler - 處理器:處理Logger產生的LogRecord
- Filter - 過濾器:更加精確地控制日誌的輸出
- Formatter - 格式化器:用戶將日誌處理成最終輸出的樣子
其中,Logger是層次結構的,有父子關係。幾個組件的協同處理流程如下圖所示
Logger
Logger類,即記錄器,用於暴露接口給用戶調用,其簡單創建和使用方法如下
import logging
logger = logging.getLogger(__name__)
logger.warning('Hello, World!!!')
創建Logger使用logging.getLogger('name')
方法,當Logger實例已存在,則直接返回,不存在則創建。
Logger具有層次結構,主要通過名稱體現,以.分隔。如名爲root的Logger實例是名爲root.hello、root.world的Logger實例的父級。
層級結構的好處在於減少配置的冗餘,以及消息的冒泡傳遞:在子Logger沒有聲明級別的情況下,使用父Logger的級別,以此類推,如果都沒有設置級別,則會使用root的Warning級別;日誌輸出時,除了執行當前Logger的所有邏輯外,還可以傳遞給父Logger,如上圖所示。
接口方面,Logger主要提供配置和消息創建兩類接口
-
配置
- Logger.setLevel() 設置當前Logger的日誌級別
- Logger.addHandler()/removeHandler() 添加/刪除處理器
- Logger.addFilter()/removeFilter 添加/刪除過濾器
-
創建消息
- Logger.debug()/info()/warning()/error()/critical() 創建不同級別的日誌
- Logger.exception() 輸出ERROR級別的日誌,不同的是還附帶堆棧信息
- Logger.log() 顯式自定義日誌輸出級別並創建日誌信息
Handler
Handler類,即處理器。決定如何處理Logger發送的消息對象,即將消息對象發送到哪個目標
Handler暴露給用戶的接口如下
- Handler.setLevel() 設置當前Handler的日誌級別。這意味着,Logger能夠處理的日誌級別和Handler的日誌級別可以不同,即Handler可以選擇性處理日誌消息
- Handler.setFormatter() 指定格式化器
- Handler.addFilter()/revmoFilter() 添加/刪除過濾器
Python內建了很多處理器,如下。通過這些Handler,可將日誌輸出到文件、內存、郵件、網絡
Filter
Filter類,即過濾器。Python只定義了一個Filter基類,該類構造器接收一個name參數,其過濾邏輯爲:僅允許與Logger同級或子級的LogRecord通過,其餘被濾出。如Filter名爲A.B,則名稱爲A.B、A.B.C、A.B.D的Logger創建的LogRecord均可通過,A.F的Logger創建的LogRecord將被濾出。
filter = logging.Filter('hello')
要實現自定義的Filter,繼承logging.Filter
類,並重寫其filter()
方法即可
Formatter
Formatter類,即格式化器。決定了日誌輸出格式,用戶只需要創建它並扔給Handler就好。
formatter = logging.Formatter(fmt='%(filename)s %(funcName)s %(message)s', datefmt=None, style='%')
參數解釋如下
-
fmt - 格式設置,和基礎部分一樣。能夠輸出LogRecord對象的所有屬性。如果臨時忘了,在Formatter類定義註釋中,有詳細的說明。
-
datefmt - 指定日期的格式化,舉例
'%m/%d/%Y %I:%M:%S %p'
-
style - 格式化風格,%和$二選一,兩個風格區別如下。
# %風格 "%s %s" % ('hello', 'world') # $風格 "{} {}".format('hello', 'world')
LogRecord
LogRecord類,用戶每調用一次Logger的輸出接口,就會創建一個LogRecord類實例,該實例作爲在日誌系統各組件的最小單位。基於其重要性,這裏單獨提了出來。
配置
Python提供了三種配置日誌的方式
- 使用各組件提供的配置方法手動配置
- 按照指定格式創建配置文件,然後使用
logging.config.fileConfig()
讀取 - 按照指定格式創建配置字典,然後使用
logging.config.dictConfig()
讀取
下面直接搬運官方文檔給出的三種方式
代碼配置
import logging
# create logger
logger = logging.getLogger('simpleExample')
logger.setLevel(logging.DEBUG)
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# add formatter to ch
ch.setFormatter(formatter)
# add ch to logger
logger.addHandler(ch)
文件配置
[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
文件配置內容格式具有自解釋作用,這裏不做講解。
唯一需要注意的是,在指定處理器的class時,類名稱是相對於Python的日誌記錄模塊(注意是Python的日誌模塊,即logging的位置),或按照導入機制能夠正常導入的絕對import路徑。
如StreamHandler,因它在logging模塊中定義,因此可以直接寫StreamHandler即可。
再如com.github.zou.HelloHandler,屬於自定義,因此需要全路徑。
import logging
import logging.config
# 上面的文件名爲logging.conf
logging.config.fileConfig('logging.conf')
# create logger
logger = logging.getLogger('simpleExample')
dict配置
version: 1
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
loggers:
simpleExample:
level: DEBUG
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]
import logging
import logging.config
import yaml
# 將yaml文件讀取爲dict,再進行配置
config = yaml.load(open('logging.yml'), Loader=yaml.FullLoader)
logging.config.dictConfig('logging.yml')
# create logger
logger = logging.getLogger('simpleExample')
有坑
配置覆蓋問題
使用fileConfig()
時,帶有默認參數disable_existing_logger=True
,因此之前的除root之外的所有Logger全都會被禁用,這一點要非常注意。如果不希望如此,可手動將其設置爲False
def fileConfig(fname, defaults=None, disable_existing_loggers=True):
"""
Read the logging configuration from a ConfigParser-format file.
This can be called several times from an application, allowing an end user
the ability to select from various pre-canned configurations (if the
developer provides a mechanism to present the choices and load the chosen
configuration).
"""
. . . . . .
無配置注意事項
如果對應的Logger沒有進行配置,會出現找不到對應處理器來處理消息的情況。根據版本不同會有如下表現
Python版本<3.2時
- 若logging.raiseException爲False,則靜默丟棄消息
- 若logging.raiseException爲True,則打印“無法找到記錄器的處理程序”
Python版本>=3.2時
- 使用logging.lastResort中存儲的處理器進行輸出。該處理器與任何Logger都沒有關聯,直接將描述信息寫入sys.sterr,並且信息不會被格式化,輸出級別爲Warning
- 如果我們將logging.lastResort手動設置爲None,其表現將和3.2之前的版本一致。