快轉到主要內容
  1. 教學文章/

Python Logging:別再 print 了,用正經的方式記錄日誌吧

·6 分鐘· loading · loading · ·
Python Logging Debug 標準庫
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 22: 本文

一. 前言
#

拍拍君知道你一定寫過這種程式碼:

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,所以如果不設定 leveldebuginfo 的訊息不會顯示喔!

四. 自訂格式:讓日誌更好讀
#

預設的格式有點陽春,來加上時間和更多資訊:

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__)

  1. 模組識別:日誌會自動顯示是哪個模組發出的
  2. 獨立控制:可以對不同模組設定不同的日誌等級
  3. 階層管理: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 最佳實踐清單
#

  1. 每個模組用 getLogger(__name__)
  2. 只在 main 設定 root logger
  3. %s 格式而非 f-string
  4. exception handler 裡用 logger.exception()
  5. 正式環境用 dictConfig 管理設定
  6. RotatingFileHandler 避免 log 爆量
  7. disable_existing_loggers: False

結語
#

從今天開始,跟 print debug 說再見吧!logging 模組雖然一開始看起來比較複雜,但它提供的彈性和專業度絕對值得學習。

從最簡單的 basicConfig 開始,慢慢加入 Handler 和 Formatter,最後進化到 dictConfig 管理——你的程式日誌會越來越專業 💪

有任何問題歡迎留言,拍拍君下次見!👋

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 22: 本文

相關文章

少寫一半程式碼:dataclasses 讓你的 Python 類別煥然一新
·6 分鐘· loading · loading
Python Dataclasses Oop 標準庫
Polars:比 Pandas 快 10 倍的 DataFrame 新選擇
·6 分鐘· loading · loading
Python Polars Dataframe 資料分析 Rust
PyTorch 神經網路入門:從零開始建立你的第一個模型
·5 分鐘· loading · loading
Python Pytorch Neural-Network Deep-Learning Machine-Learning
Ruff:用 Rust 寫的 Python Linter,快到你會懷疑人生
·4 分鐘· loading · loading
Python Ruff Linter Formatter Code-Quality
pathlib:優雅處理檔案路徑的現代方式
·6 分鐘· loading · loading
Python Pathlib 檔案處理 標準庫
Pre-commit Hooks:讓壞 Code 連 Commit 的機會都沒有
·4 分鐘· loading · loading
Python Pre-Commit Git Linter Code-Quality Ruff Mypy