範例專案地址: https://github.com/Byte-Biscuit/python-logging-inspect
本文旨在深入解析 Python 內建 logging 模組的核心設計理念,特別是 Logger 的層級結構、屬性繼承機制,以及如何實現對日誌的精準控制。
在你閱讀完本文後,能對 Python 內建 logging 模組如何使用有一個清晰的理解,如果文中出現錯誤,請您留下建議,謝謝。
名詞速查:核心術語一覽
| 術語 | 一句話解釋 |
|---|
| Logger | 你在程式碼裡呼叫的物件,負責「發出」日誌記錄 |
| Handler | 決定日誌「輸出到哪裡」(主控台 / 檔案 / 網路等) |
| Formatter | 決定日誌「長什麼樣」(時間、級別、內容等格式) |
| Filter | 可選,決定某條日誌記錄「要不要被處理」 |
四者關係:Logger 產生記錄 → Handler 決定去處 → Formatter 決定格式 → Filter 可在任意層攔截。
在開發本機專案並準備將其作為第三方套件發布時,開發者最擔憂的問題是:我的日誌設定會不會影響到使用我套件的其他專案?
0.1 擔憂的體現與解決思路
- 擔憂:強制輸出日誌,污染宿主專案的主控台或檔案。
- 解決:作為套件(Library),絕對不要在模組級別呼叫 logging.basicConfig() 或加入 StreamHandler/FileHandler。
- 在範例中的體現:在 inspect.py 中,setup_logging()(設定 Handler 和 Formatter 的操作)被嚴格限制在 if __name__ == "__main__": 區塊內。這意味著,如果其他專案 import logging_inspect,這些設定程式碼不會執行,從而保證了「零侵入」。
- 擔憂:宿主專案無法區分或控制我套件裡的日誌。
- 解決:套件內部的所有日誌記錄器必須使用統一的命名空間(通常是套件名稱)。
- 在範例中的體現:所有子模組都使用 logger = logging.getLogger(__name__)。這樣,宿主專案只需透過 logging.getLogger("logging_inspect").setLevel(logging.WARNING),就能一鍵控制你整個套件的日誌級別,而不會影響宿主專案自身的日誌。
- 最佳實踐:加入 NullHandler
1. 核心概念:Logger 樹狀層級結構
Python 的 logging 模組採用了一種基於點號分隔命名的樹狀層級結構來管理 Logger。
1.0 樹的根節點:Root Logger
在 Python 的日誌系統中,存在一個特殊的、預設的 Logger,它的名字是 root。
- 全域控制:它是所有其他 Logger 的最終祖先。如果你沒有為子 Logger 設定級別(即 NOTSET),它們最終會繼承 Root Logger 的級別(預設是 WARNING)。
- 取得方式:
- logging.getLogger()(不傳參數)回傳的就是 Root Logger。
- logging.root 也可以直接存取它。
- 快捷方法:你平時直接呼叫的 logging.info("msg")、logging.warning("msg"),實際上就是在呼叫 Root Logger 的方法。
- 全域設定:logging.basicConfig() 預設就是為 Root Logger 設定 Handler 和 Formatter。
1.1 __name__ 的妙用
在 Python 模組中,推薦使用以下方式取得 Logger:
PYTHON
- 原理:__name__ 是當前模組的完整路徑(例如 logging_inspect.level1.level2.log_inspect_level2)。
- 優勢:這會自動建構一個與你的專案套件/模組結構完全一致的 Logger 樹。
1.2 層級關係圖解
正在渲染 Mermaid 圖表...
每個節點名稱就是對應模組中 __name__ 的實際值,也是呼叫 logging.getLogger(__name__) 時所用的 key。
- logging_inspect.level1.level2 是 logging_inspect.level1.level2.log_inspect_level2 的父級。
- 這種層級關係是實現日誌繼承和統一控制的基礎。
2. 屬性繼承與控制機制
理解 Logger 的屬性如何在層級間傳遞,是實現精準控制的關鍵。
2.1 disabled 屬性:不繼承
- 作用:直接禁用某個具體的 Logger,使其不再處理任何日誌記錄。
- 特性:不繼承。禁用父級 Logger,不會影響子級 Logger 的正常工作。
- 原始碼位置參考:logging.Logger.handle 方法中會檢查 self.disabled。
- 應用場景:當你只想關閉某一個特定檔案(模組)的日誌時。
PYTHON
2.2 level 屬性(日誌級別):繼承
- 作用:決定 Logger 處理哪種嚴重程度及以上的日誌(DEBUG < INFO < WARNING < ERROR < CRITICAL)。
- 特性:繼承。如果一個 Logger 的級別是 NOTSET(預設值 0),它會沿著樹狀結構向上查找,直到找到一個顯式設定了級別的祖先 Logger,並使用該級別。
- 原始碼位置參考:logging.Logger.getEffectiveLevel 方法實現了向上查找邏輯。
- 應用場景:當你希望統一控制某個套件及其所有子模組的日誌級別時。
PYTHON
2.3 propagate 屬性與 Handler:向上傳遞
- 作用:決定子 Logger 產生的日誌記錄是否要傳遞給父級 Logger 的 Handler 進行輸出。
- 特性:預設值為 True。這意味著子模組的日誌不僅會被自己的 Handler 處理(如果有),還會一直向上傳遞,被父級、祖父級的 Handler 處理。
- 原始碼位置參考:logging.Logger.callHandlers 方法中透過 c.propagate 控制迴圈。
- 應用場景:當你希望某個子模組的日誌輸出到獨立檔案,且不再輸出到主控台時。
PYTHON
propagate 日誌傳遞流向圖(propagate=True 時的完整鏈路):
正在渲染 Mermaid 圖表...
若將 level2_logger.propagate = False,則 level2 → level1 的箭頭斷開,日誌只寫入檔案,不再出現在主控台。
3. 原始碼級原理解析 (基於 CPython 3.12)
3.1 getLogger 的單例模式
logging.getLogger(name) 實際上是從一個全域的 Manager 物件中取得 Logger。如果該名稱的 Logger 不存在,則建立並快取;如果存在,則直接回傳。這保證了在不同檔案中使用相同名稱取得的是同一個 Logger 實例。
(參考原始碼:logging.__init__.py 中的 Manager.getLogger)
3.2 日誌處理流程 (Logger.handle)
當呼叫 logger.info("msg") 時,核心流程如下:
- 級別檢查:呼叫 isEnabledFor(level),內部呼叫 getEffectiveLevel() 向上查找有效級別。如果當前日誌級別低於有效級別,直接丟棄。
- 建立 LogRecord:將日誌資訊封裝為 LogRecord 物件。
- 處理記錄:呼叫 handle(record)。
- 檢查 self.disabled,如果為 True,直接回傳。
- 呼叫 callHandlers(record)。
- 呼叫 Handlers (callHandlers):
- 遍歷當前 Logger 的所有 Handler,呼叫它們的 handle 方法。
- 檢查 self.propagate。如果為 True,則將當前 Logger 切換為其父級 Logger,重複上述 Handler 呼叫過程,直到遇到 propagate=False 或到達 Root Logger。
(參考原始碼:logging.__init__.py 中的 Logger.handle 和 Logger.callHandlers)
4. 應用實踐落地:具體實現步驟
了解了上述原理後,在實際專案中(無論是開發應用還是套件),我們應該如何落地?以下是一個標準的實踐步驟:
步驟 1:在所有模組中統一取得 Logger
在你的每一個 .py 檔案(如 module_a.py, module_b.py)的頂部,都使用以下標準寫法:
PYTHON
步驟 2:區分「套件」與「應用」的設定策略
如果你在開發一個套件(Library,供他人 import):
- 絕對不要在模組級別呼叫 logging.basicConfig() 或加入任何 StreamHandler/FileHandler。
- 在你套件的頂層 __init__.py 中,加入一個 NullHandler,這也是官方文件建議,防止在宿主未設定日誌時報錯:
如果你在開發一個應用(Application,直接執行的程式):
- 建立一個專門的日誌設定模組(如 logger.py)或在入口檔案(如 main.py)的 if __name__ == "__main__": 區塊中進行集中設定。
- 設定 Formatter(日誌格式)和 Handler(輸出目標,如主控台、檔案)。
步驟 3:在應用入口集中設定(以應用為例)
PYTHON
步驟 4:根據需求進行精準控制(在應用入口或設定模組中)
在 setup_logging 之後,你可以利用層級特性進行微調:
PYTHON
透過以上 4 個步驟,你就能在專案中建立起一個清晰、可控且不具侵入性的日誌系統。