green snake
Photo by Pixabay on Pexels.com
目次

FastAPI×オブザーバビリティ実践ガイド:構造化ログ・メトリクス・トレース・エラー監視で「見える」APIに育てる


要約(最初に全体像)

  • 本番運用のFastAPIでは、バグより怖いのは「何が起きているか分からないこと」です。そこで鍵になるのが、ログ・メトリクス・トレース・エラー監視を組み合わせた**オブザーバビリティ(観測可能性)**です。
  • ログは**構造化ログ(JSON)**で出し、リクエストID・ユーザー・重要なビジネスイベントを追えるようにします。
  • メトリクスはPrometheusprometheus-fastapi-instrumentatorなどを使って、「レイテンシ」「エラー率」「スループット」を数値として可視化します。
  • トレースはOpenTelemetryopentelemetry-instrumentation-fastapiで、リクエスト1本の処理経路を可視化し、ボトルネックや外部サービス呼び出しを一望できるようにします。
  • エラー監視はSentryなどをFastAPIに統合し、例外発生時にスタックトレース・リクエスト情報・ユーザー情報を自動で収集します。

誰が読んで得をするか(具体的なペルソナ)

  • 学習者Aさん(個人開発・副業)
    HerokuやVPSでFastAPIを動かしているが、「たまに落ちるけど原因が分からない」「ログがバラバラで追えない」と困っている。
    → 構造化ログ+Sentryを入れることで、少なくとも「エラーが起きたら通知されて、原因を追える」状態を目指します。

  • 小規模チームBさん(受託3〜5名)
    小〜中規模の業務システムをFastAPIで作っていて、障害調査に毎回時間がかかる。
    → Prometheusメトリクス+OpenTelemetryトレースで、「どのエンドポイントがいつ遅くなっているか」「どの外部APIで詰まっているか」をすばやく特定できるようにします。

  • SaaS開発Cさん(スタートアップ)
    日々デプロイしながら機能追加しており、「リリースしても怖くない状態」をつくりたい。
    → ログ・メトリクス・トレース・エラー監視をまとめて設計し、ダッシュボードを見ながらパフォーマンス劣化やエラー増加をリアルタイムで検知する基盤を整えます。


1. オブザーバビリティの3本柱を整理する

まず、「何をどう観測するのか」をざっくり揃えます。

1.1 3本柱

  1. ログ(Logs)

    • テキストベースの記録。例外や業務イベント、デバッグ情報。
    • 形に自由度があるぶん、**構造化(JSON)**しておかないと後から集計・検索しづらくなります。
  2. メトリクス(Metrics)

    • 数値の時系列データ。例:RPS、レイテンシ、エラー率、CPU、メモリ。
    • Prometheus形式で出して、Grafanaなどでグラフ化するのが定番です。
  3. トレース(Traces)

    • 「1つのリクエストが、どのサービス・どの処理をどれだけ時間をかけて通ったか」を記録したもの。
    • OpenTelemetry+Jaeger / Tempo / Application Insightsなどがよく使われます。

そしてこれらを補完するのが、**エラー監視(Sentryなど)**です。

  • 例外発生時に、スタックトレース・リクエストヘッダ・ユーザー情報を自動で収集し、通知してくれます。

2. ログ:構造化ログとリクエストID

2.1 Pythonログの基本をFastAPIに適用する

FastAPI自体には独自のロギング機構はなく、標準のloggingモジュールとUvicornのログ設定を組み合わせて使います。

まずは、JSON形式で出力するフォーマッタを用意します。

# app/core/logging.py
import json
import logging
import sys
from typing import Any

class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload: dict[str, Any] = {
            "level": record.levelname,
            "logger": record.name,
            "msg": record.getMessage(),
        }
        if hasattr(record, "request_id"):
            payload["request_id"] = record.request_id
        if record.exc_info:
            payload["exc_info"] = self.formatException(record.exc_info)
        return json.dumps(payload, ensure_ascii=False)

def setup_logging(level: str = "INFO") -> None:
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JsonFormatter())
    root = logging.getLogger()
    root.handlers[:] = [handler]
    root.setLevel(level)

2.2 リクエストIDを付与するミドルウェア

ログを追いやすくするには、リクエスト単位のIDを付けて、同じリクエストから出たログを紐付けるのが大切です。

# app/middleware/request_id.py
import uuid
from starlette.types import ASGIApp, Receive, Scope, Send

class RequestIDMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope["type"] != "http":
            return await self.app(scope, receive, send)

        request_id = str(uuid.uuid4())
        scope["state"] = scope.get("state", {})
        scope["state"]["request_id"] = request_id

        async def send_wrapper(message):
            if message["type"] == "http.response.start":
                headers = [(b"x-request-id", request_id.encode())]
                message.setdefault("headers", []).extend(headers)
            await send(message)

        await self.app(scope, receive, send_wrapper)

2.3 ロガーからリクエストIDを引き回す

FastAPIの依存性でrequestを受け取り、ログにrequest_idを含めるヘルパーを用意します。

# app/deps/logging.py
import logging
from fastapi import Request

def get_logger(request: Request) -> logging.LoggerAdapter:
    base = logging.getLogger("app")
    request_id = getattr(request.state, "request_id", None)
    return logging.LoggerAdapter(base, extra={"request_id": request_id})
# app/main.py
from fastapi import FastAPI, Depends
from app.core.logging import setup_logging
from app.middleware.request_id import RequestIDMiddleware
from app.deps.logging import get_logger

setup_logging()

app = FastAPI(title="Observable API")
app.add_middleware(RequestIDMiddleware)

@app.get("/hello")
def hello(logger = Depends(get_logger)):
    logger.info("hello endpoint called")
    return {"message": "hello"}

これで、ログにはrequest_id入りのJSONが出て、クライアントにもX-Request-IDレスポンスヘッダが返るようになります。ログ集約基盤(LokiやCloud Loggingなど)で、このIDをキーに検索できるようになると、障害調査がかなり楽になります。


3. メトリクス:Prometheusでレイテンシとエラー率を見る

3.1 Prometheusメトリクスの基本

メトリクスでは、特に次の3つが重要です(いわゆるREDメトリクス)。

  • Rate:リクエスト数(RPS)
  • Errors:エラー割合(4xx/5xx)
  • Duration:レイテンシ(P50/P95/P99)

FastAPIでは、prometheus-fastapi-instrumentatorというライブラリを使うと、これらを簡単に計測・エクスポートできます。

3.2 インストールと基本設定

pip install prometheus-fastapi-instrumentator prometheus-client
# app/metrics.py
from prometheus_fastapi_instrumentator import Instrumentator

def setup_metrics(app):
    Instrumentator().instrument(app).expose(app, endpoint="/metrics")
# app/main.py
from fastapi import FastAPI
from app.metrics import setup_metrics

app = FastAPI(title="Observable API")
setup_metrics(app)

これで、/metrics エンドポイントにPrometheus形式のメトリクスが出力されるようになります。

例:

  • http_request_duration_seconds_bucket{method="GET",path="/hello",status="2xx",le="0.1"} 42
  • http_requests_total{method="GET",path="/hello",status="2xx"} 100

3.3 Gunicornなどマルチプロセス対応

Gunicorn+UvicornWorkerのようなマルチプロセス構成では、Prometheusクライアントの「マルチプロセスモード」を有効にする必要があります。prometheus-fastapi-instrumentatorは、prometheus_multiproc_dir環境変数を設定するだけでマルチプロセス対応してくれます。

export prometheus_multiproc_dir=./metrics
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker

実運用では、この/metricsをPrometheusがスクレイプし、Grafanaなどでダッシュボード化する構成がよく使われます。


4. トレース:OpenTelemetryで1リクエストの旅路を追う

メトリクスで「どこで遅いか」の傾向は分かりますが、「なぜそのリクエストだけ遅いのか」を調べるにはトレースが役立ちます。

4.1 OpenTelemetry FastAPIインストゥルメンテーション

OpenTelemetryには、FastAPI向けの自動インストゥルメンテーションopentelemetry-instrumentation-fastapiがあります。

pip install \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp \
  opentelemetry-instrumentation-fastapi \
  opentelemetry-instrumentation-logging \
  opentelemetry-instrumentation-requests \
  opentelemetry-instrumentation-httpx

4.2 最小構成の例(OTLPエクスポート)

# app/otel.py
from opentelemetry import trace
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

def setup_tracing(service_name: str = "fastapi-app"):
    resource = Resource(attributes={
        SERVICE_NAME: service_name,
    })
    provider = TracerProvider(resource=resource)
    processor = BatchSpanProcessor(OTLPSpanExporter())
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)
# app/main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor

from app.otel import setup_tracing

setup_tracing(service_name="observable-fastapi")

app = FastAPI(title="Observable API")

FastAPIInstrumentor.instrument_app(app)
LoggingInstrumentor().instrument()
RequestsInstrumentor().instrument()
HTTPXClientInstrumentor().instrument()

OTLPエクスポータは、OpenTelemetry Collectorや各種APM(Grafana Tempo, Jaeger, Azure Monitor, Datadogなど)にトレースを送るための共通プロトコルです。

4.3 何が見えるようになるか

  • 各リクエストに対して「root span」が生成され、FastAPIのエンドポイント名やステータスコードがタグとして付与される
  • その中に、DBクエリ・外部HTTP(requests/httpx)の呼び出しが「子スパン」としてぶら下がる
  • ダッシュボード上で、1本のリクエストがどこで何msかかっているかを視覚的に確認できる

これにより、「ある時間帯だけ特定エンドポイントが遅い」「特定の外部APIへの呼び出しでタイムアウトが頻発している」といった問題をすばやく特定できます。


5. エラー監視:Sentryで例外を逃さない

5.1 Sentry SDKのFastAPI統合

Sentryは、Python/FastAPI向けのSDKと統合ガイドを提供しています。

インストールは次のようになります。

pip install "sentry-sdk[fastapi]"

基本的な初期化コードはとてもシンプルです。

# app/sentry_setup.py
import sentry_sdk

def setup_sentry(dsn: str, env: str = "dev"):
    sentry_sdk.init(
        dsn=dsn,
        enable_tracing=True,   # パフォーマンスモニタリングを有効化
        traces_sample_rate=0.1, # 必要に応じてサンプリングレートを調整
        environment=env,
    )
# app/main.py
from fastapi import FastAPI
from sentry_sdk.integrations.fastapi import FastApiIntegration
from app.sentry_setup import setup_sentry

setup_sentry(dsn="https://examplePublicKey@o0.ingest.sentry.io/0", env="prod")

app = FastAPI(title="Observable API", lifespan=None)

# sentry-sdkのFastAPI統合は、init時に自動で有効になる(ガイドに従う)

5.2 どんな情報が送られるか

  • 例外発生時のスタックトレース
  • リクエストURL・HTTPメソッド・ヘッダ・クエリなど
  • (設定すれば)ユーザーIDや組織IDなどのコンテキスト

ダッシュボード上で、同種のエラーがグルーピングされ、「いつから増え始めたか」「どのバージョンで増えたか」などが確認できます。フロントエンド側にもSentryを入れておけば、「このバックエンドエラーはどの画面から来ているか」までたどれるようになります。


6. 4つをどう組み合わせるか:設計の指針

ここまでで、ログ・メトリクス・トレース・エラー監視を個別に見てきました。最後に「どう組み合わせるか」の考え方を整理します。

6.1 オブザーバビリティ設計のゴール

  • 障害が起きたときに、5〜10分以内に原因のアタリがつけられること
  • リリース後に、レイテンシやエラー率の劣化をすぐに検知できること
  • パフォーマンスチューニングの際に、どこを改善すべきか客観的に判断できること

このゴールから逆算して、次のような役割分担を決めると整理しやすくなります。

  • ログ:詳細な文脈(パラメータ・ビジネスイベント)
  • メトリクス:全体の健康状態(REDメトリクス)
  • トレース:リクエストごとの「旅路」とボトルネック
  • エラー監視:例外の収集と通知、影響範囲の把握

6.2 ユースケース別の観測ポイント

  • 性能問題を追いたいとき

    • メトリクスでレイテンシが跳ねている時間帯を特定
    • その時間帯のサンプルトレースから、どの外部サービス・どのクエリが遅いかを見る
    • 該当部分のコードでログを詳しく出し、再現させて検証
  • エラー増加を追いたいとき

    • Sentryで新しいエラーが増えていないか確認(リリース時刻と突き合わせる)
    • 該当エラーのサンプルトレースを見て、「成功パターンとの違い」を比較
    • 必要ならログにビジネスキー(注文IDなど)を出し、影響範囲を特定

こうした流れが、日常的な運用で自然に回せると、「怖くないデプロイ」に近づいていきます。


7. サンプル構成:小規模チームでの現実的な一例

7.1 開発〜ステージング〜本番の3環境

  • 開発(dev)

    • ログ:コンソール出力(JSON)
    • メトリクス:ローカルPrometheus(任意)
    • トレース:ローカルJaeger(任意)
    • Sentry:無効、または別プロジェクト
  • ステージング(stg)

    • ログ:集中ログ基盤へ送信
    • メトリクス:ステージング用Prometheus+Grafana
    • トレース:ステージング用トレースバックエンド
    • Sentry:environment=stg で有効
  • 本番(prod)

    • ログ:本番用ログ基盤(Loki/Cloud Loggingなど)
    • メトリクス:本番用Prometheus+Grafana、ダッシュボードとアラート
    • トレース:本番トレースバックエンド(サンプリングレートは調整)
    • Sentry:environment=prod、通知連携(Slack/メール)

7.2 FastAPI側の共通初期化

# app/bootstrap.py
from app.core.logging import setup_logging
from app.otel import setup_tracing
from app.sentry_setup import setup_sentry
from app.metrics import setup_metrics

from app.core.settings import get_settings

def bootstrap(app):
    settings = get_settings()
    setup_logging(settings.log_level)
    setup_tracing(service_name=settings.app_name)
    if settings.sentry_dsn:
        setup_sentry(dsn=settings.sentry_dsn, env=settings.env)
    setup_metrics(app)
# app/main.py
from fastapi import FastAPI
from app.bootstrap import bootstrap
from app.middleware.request_id import RequestIDMiddleware

app = FastAPI(title="Observable API")

bootstrap(app)
app.add_middleware(RequestIDMiddleware)

設定値(DSN, OTLPエンドポイント, 環境名など)は、以前の記事で扱ったpydantic-settingsなどで環境変数から注入すれば、環境ごとに簡単に切り替えられます。


8. よくある落とし穴と対策

症状 原因 対策
ログがぐちゃぐちゃで検索しづらい プレーンテキスト・フォーマットばらばら JSON構造化+特定フィールド(request_id, user_id)を必ず出す
/metrics が重くなる ラベルの切りすぎ・高カーディナリティ ユーザーIDなど極端に多い値をラベルに使わない
トレースデータが多すぎてコスト増 サンプリングなしで全トレース送信 環境ごとにsamples_rateやサンプリングポリシーを調整
Sentryにノイズが多い 想定内のエラーも全部送っている 業務上の「正常系エラー」はハンドリングしてSentryに送らない
devとprodで見える情報が違いすぎて混乱 設定値や環境変数の差分が大きい 環境ごとの差分を最小限にし、envenvironmentをきちんと分ける

9. 導入ロードマップ(段階的に進めるために)

最後に、「これから実際にオブザーバビリティを整えていくときの道筋」を段階的にまとめます。

  1. 構造化ログ+リクエストID
    • JSONログ+X-Request-IDを導入し、ローカルとステージングで整合性を確認。
  2. Sentryなどのエラー監視
    • 例外が起きたら自動で通知される状態を作り、緊急度の高いエラーから順に潰していく。
  3. Prometheusメトリクス+Grafanaダッシュボード
    • REDメトリクスを中心にダッシュボードを作り、ピーク時間帯やリリース直後の動きを確認。
  4. OpenTelemetryトレース
    • 遅いエンドポイントや外部サービス呼び出しがある箇所に絞って、トレースを見ながらボトルネックを特定。
  5. アラートとSLOの設計
    • 「P95レイテンシ」「エラー率」などの指標に対して、しきい値とアラートルールを定める。
  6. 継続的な改善
    • 新しい機能を追加するたびに、「この機能をどう観測するか?」を1つセットで考えるクセをつける。

参考リンク(深く学びたい方向け)


まとめ

  • FastAPIの本番運用では、「速さ」だけでなく「見えること」が大切です。ログ・メトリクス・トレース・エラー監視の4つを組み合わせることで、障害や性能劣化にすばやく気づき、原因へたどり着けるようになります。
  • 構造化ログとリクエストIDは、どんな規模のプロジェクトでもすぐに役立つ「第一歩」です。その上に、PrometheusメトリクスとOpenTelemetryトレース、Sentryによるエラー監視を少しずつ重ねていくイメージで整えていくと、無理なくオブザーバビリティを高めていけます。
  • すべてを一気に入れる必要はありません。今の自分たちの規模・フェーズで「一番つらいところ」から、1つずつ観測の手を伸ばしていきましょう。

わたしも、あなたのFastAPIアプリが「見える」ようになって、安心して改善を続けられることを、そっと応援しています。


投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)