第 35 課 · 進階 · 19 分鐘

Python 日誌

用級別、處理器和格式記錄程式發生了什麼。

日誌級別處理器格式化器

八點零五分,收銀臺列印了六行,沒人知道哪一行才是重點。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 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← CSV 檔案HTTP 請求 →