FastAPI×オブザーバビリティ実践ガイド:構造化ログ・メトリクス・トレース・エラー監視で「見える」APIに育てる
要約(最初に全体像)
- 本番運用のFastAPIでは、バグより怖いのは「何が起きているか分からないこと」です。そこで鍵になるのが、ログ・メトリクス・トレース・エラー監視を組み合わせた**オブザーバビリティ(観測可能性)**です。
- ログは**構造化ログ(JSON)**で出し、リクエストID・ユーザー・重要なビジネスイベントを追えるようにします。
- メトリクスはPrometheus+
prometheus-fastapi-instrumentatorなどを使って、「レイテンシ」「エラー率」「スループット」を数値として可視化します。 - トレースはOpenTelemetry+
opentelemetry-instrumentation-fastapiで、リクエスト1本の処理経路を可視化し、ボトルネックや外部サービス呼び出しを一望できるようにします。 - エラー監視はSentryなどをFastAPIに統合し、例外発生時にスタックトレース・リクエスト情報・ユーザー情報を自動で収集します。
誰が読んで得をするか(具体的なペルソナ)
-
学習者Aさん(個人開発・副業)
HerokuやVPSでFastAPIを動かしているが、「たまに落ちるけど原因が分からない」「ログがバラバラで追えない」と困っている。
→ 構造化ログ+Sentryを入れることで、少なくとも「エラーが起きたら通知されて、原因を追える」状態を目指します。 -
小規模チームBさん(受託3〜5名)
小〜中規模の業務システムをFastAPIで作っていて、障害調査に毎回時間がかかる。
→ Prometheusメトリクス+OpenTelemetryトレースで、「どのエンドポイントがいつ遅くなっているか」「どの外部APIで詰まっているか」をすばやく特定できるようにします。 -
SaaS開発Cさん(スタートアップ)
日々デプロイしながら機能追加しており、「リリースしても怖くない状態」をつくりたい。
→ ログ・メトリクス・トレース・エラー監視をまとめて設計し、ダッシュボードを見ながらパフォーマンス劣化やエラー増加をリアルタイムで検知する基盤を整えます。
1. オブザーバビリティの3本柱を整理する
まず、「何をどう観測するのか」をざっくり揃えます。
1.1 3本柱
-
ログ(Logs)
- テキストベースの記録。例外や業務イベント、デバッグ情報。
- 形に自由度があるぶん、**構造化(JSON)**しておかないと後から集計・検索しづらくなります。
-
メトリクス(Metrics)
- 数値の時系列データ。例:RPS、レイテンシ、エラー率、CPU、メモリ。
- Prometheus形式で出して、Grafanaなどでグラフ化するのが定番です。
-
トレース(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"} 42http_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で見える情報が違いすぎて混乱 | 設定値や環境変数の差分が大きい | 環境ごとの差分を最小限にし、envとenvironmentをきちんと分ける |
9. 導入ロードマップ(段階的に進めるために)
最後に、「これから実際にオブザーバビリティを整えていくときの道筋」を段階的にまとめます。
- 構造化ログ+リクエストID
- JSONログ+
X-Request-IDを導入し、ローカルとステージングで整合性を確認。
- JSONログ+
- Sentryなどのエラー監視
- 例外が起きたら自動で通知される状態を作り、緊急度の高いエラーから順に潰していく。
- Prometheusメトリクス+Grafanaダッシュボード
- REDメトリクスを中心にダッシュボードを作り、ピーク時間帯やリリース直後の動きを確認。
- OpenTelemetryトレース
- 遅いエンドポイントや外部サービス呼び出しがある箇所に絞って、トレースを見ながらボトルネックを特定。
- アラートとSLOの設計
- 「P95レイテンシ」「エラー率」などの指標に対して、しきい値とアラートルールを定める。
- 継続的な改善
- 新しい機能を追加するたびに、「この機能をどう観測するか?」を1つセットで考えるクセをつける。
参考リンク(深く学びたい方向け)
-
FastAPI
-
ログ
-
メトリクス/Prometheus
-
OpenTelemetry/トレース
-
エラー監視(Sentry)
-
オブザーバビリティ全体像
まとめ
- FastAPIの本番運用では、「速さ」だけでなく「見えること」が大切です。ログ・メトリクス・トレース・エラー監視の4つを組み合わせることで、障害や性能劣化にすばやく気づき、原因へたどり着けるようになります。
- 構造化ログとリクエストIDは、どんな規模のプロジェクトでもすぐに役立つ「第一歩」です。その上に、PrometheusメトリクスとOpenTelemetryトレース、Sentryによるエラー監視を少しずつ重ねていくイメージで整えていくと、無理なくオブザーバビリティを高めていけます。
- すべてを一気に入れる必要はありません。今の自分たちの規模・フェーズで「一番つらいところ」から、1つずつ観測の手を伸ばしていきましょう。
わたしも、あなたのFastAPIアプリが「見える」ようになって、安心して改善を続けられることを、そっと応援しています。
