
改修時にログを仕込む|あとから原因を追えるログの残し方
「注文が、ときどき消えるみたいなんです」
問い合わせを受けて、サーバーのログを開きます。該当しそうな時刻を探しても、出ているのは error の1行だけ。どの注文で、どの処理の、どこで失敗したのかが分かりません。結局「次に起きたら教えてください」と返すしかなくて、なんだか申し訳ない気持ちになる。
でも、ログが足りないのは、あなたの手落ちではありません。そのコードが書かれたとき、まだ誰も困っていなかった。だから「あとで追うための目印」を置く理由がなかっただけです。
そして、目印はいまからでも置けます。いちばん置きやすいのは、改修でそのコードを触るときです。この記事では、改修のついでに、未来の自分が原因を追えるログを仕込むやり方を一緒に整理していきます。
結論:やることは4つです。①「入口」「外部呼び出し」「例外」の3か所に絞って仕込む(全部に書かない)、②1行に「何の処理が・誰の何に対して・どうなったか」を、IDつきで書く、③レベルを使い分ける(ERROR=人が対応すべき失敗/WARN=おかしいが続行できた/INFO=業務の節目)、④パスワード・トークン・個人情報は書かない。完璧なログ設計は要りません。今日触るコードに、目印を2〜3個置くだけで十分です。
なお、ここではPHPを例にしていますが、考え方は言語やフレームワークに関係なく使えます。ログの出力先や書き方は現場ごとに違うので、自分の環境に置き換えながら読んでみてください。
何が起きているのか:ログは「ある」のに、答えてくれない
原因を追えないとき、ログがまったくないことは意外と少ないものです。困るのは、たいていこんな状態です。
- 何の失敗か分からない:
errorやfailedだけで、どの処理の話か書いていない。 - 誰の話か分からない:注文ID・ユーザーID・ファイル名などがなく、問い合わせのあった1件と結びつかない。
- つながりが分からない:同じ時刻にいくつもの処理が動いていて、どの行とどの行が同じリクエストのものか追えない。
- 失敗が握りつぶされている:例外を
catchしたあと、何も書かずに処理を続けている。ログにも画面にも何も出ない。
どれも、書いた人が悪いわけではありません。書いた時点では「動いている」ことを確かめられれば十分でした。ログに求められる役割が、あとから変わったのです。
だから、足りない分を一度に全部埋めようとしなくて大丈夫です。改修で触るところから、少しずつ目印を足していけば、ログは確実に育っていきます。
ログを仕込む場所:「入口」「外部呼び出し」「例外」の3か所

ログは、多ければいいわけではありません。1行ごとに書くと、今度は量が多すぎて探せなくなります。改修で仕込むなら、まずはこの3か所に絞るのがおすすめです。
1. 入口:処理が「始まった」ことと、何を受け取ったか
画面からのリクエスト、バッチの開始、APIの受付など、処理の入口です。「この処理が、この注文IDで始まった」と1行あれば、問い合わせの1件と結びつけられます。
バッチなら、開始と終了、そして処理件数を残しておくと、「昨日だけ件数が少ない」といった変化にも気づけます。
2. 外部呼び出し:自分の外に出ていく境目
データベース、外部のAPI、メール送信、ファイルの読み書き。自分のコードの外に出ていくところは、失敗がいちばん起きやすい場所です。呼び出した相手、結果(成功・失敗・ステータスコード)、かかった時間を残しておくと、「こちらの問題か、相手の問題か」の切り分けが早くなります。外部APIの切り分けは外部API連携の不具合の切り分けでも整理しています。
3. 例外:失敗を受け止めた場所
catch で例外を受け止めているのに、何も書かずに先へ進んでいるコードは、原因調査のいちばんの落とし穴です。受け止めた例外は、少なくともメッセージと発生場所をログに残す。これだけで「何も出ていない」状態がなくなります。スタックトレースが残っていれば、読み方はスタックトレースの読み方を参考にしてください。
1行に書く中身:「何の処理が・誰の何に対して・どうなったか」
場所が決まったら、中身です。あとから検索して読むことを考えると、1行にこの要素があると助かります。
- 時刻:たいていはログの仕組みが自動でつけてくれます。
- 処理名:
order.save、batch.monthly_reportなど、何の処理か分かる短い名前。 - 対象のID:注文ID・ユーザーID・ファイル名など、問い合わせの1件と結びつけるための鍵。
- 結果:成功・失敗、件数、ステータスコード、かかった時間。
- リクエストID(あれば):同じリクエストから出た行をまとめて追うための通し番号。
書き方を比べてみると、違いがよく分かります。
# 追えないログ
error
failed to save
# 追えるログ
[ERROR] order.save failed order_id=10482 user_id=331 reason="payment api timeout" elapsed_ms=30012 req=7f3a9c
[INFO] mail.send ok order_id=10482 to_domain=example.com elapsed_ms=420 req=7f3a9c
下の2行なら、order_id=10482 で検索すれば、この注文に何が起きたかがまとまって出てきます。req=7f3a9c で検索すれば、同じリクエストの流れが追えます。
ポイントは、キー=値 の形で書くことです。文章で「注文10482の保存に失敗しました」と書くより、grep で探しやすく、あとで集計もしやすくなります。ログの絞り込み方はエラーログをgrepとtailで絞り込むで紹介しています。
PHPで書くならこんな形
フレームワークを使っていない古いPHPのコードでも、error_log で書けます。
<?php
// 例外を受け止めた場所で、何の処理・誰の何・どうなったかを残す
try {
$result = $paymentClient->charge($orderId, $amount);
} catch (Throwable $e) {
error_log(sprintf(
'[ERROR] order.charge failed order_id=%d user_id=%d reason="%s" at=%s:%d',
$orderId,
$userId,
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
throw $e; // 握りつぶさず、これまでどおり上に伝える
}
大事なのは最後の throw $e; です。ログを足すだけで、処理の流れは変えない。改修のついでに挙動まで変えてしまうと、それ自体が新しい不具合の種になります。もともと例外を握りつぶして処理を続けていたコードなら、その挙動もいったんそのままにして、ログだけ足すのが安全です。
LaravelやMonologなど、PSR-3に沿ったロガーを使っている現場なら、$logger->error('order.charge failed', ['order_id' => $orderId, 'user_id' => $userId]); のように、IDを配列(コンテキスト)で渡す形がそのまま使えます。いまの現場にある書き方に合わせるのがいちばんです。
レベルを使い分ける:ERROR・WARN・INFO・DEBUG
ログのレベルは、あとで「どれを見ればいいか」を決める目印です。細かく悩まなくて大丈夫なので、まずはこの基準で分けてみてください。
| レベル | 使う場面 | 例 |
|---|---|---|
| ERROR | 処理が失敗し、人が対応すべきもの | 決済APIがタイムアウトして注文が保存できなかった |
| WARN | おかしいが、処理は続けられたもの | 在庫APIが遅く、リトライで成功した |
| INFO | 業務の節目。正常に進んだ記録 | 注文を受け付けた/バッチが1,240件処理して終了した |
| DEBUG | 調査のときだけ見たい細かい情報 | 計算途中の値、SQLの組み立て結果 |
目安は、ERRORが出たら誰かが見に行く、と考えることです。何でもERRORにすると、本当に大事な1行が埋もれてしまいます。監視でERRORを拾っているなら、なおさらです(アラートの考え方は監視アラートの閾値の決め方)。
DEBUGは、本番では出力しない設定になっていることが多いレベルです。調査のために一時的にDEBUGを本番で出すときは、いつ戻すかを決めてからにしておきましょう。
書かないもの:パスワード・トークン・個人情報
ログは、思っている以上にいろいろな人と場所に渡ります。サーバーにログインできる人、ログを集めるサービス、調査のために外部の会社に渡すファイル。だから、ログに書かないものも決めておきます。
- パスワード、APIキー、アクセストークン、セッションID
- クレジットカード番号などの決済情報
- 氏名・住所・電話番号・メールアドレスなどの個人情報(必要なら一部を伏せる。例:
to_domain=example.comのようにドメインだけ残す)
うっかりしやすいのが、リクエストの中身をまるごと出力する書き方です。print_r($_POST, true) をそのままログに書くと、パスワード欄も一緒に残ってしまいます。まるごと出したくなったら、必要な項目だけを選んで書くようにしましょう。
このあたりは、OWASPの「Logging Cheat Sheet」がログに残すもの・残さないものを整理していて参考になります。秘密の情報の扱い全般はパスワードとAPIキーの平文放置をなくすでもまとめています。
量と置き場所:ループの中には書かない
ログを足したら、最後に量を確かめます。
- ループの中にINFOを書かない:1万件を処理するバッチで1件ごとに1行書くと、1回で1万行です。件数はループの外で「処理件数」として1行にまとめ、失敗した件だけ個別に書きます。
- 出力先を確かめる:
error_logの出力先はphp.iniの設定しだいです。書いたつもりのログが、想定と違うファイルに出ていることもあります。検証環境で一度出力させて、どこに出るかを確かめてから本番に出しましょう。 - ローテーションの対象か確かめる:新しいログファイルを作った場合は、ログローテーションの設定に入っているかを確認します。入っていないと、いつかディスクを埋めてしまいます(ディスク使用率とログローテーション)。
具体例:ログがあった改修と、なかった改修
同じ「ときどき注文が保存されない」という問い合わせでも、ログの有無でこう変わります。
- ログがなかったとき:時刻を聞いてログを探すが、
errorの1行しかない。どの注文か分からないので、DBを調べて「保存されていない注文」を探すところから始まる。再現もしないので、「次に起きたら教えてください」とお願いするしかない。 - ログを仕込んでいたとき:問い合わせの注文IDで検索すると、
order.charge failed reason="payment api timeout" elapsed_ms=30012の1行が見つかる。決済APIの呼び出しが30秒で時間切れになっていたと分かり、同じ時間帯に同じ失敗が何件あるかもgrepで数えられる。相手先に問い合わせる材料も、その場で揃う。
違いは、調査の腕前ではありません。改修のときに、目印を置いておいたかどうかだけです。再現しない不具合の追い方は「再現しない」バグの調べ方でも整理しています。
影響:ログを仕込むようになると、何が変わるか
- 問い合わせに早く答えられる:IDで検索すれば、その1件に何が起きたかが分かります。「次に起きたら」とお願いする回数が減ります。
- 「こちらか、相手か」が切り分けられる:外部呼び出しの結果と時間が残っていれば、原因の場所がはっきりします。
- 改修の効果を確かめられる:直したあとに同じERRORが出なくなったかを、ログで確かめられます。
- 次の担当者が助かる:ログは、コードの動きを説明してくれる記録でもあります。仕様書がないシステムほど、ログが読み解きの手がかりになります。
明日やること
- 次に触る改修で、ログを仕込む場所を2〜3個決める。「入口」「外部呼び出し」「例外」のどこに当たるかを見てみましょう。
- 何も書かずに進んでいる
catchを1つ探す。見つけたら、処理はそのままに、ログを1行だけ足します。 - 1行の書き方を決める。
[レベル] 処理名 結果 キー=値の形を、チームや自分用のメモに書いておきます。 - 書かないものを確かめる。パスワード・トークン・個人情報が出力されていないか、足したログを見直します。
- 検証環境で出力先を確かめる。どのファイルに出るか、ローテーションの対象かを見てから本番に出します。
今日やるのは、2の「何も書かずに進んでいる catch を1つ探す」だけでも十分です。その1行が、いつか誰かの調査を何時間も短くしてくれます。
改修時のログ仕込みチェックリスト
- 処理の「入口」に、何の処理が何に対して始まったかを書いたか
- 外部呼び出し(DB・API・メール・ファイル)の結果と時間を残したか
- 受け止めた例外を、メッセージと発生場所つきで書いたか
- ログを足すだけで、処理の流れ(例外を投げ直す・続行するなど)は変えていないか
- 1行に処理名・対象のID・結果が入っているか
-
キー=値の形で、検索しやすく書いたか - ERROR・WARN・INFO・DEBUGを基準どおりに使い分けたか
- パスワード・トークン・個人情報を出力していないか
- ループの中で大量の行を出していないか
- 検証環境で出力先とローテーションの対象を確かめたか
全部できなくても大丈夫です。「例外を書いた」の1つだけでも、次の問い合わせのときにはっきり違いが出ます。
よければ、こちらも
- エラーログをgrepとtailで絞り込む:仕込んだログを、調査のときに素早く引くために。
- 「再現しない」バグの調べ方|環境とタイミングを疑う順番:ログがいちばん活きる場面の進め方です。
- バグ報告から再現・原因特定までの最短ルート:ログを手がかりに、原因までたどる流れを整理しています。
- 小さな改修でも本番事故を防ぐ「変更前チェックリスト」:ログを足す改修も、ほかの改修と同じ手順で安全に出すために。

ログは、いまの自分のためではなく、少し先の誰かのために置く道しるべです。その誰かは、たいてい数か月後の自分です。
ログが足りなくて困った経験があるなら、それはもう「どこに目印があれば助かったか」を知っているということです。その感覚は、いちばん確かな設計図になります。
まずは次の改修で、何も書かずに進んでいる catch を1つだけ探してみてください。
ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。