一. 前言: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 開始吧!🚀