構造化ログ設計
このドキュメントの目的
構造化ログの設計原則を理解し、効果的なログ設計ができるようになることが目標です。なぜ構造化ログが必要なのか、どのようにログレベルやコンテキスト情報を設計するのか、そしてログ集約パイプラインの全体像を把握します。
IDサービスを題材にした具体例を使用しますが、設計原則自体はあらゆるSaaSに共通して適用できます。
なぜ構造化ログか
テキストログの限界
従来のテキストログでは、大規模システムでの調査に限界があります。
テキストログの例:
2026-03-04 10:23:45 INFO Authentication successful for user alice on tenant-a
2026-03-04 10:23:46 ERROR Failed to validate OTP code for user bob on tenant-b: expired
2026-03-04 10:23:46 WARN Rate limit approaching for client app-1 on tenant-a
テキストログの問題点:
Q: 「テナントBで過去1時間にOTPエラーが何件あった?」
テキストログの場合:
┌──────────────────────────────────────────┐
│ grep "tenant-b" | grep "OTP" | grep │
│ "ERROR" | wc -l │
│ │
│ → フォーマットが変わると壊れる │
│ → "tenant-b" が別のフィールドに含まれる │
│ 可能性がある │
│ → 正規表現のメンテナンスが大変 │
└──────────────────────────────────────────┘
構造化ログの利点
構造化ログ(JSON形式)では、各フィールドが明確に分離されています。
構造化ログの例(JSON):
{
"timestamp": "2026-03-04T10:23:46.123Z",
"level": "ERROR",
"logger": "org.idp.server.otp.OtpVerifier",
"message": "OTP verification failed",
"tenant_id": "tenant-b",
"user_id": "user-bob-456",
"request_id": "req-abc-123",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"error_code": "OTP_EXPIRED",
"otp_type": "TOTP",
"client_id": "app-2"
}
Q: 「テナントBで過去1時間にOTPエラーが何件あった?」
構造化ログの場合:
┌──────────────────────────────────────────┐
│ 検索条件: │
│ tenant_id = "tenant-b" │
│ AND error_code LIKE "OTP_*" │
│ AND level = "ERROR" │
│ AND timestamp > now() - 1h │
│ │
│ → フィールド単位で正確に検索 │
│ → 集計・グラフ化も容易 │
│ → フォーマット変更の影響を受けにくい │
└──────────────────────────────────────────┘
比較まとめ
| 観点 | テキストログ | 構造化ログ |
|---|---|---|
| 検索性 | 正規表現に依存、壊れやすい | フィールド単位の正確な検索 |
| 集計・分析 | 困難(パース処理が必要) | 容易(フィールドで集計可能) |
| 機械処理 | パーサーの開発・保守が必要 | JSONパーサーで統一的に処理 |
| 人間の可読性 | 高い | やや低い(ツールで補完) |
| トレースとの連携 | 困難 | trace_id フィールドで容易 |
| ストレージ効率 | 高い | やや低い(フィールド名の重複) |
ログレベル設計
ログレベルの使い分け基準
ログレベルの選択基準を明確にすることで、運用時のノイズを減らし、重要なイベントを見逃さないようにします。
| レベル | 基準 | 誰が対応するか | IDサービスでの例 |
|---|---|---|---|
| ERROR | 即座の対応が必要な問題 | オンコール担当者 | DB接続失敗、JWT署名鍵の読み込みエラー |
| WARN | 注意が必要だが即座の対応は不要 | 翌営業日に確認 | レートリミット接近、証明書の有効期限が30日以内 |
| INFO | 正常な業務イベントの記録 | 調査時に参照 | ユーザー認証成功、トークン発行、テナント作成 |
| DEBUG | 開発・調査用の詳細情報 | 開発者 | リクエストパラメータの詳細、SQLクエリ |
ログレベル判断のフローチャート
ログレベルの判断基準:
イベントが発生
│
▼
サービスの機能に影響があるか?
│
┌───┴───┐
Yes No
│ │
▼ ▼
自動復旧 ビジネス上の
できるか? 意味がある
│ イベントか?
┌┴──┐ ┌───┴───┐
Yes No Yes No
│ │ │ │
▼ ▼ ▼ ▼
WARN ERROR INFO DEBUG
ERRORログの設計指針
ERRORは「アクション可能」なイベントにのみ使用します。
良いERRORログの基準:
┌─────────────────────────────────────────────────┐
│ 1. 誰かが対応する必要がある │
│ 2. 対応方法が存在する(ランブックがある) │
│ 3. ユーザーに影響がある │
└─────────────────────────────────────────────────┘
OK: ERROR - データベース接続が失敗した(リトライ上限超過)
OK: ERROR - JWT署名鍵ファイルが見つからない
NG: ERROR - ユーザーがパスワードを間違えた(正常なビジネスイベント → INFO)
NG: ERROR - 無効なリクエストパラメータ(クライアントの問題 → WARN or INFO)
コンテキスト情報の設計
リクエストスコープのコンテキスト伝播
リクエストのライフサイクル全体で一貫したコンテキスト情報を付与します。この考え方は言語やフレームワークを問わず共通ですが、実現方法は言語ごとに異なります。
リクエストスコープのコンテキスト伝播:
リクエスト受信
│
▼
┌─────────────────────────┐
│ コンテキスト初期化 │
│ ・request_id を生成/取得 │
│ ・tenant_id を抽出 │
│ ・trace_id を取得 │
│ → リクエストスコープに保存│
└────────┬────────────────┘
│
┌─────┼─────┐
▼ ▼ ▼
Handler Service Repository ← すべてのログに自動付与
│ │ │
└─────┼─────┘
│
▼
┌─────────────────────────┐
│ コンテキストクリア │
│ → スコープを破棄 │
└─────────────────────────┘
言語ごとの実現方法:
| 言語/フレームワーク | 仕組み | 概要 |
|---|---|---|
| Java(SLF4J/Logback) | MDC(Mapped Diagnostic Context) | ThreadLocalにキーバリューを保存、ログ出力時に自動付与 |
| .NET | ILogger + BeginScope | スコープ内のログに構造化データを自動付与 |
| Node.js | AsyncLocalStorage | 非同期コンテキストでリクエストスコープを実現 |
| Go | context.Context | 関数引数としてコンテキストを明示的に伝播 |
| Python | structlog + contextvars | コンテキスト変数でリクエストスコープを実現 |
推奨コンテキストフィールド
| フィールド | 説明 | 付与タイミング | 必須/推奨 |
|---|---|---|---|
request_id | リクエスト一意識別子 | リクエスト受信時 | 必須 |
tenant_id | テナント識別子 | 認証/ルーティング時 | 必須(マルチテナント) |
trace_id | 分散トレースのトレースID | リクエスト受信時 | 推奨 |
span_id | 分散トレースのスパンID | リクエスト受信時 | 推奨 |
user_id | ユーザー識別子 | 認証完了後 | 推 奨 |
client_id | OAuthクライアント識別子 | クライアント認証後 | 推奨 |
session_id | セッション識別子 | セッション確立後 | 推奨 |
コンテキスト情報の活用例
障害調査でのコンテキスト活用:
1. アラート受信: 「認証エラー率上昇」
│
▼
2. tenant_id でフィルタ
→ テナントBに集中していることを特定
│
▼
3. request_id で1リクエストを追跡
→ 認証→OTP検証→外部API呼び出しの流れを確認
│
▼
4. trace_id でトレースと連携
→ 外部SMS APIのレイテンシが原因と特定
│
▼
5. client_id でクライアントを特定
→ 特定のモバイルアプリからのリクエストに集中
ログ集約パイプライン
全体アーキテクチャ
ログ集約パイプラインの構成:
アプリケーション 収集・転送 保存・検索
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ IDサービス │ │ Fluent Bit / │ │ Elasticsearch│
│ (JSON出力) │─────▶│ Fluentd / │───▶│ / Loki / │
│ │ │ Vector │ │ CloudWatch │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌──────────────┐ ┌──────────────┐ │
│ 他サービス │─────▶│ 同上 │───────────┘
└──────────────┘ └──────────────┘ ┌──────▼───────┐
│ Grafana / │
│ Kibana │
│ (可視化) │
└──────────────┘
パイプライン設計のポイント
| 設計観点 | 推奨アプローチ | 理由 |
|---|---|---|
| 出力形式 | アプリはstdout/stderrにJSON出力 | コンテナ環境との親和性 |
| 収集方式 | サイドカーまたはDaemonSet | アプリとの疎結合 |
| バッファリング | 収集エージェントでバッファ | ログ欠損の防止 |
| フィルタリング | 収集エージェントで不要ログを除外 | ストレージコスト削減 |
| 保存期間 | ホットデータ30日、コールドデータ1年 | コストと調査要件のバランス |
ログのアンチパターン
避けるべきパターン
| アンチパターン | 問題点 |
|---|