一. 前言 #
拍拍君知道你一定寫過這種程式碼:
print("=== 開始處理 ===")
print(f"收到 {len(data)} 筆資料")
print("出錯了!!!")
print("???到底哪裡壞了???")
Debug 的時候 print 超方便,但等程式上線後就會遇到一堆問題:
- 🤔 哪些是重要的錯誤、哪些是普通資訊?分不清楚
- 📁 訊息全部混在 terminal,沒辦法存檔回查
- 🗑️ 上線前還得手動刪掉一堆
print
Python 標準庫的 logging 模組正是為了解決這些痛點而生的!今天拍拍君就帶你從零開始,一步步掌握它 🎉
二. 安裝 #
好消息——不用裝任何東西!logging 是 Python 的標準庫模組:
import logging
就是這麼簡單,開箱即用 ✨
三. 最快上手:basicConfig #
先來看最簡單的用法:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("這是 debug 訊息")
logging.info("這是 info 訊息")
logging.warning("這是 warning 訊息")
logging.error("這是 error 訊息")
logging.critical("這是 critical 訊息")
輸出:
DEBUG:root:這是 debug 訊息
INFO:root:這是 info 訊息
WARNING:root:這是 warning 訊息
ERROR:root:這是 error 訊息
CRITICAL:root:這是 critical 訊息
日誌等級一覽 #
| 等級 | 數值 | 用途 |
|---|---|---|
DEBUG |
10 | 開發時的詳細資訊 |
INFO |
20 | 正常運作的確認訊息 |
WARNING |
30 | 警告,程式還能跑但需注意 |
ERROR |
40 | 發生錯誤,部分功能受影響 |
CRITICAL |
50 | 嚴重錯誤,程式可能無法繼續 |
預設等級是 WARNING,所以如果不設定 level,debug 和 info 的訊息不會顯示喔!
四. 自訂格式:讓日誌更好讀 #
預設的格式有點陽春,來加上時間和更多資訊:
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logging.info("拍拍君的伺服器啟動了")
logging.warning("記憶體使用率超過 80%%")
輸出:
2026-02-23 12:00:00 [INFO] root - 拍拍君的伺服器啟動了
2026-02-23 12:00:00 [WARNING] root - 記憶體使用率超過 80%
常用格式變數 #
| 變數 | 說明 |
|---|---|
%(asctime)s |
時間戳記 |
%(levelname)s |
日誌等級名稱 |
%(name)s |
Logger 名稱 |
%(filename)s |
檔案名稱 |
%(lineno)d |
行號 |
%(message)s |
日誌內容 |
%(funcName)s |
函式名稱 |
💡 拍拍君小提醒:
format字串中如果要顯示%符號本身,記得用%%跳脫!
五. 用 Logger 取代 root:正確的做法 #
在正式專案中,不建議直接用 logging.info() 這種 root logger。應該為每個模組建立自己的 logger:
import logging
# 用模組名稱當 logger 名稱(最佳實踐)
logger = logging.getLogger(__name__)
def fetch_data(url: str) -> dict:
logger.info("開始抓取資料:%s", url)
try:
# 假裝在抓資料
result = {"status": "ok"}
logger.debug("抓取成功,回傳 %d 個欄位", len(result))
return result
except Exception as e:
logger.error("抓取失敗:%s", e, exc_info=True)
raise
為什麼要用 getLogger(__name__)?
- 模組識別:日誌會自動顯示是哪個模組發出的
- 獨立控制:可以對不同模組設定不同的日誌等級
- 階層管理:logger 名稱用
.分隔形成樹狀結構,子 logger 會繼承父 logger 的設定
# 這些 logger 形成階層關係:
# chatptt
# chatptt.api
# chatptt.api.auth
parent = logging.getLogger("chatptt")
child = logging.getLogger("chatptt.api")
grandchild = logging.getLogger("chatptt.api.auth")
💡 拍拍君小提醒:
logger.error("失敗:%s", e, exc_info=True)會把完整的 traceback 一起記錄下來,debug 超方便!
六. Handler:日誌要送去哪裡? #
Handler 決定日誌的「目的地」。一個 logger 可以同時有多個 handler!
同時輸出到 console 和檔案 #
import logging
logger = logging.getLogger("pypyapp")
logger.setLevel(logging.DEBUG)
# Handler 1: 輸出到 console(只顯示 WARNING 以上)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_fmt = logging.Formatter("[%(levelname)s] %(message)s")
console_handler.setFormatter(console_fmt)
# Handler 2: 輸出到檔案(記錄所有等級)
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_fmt = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s"
)
file_handler.setFormatter(file_fmt)
# 加入 logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 測試
logger.debug("這只會出現在檔案裡")
logger.info("這也只會出現在檔案裡")
logger.warning("這會同時出現在 console 和檔案")
logger.error("這也是兩邊都有")
這樣就能做到:開發時在 terminal 看重要訊息,同時把完整日誌存到檔案 🎯
常用 Handler 種類 #
| Handler | 用途 |
|---|---|
StreamHandler |
輸出到 console(stdout/stderr) |
FileHandler |
寫入檔案 |
RotatingFileHandler |
檔案超過大小自動輪替 |
TimedRotatingFileHandler |
按時間(每天/每小時)輪替 |
SMTPHandler |
寄 email(適合 critical 通知) |
HTTPHandler |
送到遠端 HTTP server |
七. RotatingFileHandler:別讓 log 檔爆掉你的硬碟 #
線上服務跑久了,log 檔會越來越大。用 RotatingFileHandler 自動管理:
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("pypyapp")
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler(
"app.log",
maxBytes=5 * 1024 * 1024, # 每個檔案最大 5MB
backupCount=3, # 保留 3 個備份
encoding="utf-8",
)
handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)
logger.addHandler(handler)
# 當 app.log 超過 5MB,會自動變成:
# app.log.1, app.log.2, app.log.3(最舊的被刪除)
按時間輪替也很常用:
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
"app.log",
when="midnight", # 每天午夜輪替
interval=1,
backupCount=30, # 保留 30 天
encoding="utf-8",
)
handler.suffix = "%Y-%m-%d" # 備份檔名格式
八. dictConfig:用字典管理複雜設定 #
當專案變大,用程式碼設定 logging 會很亂。dictConfig 讓你用結構化的方式管理:
import logging
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "[%(levelname)s] %(message)s",
},
"detailed": {
"format": "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detailed",
"filename": "app.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"pypyapp": {
"level": "DEBUG",
"handlers": ["console", "file"],
"propagate": False,
},
},
"root": {
"level": "WARNING",
"handlers": ["console"],
},
}
logging.config.dictConfig(LOGGING_CONFIG)
# 使用
logger = logging.getLogger("pypyapp")
logger.info("設定完成!")
💡 拍拍君小提醒:
"disable_existing_loggers": False很重要!設為True(預設值)的話,之前建立的 logger 會全部被停用,常常造成莫名其妙的 log 消失問題。
九. 實戰:搭配 structlog 輸出 JSON #
如果你的 log 要送到 ELK、Datadog 或其他日誌系統,JSON 格式會更方便解析。這裡介紹搭配 structlog 的做法:
pip install structlog
import structlog
import logging
# 設定 structlog 使用標準 logging
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
logger_factory=structlog.PrintLoggerFactory(),
)
logger = structlog.get_logger()
# 可以帶任意 key-value
logger.info("使用者登入", user="拍拍醬", ip="192.168.1.1")
logger.warning("API 回應緩慢", endpoint="/api/data", latency_ms=3200)
輸出:
{"user": "拍拍醬", "ip": "192.168.1.1", "log_level": "info", "timestamp": "2026-02-23T12:00:00Z", "event": "使用者登入"}
{"endpoint": "/api/data", "latency_ms": 3200, "log_level": "warning", "timestamp": "2026-02-23T12:00:00Z", "event": "API 回應緩慢"}
結構化日誌的好處:
- 🔍 方便搜尋和過濾
- 📊 可以直接匯入分析工具
- 🏷️ 每個欄位都有明確的 key
十. 常見雷區與最佳實踐 #
❌ 不要用 f-string 組日誌訊息 #
# 不好:即使 log 被過濾掉,f-string 還是會被計算
logger.debug(f"處理了 {len(expensive_list)} 筆資料")
# 好:用 % 格式,只有真正要輸出時才會格式化
logger.debug("處理了 %d 筆資料", len(expensive_list))
❌ 不要在每個函式裡都 basicConfig
#
# 不好:basicConfig 只有第一次呼叫有效
def process():
logging.basicConfig(level=logging.DEBUG) # 第二次呼叫會被忽略!
...
# 好:在程式進入點(main)設定一次就好
def main():
logging.basicConfig(level=logging.DEBUG)
process()
❌ 不要忘記 exc_info
#
try:
risky_operation()
except Exception as e:
# 不好:只有錯誤訊息,沒有 traceback
logger.error("操作失敗:%s", e)
# 好:完整的 traceback
logger.error("操作失敗:%s", e, exc_info=True)
# 更好:用 exception() 自動帶 exc_info
logger.exception("操作失敗")
✅ 拍拍君的 logging 最佳實踐清單 #
- 每個模組用
getLogger(__name__) - 只在 main 設定 root logger
- 用
%s格式而非 f-string - exception handler 裡用
logger.exception() - 正式環境用
dictConfig管理設定 - 用
RotatingFileHandler避免 log 爆量 - 設
disable_existing_loggers: False
結語 #
從今天開始,跟 print debug 說再見吧!logging 模組雖然一開始看起來比較複雜,但它提供的彈性和專業度絕對值得學習。
從最簡單的 basicConfig 開始,慢慢加入 Handler 和 Formatter,最後進化到 dictConfig 管理——你的程式日誌會越來越專業 💪
有任何問題歡迎留言,拍拍君下次見!👋