猫でもわかるWeb開発・プログラミング

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

AWS CloudWatch Logs 向けに Laravel のログを JSON で出力する方法

はじめに

この記事は Qiita Laravel Advent Calendar 2023 の3日目の記事です。

1日目はこのカレンダー作成者 @uchan-lab さんの記事で、Laravel アンチパターンと対策まとめ です。

特に「マイグレーション編」「日付編」「config編」は、学びあり、僕が過去にハマったことのある PHP / Laravel あるあるありで、いい記事だと思いましたので、ぜひ読んでみてください。

qiita.com



2日目は なお さんの Laravelで単一テーブル継承の実装を試してみた

単一テーブル継承の機能が Laravel にあるのは知らなかったのですが、僕も使ってみたいと思ったので、気になる人は読んでみてください。

zenn.dev



JSON 形式のログを送ると何がいいのか

AWS CloudWatch Logs に限らず、最近はログを JSON 形式で出力するのがスタンダードになりつつあります。

  • JSON 形式で出力することで、ログの検索性が上がるためです


CloudWatch Logs では、JSON のフィールドを指定してログの検索ができます。

  • AWS コンソールの CloudWatch から、「ログのインサイト」を開くと、ログの検索ができます。
  • 以下のようにログが検索できます

  • @timestamp のように、 @ がついているのは、AWS 側で付与された値です
  • level_name, channel, message は、JSON のフィールド名です
    • CloudWatch Logs に JSON 形式のログを送ると、自動的にパースされて、このようにフィールド名で検索できるようになります



ちなみに、 Laravel からは以下の形式でのログを出力していまして(実際には改行はしておらず、1行で JSON を出力しています)

{
    "message": "議事録を開きました",
    "level": 200,
    "level_name": "INFO",
    "channel": "development",
    "datetime": "2023-12-03T16:36:29.725781+09:00",
}

検索結果はこのような感じです



今回のサーバー構成

今回は、PHP + Laravel のアプリケーションを、AWS ECS (Docker)で動かしています。

  • Docker 特有の部分が一部ありますが、JsonFormatter の部分などは共通して使えます



Laravel から JSON でログを送るには

Laravel のログの設定は config/logging.php にあります。

AWS ECS で CloudWatch Logs の設定をしていると、Docker の標準出力、あるいは標準エラーに出力されたログが自動的に CloudWatch Logs に送られます。

  • このとき、ログ1行が、CloudWatch Logs の1レコードになります
  • Laravel デフォルトのログ出力だと、エラーのスタックトレースが複数行に渡ってしまい、CloudWatch Logs 上で非常に見づらくなります。そういう意味でも、ログをJSONにして1行にまとめることが重要になってきます


Laravel でプロジェクトを作成したときに、デフォルトでいくつかのログ設定があると思いますが、この中の stderr が、Docker の標準エラーにログを出力するものになります。

  • PHP は言語仕様が特殊なため、基本的には標準エラーにログを出力していくことになります
<?php

'stderr' => [
    'driver' => 'monolog',
    'handler' => StreamHandler::class,
    'formatter' => env('LOG_STDERR_FORMATTER'),
    'with' => [
        'stream' => 'php://stderr',
    ],
],

デフォルトだと上記のような設定になっています。この formatter の部分に、 Monolog\Formatter\JsonFormatter を指定することで、ログが JSON 形式で出力されるようになります。



logging.php の formatter の部分を直接書き換えてもいいですし

<?php

'stderr' => [
    'driver' => 'monolog',
    'handler' => StreamHandler::class,
    'formatter' => Monolog\Formatter\JsonFormatter::class,
    'with' => [
        'stream' => 'php://stderr',
    ],
],

.env ファイルで指定してもいいです

LOG_STDERR_FORMATTER=Monolog\Formatter\JsonFormatter



Laravel が勝手に出力するログは、 default の設定が利用されるため、config/logging.php の default の設定も変更しておきましょう。

<?php


'default' => env('LOG_CHANNEL', 'stderr'),



スタックトレースも出したい

この Monolog の JsonFormatter は、デフォルトだとエラーのスタックトレースが出ません。

  • Monolog\Formatter\JsonFormatter のコードを確認すると、$includeStacktraces というプロパティがあり、これが false に指定されているためです
  • これを true にしたいのですが、Laravel ではデフォルト値で初期化してしまっているようです

ということで、Monolog\Formatter\JsonFormatter を継承した独自フォーマッタを作成することにします

  • もしかしたら、ServiceProvider まわりの実装をすれば変更できるのかな?と思いますが、今回は継承して実装することにします。



実装した Formatter は以下の通りです。

<?php

namespace App\Logging;

use Monolog\Formatter\JsonFormatter;

/**
 * AWS CloudWatch に送った時に見やすくなるように、ログをJsonで出力しつつ、
 * Stacktrace も出力するための Formatter
 */
class MyMinutesJsonFormatter extends JsonFormatter
{
    public function __construct($batchMode = self::BATCH_MODE_JSON, $appendNewline = true, bool $ignoreEmptyContextAndExtra = false)
    {
        // $includeStacktraces に強制的に true を指定する
        parent::__construct($batchMode, $appendNewline, $ignoreEmptyContextAndExtra, true);
    }
}

この継承した Formatter を config/logging.php で指定すれば、スタックトレースも出力されます。



課題

ただ、このスタックトレースの出力、AWS CloudWatch Logs と微妙に噛み合いが悪いという課題があります。

こちらを御覧ください

これは「ログのインサイト」でログの詳細を確認したときの画面ですが、トレースのキーが文字列で昇順になってしまっているため、正しい順番に並んでいません。

これは Laravel や Molonog の問題というより、Laravel と CloudWatch Logs のかみ合わせの問題ですが、trace の中身の key を 0 パディングしてあげる(00, 01, 02, ..., 10, 11, 12, ....)などすると良いのかなと思います。

<?php

class MyMinutesJsonFormatter extends JsonFormatter
{
...
    protected function normalizeException(Throwable $e, int $depth = 0): array
    {
        $data = parent::normalizeException($e, $depth);

        // ここで $data['trace'] の key を 0 padding してから return $data するようにする

        return $data;
    }
}

まとめ

  • CloudWatch Logs にログを送るには JSON 形式が便利
  • Monolog\Formatter\JsonFormatter を使うと良い
  • デフォルトだとスタックトレースが出ないので、設定変更する必要がある
  • CloudWatch Logs でスタックトレースを綺麗に出力するには、更に工夫が必要