Claude Media
Claude CodeでApexのデバッグログを解析 — 例外の原因行を特定

Claude CodeでApexのデバッグログを解析 — 例外の原因行を特定

sf apex get logで取得したログをgrepで絞り、FATAL_ERRORのスタックとSOQL発行元をClaude Codeに突き合わせさせる手順です。

Apexの例外は、画面に出るメッセージだけでは原因行まで辿れないことがあります。デバッグログには例外の型・メッセージ・スタックトレースが残っているので、これを読めば原因行は分かります。ただしログは数万行になりがちで、全文をそのまま渡しても読み違いの元です。

Claude Codeに任せるなら、先にコマンドで要点の行だけを絞り、スタックとソースコードを突き合わせさせるのが近道です。この記事では、sf apex get log での取得、grep と awk による絞り込み、原因行とSOQL発行元の特定、修正後の再確認までを順に組みます。

ログを取得する — 20MBの上限と24時間の保持期限

デバッグログはSalesforce CLIで取得できます。まず sf apex list log でIDを調べ、sf apex get log に渡します。

mkdir -p logs
sf apex list log --target-org my-sandbox
sf apex get log --log-id <ログID> \
  --output-dir logs --target-org my-sandbox

sf apex get log はIDを指定した1件か、--number で指定した直近の件数を取得します。フラグなしで実行すると、直近の1件が返ります。--output-dir を付けるとログがファイルとして保存され、指定しなければカレントディレクトリが保存先です。

取得する前に知っておきたい制限が3つあります。

制限

ログの取得前に確認する制限

  • 1件20MBまで

    20MBを超えたログは、古い行を削って小さくされます。削られる行は先頭とは限らず、以前のSystem.debug文の行など、ログのあちこちから消えます。

  • 保持は24時間

    System debug logsの保持期間は24時間です。Monitoring debug logsは7日間残ります。再現した日のうちに取得します。

  • トレースフラグの停止

    15分間に1,000MBを超えるログを出すと、トレースフラグが無効になります。アクセスの多いクラスに長時間フラグを立てっぱなしにしない運用が前提です。

20MBで切り詰められる点は、解析結果の読み方にも響きます。原因の行が見つからないときは、コードの問題ではなく、ログ側から該当行が削られている可能性があります。その場合はログレベルを下げ、再現の範囲を狭めて取り直します。

ログの読み方 — 例外とSOQLを探す手がかりになる行

ログの各行は、パイプ(|)で区切られた項目の並びです。先頭がタイムスタンプ、次がイベント名で、行番号やクラス名などの詳細が続きます。Claude Codeに渡す前に、この構造のうち原因特定に使うイベントを決めておくと、絞り込みの条件がぶれません。

イベント記録される内容カテゴリと出力されるレベル
EXCEPTION_THROWN記録される内容行番号・例外の型・メッセージカテゴリと出力されるレベルApex Code(INFO以上)
FATAL_ERROR記録される内容例外の型・メッセージ・スタックトレースカテゴリと出力されるレベルApex Code(ERROR以上)
SOQL_EXECUTE_BEGIN記録される内容行番号・集約の数・クエリカテゴリと出力されるレベルDB(INFO以上)
DML_BEGIN記録される内容行番号・操作の種類・オブジェクト・件数カテゴリと出力されるレベルDB
CODE_UNIT_STARTED / CODE_UNIT_FINISHED記録される内容トリガーやWebサービスなど処理の単位の開始と終了カテゴリと出力されるレベルApex Code
CUMULATIVE_LIMIT_USAGE記録される内容ガバナ制限の使用量(SOQLクエリ数など)カテゴリと出力されるレベルトリガー等のコードユニットの終了時に出力

FATAL_ERROR が「最後に何が失敗したか」を示し、EXCEPTION_THROWN が「どの行で投げられたか」を示します。SOQL_EXECUTE_BEGIN は、同じ行から何回クエリが出ているかを数える材料です。

CODE_UNIT_STARTED の末尾には、トリガーなら __sfdc_trigger/トリガー名、クラスならクラス名が付きます。どの処理単位の中で例外が出たかを、ここから逆引きできます。

なお、行番号が特定できない呼び出しには [EXTERNAL] が出ます。組み込みのApexクラスや、マネージドパッケージ内のコードがこれにあたります。[EXTERNAL] の行は、手元のソースでは追えません。

要点の行だけに絞って渡す

全文を渡さずに、必要な行だけを先に抜き出します。抜き出しはコマンドの仕事で、Claude Codeの仕事は抜き出した行の解釈とソースとの突き合わせです。

LOG=logs/07L5g00000XXXXX.log
 
# 例外とスタック、処理単位の境目を行番号つきで
grep -n -E 'FATAL_ERROR|EXCEPTION_THROWN|CODE_UNIT_(STARTED|FINISHED)' \
  "$LOG" > logs/summary.txt
 
# SOQLが同じ行から何回出たか(3列目が行番号の想定)
awk -F'|' '/SOQL_EXECUTE_BEGIN/ {print $3}' "$LOG" \
  | sort | uniq -c | sort -rn | head -10 > logs/soql-by-line.txt
 
# 制限の使用量
grep -n -A6 'LIMIT_USAGE_FOR_NS' "$LOG" > logs/limits.txt

awk の $3 は、DML_BEGIN の例で行番号が3列目に来る形から類推した指定です。SOQL_EXECUTE_BEGIN でも同じ並びになるかは、手元のログを1行見て確かめてから使います。列がずれているときは、$3 を実際の位置に直します。

ここでの工夫は2つです。

  • 行番号つき(grep -n)にしておくと、元のログの該当箇所へ戻れます。
  • SOQLを「行番号ごとの発行回数」に畳むと、数千行のログが10行ほどになります。

行番号が同じでも、クラスが違えば別の場所です。回数の多い行番号が出たら、CODE_UNIT_STARTED の並びと見比べて、どのクラスの話かを決めます。

Claude Codeに原因行を特定させる

絞ったファイルと、疑わしいソースを一緒に渡します。@ でファイルを指定すると、読み込みを待たずに内容が会話に入ります。

logs/summary.txt と logs/soql-by-line.txt は、同じデバッグログから
抜き出したものです。次の順で調べてください。
 
1. FATAL_ERROR のスタックトレースを読み、例外の型・メッセージと、
   最も上にある自分たちのクラスの行を挙げる
2. その行を force-app/main/default/classes 以下のソースで開き、
   例外が起きる条件を説明する
3. soql-by-line.txt で回数が多い行番号を、クラスごとに突き合わせる
4. 原因の候補を、根拠の行番号つきで3つ以内に絞る
 
ソースは変更しないでください。ログに無いことは推測と書き分けること。

指示の最後の2行が効きます。「変更しない」で調査と修正を分け、「推測と書き分ける」で、ログから読めたことと仮説を混ぜない報告を求めています。

Claude Codeの公式ガイドでは、バグの調査は「エラーを共有する」「修正案を聞く」「適用する」の順で進める流れが示されています。あわせて、再現するコマンドとスタックトレース、再現条件が安定しているかどうかを伝えると調べやすくなる、とされています。Apexの場合、再現するコマンドにあたるのは「どの操作でログが出たか」です。プロンプトには、操作内容(一括更新、画面からの更新、バッチ実行など)とログを取った日時を足します。

返ってきた候補は、次の形で読むと検証しやすくなります。

(説明用の例。実際の出力は組織とログによって変わります)
候補1: OpportunityHandler.recalc の42行目。
  根拠: summary.txt の FATAL_ERROR がこのメソッドを指す。
        Trigger.new の要素が null のとき NullPointerException になる。
候補2: AccountService.load の17行目。
  根拠: soql-by-line.txt で [17] が 101 回。ループ内のSOQLの疑い。
        ただし FATAL_ERROR は別の行を指しており、直接の原因とは断定できない。

2つ目のように、直接の原因と手前の疑いが別々に挙がるのは正常な形です。「SOQLが多い」ことと「その例外が出た」ことは別の事実なので、報告の中で分けて書かせます。

例外の型で見る場所が変わる

原因行の探し方は、例外の型で変わります。Claude Codeに先に渡す手がかりも、型によって違います。

ログに出る例外先に見る行突き合わせるソース
NullPointerException など型の例外先に見る行EXCEPTION_THROWN の行番号と、直前の CODE_UNIT_STARTED突き合わせるソース例外の行の変数の初期化
ガバナ制限の例外先に見る行LIMIT_USAGE_FOR_NS の out of の前後突き合わせるソースループ内のSOQL・DML
DmlException先に見る行DML_BEGIN の操作・オブジェクト・件数突き合わせるソースDMLの直前の項目設定
値の欠落が疑われる例外先に見る行USER_DEBUG の出力突き合わせるソースデバッグ文を足した周辺

制限系の例外は、CUMULATIVE_LIMIT_USAGE の Number of SOQL queries: 101 out of 100 のような行で、使用量と上限が並んで見えます。この使用量は、トリガー・executeAnonymous・バッチApex・@future・テストメソッドなどのコードユニットの終了時に記録されます。ループ内のSOQLを洗い出して一括処理に直す手順は、Apexのガバナ制限違反を探して一括処理に直す記事にまとめています。この記事は「本番で実際に落ちたログから入る」側にあたります。

トランザクションをまたぐログは、処理単位で区切って読む

1つのログに複数の処理単位が入っている場合、例外の行だけを見ると別のトランザクションの出来事と取り違えます。トリガーの中から別のトリガーが起動する連鎖や、executeAnonymous の中でトリガーが動く場合が典型です。

EXECUTION_STARTED と EXECUTION_FINISHED の間が1つのトランザクションで、その内側に CODE_UNIT_STARTED と CODE_UNIT_FINISHED の入れ子ができます。FATAL_ERROR がどの入れ子の中にあるかを、summary.txt の行番号から数えさせます。「例外の行より前で、まだ閉じていない CODE_UNIT_STARTED を列挙して」と頼むと、例外が出た時点の呼び出しの連鎖をそのまま取り出せます。

トランザクションの最後には、累積のプロファイル情報も1回だけ出力されます。ここにはDMLの呼び出しや、リソースを多く使った「高コストなクエリ」の情報が入ります。soql-by-line.txt の回数と食い違うときは、このプロファイルを grep の対象に足して、どのクエリが重いのかを併せて見ます。

再現して、ログを取り直して確かめる

原因の仮説は、ログを取り直して確かめます。再現用のApexを sf apex run で実行すると、デバッグレベルを指定してログを出せます。

sf apex run --file scripts/repro.apex \
  --category-level Apex_code=FINE --category-level Db=FINE \
  --target-org my-sandbox

--debug-level には NONE・DEBUGONLY・DB・PROFILING・CALLOUT・DETAIL を指定できます。カテゴリごとに細かく決めたいときは、--category-level Apex_code=FINEST のように カテゴリ=レベル の形で複数回指定します。--debug-level と --category-level は同時に使えません。

落とし穴

FINESTを付けると、変数の値がログに入る

Apex CodeのログレベルをFINESTにすると、Apexの変数への代入がすべて記録されます。パスワードのように文字列変数へ代入される値があるコードでは、機密がログに残ります。

そのログをClaude Codeに渡すと、内容は会話に入ります。FINESTを使うのは、機密を扱わないコードだけに限り、ログは .gitignore に入れたディレクトリに置きます。

修正を入れたら、同じ再現手順でログを取り直し、先ほどの grep と awk を再び回します。確かめる観点は次の3点です。

  1. FATAL_ERROR の行が消えたか。
  2. soql-by-line.txt の回数が、想定した件数(1回など)まで下がったか。
  3. CUMULATIVE_LIMIT_USAGE の使用量が上限に近づいていないか。

ここまでをテストでも固定するなら、Apexテストクラスのカバレッジを75%に上げる手順のように、再現条件をテストメソッドへ移します。ログを読み直す作業を、次回からは赤いテストが肩代わりしてくれます。

毎回の絞り込みをCLAUDE.mdとclaude -pに載せる

絞り込みの手順を毎回プロンプトに書くのは手間なので、規約として CLAUDE.md に置けます。

## Apexのデバッグログ解析
- ログは logs/ に保存済みのものを使う。logs/ はコミットしない
- 全文を読み込まず、FATAL_ERROR / EXCEPTION_THROWN / CODE_UNIT_* と
  SOQL_EXECUTE_BEGIN の行番号別回数を先に抜き出す
- 報告は「ログから読めた事実」と「推測」を分ける
- 原因行は force-app 以下のクラス名と行番号で書く

絞り込み済みのテキストなら、非対話モードの claude -p にパイプで渡せます。公式の例は、ビルドエラーを標準入力から渡して原因を説明させる形です。

cat logs/summary.txt \
  | claude -p 'FATAL_ERRORのスタックから、原因のクラスと行を挙げて' \
  > logs/diagnosis.txt

パイプで渡せる標準入力には10MBの上限があり、超えるとClaude Codeはエラーで終了します。デバッグログは1件20MBまでありうるので、全文のパイプは超える場合があります。絞り込み済みの summary.txt なら収まるのが普通です。大きいまま渡したいときは、ファイルに書いてパスをプロンプトに書く形にします。claude -p の権限や出力形式は、claude -pオプションの記事で扱っています。

まとめ

Apexの例外は、FATAL_ERROR のスタックで終点を、EXCEPTION_THROWN の行番号で発生源を、SOQLの行別回数で手前の要因を見ます。この3つをコマンドで先に抜き出し、Claude Codeにはソースとの突き合わせだけを任せると、ログの読み違いが減ります。

押さえておくのは、20MBで古い行が削られることと、FINESTが変数の値を記録することです。前者は「原因の行が無い」ときにログ側を疑う理由になり、後者は機密を会話へ渡さないための歯止めになります。同じ構成は、JavaのGCログのように、巨大なテキストを先に絞ってから解釈させる他のログ解析にもそのまま当てはまります。具体例はJavaのGCログの解析にあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/common-workflows
  2. code.claude.com/docs/en/headless
  3. developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_debugging_deb…
  4. developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_debugging_sys…
  5. github.com/salesforcecli/plugin-apex
この記事を共有:XはてブLinkedIn