laravel のログにはPHPでよく使われているらしい Monolog が実装されている。
こいつをごにょごにょしていい感じにしたいのが本記事。
(簡潔に書きたかったがソースも書くと長くなるねぇ…)
Laravel は ver8.9.0。(v6くらいから同じだと思う)
目的はログのフォーマットの変更とカラーリング。
- ソース:ログ 8.x Laravel
- monolog:Seldaek/monolog: Sends your logs to files, sockets, inboxes, databases and various web services
- カラーリング: bramus/monolog-colored-line-formatter: Colored/ANSI Line Formatter for Monolog
- 大いに参考:Laravelでログのフォーマットを変えたい - Qiita
1. ログの出力方法
Laravel のログは RFC5424 ってので定義されている8つのレベルが扱える。
emergency > alert > critical > error > warning > notice > info > debug
ログを出力する方法は facade を使う方法と helper を使う方法の2種類(たぶん)。
デフォルトでは storage/logs/laravel.log に出力される。
Log Facade でログを出力する。
use Illuminate\Support\Facades\Log;
$name = 'sample kunn';
Log::info('Showing user: '.$name);
// => [2020-10-13 19:27:04] local.INFO Showing user: sample kunn
Log::notice('User failed to login.', ['id' => 3]);
// => [2020-10-13 19:32:38] local.INFO User failed to login. {"id":3}
第2引数(context) に渡したデータはシリアライズされて出力される(デフォルト)。
Logger Helper でログを出力する
logger()->info('sample text');
// => [2020-10-13 19:35:43] local.INFO sample text
どちらとも LogManager を内部で呼び出しているので使い方は同じっぽい。
RFC5424 の 8 つのレベルの関数が定義されているので使い分けるべし。
Facade と Helper、決めた方に統一して使うのが賢そう。
参考:Illuminate\Log\LogManager | Laravel API
local.xxx は .env の APP_ENV の値かな?
production とか local といった実行環境の値となる。
他のロガーでいうカテゴリやタイプといった塊は存在しない。(ほんとぉ?)
必要ならラッパーを作るなりする必要がありそう。
- Laravel ConsoleとHttpのログファイルを分ける - Qiita
- この辺りを参考に
processorにフィールドを追加させるのが最適解だと思う。
Exception ログ
use Exception;
logger()->error(new Exception('Reigai'));
Exception 系列を渡せば stackTrace も出力される。
2. ログの設定ファイル
基本的には app/config/logging.php に設定を書き込むだけで使える。
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 14,
],
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
],
...
]
channel というのはログの出力方法を指したもので、これを使ってログを出力する。
TVのチャンネルみたいに電波を分ける役割(でいいのかな?)。
デフォルトでは stack というものが指定されている。
stack だけは特殊で、プロパティの channels で指定した複数のチャンネルに処理を渡すことができる。
TVでいう多チャンネル同時視聴。
デフォルトでは ['single'] が指定されているので、single というチャンネル1つにログの出力を渡している。
single は logs/laravel.log に書き込むチャンネルなので、logファイルに追記されるわけだ。
初めから定義されていたチャンネルを以下にまとめる。
| channel名 | 説明 |
|---|---|
| stack | 複数のチャンネルへログを渡す |
| single | 指定したファイルにログを書き出す |
| daily | 指定したファイルにログを書き出す(所謂ローテート) 一日ごとにファイルを区切り、一定の日数が経過したら削除する(任意) |
| slack | Slack に出力する |
| papertrail | Papertrail に出力する |
| stderr | 標準エラー出力に出力する |
| syslog | Linux 等の syslog に出力する |
| errorlog | PHPの error_log() に出力する |
| null | 何もしない |
| emergency | (emergency ログだけ出力?) |
他にも SNS に送ったり DB に送ったりと、大抵のやりたいことは誰かがライブラリを実装してくれてる。
3. stdout channel の追加
log を使ってて思ったのが、cli を操作している時は標準出力にも出力して欲しい点。
stderr チャンネルは存在するので、それをベースに stdout チャンネルを作成してみる。
'stdout' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stdout',
],
'level' => 'debug', // all log
],
StreamHandler の stream パラメータにPHPの標準出力(stdout)を指定した。
level によって出力するログレベルを制限できるが、最低値の debug にすることにより、全てのログを対象としている。
試しに channels.stack.channels に stdout を追加すると標準出力にも出力されるようになるはず。
Handlerは monolog 側のシステムなので今回は省く。
詳しくはこのあたり。
[オプション] CLI実行時(php artisan)のみ標準出力にも出力する
このまま stack に追記でもいいのだが、CLIを操作していない時に標準出力に出力されるのはあまり好みではない。
(schedule 処理、http のアクセス時等にも出力されてしまう(握りつぶされるが))
なので CLI で実行したときに限って出力するようにしてみる。
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
...
// logger cli mode
if(strpos(strtolower(php_sapi_name()), 'cli') !== false) {
$path = 'logging.channels.stack.channels';
$stacks = collect(config($path, []))
->push('stdout')
->unique();
config([$path => $stacks]);
}
}
}
php_sapi_name() という関数を実行すると、apache や cli、cgi といった文字列が帰ってくる。
これを利用して、名前にCLIが含まれていたら stack に stdout を追加する仕組み。
collection の unique() を使うことで重複実装を防いでいる。
とりあえず AppServiceProvider に実装したが、 LoggerServideProvider とかを作って実装したほうが賢い。
参考:php - Detect if running from the command line in Laravel 5 - Stack Overflow
4. ログ文字列のフォーマット
さて本題。
デフォルトの出力文字列は以下のような形式だった。
これは monolog のデフォルトの Formatter\LineFormatter で以下が適用されているためである。
[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n
参照:monolog/LineFormatter.php at master · Seldaek/monolog
こいつを変更したいので LineFormatter を拡張した class を作成する。
<?php
namespace App\Logging\Formatters;
use Monolog\Formatter\LineFormatter;
class CustomLineFormatter extends LineFormatter
{
public function __construct()
{
// 2020-10-13 17:12:15.375 [local.INFO] log message という形式で出力
$lineFormat = "%datetime% [%channel%.%level_name%] %message%" . PHP_EOL;
$dateFormat = "Y-m-d H:i:s.v"; // PHP: DateTime::format
parent::__construct($lineFormat, $dateFormat, true, true);
}
public function format(array $record): string
{
// var_dump($record);
$output = parent::format($record);
// var_dump($output);
return $output;
}
}
LineFormatter のコンストラクタは以下を引数に取る。
| 引数 | 説明 |
|---|---|
| $format | フォーマット文字列 |
| $dateFormat | 日付(%datetime%)のフォーマット文字列 参考:PHP: DateTime::format - Manual |
| $allowInlineLineBreaks | ログの改行を許すか |
| $ignoreEmptyContextAndExtra | context と extra の値が空のときに [] を削除するかどうか |
参照:monolog/LineFormatter.php at master · Seldaek/monolog
format() は今回は親のものを呼び出しているだけだが、ここでフォーマットのやり方を定義することができる。
引数の record 配列は以下の通り。
$record = [
'message' => $message,
'context' => $context,
'level' => $level,
'level_name' => $levelName,
'channel' => $this->name,
'datetime' => new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
'extra' => [],
];
参照:monolog/Logger.php at master · Seldaek/monolog
ここで level や message を加工して parent::format($record) を呼べば、かなり自由度の高いことが可能だ。
context はログ関数実行時の第2引数(配列?オブジェクト?)。
今のままだと context は握りつぶされているので、
$record['message] .= json_encode($record['context']);みたいな実装が要るかも。
ここで DB に保存をかけたりしても面白そう。
参考にした文献だと context を vsprintf() に突っ込むことで printf() like な実装を示してた。
Levelに応じた絵文字とかタイマーとか実装できたら楽しそう。
extraは実行された行番号やIPアドレスなどを取得できるが、今回は扱わない。
processorという仕組みを使うのだが、この辺りが参考になるかも。
Laravel にフォーマットを適用する
laravel に適用するには tap という配列パラメータに __invoke() が実装されているクラスを指定してあげる必要がある。
参考:ログ 8.x/ Laravel
の下の方の「チャンネル用Monologカスタマイズ」
<?php
namespace App\Logging;
use App\Logging\Formatters\CustomLineFormatter;
class LineFormatterApply
{
public function __invoke($logging)
{
$customLineFormatter = new CustomLineFormatter();
foreach($logging->getHandlers() as $handler) {
$handler->setFormatter($customLineFormatter);
}
}
}
おそらく CustomLineFormatter を指定があった handler 全てに渡すのだと思う。
(extend とか implements とかが無い暗黙実装は嫌いだぁ…)
これを config/logging の使わせたいチャンネルに指定してあげる。
今は stdout と single に。
(stack に渡すと全チャンネルに適用できるが、子で指定していた場合上書きされた)
(formatter は一つしか追加できない?)
use App\Logging\LineFormatterApply;
'stdout' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stdout',
],
'level' => 'debug',
'tap' => [ColorFormatterApply::class], // <-- これ
],
こんな感じの出力に変えられた。
5. ログをカラフルに出力する
標準出力のログはやっぱり色が欲しい。
視認性も上がるし、なによりログの把握速度が圧倒的である。
調べると monolog 側に良さそうなライブラリがあった。
bramus/monolog-colored-line-formatter: Colored/ANSI Line Formatter for Monolog
composer require bramus/monolog-colored-line-formatter ~3.0
これをそのまま formatter 扱いで定義すると色が付くのだが、それでは先程作成した文字列のフォーマットが適用されない。
なので、CustomLineFormatter を拡張して色を付けることにする。
<?php
namespace App\Logging\Formatters;
use Bramus\Monolog\Formatter\ColorSchemes\DefaultScheme;
// quote by:
// https://github.com/bramus/monolog-colored-line-formatter/blob/master/src/Formatter/ColoredLineFormatter.php
class ColorLineFormatter extends CustomLineFormatter
{
private $colorScheme = null;
public function getColorScheme()
{
if (!$this->colorScheme) {
$this->colorScheme = new DefaultScheme();
}
return $this->colorScheme;
}
public function format(array $record) : string
{
$output = parent::format($record);
$colorScheme = $this->getColorScheme();
return $colorScheme->getColorizeString($record['level']).trim($output).$colorScheme->getResetString()."\n";
}
}
parent::format($record) した後に、その文字列に level に応じた色を付ける。
ライブラリのフォーマット部分を参考に必要な処理を移植した形だ。
参考:monolog-colored-line-formatter/ColoredLineFormatter.php at master · bramus/monolog-colored-line-formatter
<?php
namespace App\Logging;
use App\Logging\Formatters\ColorLineFormatter;
class ColorFormatterApply
{
public function __invoke($logging)
{
$coloredLineFormattetr = new ColorLineFormatter();
foreach($logging->getHandlers() as $handler) {
$handler->setFormatter($coloredLineFormattetr);
}
}
}
で、これを stdout のチャンネルの tap に指定すればOK。
※制御文字を使用しているので、ファイル出力には使わないように。
とてもいい感じになった。
ログのメッセージ部は白のままにしたい時は、正規表現でその部分だけ抜き取って色を付けると良いかも。
6. サンプル
自分が使用したいログの仕様をまとめて、それを実装したサンプルを置いておく。
- NOTICE 以上のログが1日毎に別のファイルに保存される
dailyチャンネル- 保存期間(
days)は7日
- 保存期間(
- ERROR 以上のログが1つのファイルに保存される
singleErrorチャンネル - CLIで実行した際、全てのログが標準出力に色付きで出力される
stdoutチャンネル- これは
AppServiceProvideで追加するのでstackでは触らない
- これは
use Monolog\Handler\StreamHandler;
use App\Logging\LineFormatterApply;
use App\Logging\ColorFormatterApply;
...
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['dailyNotice', 'singleError'],
'ignore_exceptions' => false,
],
'singleError' => [
'driver' => 'single',
'path' => storage_path('logs/laravel_error.log'),
'level' => 'error', // upper error log
'tap' => [LineFormatterApply::class],
],
'dailyNotice' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'notice', // upper notice log
'days' => 7,
'tap' => [LineFormatterApply::class],
],
'stdout' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stdout',
],
'level' => 'debug', // all log
'tap' => [ColorFormatterApply::class],
],
],
出力は以下の感じ。
log ファイルのカラーリングは VSCode の Log File Highlighter を使用している。
おわりに
monolog の handlerとprocessor 周りをまだ理解しきれてないので、とりあえず基本をまとめた。
あと laravel の Exception 周りも知らないといけないかも。
これ node-log4js より楽かもしれぬ。
個人的にタイマーは実装してみたい。
綺麗なログを吐けると開発が捗る捗る…。
続きは、今のログに手を入れるとき(書くとは言っていない)。






