logging 模組提供了靈活的日誌記錄功能。相較於 print(),日誌系統提供了等級控制、格式化和輸出目標管理等功能。

為什麼用 logging 而非 print?

 1# 使用 print 的問題
 2print("Processing started")        # 無法控制輸出等級
 3print(f"Error: {error}")          # 無法區分一般訊息和錯誤
 4print("Debug: x =", x)            # 生產環境也會輸出
 5
 6# 使用 logging 的好處
 7import logging
 8logger = logging.getLogger(__name__)
 9
10logger.info("Processing started")   # 可以控制等級
11logger.error(f"Error: {error}")    # 明確標示為錯誤
12logger.debug(f"x = {x}")           # 只在 DEBUG 模式輸出

日誌等級

等級數值使用時機
DEBUG10詳細的除錯資訊
INFO20一般的操作資訊
WARNING30警告但程式仍可運行
ERROR40錯誤但程式仍可運行
CRITICAL50嚴重錯誤,程式可能無法繼續

實際範例:Hook 日誌系統

來自 .claude/lib/hook_logging.py

 1import logging
 2import os
 3from datetime import datetime
 4from pathlib import Path
 5from typing import Optional
 6
 7
 8def setup_hook_logging(
 9    hook_name: str,
10    log_subdir: Optional[str] = None,
11    log_level: Optional[int] = None,
12    include_stderr: bool = False
13) -> logging.Logger:
14    """
15    設定 Hook 日誌系統
16
17    Args:
18        hook_name: Hook 名稱,用於識別日誌來源
19        log_subdir: 日誌子目錄,預設為 hook_name
20        log_level: 日誌等級,預設根據環境變數決定
21        include_stderr: 是否同時輸出到 stderr
22
23    Returns:
24        logging.Logger: 配置好的 Logger 實例
25    """
26    # 決定日誌等級
27    if log_level is None:
28        debug_mode = os.getenv("HOOK_DEBUG", "").lower() == "true"
29        log_level = logging.DEBUG if debug_mode else logging.INFO
30
31    # 建立 Logger
32    logger = logging.getLogger(hook_name)
33    logger.setLevel(log_level)
34
35    # 避免重複添加 handler
36    if logger.handlers:
37        return logger
38
39    # 建立日誌目錄
40    project_root = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
41    subdir = log_subdir or hook_name
42    log_dir = Path(project_root) / ".claude" / "hook-logs" / subdir
43    log_dir.mkdir(parents=True, exist_ok=True)
44
45    # 日誌檔案路徑
46    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
47    log_file = log_dir / f"{hook_name}-{timestamp}.log"
48
49    # 設定 formatter
50    formatter = logging.Formatter(
51        "[%(asctime)s] %(levelname)s - %(message)s",
52        datefmt="%Y-%m-%d %H:%M:%S"
53    )
54
55    # 檔案 handler
56    file_handler = logging.FileHandler(log_file, encoding="utf-8")
57    file_handler.setFormatter(formatter)
58    logger.addHandler(file_handler)
59
60    # 可選的 stderr handler
61    if include_stderr:
62        import sys
63        stderr_handler = logging.StreamHandler(sys.stderr)
64        stderr_handler.setFormatter(formatter)
65        logger.addHandler(stderr_handler)
66
67    return logger

使用 Logger

在 Hook 腳本中使用

 1#!/usr/bin/env python3
 2from hook_logging import setup_hook_logging
 3
 4# 初始化 logger
 5logger = setup_hook_logging("branch-verify")
 6
 7def main():
 8    logger.info("Hook started")
 9
10    branch = get_current_branch()
11    logger.debug(f"Current branch: {branch}")
12
13    if is_protected_branch(branch):
14        logger.warning(f"Operating on protected branch: {branch}")
15
16    try:
17        # 執行操作
18        result = do_something()
19        logger.info(f"Operation completed: {result}")
20    except Exception as e:
21        logger.error(f"Operation failed: {e}")
22        raise
23
24if __name__ == "__main__":
25    main()

核心概念

Logger

日誌記錄器,用於發送日誌訊息:

1import logging
2
3# 取得 logger(使用模組名稱作為標識)
4logger = logging.getLogger(__name__)
5
6# 或使用自訂名稱
7logger = logging.getLogger("my_app")

Handler

決定日誌輸出到哪裡:

 1import logging
 2
 3logger = logging.getLogger("my_app")
 4
 5# 輸出到檔案
 6file_handler = logging.FileHandler("app.log", encoding="utf-8")
 7logger.addHandler(file_handler)
 8
 9# 輸出到控制台
10console_handler = logging.StreamHandler()
11logger.addHandler(console_handler)

Formatter

決定日誌的格式:

1import logging
2
3formatter = logging.Formatter(
4    "[%(asctime)s] %(levelname)s - %(name)s - %(message)s",
5    datefmt="%Y-%m-%d %H:%M:%S"
6)
7
8handler = logging.FileHandler("app.log")
9handler.setFormatter(formatter)

格式化字串變數

變數說明
%(asctime)s時間戳
%(levelname)s日誌等級名稱
%(name)sLogger 名稱
%(message)s日誌訊息
%(filename)s檔案名稱
%(lineno)d行號
%(funcName)s函式名稱

實用技巧

避免重複 Handler

 1def setup_logger(name: str) -> logging.Logger:
 2    logger = logging.getLogger(name)
 3    logger.setLevel(logging.INFO)
 4
 5    # 重要:檢查是否已有 handler
 6    if logger.handlers:
 7        return logger
 8
 9    handler = logging.FileHandler("app.log")
10    logger.addHandler(handler)
11    return logger

環境變數控制日誌等級

 1import os
 2import logging
 3
 4def get_log_level() -> int:
 5    """從環境變數取得日誌等級"""
 6    level_name = os.getenv("LOG_LEVEL", "INFO").upper()
 7    return getattr(logging, level_name, logging.INFO)
 8
 9logger = logging.getLogger(__name__)
10logger.setLevel(get_log_level())

日誌輪替

1from logging.handlers import RotatingFileHandler
2
3handler = RotatingFileHandler(
4    "app.log",
5    maxBytes=10*1024*1024,  # 10MB
6    backupCount=5           # 保留 5 個備份
7)

日誌檔案結構

Hook 系統的日誌結構:

1.claude/hook-logs/
2├── branch-verify/
3│   ├── branch-verify-20240120-153000.log
4│   └── branch-verify-20240120-160000.log
5├── ticket-quality-gate/
6│   └── ticket-quality-gate-20240120-155000.log
7└── ...

最佳實踐

1. 使用 __name__ 作為 Logger 名稱

1import logging
2
3# 好:使用模組名稱,便於追蹤
4logger = logging.getLogger(__name__)
5
6# 不好:使用固定字串,難以區分來源
7logger = logging.getLogger("my_logger")

2. 在適當的等級記錄訊息

1logger.debug("Variable x = %s", x)           # 詳細除錯
2logger.info("Processing file %s", filename)  # 一般操作
3logger.warning("Config not found, using default")  # 警告
4logger.error("Failed to connect: %s", error)  # 錯誤

3. 使用延遲格式化

1# 好:使用 % 格式化(只在需要時才格式化)
2logger.debug("Data: %s", expensive_function())
3
4# 不好:f-string 總是會執行
5logger.debug(f"Data: {expensive_function()}")

思考題

  1. 為什麼 setup_hook_logging 要檢查 logger.handlers
  2. logging.DEBUGlogging.INFO 的差別是什麼?什麼時候用哪個?
  3. 如何讓日誌同時輸出到檔案和控制台?

實作練習

  1. 修改 setup_hook_logging,添加日誌輪替功能
  2. 實作一個裝飾器,自動記錄函式的進入和離開
  3. 建立一個日誌分析腳本,統計各等級日誌的數量

上一章:re - 正規表達式 下一章:argparse - CLI 介面