Python 的 logging 模組提供強大的日誌記錄功能,而過濾器(Filters)更是其中一個鮮為人知卻非常實用的工具。相較於單純依靠日誌等級來控制輸出,過濾器能根據更複雜的條件,例如函式名稱、特定上下文資訊等,決定哪些日誌需要記錄,哪些可以忽略。這對於大型專案或需要精細控制日誌輸出的場景至關重要。本文將探討如何建立和應用過濾器,並提供實際案例,幫助讀者更好地掌握這個工具。一般來說,開發者會使用 logging.basicConfig() 快速設定日誌輸出,但對於需要更進階控制的應用,則需要更深入瞭解 logging 模組的架構。過濾器可以新增到處理程式(Handler)或記錄器(Logger)上,實作更精細的日誌篩選。例如,可以建立一個過濾器,僅記錄特定模組或函式的日誌,或者根據日誌訊息的內容進行篩選。文章中提到的繼承 logging.Filter、建立包含 filter() 方法的類別,以及使用可呼叫物件(Python 3.12 起支援)等方法,都提供了不同的靈活性,讓開發者可以根據實際需求選擇最合適的方式。此外,文章也示範瞭如何在多模組應用中組態日誌系統,確保不同模組的日誌訊息能被正確記錄和管理,這對於大型專案的除錯和維護非常重要。

Log Filters 的深度剖析與實戰應用

在 Python 的日誌記錄模組中,過濾器(Filters)是一個強大且靈活的工具,可以讓我們進行更精細的日誌控制。相比於僅依賴日誌等級(Log Levels),過濾器允許我們根據更複雜的條件來決定哪些日誌應該被記錄或忽略。這篇文章將探討如何建立和應用過濾器,並提供實際案例來幫助讀者更好地理解其應用。

過濾器的基本概念

過濾器可以被新增到處理程式(Handler)或記錄器(Logger)物件中,以實作更精細的日誌控制。根據 Python 官方檔案,過濾器初始化時可以接受一個字串,該字串定義了哪些記錄器可以透過過濾器。例如,初始化為 'A.B' 的過濾器會允許 A.BA.B.C 等記錄器的日誌透過,但不允許 A.BBB.A.B 等記錄器的日誌透過。

建立過濾器

建立過濾器有多種方法,以下是三種主要的方法:

  1. 繼承 logging.Filter
  2. 建立一個包含 filter() 方法的類別
  3. 使用可呼叫物件

繼承 logging.Filter

首先,讓我們看看如何透過繼承 logging.Filter 來建立一個過濾器。

# logging_filter.py
import logging

class MyFilter(logging.Filter):
    def filter(self, record):
        if record.funcName.lower().startswith("a"):
            return False
        return True

在這個範例中,我們建立了一個名為 MyFilter 的類別,並覆寫了 filter() 方法。該方法接受一個日誌記錄(Log Record)物件作為引數,並在該物件的函式名稱以 'a' 開頭時傳回 False,從而忽略這些日誌。

建立包含 filter() 方法的類別

我們也可以不繼承 logging.Filter,而直接建立一個包含 filter() 方法的類別。

# logging_filter_no_subclass.py
class MyFilter():
    def filter(self, record):
        if record.funcName.lower().startswith("a"):
            return False
        return True

這段程式碼看起來與前一段幾乎相同,但它並未繼承任何類別。這樣做的好處是程式碼更加簡潔,但缺點是失去了某些繼承自 logging.Filter 的功能。

使用可呼叫物件

從 Python 3.12 開始,我們還可以使用函式或其他可呼叫物件作為過濾器。以下是一個簡單的函式範例:

def filter(record: logging.LogRecord):
    if record.funcName.lower().startswith("a"):
        return False
    return True

應用過濾器到記錄器

接下來,讓我們看看如何將這些過濾器應用到記錄器中。以下是一個完整的範例:

# logging_filter.py
import logging
import sys

class MyFilter(logging.Filter):
    def filter(self, record):
        if record.funcName == "a":
            return False
        return True

def a():
    """
    忽略此函式的日誌訊息
    """
    logger.debug("來自函式 a 的訊息")

def b():
    logger.debug("來自函式 b 的訊息")

if __name__ == "__main__":
    logging.basicConfig(
        stream=sys.stderr,
        level=logging.DEBUG)
    logger = logging.getLogger("filter_test")
    logger.addFilter(MyFilter())
    a()
    b()

在這個範例中,我們建立了一個名為 MyFilter 的過濾器類別,並將其新增到記錄器中。然後,我們定義了兩個函式 a()b(),分別在其中生成日誌訊息。當執行這段程式碼時,只有來自函式 b() 的日誌會被記錄。

應用過濾器到處理程式

將過濾器應用到處理程式也是非常直觀的。以下是一個範例:

# context_filter.py
import logging
from random import choice

class ContextFilter:
    USERS = ["Mike", "Stephen", "Rodrigo"]
    LANGUAGES = ["Python", "PHP", "Ruby", "Java", "C++"]

    def filter(self, record):
        record.user = choice(ContextFilter.USERS)
        record.language = choice(ContextFilter.LANGUAGES)
        return True

if __name__ == "__main__":
    levels = (
        logging.DEBUG,
        logging.INFO,
        logging.WARNING,
        logging.ERROR,
        logging.CRITICAL,
    )
    logger = logging.getLogger(name="test")
    logger.setLevel(logging.DEBUG)

    handler = logging.StreamHandler()
    my_filter = ContextFilter()
    handler.addFilter(my_filter)
    logger.addHandler(handler)

    fmt = ("%(name)s - %(levelname)-8s "
           "User: %(user)-8s Lang: %(language)-7s "
           "%(message)s")

在這個範例中,我們建立了一個名為 ContextFilter 的類別,並將其新增到處理程式中。該類別在每次記錄日誌時會隨機選擇一個使用者和語言,並將其新增到日誌記錄中。

內容解密:

在這段程式碼中:

  1. 定義了 ContextFilter 類別,該類別包含兩個屬性:USERSLANGUAGES
  2. 定義了 filter() 方法,該方法會隨機選擇一個使用者和語言,並將其新增到日誌記錄中。
  3. 設定了處理程式,並將 ContextFilter 新增到處理程式中。
  4. 格式化輸出,使得每條日誌訊息都包含使用者和語言資訊。

日誌篩選器的應用與實務案例

日誌篩選器是日誌系統中一個強大的工具,它能夠讓你更精細地控制哪些資訊會被記錄下來。雖然大多數工程師會依賴日誌等級來控制日誌的輸出,但篩選器可以提供更多的靈活性。在這一章節中,我們將探討如何建立和應用日誌篩選器,並且透過具體的實務案例來理解其應用。

篩選器的建立與應用

首先,我們需要了解如何建立一個篩選器。篩選器是一個類別,它會覆寫 filter 方法,並在這個方法中新增自定義的邏輯來決定是否允許某條日誌記錄被輸出。以下是一個簡單的篩選器示例:

import logging
from random import choice

class ContextFilter(logging.Filter):
    def filter(self, record):
        users = ["Rodrigo", "Mike", "Stephen"]
        languages = ["C++", "Python", "Ruby", "PHP"]
        record.user = choice(users)
        record.language = choice(languages)
        return True

內容解密:

在這段程式碼中,我們定義了一個名為 ContextFilter 的類別,它繼承自 logging.Filterfilter 方法會在每條日誌記錄被輸出之前被呼叫。在這個方法中,我們隨機選擇了一個使用者和語言,並將這些資訊新增到日誌記錄中。最後,我們傳回 True 表示允許這條日誌記錄被輸出。

日誌系統的設定

接下來,我們需要設定日誌系統來使用這個篩選器。以下是完整的程式碼:

import logging
from random import choice

# 定義篩選器
class ContextFilter(logging.Filter):
    def filter(self, record):
        users = ["Rodrigo", "Mike", "Stephen"]
        languages = ["C++", "Python", "Ruby", "PHP"]
        record.user = choice(users)
        record.language = choice(languages)
        return True

# 設定日誌系統
logger = logging.getLogger(name="test")
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
my_filter = ContextFilter()
handler.addFilter(my_filter)

fmt = "%(name)s - %(levelname)-8s User: %(user)s Lang: %(language)s %(message)s"
formatter = logging.Formatter(fmt)
handler.setFormatter(formatter)

logger.addHandler(handler)

# 設定日誌等級
levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]

logger.info("This is an info message with silly parameters")
for _ in range(10):
    level = choice(levels)
    level_name = logging.getLevelName(level)
    logger.log(level, "A message with %s level", level_name)

內容解密:

在這段程式碼中,我們首先定義了一個名為 ContextFilter 的類別,並且在其中實作了 filter 方法。接下來,我們設定了一個名為 test 的日誌物件,並將其等級設定為 DEBUG

然後,我們建立了一個 StreamHandler 例項,並將其設定為輸出到標準輸出(stdout)。接著,我們建立了一個 ContextFilter 例項,並將其新增到處理器中。

接著,我們定義了一個格式字串 fmt,並使用它建立了一個 Formatter 例項。最後,我們將處理器和格式化程式新增到日誌物件中。

輸出結果與格式化

當我們執行這段程式碼時,會看到以下輸出:

test - INFO     User: Rodrigo Lang: C++ This is an info message with silly parameters
test - CRITICAL User: Rodrigo Lang: PHP A message with CRITICAL level
test - CRITICAL User: Mike Lang: Python A message with CRITICAL level
test - WARNING  User: Mike Lang: Python A message with WARNING level
test - CRITICAL User: Stephen Lang: Ruby A message with CRITICAL level
test - ERROR    User: Mike Lang: Ruby A message with ERROR level
test - ERROR    User: Stephen Lang: Python A message with ERROR level
test - CRITICAL User: Stephen Lang: C++ A message with CRITICAL level
test - INFO     User: Rodrigo Lang: PHP A message with INFO level
test - WARNING  User: Rodrigo Lang: Ruby A message with WARNING level
test - DEBUG    User: Rodrigo Lang: Python A message with DEBUG level

內容解密:

在這段輸出中,每條日誌記錄都包含了使用者和語言資訊。這些資訊是由 ContextFilter 隨機生成的。格式字串中的 %(levelname)-8s 指示符表示將等級名稱格式化為八個字元寬度的字串。如果插入的字串小於八個字元,則會用空格填充。

日誌篩選器的應用與實務案例

在實務中,日誌篩選器可以用來過濾掉不必要的日誌記錄,或是新增額外的上下文資訊。例如,你可以使用篩選器來過濾掉某些特定使用者或語言的日誌記錄。以下是一個具體的實務案例:

class LanguageFilter(logging.Filter):
    def __init__(self, language):
        self.language = language

    def filter(self, record):
        return record.language == self.language

# 建立處理器並新增語言篩選器
handler_language_filtered = logging.StreamHandler()
language_filter = LanguageFilter("Python")
handler_language_filtered.addFilter(language_filter)

# 新增處理器到日誌物件
logger.addHandler(handler_language_filtered)

內容解密:

在這段程式碼中,我們定義了一個名為 LanguageFilter 的類別,它繼承自 logging.Filterfilter 方法會檢查日誌記錄中的語言資訊是否與指定的語言相比對。

接著,我們建立了一個 StreamHandler 例項,並將其設定為輸出到標準輸出(stdout)。然後,我們建立了一個 LanguageFilter 例項,並將其新增到處理器中。

最後,我們將處理器新增到日誌物件中。這樣一來,只有包含指定語言(在此例中是 Python)的日誌記錄才會被輸出。

模組化應用中的多模組日誌紀錄

在開發應用程式時,隨著程式碼量的增加,你可能會將程式碼分割成多個模組以便更好地管理和維護。然而,這也帶來了新的挑戰:如何在多模組之間進行有效的日誌紀錄?本章節將介紹如何在多模組應用程式中進行日誌紀錄。

模組化應用概述

在 Python 中,「模組」通常指的是單獨的一個 .py 檔案。「包」則是由多個相關模組組成的一組目錄和子目錄結構。模組化設計有助於程式碼重複使用、提高可維護性以及隔離不同功能模組。

日誌紀錄基礎設施

首先介紹如何組態基本的Python應用程式以進行跨模組地紀錄事件(event log)。

主要入口點:main.py

# main.py

import logging
import other_mod

def main():
    """
    The main entry point of the application.
    """
    logger = logging.getLogger(name="test")
    logger.setLevel(logging.DEBUG)

    file_handler = logging.FileHandler("multi.log")
    logger.addHandler(file_handler)

    formatter = logging.Formatter(
        "%(asctime)s - %(filename)s - %(levelname)s - %(message)s"
    )
    file_handler.setFormatter(formatter)

    logger.info("Program started")
    result = other_mod.add(7, 8)
    logger.info("Done!")
    return result

if __name__ == "__main__":
    main()

內容解密:

在上述程式碼中,

  • 首先匯入了所需的模組:logging 和其他自己編寫模組。
  • 接著定義了主函式(main),該函式負責設定 Logger 和 Handler。
  • 建立一個 Logger 名稱為「test」,設定其邏輯層級為 DEBUG。
  • 建立一個 File Handler ,指向檔案 multi.log。
  • 接著設定 Formatter ,以 %(asctime)s - %(filename)s - %(levelname)s - %(message)s 作為格式。
  • 在主函式內部記錄一些事件:開啟應用程式、呼叫其他模組、完成等。
  • 若本模組是主執行模組(即 name == “main"),則執行 main 函式。

其他模組:other_mod.py

# other_mod.py

import logging

def add(x, y):
    logger = logging.getLogger(name="test")
    logger.info("added %s and %s to get %s", x, y, x + y)
    return x + y

內容解密:

在其他模組 other_mod.py 中,

  • 首先匯入了所需的模組:logging。
  • 接著定義了一個 add 函式 ,該函式負責計算兩數之和。
  • 在函式內部取得 Logger 名稱為「test」,並記錄事件:計算兩數之和。
  • 傳回兩數之和。

測試跨模組紀錄功能

執行 main.py ,系統會產生檔案 multi.log 。檔案內容如下所示:

2023-10-14 12:34:56,789 - main.py - INFO - Program started
2023-10-14 12:34:56,789 - other_mod.py - INFO - added 7 and 8 to get 15
2023-10-14 12:34:56,790 - main.py - INFO - Done!

內容解密:

從上述 log 檔案內容可見,

  • 主要程式(main)啟動並記錄開始事件。
  • 其他模組(other_mod)呼叫時也記錄事件資訊。
  • 主要程式完成時再次記錄結束事件。

組態檔案方式設定 Logging(dictConfig)

dictConfig 提供更靈活及結構化方式設定 Logging 組態專案。

# dict_config.py
import yaml #需要安裝 PyYAML 模組.

log_config_dict = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        }
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {
            'class': 'logging.FileHandler',
            'filename': 'application.log',
            'formatter': 'simple'
        }
    },
    'loggers': {
        'simpleExample': {
            'handlers': ['console', 'file'],
            'level': 'DEBUG',
            'propagate': True,
        }
    }
}

with open('log_config.yaml', 'w') as file:
     yaml.dump(log_config_dict, file)

組態檔案 log_config.yaml 內容如下:

version: 1
disable_existing_loggers: False
formatters:
   simple:
     format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
   console:
     class : "logging.StreamHandler"
     formatter : simple
   file:
     class : "logging.FileHandler"
     filename : application.log # 指向存放 log 的檔案位置.
     formatter : simple # 指向 Formatters 中簡單格式.
loggers:
   simpleExample:
     handlers : [console, file] # 包含 Handler 條目.
     level : DEBUG # 語境層級.
     propagate : True # 是否傳播至 root logger.

載入及應用組態檔案:

# load_config.py

import yaml # 需要安裝 PyYAML 模組.
import logging.config

with open('log_config.yaml', encoding='utf-8') as config_file:
     config_dict= yaml.load(config_file, Loader=yaml.FullLoader) # 載入組態檔案.
logging.config.dictConfig(config_dict) # 應用組態檔案.

logger=logging.getLogger('simpleExample')
logger.debug("This is a debug log.")
logger.info("This is an info log.")
logger.warning("This is a warning log.")
logger.error("This is an error log.")
logger.critical("This is a critical log.")

此處主要說明如何透過 YAML 組態檔來組態 Logging ,以避免手動編寫重複性高、易錯誤、且不易管理之程式碼片段。透過 dictConfig ,可以更靈活及有結構地管理 Logging 組態專案與設定專案。