本文 AI 產出,尚未審核

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.confdictConfig 中設定全域層級,僅在特殊情況下局部調整。
日誌格式缺少關鍵資訊 沒有時間戳、請求 ID 或模組名稱,難以追蹤問題。 在 formatter 中加入 %(asctime)s, %(name)s, %(request_id)s 等欄位。
在 async 環境中使用阻塞 I/O FileHandler 會同步寫檔,可能阻塞 event loop。 使用 ConcurrentLogHandler 或將日誌寫入 queue,最後由背景執行緒處理。
測試時未調整 log level 大量 DEBUG 訊息會干擾測試報告。 於測試設定檔或 fixture 中降低 log level。

實際應用場景

  1. API 監控與警示

    • 透過 ERRORCRITICAL 日誌,結合 Prometheus Alertmanager,當服務拋出未捕獲例外時即時發送 Slack 通知。
  2. 分布式追蹤(Distributed Tracing)

    • 配合 OpenTelemetry,將 request_id 注入 trace context,讓日誌與 trace 完美對應。
  3. 客製化存取日誌(Access Log)

    • 自訂 uvicorn.access 的 formatter,記錄 client_ip, method, path, status_code,方便分析流量與安全事件。
  4. CI/CD 中的日誌驗證

    • 在 GitHub Actions 或 GitLab CI 中,使用 pytest + log_cli_level=ERROR,確保只有錯誤訊息會導致 pipeline 失敗。
  5. 多租戶(Multi‑tenant)系統

    • 為每個租戶產生獨立的 log file,使用 logging.handlers.TimedRotatingFileHandler 按天切割,並在檔名加入租戶代碼。

總結

  • Logging 是 FastAPI 除錯與運維的基礎,正確的設定能讓開發、測試與生產環境保持一致的觀測能力。
  • 透過 Python 標準 logginguvicorn 內建 logger、或 loguru,我們可以彈性調整層級、格式與輸出位置。
  • 自訂 Middleware 讓每筆請求自帶 Correlation ID,大幅提升跨服務除錯效率。
  • 測試 時調低 log level,避免噪音干擾測試結果;同時確保在正式環境使用 FileHandler 或集中式日誌平台(ELK、Loki)。
  • 最佳實踐:統一配置、避免阻塞 I/O、加入關鍵資訊、在 CI/CD 中驗證日誌。

掌握以上技巧後,你的 FastAPI 專案將能在開發階段快速定位問題,並在上線後提供可靠的可觀測性,為團隊的持續交付奠定堅實基礎。祝你開發順利,日誌寫得漂亮! 🚀