「ログ、とりあえず出してますか?」
正直に告白します。僕はこれまで、なんとなくログを出していました。
「エラーが起きたら怖いから、とりあえず try-catch してログに残しておこう」
「ないよりはマシだろう」
でも、いざ本番で障害が起きたとき、そのログは僕を助けてくれませんでした。
深夜のアラート対応で、システムエラーが発生しました とだけ書かれたログを前に途方に暮れた経験、皆さんにもありませんか?
今回は、そんな「なんとなくログ」を卒業し、トラブルシューティングや分析に本当に役立つ「使えるログ」を設計するために学んだことをまとめました。
ログの目的を再定義する
そもそも、なぜログを書くのでしょうか? チームで決まっているからではなく、エンジニアとして以下の3つの目的を意識する必要があります。
- トラブルシューティング(Why & Where?)
- 「なぜ落ちたのか」「どこで落ちたのか」を特定し、バグを再現・修正するため。
- 可観測性・分析(Observability)
- 「この機能、どれくらい使われてる?」「処理に何秒かかってる?」といったシステムの健康状態やユーザー行動を知るため。
- 監査・セキュリティ(Security)
- 「いつ、誰が、重要なデータを変更したか」の証跡を残すため。
これらを意識すると、「とりあえず出力」ではなく「目的に合わせて出力」する必要があることに気づきます。
構造化ログってなんや
ログ設計を学ぶと必ず出てくるのが「構造化ログ(Structured Logging)」です。
構造化ログとは「ログを人間への手紙ではなく、機械へのデータとして扱う」ということです。
非構造化ログ
従来のテキスト形式のログです。
// 人間には読めるけど、機械(検索)には辛い
Log::error("User 123 failed to login from 192.168.0.1");
これだと、「ユーザーID 123 のエラーだけ集計したい」と思ったときに、正規表現で頑張って文字列解析をしなければなりません。
ログの文言が少し変わっただけで検索できなくなるので、運用が非常に辛くなります。
構造化ログ
ログをJSONなどの形式で出力します。
// メッセージとデータを分ける
Log::error("Login failed", [
"user_id" => 123,
"ip_address" => "192.168.0.1",
"event" => "auth_error"
]);
これが出力されると、以下のようなJSONになります。
{
"level": "ERROR",
"message": "Login failed",
"context": {
"user_id": 123,
"ip_address": "192.168.0.1",
"event": "auth_error"
},
"datetime": "2024-01-07T12:00:00+09:00"
}
こうしておけば、DatadogやCloudWatch Logsなどのログ管理ツールで context.user_id = 123 のようにクエリ一発で検索・集計ができます。
現代のWeb開発では、ログは「読むもの」ではなく「検索・集計するもの」です。
構造化ログはマストと言えます。
ログ設計の実践
では、実際にコードを書くときに何を意識すべきでしょうか。
やるべきこと
コンテキスト(文脈)を含める
「エラーです」だけでは無意味です。
「その時何が起きていたか」の情報を連想配列(Context)として渡しましょう。
何が起こったかを把握するためにも 5W1H を意識するといいかもですね。
- 誰が? (User ID)
- 何を? (Input Data)
- どこで? (Request URL, IP Address)
トレースID (Trace ID / Request ID) を通す
これが今回一番の学びでした。
「1回のリクエスト」に対して、ユニークなID(Trace ID)を割り当て、それを全てのログに含めます。
// 理想的なログ出力
{
"message": "DB error",
"request_id": "a0eebc99-9c0b...", // これ
"user_id": 101
}
これがあれば、マイクロサービスや非同期処理でログがバラバラになっても、IDで検索して「一連の処理の流れ」を追うことができます。
Laravelなどでは、ミドルウェアで自動的にIDを発行し、Log::shareContext() 等を使って全ログに自動付与する設定を入れておくと、コードを書くときに意識しなくて済むので最高です。
ログレベルを適切に使い分ける
チーム内の基準に従いましょう。ない場合は基準を決めておきましょう。
下記は一例です。
- ERROR: 即時対応が必要(深夜でも電話がかかってくるレベル)。
- WARN: 異常だがシステムは稼働継続可能(後で要確認)。
- INFO: 正常系イベント(KPI分析や動作確認用)。
- DEBUG: 開発時のデバッグ用(本番では出さない)。
アンチパターン
機密情報の出力
パスワード、アクセストークン、クレジットカード番号、個人情報(PII)をログに出してはいけません。
ログファイル自体が漏洩したときのリスクが甚大です。平文なのでね。
「とりあえずcatchしてログ」による握りつぶし
一番やりがちなやつです。
try {
$user->save();
} catch (Exception $e) {
// 最悪なパターン:エラーが起きた事実だけログして、処理を続行してしまう
Log::error("Error happened");
}
これだと、データ不整合が起きているのにシステムが動き続け、後で原因不明のバグになります。
対処できないエラーはログに出すだけでなく、適切に例外を再スローするか、エラーレスポンスを返す必要があります。
ログ運用
「ログをどこに出すか」も重要です。
DockerやKubernetesなどのコンテナ環境では、「標準出力(stdout/stderr)に吐く」のが定石です(The 12-Factor Appの思想)。
アプリは標準出力にJSONを流すだけ。あとはFluentdやDatadog Agentがそれを拾って、S3やログ管理基盤に転送する。アプリは「ログの保存場所」に関知しない設計にするのがモダンな運用です。
5. ログローテート
最後に、地味だけど大事な「お掃除」の話です。 もしファイルにログを出力する場合、書き続けるといつかディスクが溢れます。数GBのテキストファイルを開こうとしてエディタが固まった経験、ありますよね?
(いつぞやに対応したEC2のアラート対応で、15GBのログファイルがサーバーを圧迫していた事件がありました。)
- ローテート(Rotation): 日次やサイズ指定でファイルを分割する(例:
app.log->app.log.2024-01-07)。 - 保持期間(Retention): 「14日経過したら削除する」といったルールを決める。
PHP(Monolog)なら RotatingFileHandler を使うことで簡単に実装できますし、Linuxの logrotate コマンドで管理することもあります。
「ディスクフルでサーバーダウン」は一番悲しい障害なので、リリース前に必ず確認しましょう。
おわりに
ログ設計を見直すことは、「未来の自分やチームメイトへの思いやり」です。
障害発生時、適切なログ(構造化され、Trace IDがあり、必要な情報が詰まっているログ)があれば数分で解決できる問題が、ログがないために数日かかることもあります。
「何かあったときに、このログを見て原因がわかるか?」 「一年後の自分がこのログを見て感謝するか?」
そう自問しながら、明日からの Log::info() を書いていきたいと思います。

