FastAPI 教學 – 測試與除錯:Logging 設定與除錯
簡介
在開發 FastAPI 應用程式時,日誌(Logging) 是除錯與維運不可或缺的工具。良好的日誌配置不僅能讓開發者快速定位問題,還能在正式環境中提供運行狀態的可觀測性,協助團隊在發生異常時迅速回應。
本單元將說明如何在 FastAPI 中正確設定 logging、結合 uvicorn、自訂日誌格式與層級,並示範在測試與除錯階段常用的技巧。文章以 繁體中文(台灣) 撰寫,適合剛接觸 FastAPI 的新手以及想要提升除錯能力的中階開發者。
核心概念
1️⃣ 為什麼要自行設定 Logging?
FastAPI 預設使用 uvicorn 作為 ASGI 伺服器,它本身已提供基本的日誌輸出。但在實務專案中,我們往往需要:
- 區分不同層級(DEBUG、INFO、WARNING、ERROR、CRITICAL)以控制訊息密度。
- 自訂輸出格式(時間、模組、請求 ID 等),方便在 log aggregation 平台(如 ELK、Grafana Loki)中搜尋。
- 將日誌寫入檔案或外部服務,避免只在 console 中顯示而失去持久紀錄。
- 在測試環境中關閉或調整日誌,避免大量噪音干擾測試結果。
重點:日誌不是除錯的「最後手段」,而是 持續觀測 應用的重要手段。
2️⃣ Python 標準 logging 模組
Python 內建的 logging 模組提供了完整的日誌系統,主要概念如下:
| 物件 | 說明 |
|---|---|
| Logger | 產生日誌訊息的入口,通常以 logging.getLogger(__name__) 取得。 |
| Handler | 決定日誌訊息的輸出位置(Console、File、HTTP 等)。 |
| Formatter | 定義日誌訊息的字串格式。 |
| Filter | 進一步過濾訊息(例如只允許特定模組)。 |
以下示範最基礎的設定方式:
import logging
logging.basicConfig(
level=logging.INFO, # 設定全域層級
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
logger.info("FastAPI logging 初始化完成")
3️⃣ 在 FastAPI 中整合 Logging
3.1 基本整合
FastAPI 本身是一個 APIRouter,我們可以在路由函式內直接使用 logger:
from fastapi import FastAPI, Request
import logging
app = FastAPI()
logger = logging.getLogger("myapp")
@app.get("/items/{item_id}")
async def read_item(item_id: int, request: Request):
logger.info(f"收到請求: {request.method} {request.url}")
# 你的業務邏輯...
return {"item_id": item_id}
小技巧:將 logger 命名為套件或服務名稱(如
"myapp"),在多模組專案中更易於區分。
3.2 使用 uvicorn 的內建 logger
uvicorn 會自動建立名為 uvicorn.error(錯誤)與 uvicorn.access(存取)兩個 logger。若想統一管理,可在 logging.config.dictConfig 中覆寫:
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
},
"file": {
"class": "logging.FileHandler",
"filename": "logs/app.log",
"formatter": "standard",
"level": "DEBUG",
},
},
"loggers": {
"uvicorn.error": {"handlers": ["console", "file"], "level": "INFO"},
"uvicorn.access": {"handlers": ["console"], "level": "INFO"},
"myapp": {"handlers": ["console", "file"], "level": "DEBUG"},
},
}
logging.config.dictConfig(LOGGING_CONFIG)
提醒:
disable_existing_loggers設為False,才能保留uvicorn內建的 logger。
3.3 為每個請求產生唯一 ID(Correlation ID)
在微服務或分散式系統中,追蹤每筆請求的 ID 能大幅提升除錯效率。以下示範使用 starlette 的中介層(middleware)自動注入 X-Request-ID:
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
# 把 request_id 放入 log record
logging.LoggerAdapter(logging.getLogger("myapp"), {"request_id": request_id})
# 也可以把 request_id 存在 request.state,供後續使用
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
app.add_middleware(RequestIDMiddleware)
接著在 formatter 中加入 %(request_id)s:
"format": "%(asctime)s | %(levelname)s | %(name)s | %(request_id)s | %(message)s",
這樣每筆日誌都會自動帶上請求 ID,方便在 ElasticSearch 中搜尋。
3.4 與 loguru 結合(進階選項)
如果想要更簡潔的 API,loguru 是一個受歡迎的替代方案。以下示範把 loguru 與 FastAPI 結合,仍保留 uvicorn 的日誌:
from loguru import logger
import sys
# 移除預設的根 logger,避免重複
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time} | {level} | {name} | {message}")
# 讓 uvicorn 使用 loguru
import uvicorn
import logging
class InterceptHandler(logging.Handler):
def emit(self, record):
# 轉換標準 logging 訊息到 loguru
logger_opt = logger.opt(depth=6, exception=record.exc_info)
logger_opt.log(record.levelname, record.getMessage())
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
注意:
depth參數需要根據呼叫堆疊調整,否則顯示的檔案與行號會不正確。
4️⃣ 在測試環境中控制 Logging
測試(例如使用 pytest)時,我們往往不希望看到大量的 INFO/DEBUG 訊息。可以在 conftest.py 中統一設定:
# conftest.py
import logging
import pytest
@pytest.fixture(autouse=True)
def set_test_logging():
logging.getLogger("myapp").setLevel(logging.WARNING)
yield
# 測試結束後恢復
logging.getLogger("myapp").setLevel(logging.DEBUG)
或直接在 pytest.ini 中使用 log_cli:
[pytest]
log_cli = true
log_cli_level = WARNING
log_cli_format = %(asctime)s | %(levelname)s | %(message)s
這樣測試執行時只會顯示 WARNING 以上的訊息,保持輸出乾淨。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
| 忘記關閉檔案處理程序 | FileHandler 若未正確關閉,可能導致檔案被鎖住或遺失最後的日誌。 |
使用 logging.config.dictConfig,或在程式結束前呼叫 logging.shutdown()。 |
| 層級設定不一致 | 在不同模組使用不同層級,導致關鍵錯誤被過濾掉。 | 統一在 logging.conf 或 dictConfig 中設定全域層級,僅在特殊情況下局部調整。 |
| 日誌格式缺少關鍵資訊 | 沒有時間戳、請求 ID 或模組名稱,難以追蹤問題。 | 在 formatter 中加入 %(asctime)s, %(name)s, %(request_id)s 等欄位。 |
| 在 async 環境中使用阻塞 I/O | FileHandler 會同步寫檔,可能阻塞 event loop。 |
使用 ConcurrentLogHandler 或將日誌寫入 queue,最後由背景執行緒處理。 |
| 測試時未調整 log level | 大量 DEBUG 訊息會干擾測試報告。 | 於測試設定檔或 fixture 中降低 log level。 |
實際應用場景
API 監控與警示
- 透過 ERROR 或 CRITICAL 日誌,結合 Prometheus Alertmanager,當服務拋出未捕獲例外時即時發送 Slack 通知。
分布式追蹤(Distributed Tracing)
- 配合 OpenTelemetry,將
request_id注入 trace context,讓日誌與 trace 完美對應。
- 配合 OpenTelemetry,將
客製化存取日誌(Access Log)
- 自訂
uvicorn.access的 formatter,記錄client_ip,method,path,status_code,方便分析流量與安全事件。
- 自訂
CI/CD 中的日誌驗證
- 在 GitHub Actions 或 GitLab CI 中,使用
pytest+log_cli_level=ERROR,確保只有錯誤訊息會導致 pipeline 失敗。
- 在 GitHub Actions 或 GitLab CI 中,使用
多租戶(Multi‑tenant)系統
- 為每個租戶產生獨立的 log file,使用
logging.handlers.TimedRotatingFileHandler按天切割,並在檔名加入租戶代碼。
- 為每個租戶產生獨立的 log file,使用
總結
- Logging 是 FastAPI 除錯與運維的基礎,正確的設定能讓開發、測試與生產環境保持一致的觀測能力。
- 透過 Python 標準 logging、
uvicorn內建 logger、或 loguru,我們可以彈性調整層級、格式與輸出位置。 - 自訂 Middleware 讓每筆請求自帶 Correlation ID,大幅提升跨服務除錯效率。
- 在 測試 時調低 log level,避免噪音干擾測試結果;同時確保在正式環境使用 FileHandler 或集中式日誌平台(ELK、Loki)。
- 最佳實踐:統一配置、避免阻塞 I/O、加入關鍵資訊、在 CI/CD 中驗證日誌。
掌握以上技巧後,你的 FastAPI 專案將能在開發階段快速定位問題,並在上線後提供可靠的可觀測性,為團隊的持續交付奠定堅實基礎。祝你開發順利,日誌寫得漂亮! 🚀