メインコンテンツまでスキップ

構造化ログ設計

このドキュメントの目的

構造化ログの設計原則を理解し、効果的なログ設計ができるようになることが目標です。なぜ構造化ログが必要なのか、どのようにログレベルやコンテキスト情報を設計するのか、そしてログ集約パイプラインの全体像を把握します。

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にキーバリューを保存、ログ出力時に自動付与
.NETILogger + BeginScopeスコープ内のログに構造化データを自動付与
Node.jsAsyncLocalStorage非同期コンテキストでリクエストスコープを実現
Gocontext.Context関数引数としてコンテキストを明示的に伝播
Pythonstructlog + contextvarsコンテキスト変数でリクエストスコープを実現

推奨コンテキストフィールド

フィールド説明付与タイミング必須/推奨
request_idリクエスト一意識別子リクエスト受信時必須
tenant_idテナント識別子認証/ルーティング時必須(マルチテナント)
trace_id分散トレースのトレースIDリクエスト受信時推奨
span_id分散トレースのスパンIDリクエスト受信時推奨
user_idユーザー識別子認証完了後推奨
client_idOAuthクライアント識別子クライアント認証後推奨
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年コストと調査要件のバランス

ログのアンチパターン

避けるべきパターン

アンチパターン問題点改善策
機密情報の出力パスワード、トークン、個人情報の漏洩リスクマスキング処理の実装
過剰ログストレージコスト増大、重要ログの埋没ログレベルの適切な使い分け
ログとメトリクスの混同ログでリクエスト数をカウントするなどメトリクスライブラリを使用
一貫性のないフォーマットパース不能、検索困難ログライブラリの統一
コンテキスト情報の欠落調査時に関連ログを追跡できないリクエストスコープのコンテキスト伝播の導入
例外スタックトレースの分断複数行ログが別レコードに分割される例外情報をJSON内に含める

機密情報のマスキング

マスキングが必要なデータの例(IDサービス):

┌──────────────────────────────────────────────────┐
│ 絶対にログに出力してはいけないもの │
├──────────────────────────────────────────────────┤
│ ・パスワード / パスワードハッシュ │
│ ・アクセストークン / リフレッシュトークン │
│ ・クライアントシークレット │
│ ・秘密鍵 / 署名鍵 │
│ ・OTPコード │
└──────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────┐
│ マスキングして出力するもの │
├──────────────────────────────────────────────────┤
│ ・メールアドレス → u***@example.com │
│ ・電話番号 → ***-****-7890 │
│ ・IPアドレス → ポリシーに応じて判断 │
└──────────────────────────────────────────────────┘

まとめ

設計領域ポイント
構造化ログJSON形式でフィールドを明確に分離し、検索・集計を容易にする
ログレベルERRORは「アクション可能」なイベントのみ、ユーザーの操作ミスはERRORにしない
コンテキスト情報リクエストスコープのコンテキスト伝播でrequest_id、tenant_idを全ログに自動付与する
ログ集約アプリはstdoutにJSON出力、収集・転送・保存を分離する
アンチパターン機密情報の出力禁止、過剰ログの抑制、メトリクスとの混同を避ける

最終更新: 2026-03-04 対象: SaaSアプリケーション開発者、SRE