はじめに
この記事は Qiita Laravel Advent Calendar 2023 の3日目の記事です。
1日目はこのカレンダー作成者 @uchan-lab さんの記事で、Laravel アンチパターンと対策まとめ です。
特に「マイグレーション編」「日付編」「config編」は、学びあり、僕が過去にハマったことのある PHP / Laravel あるあるありで、いい記事だと思いましたので、ぜひ読んでみてください。
2日目は なお さんの Laravelで単一テーブル継承の実装を試してみた
単一テーブル継承の機能が Laravel にあるのは知らなかったのですが、僕も使ってみたいと思ったので、気になる人は読んでみてください。
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 でスタックトレースを綺麗に出力するには、更に工夫が必要