八點零五分,收銀臺列印了六行,沒人知道哪一行才是重點。print 只說有事發生,logging 會說何時、何地、多嚴重。級別到底過濾掉了什麼?
print 適合快速檢查,但 logging 多了嚴重級別、輸出目的地和統一格式。日誌器把記錄交給處理器,每個處理器都可以格式化或過濾。小而明確的設定比全局日誌設定更容易測試。 你還會添加上下文、過濾敏感記錄,並安全捕獲 traceback。
把記錄交給處理器
日誌器會發出達到或高於自身級別的記錄,處理器決定它們去哪裡。這裡用 StringIO 捕獲輸出,讓範例自成一體,只在最後列印到標準輸出。
import logging
from io import StringIO
stream = StringIO()
handler = logging.StreamHandler(stream)
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.info("started")
print(stream.getvalue().strip())帶目的地的日誌器
用級別過濾
級別是門檻值。設為 WARNING 時,info 記錄會被忽略,warning 和 error 記錄會通過。這樣開發時可以保留診斷細節,生產環境只顯示重要訊息。
import logging
from io import StringIO
stream = StringIO()
handler = logging.StreamHandler(stream)
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.WARNING)
logger.propagate = False
logger.info("hidden")
logger.warning("shown")
print(stream.getvalue().strip())只顯示警告及以上
格式化訊息
格式化器把記錄變成文字。格式字串裡可以使用 levelname、name 和 message 等欄位。把格式化器附加到擁有輸出目的地的處理器上。
import logging
from io import StringIO
stream = StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
logger = logging.getLogger("demo")
logger.handlers[:] = [handler]
logger.setLevel(logging.INFO)
logger.propagate = False
logger.warning("check")
print(stream.getvalue().strip())可預測的單行格式
用 LoggerAdapter 添加上下文
LoggerAdapter 會給每條記錄加上同一份上下文,例如請求 ID。只有把該欄位寫進 formatter,值才會真正顯示。配接器只是小包裝,不會建立第二套 logger 層級。
import io
import logging
stream = io.StringIO()
logger = logging.getLogger("gift.request")
logger.handlers[:] = [logging.StreamHandler(stream)]
handler = logger.handlers[0]
handler.setFormatter(logging.Formatter("%(request_id)s %(message)s"))
logger.setLevel(logging.INFO)
logger.propagate = False
adapter = logging.LoggerAdapter(logger, {"request_id": "r-17"})
adapter.info("loaded %s", "menu")
print(stream.getvalue().strip())一條記錄,一個請求 ID
讓記錄在離開前被過濾
Filter 可以在任何 handler 格式化之前拒絕記錄。它適合做策略檢查,但不能代替「絕不把秘密寫進日誌呼叫」。這裡的過濾器丟掉提到 password 的訊息,安全記錄仍然能到達 handler。
import io
import logging
class NoSecrets(logging.Filter):
def filter(self, record):
return "password" not in record.getMessage().lower()
stream = io.StringIO()
logger = logging.getLogger("gift.safe")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.handlers[0].addFilter(NoSecrets())
logger.setLevel(logging.INFO)
logger.propagate = False
logger.info("user %s", "maya")
logger.info("password=%s", "hunter2")
print(repr(stream.getvalue()))敏感記錄提前丟掉
不寫到 stderr 也能捕獲例外
logger.exception() 會記錄訊息和目前 traceback。測試時把 handler 指向 StringIO,traceback 就不會漏到 stderr。生產環境裡,別把 traceback 直接回傳給使用者,記錄前也要清理敏感值。
import io
import logging
stream = io.StringIO()
logger = logging.getLogger("gift.checkout")
logger.handlers[:] = [logging.StreamHandler(stream)]
logger.setLevel(logging.INFO)
logger.propagate = False
try:
1 / 0
except ZeroDivisionError:
logger.exception("checkout failed")
text = stream.getvalue()
print(text.splitlines()[0])
print("ZeroDivisionError" in text)把 traceback 收進受控流
本課要點
- 日誌器發出記錄,處理器決定記錄去哪裡。
- 日誌器級別是記錄的門檻值。
- 處理器可以有自己的級別和格式化器。
- propagate=False 可以避免示範日誌器同時使用根日誌器。
- StringIO 讓日誌範例確定且容易測試。
- 配接器補上下文,過濾器執行策略,exception() 捕獲 traceback。
常見錯誤
- 還沒有設定任何處理器就呼叫 logging.info,然後疑惑輸出去了哪裡。
- 在庫模組裡設定根日誌器。
- 忘記記錄通過日誌器後,還可能被更低級別的處理器過濾。
- 以為 logging 預設靜默:記錄去哪裡由 handler 和 propagate 決定。
建立 INFO 級別的日誌器,附加 StringIO 處理器,記錄一條訊息,再列印捕獲的文字。
動手試試
改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。