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

Python loguru 實戰:告別複雜的 logging 設定,寫出漂亮的日誌

·6 分鐘· loading · loading · ·
Python Logging Loguru 除錯 工具
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 43: 本文

featured

一. 前言:logging 模組的痛苦
#

相信每個 Python 工程師都曾被這段程式碼折磨過:

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)
logger.info('程式啟動')

光是「最基本的設定」就要寫這麼多行,而且:

  • 格式字串醜到哭,%(levelname)s 這種寫法完全不直覺
  • 要同時輸出到檔案和終端,要手動加兩個 handler
  • 例外堆疊追蹤預設格式很難看
  • 顏色?不好意思,預設沒有

loguru 用一個設計哲學解決了這一切:「日誌應該要好用、好看、不費力。」

2026-04-14 10:08:23.145 | INFO     | __main__:main:12 - 程式啟動

開箱即用,有顏色、有時間戳、有行號。一行設定都不需要。


二. 安裝
#

pip install loguru

# 或用 uv(推薦)
uv add loguru

loguru 沒有任何外部依賴,輕量到不行。


三. 基本用法:開箱即用
#

loguru 最大的特色是「零設定」。引入 logger 物件就能直接用:

from loguru import logger

logger.debug("這是 debug 訊息")
logger.info("程式開始執行")
logger.warning("警告:磁碟空間不足")
logger.error("發生錯誤!")
logger.critical("嚴重錯誤!系統即將關閉")

終端輸出(有顏色,各 level 顏色不同):

2026-04-14 10:08:23.145 | DEBUG    | __main__:<module>:3 - 這是 debug 訊息
2026-04-14 10:08:23.146 | INFO     | __main__:<module>:4 - 程式開始執行
2026-04-14 10:08:23.146 | WARNING  | __main__:<module>:5 - 警告:磁碟空間不足
2026-04-14 10:08:23.146 | ERROR    | __main__:<module>:6 - 發生錯誤!
2026-04-14 10:08:23.146 | CRITICAL | __main__:<module>:7 - 嚴重錯誤!系統即將關閉

注意到了嗎?每一行都自動包含:

  • 時間戳(精確到毫秒)
  • Log level(彩色)
  • 模組名稱、函式、行號
  • 訊息內容

這就是標準 logging 模組要設定半天才能達到的效果。


四. Sink:控制輸出目標
#

loguru 用 sink(水槽) 的概念管理輸出目標。預設的 sink 是 sys.stderr(終端),你可以增加更多:

4.1 輸出到檔案
#

from loguru import logger

# 最簡單的方式
logger.add("app.log")

logger.info("這行會同時出現在終端和 app.log")

4.2 移除預設的終端輸出
#

import sys
from loguru import logger

# 移除預設的 stderr sink
logger.remove()

# 只輸出到檔案
logger.add("app.log")

logger.info("只有檔案看得到這行")

4.3 自訂 level 過濾
#

import sys
from loguru import logger

logger.remove()

# 終端只顯示 INFO 以上
logger.add(sys.stderr, level="INFO")

# 檔案記錄所有 DEBUG 以上
logger.add("debug.log", level="DEBUG")

logger.debug("只有 debug.log 有這行")
logger.info("兩邊都有這行")

4.4 自訂格式
#

logger.add(
    "app.log",
    format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)

format 支援的變數(比 %(levelname)s 人性化多了):

變數 說明
{time} 時間戳,可加格式 {time:YYYY-MM-DD}
{level} Log level
{message} 訊息內容
{name} 模組名稱
{function} 函式名稱
{line} 行號
{elapsed} 程式執行時間
{process} 程序 ID

五. 檔案輪替(Rotation)與保留
#

實際上線的服務,日誌檔案不能無限增長。loguru 內建了強大的輪替機制:

5.1 依大小輪替
#

from loguru import logger

logger.add("app.log", rotation="100 MB")  # 超過 100MB 就開新檔

5.2 依時間輪替
#

# 每天午夜開新檔
logger.add("app.log", rotation="00:00")

# 每週一開新檔
logger.add("app.log", rotation="1 week")

# 每小時輪替
logger.add("app.log", rotation="1 hour")

5.3 設定保留時間與壓縮
#

logger.add(
    "logs/app_{time}.log",    # 檔名加時間戳
    rotation="1 day",          # 每天輪替
    retention="30 days",       # 保留最近 30 天
    compression="zip"          # 舊檔自動壓縮
)

這三個參數就取代了整個 RotatingFileHandler + TimedRotatingFileHandler 的設定。


六. 例外追蹤
#

這是 loguru 最讓人驚豔的功能。

6.1 用 @logger.catch 裝飾器
#

from loguru import logger

@logger.catch
def 計算除法(a, b):
    return a / b

計算除法(10, 0)

輸出:

2026-04-14 10:08:23.200 | ERROR    | __main__:計算除法:4 - An error has been caught...
Traceback (most recent call last):

> File "main.py", line 7, in <module>
    計算除法(10, 0)
  File "main.py", line 4, in 計算除法
    return a / b
           │   └── 0
           └── 10

ZeroDivisionError: division by zero

看到了嗎?loguru 會顯示每個變數的實際值│ └── 0),讓你一眼看出問題所在。這個功能叫做 Better Exceptions,在除錯時超級有用。

6.2 在 try/except 中使用
#

from loguru import logger

try:
    data = {"name": "拍拍君"}
    print(data["age"])  # KeyError
except Exception:
    logger.exception("讀取資料時發生錯誤")

logger.exception() 會自動包含完整的 traceback,比 logger.error() 更適合在 except 區塊使用。

6.3 用 context manager
#

from loguru import logger

with logger.catch():
    有可能會爆炸的程式碼()

七. 結構化日誌:bind 與 contextualize
#

當你需要在多個 log 訊息中附加相同的 context 資訊,bind 是你的好朋友:

7.1 bind — 新增固定欄位
#

from loguru import logger

# 建立一個帶有 user_id 的 logger
user_logger = logger.bind(user_id="paipai_123")

user_logger.info("使用者登入")
user_logger.info("使用者查詢訂單")
user_logger.info("使用者登出")

配合自訂 format 可以讓結構化資訊更清晰:

import sys

logger.add(
    sys.stderr,
    format="{time} | {level} | {extra[user_id]} | {message}"
)

7.2 contextualize — 用 context manager 綁定
#

適合在一段程式執行期間暫時綁定 context:

from loguru import logger

def 處理請求(request_id: str):
    with logger.contextualize(request_id=request_id):
        logger.info("開始處理請求")
        do_something()
        logger.info("請求處理完成")

處理請求("req_abc_001")

contextualize 在 async 環境(asyncio)中同樣有效,context 會自動跟隨 coroutine,不用擔心多個請求的 log 混在一起。


八. 進階技巧
#

8.1 攔截標準 logging 模組
#

很多第三方套件(如 SQLAlchemy、uvicorn、requests)仍然使用標準 logging 模組。你可以讓 loguru 接管所有的 log:

import logging
from loguru import logger


class InterceptHandler(logging.Handler):
    def emit(self, record):
        # 把標準 logging 的 record 轉成 loguru 的訊息
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno

        frame, depth = logging.currentframe(), 2
        while frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1

        logger.opt(depth=depth, exception=record.exc_info).log(
            level, record.getMessage()
        )


# 讓所有 logging 都走 loguru
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)

設定完之後,所有套件的 log 都會統一由 loguru 處理,格式一致。

8.2 透過環境變數動態設定 level
#

import sys
import os
from loguru import logger

log_level = os.getenv("LOG_LEVEL", "INFO").upper()

logger.remove()
logger.add(sys.stderr, level=log_level)

在 production 環境設 LOG_LEVEL=WARNING,開發時設 LOG_LEVEL=DEBUG,不用改程式碼。

8.3 序列化為 JSON
#

logger.add("app.json.log", serialize=True)

加上 serialize=True,每一行 log 都會以 JSON 格式輸出,方便日誌收集系統(如 ELK Stack、Datadog)解析:

{
  "text": "程式啟動\n",
  "record": {
    "elapsed": {"repr": "0:00:00.003421", "seconds": 0.003421},
    "exception": null,
    "extra": {},
    "file": {"name": "main.py", "path": "/app/main.py"},
    "function": "main",
    "level": {"icon": "ℹ️", "name": "INFO", "no": 20},
    "line": 12,
    "message": "程式啟動",
    "module": "main",
    "name": "__main__",
    "time": {"repr": "2026-04-14 10:08:23.145+08:00", "timestamp": 1744592903.145}
  }
}

8.4 非同步安全(enqueue)
#

logger.add("app.log", enqueue=True)

enqueue=True 讓寫入操作在背景 thread 執行,不阻塞主程式。在多執行緒或高效能應用中特別重要。


九. 實際專案範例:FastAPI 完整日誌設定
#

以下是整合了所有技巧的 FastAPI 應用程式日誌設定:

# logging_config.py
import sys
import logging
from loguru import logger


class InterceptHandler(logging.Handler):
    def emit(self, record):
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno
        frame, depth = logging.currentframe(), 2
        while frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1
        logger.opt(depth=depth, exception=record.exc_info).log(
            level, record.getMessage()
        )


def setup_logging(log_level: str = "INFO"):
    logger.remove()

    # 終端:彩色輸出,開發友好
    logger.add(
        sys.stderr,
        level=log_level,
        format=(
            "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
            "<level>{level: <8}</level> | "
            "<cyan>{name}</cyan>:<cyan>{line}</cyan> | "
            "<level>{message}</level>"
        ),
        colorize=True,
    )

    # 檔案:JSON 格式,每日輪替,保留 30 天
    logger.add(
        "logs/app_{time:YYYY-MM-DD}.log",
        level="DEBUG",
        serialize=True,
        rotation="00:00",
        retention="30 days",
        compression="zip",
        enqueue=True,
    )

    # 攔截所有標準 logging
    logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
# main.py
import time
from fastapi import FastAPI, Request
from loguru import logger
from logging_config import setup_logging

setup_logging(log_level="INFO")

app = FastAPI()


@app.middleware("http")
async def log_requests(request: Request, call_next):
    start = time.time()
    with logger.contextualize(
        method=request.method,
        url=str(request.url),
    ):
        logger.info("收到請求")
        response = await call_next(request)
        elapsed = (time.time() - start) * 1000
        logger.info(f"回應完成,耗時 {elapsed:.1f}ms,狀態碼 {response.status_code}")
    return response


@app.get("/hello")
@logger.catch
async def hello():
    logger.debug("進入 /hello endpoint")
    return {"message": "哈囉,拍拍君!"}

這套設定一次做到:

  • ✅ 終端有顏色,人眼友好
  • ✅ 檔案是 JSON,機器易解析
  • ✅ 每日輪替,自動壓縮清理
  • ✅ 請求自動帶上 context(method、URL)
  • ✅ 例外自動捕捉並記錄完整 traceback(含變數值)
  • ✅ 攔截 uvicorn / SQLAlchemy 等套件的 log

結語
#

loguru 是那種「用過之後回不去」的工具。它保留了標準 logging 模組的所有彈性,卻把繁瑣的設定降到最低。

拍拍君的一句話總結:

「讓日誌幫你說話,而不是讓你替日誌服務。」

下次開新專案,就從 uv add loguru 開始吧!🚀


延伸閱讀
#

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

相關文章

Python Logging:別再 print 了,用正經的方式記錄日誌吧
·6 分鐘· loading · loading
Python Logging Debug 標準庫
pathlib:優雅處理檔案路徑的現代方式
·6 分鐘· loading · loading
Python Pathlib 檔案處理 標準庫
httpx:Python 新世代 HTTP 客戶端完全攻略
·4 分鐘· loading · loading
Python Httpx HTTP Async Requests
Python match/case:結構化模式匹配完全攻略
·6 分鐘· loading · loading
Python Match Pattern-Matching Python 3.10
Python enum:打造型別安全的常數管理
·5 分鐘· loading · loading
Python Enum 型別安全 設計模式
Python contextlib:掌握 Context Manager 的進階魔法
·7 分鐘· loading · loading
Python Contextlib Context Manager With 資源管理