Gakugeki プロジェクト ログ設計指針 (logging-guide.md)

このドキュメントは、Unity クライアントおよび Azure サーバーにおけるログ設計の統一ルールを定めたものです。エージェントは常にこの指針に従ってログの実装・提案を行ってください。

1. 基本方針

  • コスト最適化: サーバー(Application Insights)への送信回数とデータ量を最小限に抑えつつ、トラブルシューティングに必要な文脈を確保する。
  • 構造化ログ: 集計や検索が容易な形式(EventName, Message, Context)を維持する。

2. 言語ルール

  • EventName (イベント名): 英語 で記述。命名規則は カテゴリ.アクション.状態(例: Auth.Signin.Success)。
  • Message (日本語説明): 日本語 または英語。エンジニアが調査時に直感的に理解できる内容にする。
  • Context (追加情報): キー名は 英語。値は必要に応じて任意の型を使用する。

3. ログレベルと送信タイミング

ログレベルは、そのイベントが「システムの健全性やユーザー行動の分析にどれだけ不可欠か」および「発生頻度」に基づいて決定します。

ログレベル 送信タイミング 主な用途・判定基準
EssentialInfo 即時/定期送信 最重要マイルストーン。ログイン完了、課金完了、ワールド入場成功、ログアウト完了、致命的な設定変更など。
Info バッファ経由 (5分/Error時) 通常の進捗・結果。シーン読み込み完了、アセットDL成功、ナビゲーション完了、重要なUI操作の結果。
Warn バッファ経由 予測可能な問題。バリデーションエラー、リトライ、ユーザーによるキャンセル、非致命的なリソース不足。
Error / Fatal 即時送信 予期せぬ失敗・例外。通信エラー、NullReferenceException、ロジックの破綻。
Debug / Trace 送信しない 開始(Start)イベント、高頻度イベント、開発・内部詳細用。メソッドの入り口、ボタンクリック、内部ステップの遷移、成功/失敗の一次報告。
Diagnostic 送信しない 高度な計測・分析用計測値(ms/MB)を伴うログに限定。パフォーマンス計測、メモリ増分、スパイク解析。診断設定が有効時のみ出力・計算される。

3.1. 開始(Start) と 結果(Success/Failed) の使い分け

原則として、一つの処理フローに対して以下のルールを適用します。

  • 開始(.Start): 原則として Debug レベル。ユーザーの「意図」を記録するが、サーバーへは送らない。
  • 結果(.Success, .Failed): Info 以上のレベル。処理の「確定した事実」を記録し、サーバーへ送る。
  • 例外: ログインの開始(Auth.Login.Start)など、問題発生時の調査で「そもそも開始されたか」がクリティカルな項目に限り、開始も Info にすることを検討する。

3.2. 高頻度イベントの判定

以下のいずれかに該当する場合は、たとえ「結果」であっても Debug レベル(送信対象外)に落とすことを強く推奨します。

  • 1ユーザーあたり1分間に数回以上発生する可能性がある。
  • シナリオの各ステップ、またはループ処理内で発生する。
  • UIの開閉(トースト通知、フローティングメッセージ)などの「表示完了」ログ。

3.3. 診断ログ (Diagnostic) の活用

Diagnostic は一般的なログとは異なり、高度な分析を目的とした「レントゲン」のような役割を果たします。

  • 自動計測: Diagnostic 命令は、呼び出し前後のヒープメモリの増分(Delta)を自動的に計測し、コンテキストに付加します。
  • 実行負荷の分離: DiagnosticSettings.IsEnabledfalse の場合、ログの生成処理自体が完全にスキップされるため、本番環境のオーバーヘッドになりません。
  • 棲み分けの基準:
    • Debug: 「〇〇を開始します」「ステップ 2/5 完了」などの意図・節目・詳細な内部進捗
    • Diagnostic: 「所要時間: 300ms」「メモリ増分: 2MB」といった計測値そのものが目的の情報
  • 主な使い所:
    • パフォーマンス計測: Stopwatch を用いたロード時間や実効速度の記録。
    • メモリ解析: どのアセットロードで実メモリが何 MB 消費されたかの追跡。

4. 実装場所のルール

  • UseCase 層 (原則): 「何をしたか」の意図(.Start)と「結果どうなったか」の事実(.Success, .Failed)を記録する。
  • API 層: ネットワークリクエストの成否、リトライ回数、低レイヤーのエラー詳細を記録する。
  • Presenter / Controller: 入力イベント(ボタン押下、スライダー操作)の記録(Debugレベル)。
  • View: 原則としてログを書かない。

4. 相関ID (CorrelationId) の運用ルール

CorrelationId は、一連のユーザー操作やシステムフローを追跡するための重要な識別子です。本プロジェクトでは 階層型相関 ID(Hierarchical Correlation ID) を採用しています。

4.1. 基本構造

AsyncLocal を使用したスタック構造により、using スコープ内でのみ有効なコンテキストを付与します。

  • 形式: {上位コンテキスト}/{現在のコンテキスト名}-{短縮ID}
  • : World.Intro/Auth.SignIn-7fafd5f2/PF.Auth.Profile-i9j0k1l2

4.2. 命名規則

コンテキスト名は、その処理の内容が直感的にわかる名称にします。

  • 規則: {Category}.{Action} (PascalCase)
  • : Auth.SignIn, Item.Purchase, PF.Inventory.Get

コンテキストの開始時に、IAppLogger (内部で ICorrelationIdProvider をラップ) を使用してスコープを定義します。 原則として、UseCase 等のアプリケーション層では ICorrelationIdProvider を直接注入せず、IAppLogger を使用して相関 ID とログを一元管理してください。

新しいルートの開始 (Push)

原則として、**エントリポイント(UseCase の初期化、ボタンコールバック、ネットワークイベント等)**ではこのメソッドを使用します。 既存のコンテキストを切り離し、新しいルート ID を発行することで、非同期処理による ID の長大化(Bloat)を防ぎます。

// AppLogger 経由で Push (推奨)
using var _ = _logger.Push("Category.Action");

階層的な追跡 (PushChild)

親のコンテキストを引き継ぎたい(一連の流れとして追跡したい)場合に使用します。 主に API 層(PlayFab などの通信処理) で、呼び出し元の UseCase との関連性を維持するために使用します。

// AppLogger 経由で PushChild (推奨)
using var _ = _logger.PushChild("Category.Action");
  • using var _ を使用することで、メソッド終了時に自動的に元の状態に復帰します。
  • 変数名 _ は、スコープ管理のみを目的とし、参照しないことを明示します。

コンテキストの完全リセット (SetNew)

既存の階層スタックをすべて破棄し、新しい Root ID を発行します。 これは「プロセスやシーンの生存状態」に関わらず、論理的な「エリア遷移」や「主要機能の切り替え」の節目 として、ID の長大化 (Bloat) を防ぎ、Application Insights 等での検索性を向上させるために使用します。

// 過去の因果関係を引き継がず、ここから「新しい章」を開始する
_correlation.SetNew("Category.Action");

非同期伝送(Propagate via Forget)

await せず呼び出す非同期メソッド(Forget() 等)であっても、呼び出し時点で有効な CorrelationIdAsyncLocal によって新しいタスクへコピーされます。 そのため、呼び出し元で using スコープが終了して ID が復帰した後も、非同期先(Forget したタスク)ではコピーされた ID が維持され、その先での PushChild 等の効果も正確に反映されます。

4.4. 長期フローにおけるライフサイクル管理

アバター作成や複雑なセットアップなど、複数の画面や非同期ステップを跨ぐ「一連の体験」を設計する場合は、以下の指針に従って CorrelationId を運用します。

  1. フロー全体の Root 化: エントリーポイント(例:StartCreationFlow)で Push を行い、その一連の体験を象徴する Root ID(例:Avatar.Creation)を発行します。
  2. 節目を Child で記録: フロー内の各フェーズ(保存、キャンセル、特定の重い処理)では、Root ID を継承したまま PushChild を使用し、文脈を維持します。
    • 例: Avatar.Creation-xxxx/Avatar.Save-yyyy
  3. セッションの串刺し検索: これにより、不具合調査時に「ある特定の作成セッション」の始まりから終わりまでを、Root ID をキーにして Application Insights 等で一貫して追跡可能になります。
  4. ネストの許容範囲: 論理的な親子関係であれば 2〜3 階層程度のネストは許容されます(例:作成 Root/ポーリング/API実行)。逆に、親子関係がない独立したアクション(例:メニューの開閉)は、常に Push でクリーンな ID から開始してください。

4.5. 判断基準:意図 (Intent) か 手段 (Implementation) か

PushPushChild のどちらを使うか迷った際は、以下の基準で判断します。

種類 役割 判断基準
Push (Root) 意図 (Intent) ユーザーやシステムが起こした「最初の目的」 Avatar.Creation, Auth.Login, World.Enter
PushChild 手段 (Implementation) 意図を実現するための「具体的な手順・工程」 Avatar.Save, Avatar.Change, PF.Inventory.Get

なぜ Push (Root) が必要なのか

技術的にはすべて PushChild でも動作しますが、あえて Push を使って「歴史を断絶」させるのには以下の理由があります:

  1. ID 肥大化(Bloat)の防止: すべてを継承すると ID が無限に伸び続け、通信量増大や検索性の低下を招きます。大きな節目でリセットすることで ID を短く保ちます。
  2. 論理的な「新しい物語」の開始: 「アバターを選ぶ」「メニューを開く」といった操作は、それ自体が独立したユーザーの意図であり、前の文脈を引きずるより、クリーンな ID で開始した方が調査時のノイズが減ります。

逆に、PushChild は「なぜこれが起きたのか」という理由が重要な場合に、親の文脈にぶら下がることで強力な追跡力を提供します。

4.5.1. 伝搬性の高い Core UseCase の設計

コアロジックを実装する UseCase では、原則として PushChild を使用してください。

  • メリット: UI(Root)から呼ばれた場合はその操作の子として記録され、単体テストやバックグラウンド処理で直接呼ばれた場合は自律的に Root として機能します。これにより、利用シーンを問わず常に最適な階層でログを記録できます。
【重要】基盤サービス (Base Service) における制約

NetworkUseCaseStorageService など、アプリケーション全域から「共通手段」として呼ばれるクラスでは、原則として Push (Root) を行わないでください

  • 基盤レイヤーが勝手に Root を作成すると、呼び出し元(UseCase)の意図が途切れてしまいます。常に PushChild を使用して上位の文脈を継承してください。

4.5.2. UI イベントハンドラ(Subscribe)の扱い

Unity の onClick や UniRx の Subscribe コールバックは、実行コンテキストが分断される可能性が高いため、原則として Push (Root) で新しい意図を定義してください。

  • 「ボタンを押す」という行為は、システムにとっての「新しい物語の始まり」です。

4.5.3. 階層化における命名の冗長性回避(Suffix 方式)

Controller (Root) と UseCase (Child) でカテゴリやアクションが同じになる場合は、単純な重複を避けつつ、情報の自己完結性を保つために .Execute.Process などの接尾辞を追加して区別してください。

  • 悪い例: Auth.SignIn (Root) / Auth.SignIn (Child) → 重複して階層が判別しにくい。
  • 不十分な例: Auth.SignIn (Root) / Execute (Child) → ID 単体で何を「実行」しているか追跡不能になる。
  • 推奨: Auth.SignIn (Root) / Auth.SignIn.Execute (Child)
    • これにより、単一ログ行からも「どの機能のどのフェーズか」が 100% 判別可能になります。

4.5.4. 変数名の競合(Shadowing)への対処

using var _ = ... という書き方は簡潔ですが、ラムダ式の中やネストしたスコープで名前が重なり、コンパイルエラーになることがあります。

  • その場合は、_s1, _s2_s_apply のように 一意な変数名 を使用して、スコープの独立性を確保してください。

4.5.5. UniRx Subscribe と非同期メソッド (.Forget) の罠

Subscribe 内で非同期メソッドを .Forget() で呼び出す場合、ラムダ式内に using var _ = _correlation.Push(...) を書くと、非同期処理の待機中(最初の await 到達時)に using スコープが終了してコンテキストが即座に失われます。

  • 悪い例: Subscribe(_ => { using var _ = Push(); DoAsync().Forget(); })
  • 正しい例: Subscribe(_ => DoAsync().Forget()); とし、飛び先である DoAsync() のメソッド側先頭で Push を行う。 これにより、非同期待機中もコンテキストが適切に保護されます。
IInitializable / Awake とコールバックの罠

クラスの初期化メソッド(InitializeAwake)で Push し、その中で Subscribe によるイベント登録だけを行うパターンに注意してください。

  • Initialize 時に定義した using スコープは、初期化が完了した時点で終了します。
  • そのため、数分後にイベントが発生してコールバックが呼ばれた時には、初期化時のコンテキストはすでに消滅しています
  • イベント発火時のライフサイクルを正しく捉えるには、コールバックメソッドの先頭で改めて Push (Root) を定義する必要があります。

4.5.6. Pub/Sub (MessageBroker) における設計指針

MessageBroker を介した疎結合な通信では、発行側(Publisher)と受信側(Subscriber)で相関 ID の役割を明確に分担します。

  1. 発行側 (Publisher) - 「お願い」の記録:
    • Publish 前に PushChild("Category.Action.Request") を行い、誰がどのコンテキストでイベントを投げたかを記録します。
  2. 受信側 (Subscriber) - 「実務」の記録:
    • 受信した非同期ハンドラ(.Forget() 先)内で Push("Category.Action.Execute") を行い、独立した実行単位として記録します。
    • これにより、ログ上で「誰のお願いを、誰がいつ実行したか」という因果関係が Suffix (Request/Execute) によって明確になります。
    • ただし、単純な値の同期や状態の受け取り(例: オブジェクトの登録、位置の同期など)を行うだけの Receive では、PushPushChild を使用しないでください。
  3. 相関の断絶の許容:
    • 疎結合なシステムでは、発行側から受信側へ ID を物理的に伝搬させることはせず、受信側は常に新しい Root として開始することを原則とします。ただし、同一クラス内でのロード演出など、文脈が明らかな場合に限り PushChild による継承を検討してください。
  4. 登録や購読の高頻度ログ(ノイズ)の排除:
    • MessageBroker を通じて複数回自動的に呼ばれる SpawnEvent の登録処理や更新イベントにおいて、Debug レベルであってもログがコンソールを埋め尽くしノイズになることがあります。
    • このような高頻度の単純なバインド・登録処理では、.Do(e => _logger.Debug(...)) などのログ出力を記述しない、または動作確認完了後に削除することを徹底してください。

4.6. 伝搬と外部連携

  • Client 内部: async/await を跨いで自動的にスタックが維持されます。
  • Client -> Server: HTTP ヘッダー X-Correlation-Id に完全な階層 ID が付与されます。
  • Server (Azure): ヘッダーから抽出した ID を自身のログに付与し、フロントからバックまでの追跡を可能にします。

5. サーバー側(Azure Functions)のルール

  • 相関ID (CorrelationId) の維持: クライアントから送られてきた X-Correlation-Id ヘッダーを抽出し、サーバー側のログにも付与する。これにより、一連のユーザー操作をフロントからバックまで追跡可能にする。
  • EventName の一貫性: クライアントと同じ命名規則(カテゴリ.アクション.状態)を使用する。
    • 例: Auth.Signup.VerifyToken.Success
  • ログレベルの使い分け:
    • Information: リクエストの受付、主要な処理の完了、外部サービス(PlayFab等)へのリクエスト成功。
    • Warning: バリデーションエラー、リトライ可能な一時的エラー。
    • Error: 予期せぬ例外、回復不能なエラー。
  • 機密情報の除外: パスワード、個人のメールアドレス、アクセストークンなどの機密情報は、Message や Context に直接含めない。

6. ログカタログ (log_catalog.md) の保守

プロジェクト全体のログの一貫性を保つため、以下のファイルを保守します。

  • log_catalog.md: 個別のログイベント(EventName)の一覧。
  • correlation_catalog.md: 階層型相関 ID のコンテキスト名(Push 時の名前)の一覧。

更新の義務

ログの EventName や相関 ID のコンテキスト名を新設・変更した際は、必ず対応するカタログを更新してください。