使用python作日誌輸出時,首先咱們須要一個建立一個Logger
對象:import logging; logger = logging.getLogger()
。而後,咱們就能夠用logger.info/debug/error(msg)
來輸出日誌了。python
若是隻是單純地打印日誌,這樣作和醜陋的print沒有任何區別。咱們指望log能有必定的格式,這時你就會用到logging.Formatter
;咱們還但願日誌不只在console中輸出,還要向文件輸出;這樣你須要給咱們的logger
添加handler
,一個handler
指向標準輸出流,一個handler
指向文件handler
。logging.handlers
提供了一些這些經常使用的handler
。git
而後,你但願對這些不一樣的輸出流進行精準的控制,好比:在console中只輸出某些高級別的日誌,而在文件日誌中輸出全部日誌。在console中,使用一種輸出formatter
,在文件輸出中使用另外一種formatter
。你不知足於python
提供的DEBUG/INFO/WARNING/ERROR/CRITICAL
的控制粒度,想要更精細地控制日誌。你就須要理解日誌是如何流轉、繼承地。github
這是python官方提供的一張log輸出圖
框架
也就是說,咱們有能夠從以下幾個層面控制日誌的輸出:google
此外,咱們要注意到日誌的輸出是流式的,只要有一個地方日誌被過濾掉了,他就不能輸出了。再設計多級別的日誌系統時,尤爲要注意這點。若是咱們要設置過濾條件,要在上圖所示的日誌流中,逐漸提升level
級別。url
python
日誌支持的默認字段比較少:spa
其實Filter隱含了一個比較dirty的接口,讓你可以修改logRecord
的屬性。讓你可以給日誌增長一個新的字段。代碼以下:debug
class ContextFilter(logging.Filter): hostname = os.getenv('HOSTNAME') def filter(self, record): record.hostname = self.hostname return True
將ContextFilter
添加到你的某個handler
,而後給這個handler
增長一個這樣的formatter
: [%(asctime)s] [%(levelname)s] [HOSTNAME: %(hostname)s] %(message)s
;這個handler
就能夠輸出主機名了。固然,這樣作實際上是有悖於fileter
的本意的。不過我尚未找到更好的辦法。設計
python
的日誌粒度以下:日誌
若是咱們向定義一個比debug
級別更低的日誌怎麼辦呢?代碼以下:
VERBOSE_LOG_LEVEL = 5 def VERBOSE(self, message, *args, **kwargs): if self.isEnabledFor(VERBOSE_LOG_LEVEL): self._log(VERBOSE_LOG_LEVEL, message, args, **kwargs) logging.Logger.VERB = VERBOSE
這樣咱們就定義了一個級別爲5
的輸出。這樣作的好處是,好比有些特別瑣碎的、系統級別的輸出,你不但願框架使用者看到,而只是做爲日誌分析用。你能夠定義一個很是低的日誌級別。而後把絕大多數的handler
的控制級別設置的都比5
高,只留一個接口給日誌收集者。這樣,就能夠大大提高框架使用者的體驗。
偶然的緣由復現了別人的一個bug.
觸發錯誤的代碼很簡單:
import requests import logging logger = logging.getLogger() logStdOut = logging.StreamHandler() LOGFORMATCNSL=logging.Formatter("%(asctime)s %(message)s %(aVar)s %(bVar)s") logStdOut.setFormatter(LOGFORMATCNSL) logStdOut.setLevel(logging.DEBUG) logger.setLevel(logging.NOTSET) logger.addHandler(logStdOut) def tryThis(): logger.error("deneme", extra={"aVar": "aVal", "bVar": "bVal"}) conn = requests.get("http://www.google.com") conn.close() tryThis()
錯誤最終定位到了在urllib3/connectionpool.py
下的日誌打印命令
log.debug("%s://%s:%s \"%s %s %s\" %s %s", self.scheme, self.host, self.port, method, url, http_version, httplib_response.status, httplib_response.length)
經過debug
模式,咱們能夠看到,在這裏,RootLogger
被賦予了一個formatter
, '%(asctime)s %(message)s %(aVar)s %(bVar)s'
。
RootLogger
是什麼?是一個python程序內全局惟一的,全部Logger
對象的祖先。它是怎麼產生的呢?
logger = logging.getLogger()
,這個logger
就是RootLogger
。咱們對logger
的設定,天然會影響到全部的日誌輸出。
所以,直接修改RootLogger
是危險的。而若是咱們給getLogger
傳一個參數,它會生成一個非root
的Logger
。問題就解決了。
修正後的代碼以下:
import requests import logging # code with out bug logger = logging.getLogger("__abc__") # code will trigger the keyError bug # logger = logging.getLogger() logStdOut = logging.StreamHandler() print(isinstance(logger, logging.RootLogger)) LOGFORMATCNSL=logging.Formatter("%(asctime)s %(message)s %(aVar)s %(bVar)s") logStdOut.setFormatter(LOGFORMATCNSL) logStdOut.setLevel(logging.DEBUG) logger.setLevel(logging.NOTSET) logger.addHandler(logStdOut) def tryThis(): logger.error("deneme", extra={"aVar": "aVal", "bVar": "bVal"}) conn = requests.get("http://www.baidu.com") conn.close() tryThis()