3.5 logging - 日誌系統
3.5 logging - 日誌系統
1. 使用
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 模式輸出日誌等級
| 等級 | 數值 | 使用時機 |
|---|---|---|
| DEBUG | 10 | 詳細的除錯資訊 |
| INFO | 20 | 一般的操作資訊 |
| WARNING | 30 | 警告但程式仍可運行 |
| ERROR | 40 | 錯誤但程式仍可運行 |
| CRITICAL | 50 | 嚴重錯誤,程式可能無法繼續 |
實際範例: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)s | Logger 名稱 |
%(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()}")思考題
- 為什麼
setup_hook_logging要檢查logger.handlers? logging.DEBUG和logging.INFO的差別是什麼?什麼時候用哪個?- 如何讓日誌同時輸出到檔案和控制台?
實作練習
- 修改
setup_hook_logging,添加日誌輪替功能 - 實作一個裝飾器,自動記錄函式的進入和離開
- 建立一個日誌分析腳本,統計各等級日誌的數量
上一章:re - 正規表達式 下一章:argparse - CLI 介面