Python 的 logging 模組提供強大的日誌管理功能,尤其在長時間執行的應用程式中,日誌旋轉是不可或缺的機制。本文不僅涵蓋根據大小和時間的日誌旋轉,更進一步示範如何自定義旋轉行為,例如壓縮日誌檔案。透過 RotatingFileHandlerTimedRotatingFileHandler,搭配自定義的 rotatornamer 函式,可以精確控制日誌檔案的命名和處理方式,例如使用 gzip 壓縮舊日誌。此外,多執行緒環境下,logging 模組本身的執行緒安全特性簡化了日誌記錄流程,無需額外同步機制。然而,在多程式場景中,需要藉助佇列機制來序列化日誌訊息,避免多個程式同時寫入日誌檔案造成混亂。文章提供多程式日誌處理的完整範例,包含監聽程式和工作程式的協同工作,確保日誌訊息有序地寫入檔案。最後,文章也比較了多執行緒和多程式的優缺點,並提供選擇建議,協助開發者根據應用場景選擇合適的日誌處理策略。

Python日誌旋轉技術探討

在實務開發中,日誌管理是一項重要的技能,特別是在需要長時間執行的應用程式中。透過適當的日誌旋轉機制,可以有效地管理日誌檔案的大小和數量,避免系統資源被過度消耗。玄貓將探討Python中的日誌旋轉技術,並提供具體的程式碼範例及詳細解說。

日誌旋轉的基本概念

日誌旋轉(Log Rotation)是指在日誌檔案達到一定大小或時間間隔後,自動將舊的日誌檔案重新命名或壓縮,並建立新的日誌檔案進行記錄。這樣可以避免單個日誌檔案過大,影響系統效能。

在Python中,logging 模組提供了多種旋轉日誌的方式,包括根據檔案大小和時間間隔進行旋轉。以下是使用Python標準函式庫logging模組進行日誌旋轉的基本步驟。

基本日誌組態與旋轉

首先,我們需要組態基本的日誌設定。以下是一個簡單的範例,展示如何組態並使用根據檔案大小的日誌旋轉:

import logging.config
import settings
import time

logging.config.dictConfig(settings.LOGGING_CONFIG)

def main():
    logger = logging.getLogger(__name__)
    logger.debug("Logging is configured.")

    for i in range(10):
        logger.info("This is test log line %s", i)
        time.sleep(1.5)

if __name__ == "__main__":
    main()

內容解密:

  • import logging.config: 呼叫 logging.config 模組來處理日誌組態。
  • import settings: 呼叫自定義設定模組,這裡假設已經定義了 settings.LOGGING_CONFIG
  • import time: 呼叫時間模組來控制日誌寫入的間隔。
  • logging.config.dictConfig(settings.LOGGING_CONFIG): 使用字典組態方式載入自定義日誌設定。
  • logger = logging.getLogger(__name__): 建立一個名為當前模組名稱的 logger 物件。
  • logger.debug("Logging is configured."): 記錄一條除錯級別的日誌訊息。
  • for i in range(10): 迴圈十次,每次記錄一條資訊級別的日誌訊息。
  • time.sleep(1.5): 每次寫入日誌後暫停1.5秒。

自訂旋轉器與命名器

在Python中的兩種常見的旋轉處理器:RotatingFileHandlerTimedRotatingFileHandler 提供了兩個特殊屬性:rotatornamer。這些屬性允許我們在日誌旋轉時執行自訂的操作。

自訂命名器與壓縮器

以下是如何自訂命名器和壓縮器來實作壓縮日誌功能:

import gzip
import logging.config
import os
import shutil
import time
import settings

logging.config.dictConfig(settings.LOGGING_CONFIG)

def namer(filename):
    return f"{filename}.gz"

def rotator(source, destination):
    with open(source, "rb") as f_source:
        with gzip.open(destination, "wb") as f_dest:
            shutil.copyfileobj(f_source, f_dest)
    os.remove(source)

def main():
    logger = logging.getLogger(__name__)

    for handler in logger.handlers:
        if handler.name == "rotatorFileHandler":
            handler.namer = namer
            handler.rotator = rotator

    logger.debug("Logging is configured.")

    for i in range(10):
        logger.info("This is test log line %s", i)
        time.sleep(1.5)

if __name__ == "__main__":
    main()

內容解密:

  • import gzip: 呼叫 gzip 模組來壓縮檔案。
  • def namer(filename): 自訂命名器函式,將原始檔案名稱改為 .gz 結尾。
  • def rotator(source, destination): 自訂壓縮函式,將源檔案壓縮後儲存為目標檔案,然後刪除源檔案。
  • for handler in logger.handlers: 遍歷所有記錄器處理程式。
  • if handler.name == "rotatorFileHandler": 搜尋名為 “rotatorFileHandler” 的處理程式。
  • handler.namer = namer: 將處理程式的命名器設定為自訂函式。
  • handler.rotator = rotator: 將處理程式的壓縮器設定為自訂函式。

日誌多執行緒與併發

在多執行緒和併發場景下,Python 的標準函式庫模組提供了對應支援。雖然執行緒安全性已經得到保障,但在處理多程式和非同步場景時需要額外工作。以下是主要場景:

使用執行緒進行記錄

import threading
import logging

# 組態基本的記錄器
logging.basicConfig(level=logging.DEBUG,
                    format='%(threadName)s: %(message)s')

def worker(num):
    logger = logging.getLogger('threading_example')
    while not event.is_set():
        logger.debug('Thread %s', num)
        event.wait(2)

# 主函式建立執行緒事件
if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    events = []
    for i in range(3):
        event = threading.Event()
        x = threading.Thread(name='thread' + str(i), target=worker, args=(i,))
        x.start()
        events.append((event, x))

    for event, x in events:
        event.set()

內容解密:

  • import threading: 呼叫 thread 模組用於建立及管理執行緒。
  • import logging: 呼叫 logging 模組用於記錄功能。
  • def worker(num): 定義工作執行緒函式,接收引數 num 作為執行緒識別符號號並記錄訊息。
  • if name == “main”: 主函式中建立事件物件及執行緒物件並啟動執行緒。
說明:

此圖示展示了從開始到完成的一系列步驟:設定基本日誌組態、啟動執行緒記錄、遍歷所有處理手段、檢查並設定自定義 rotator 與 namer、記錄十條測試資訊。

多執行緒與多處理的日誌處理

在 Python 中,多執行緒和多處理是常見的平行處理方法,但它們在日誌管理上有不同的挑戰。本文將探討如何在多執行緒和多處理環境中有效地使用日誌模組,並提供具體的實作範例。

多執行緒的日誌處理

Python 的 Global Interpreter Lock (GIL) 限制了執行緒的平行能力,但這並不影響日誌模組的使用。Python 的 logging 模組本身就是執行緒安全的,這意味著我們可以直接在多執行緒環境中使用它而不需要額外的同步機制。

以下是一個使用 logging 模組來處理多執行緒日誌的範例:

# threaded_logging.py

import logging
import threading
import time

FMT = '%(relativeCreated)6d %(threadName)s %(message)s'

def worker(message):
    logger = logging.getLogger("main-thread")
    while not message["stop"]:
        logger.debug("Hello from worker")
        time.sleep(0.05)

def main():
    logger = logging.getLogger("main-thread")
    logger.setLevel(logging.DEBUG)
    file_handler = logging.FileHandler("threaded.log")

    formatter = logging.Formatter(FMT)
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)

    msg = {"stop": False}
    thread = threading.Thread(target=worker, args=(msg, ))
    thread.start()
    logger.debug("Hello from main function")
    time.sleep(2)
    msg["stop"] = True

    thread.join()

if __name__ == '__main__':
    main()

內容解密:

  • FMT 定義了日誌格式,包括相對建立時間、執行緒名稱和訊息。
  • worker 函式是一個迴圈函式,當 message["stop"]False 時持續執行,每次迴圈都會記錄一條訊息並暫停 0.05 秒。
  • main 函式設定了日誌記錄器和檔案處理器,然後啟動一個新執行緒來執行 worker 函式。
  • 主執行緒在啟動子執行緒後會記錄一條訊息,然後暫停 2 秒以讓子執行緒有時間執行,最後將 msg["stop"] 設為 True 以停止子執行緒。

多處理的日誌處理

在多處理環境中,每個程式都會單獨寫入日誌檔案,這可能會導致日誌資訊混亂。為瞭解決這個問題,我們可以使用佇列來序列化程式間的日誌訊息。

以下是一個使用 multiprocessing 模組來處理多處理日誌的範例:

# process_logging.py

import logging
import logging.handlers
import multiprocessing
import time
from random import choice, randint

LEVELS = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
LOGGERS = ["py_worker", "php_worker"]
MESSAGES = ["working hard", "taking a nap", "ERROR, ERROR, ERROR", "processing..."]

def setup_logging():
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)

    file_handler = logging.FileHandler("processed.log")
    formatter = logging.Formatter(
        ("%(asctime)s - %(processName)-10s - "
         "%(levelname)s - %(message)s")
    )
    file_handler.setFormatter(formatter)
    root.addHandler(file_handler)

def listener_process(queue, configurer):
    configurer()
    while True:
        try:
            log_record = queue.get()
            if log_record is None:
                break
            logger = logging.getLogger(log_record.name)
            logger.handle(log_record)
        except Exception:
            import sys, traceback
            print("Error occurred in listener", file=sys.stderr)
            traceback.print_exc(file=sys.stderr)

內容解密:

  • setup_logging 函式設定了根記錄器及其檔案處理器和格式器。
  • listener_process 函式從佇列中取得日誌記錄並將其寫入日誌檔案。當取得到 None 值時表示結束。
  • 接下來我們將繼續完成這段程式碼。
# process_logging.py (繼續)

def worker(logger_name, level, message):
    logger = logging.getLogger(logger_name)
    logger.log(level, message)
    time.sleep(randint(0, 3))

def main():
    queue = multiprocessing.Queue()
    listener = multiprocessing.Process(target=listener_process,
                                        args=(queue, setup_logging))
    listener.start()

    for i in range(10):
        p = multiprocessing.Process(target=worker,
                                    args=(choice(LOGGERS),
                                          choice(LEVELS),
                                          choice(MESSAGES)))
        p.start()

        queue.put_nowait(logging.LogRecord(
            logger_name=LOGGERS[i % len(LOGGERS)],
            level=LEVELS[i % len(LEVELS)],
            pathname='',
            lineno=0,
            msg=MESSAGES[i % len(MESSAGES)],
            args=None,
            exc_info=None))
        p.join()

if __name__ == '__main__':
    main()

內容解密:

  • main 函式中,我們建立了一個程式佇列和一個監聽程式來處理日誌記錄。
  • 接著,我們啟動了多個工作程式,每個工作程式都會生成一條日誌記錄並將其放入佇列中。
  • 工作程式完成後會立即加入等待。
  • 每條訊息都會被監聽程式序列化地寫入到單一日誌檔案中。

比較與選擇

在選擇使用多執行緒還是多處理時,需要考慮到以下幾點:

  1. GIL 的影響:由於 Python 的 GIL,多執行緒不適合 CPU 型任務,但對於 I/O 型任務則相對友好。
  2. 資源消耗:多處理會消耗更多的系統資源(如記憶體和 CPU),但能夠充分利用多核心處理器。
  3. 複雜度:多處理比多執行緒更難以管理和除錯,特別是在進行資料分享時。