ソフトウェアをリリースしました

かいふく という訪問介護の副業に特化した求人サイトを公開しています。

お近くの介護資格保有者にご紹介頂けると嬉しいです 👏

https://kai-fuku.com/

このサイトにはプロモーションが含まれます

「ログ、とりあえず出してますか?」

正直に告白します。僕はこれまで、なんとなくログを出していました。
「エラーが起きたら怖いから、とりあえず try-catch してログに残しておこう」
「ないよりはマシだろう」

でも、いざ本番で障害が起きたとき、そのログは僕を助けてくれませんでした。
深夜のアラート対応で、システムエラーが発生しました とだけ書かれたログを前に途方に暮れた経験、皆さんにもありませんか?

今回は、そんな「なんとなくログ」を卒業し、トラブルシューティングや分析に本当に役立つ「使えるログ」を設計するために学んだことをまとめました。

ログの目的を再定義する

そもそも、なぜログを書くのでしょうか? チームで決まっているからではなく、エンジニアとして以下の3つの目的を意識する必要があります。

  1. トラブルシューティング(Why & Where?)
    • 「なぜ落ちたのか」「どこで落ちたのか」を特定し、バグを再現・修正するため。
  2. 可観測性・分析(Observability)
    • 「この機能、どれくらい使われてる?」「処理に何秒かかってる?」といったシステムの健康状態やユーザー行動を知るため。
  3. 監査・セキュリティ(Security)
    • 「いつ、誰が、重要なデータを変更したか」の証跡を残すため。

これらを意識すると、「とりあえず出力」ではなく「目的に合わせて出力」する必要があることに気づきます。

やんやん

プログラマーとしてLEMP環境に主に生息しており、DevOps 的な立ち回りをしながらご飯を食べている当ブログの管理人のやんやんと申します。
最近はTmux使うのを辞めました。

構造化ログってなんや

ログ設計を学ぶと必ず出てくるのが「構造化ログ(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() を書いていきたいと思います。

おすすめの記事