Laravelのログレベルをカスタマイズする

### やりたいこと
**Laravelの標準で用意されているログレベルに、独自に作成したログレベルを追加したい**

Laravelで標準で使用可能なログライブラリについては公式サイトを参照。
https://readouble.com/laravel/8.x/ja/logging.html

ドキュメントを読めばわかりますが、Laravelでは以下の8つのログレベルが標準で用意されています。
**emergency、alert、critical、error、warning、notice、info、debug**
素直にこれら8つのログレベルをうまく使って開発すれば困ることはほぼないですが、まれにログレベルを独自で増やしたいとか、既存のログレベルを別の名前のログレベルに置き換えたといった要望が出る開発案件があります。
Laravelのログ出力は公式サイトを参考にすれば柔軟にカスタマイズできますが、ログレベルを増やしたり既存のログレベルを置き換える方法については載っていないので、その時の作業の備忘録です。

### 手順
1. MonologのLoggerを拡張したLoggerクラスを作成
* Loggerのファクトリクラスの作成
* フォーマッタの作成
* コンフィグファイルの修正
* ログ出力メソッドを持つクラスの作成


### 1. MonologのLoggerを拡張したLoggerクラスを作成
Laravelのロギングは[Monolog](https://github.com/Seldaek/monolog)というライブラリを使用しています。
Monolog\Loggerクラスの中でログレベルの定義とログレベルに応じたメソッドが用意されています。
ですので、ログレベルを追加したい場合、Monolog\Loggerクラスを拡張して新たなクラスを作成する必要があります。
ここでは**major**というログレベルを追加する想定でカスタマイズします。

```Logger.php

<?php

namespace App\Logging;

class Logger extends \Monolog\Logger
{
    public const MAJOR = 360; // 数字は適当。ほかのログレベルと被らないように

    protected static $levels = [
        self::DEBUG     => 'DEBUG',
        self::INFO      => 'INFO',
        self::NOTICE    => 'NOTICE',
        self::WARNING   => 'WARNING',
        self::ERROR     => 'ERROR',
        self::CRITICAL  => 'CRITICAL',
        self::ALERT     => 'ALERT',
        self::EMERGENCY => 'EMERGENCY',
        self::MAJOR     => 'MAJOR', // 追加
    ];

    /**
     * Adds a log record at the MAJOR level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string  $message The log message
     * @param mixed[] $context The log context
     */
    public function major($message, array $context = []): void
    {
        $this->addRecord(static::MAJOR, (string) $message, $context);
    }
}
```

### 2. Loggerのファクトリクラスの作成
続いては、自分で拡張したクラスのインスタンスがLoggerとして使用されるようにファクトリクラスを作成します。
ファクトリクラスについては公式サイトにも説明がありますが、__invokeメソッドの中でLoggerのインスタンスを返します。
コンストラクタの引数の意味は正直あまり理解していませんが、実際にLoggerのインスタンスを作成しているソースを参考にして作成しました。

```CreateCustomLogger.php
<?php

namespace App\Logging;

use Monolog\Handler\StreamHandler;
use App\Logging\Logger;

class CreateCustomLogger
{
    public function __invoke(array $config)
    {
        return new Logger($config['name'] ?? '', [new StreamHandler($config['path'])]);
    }
}
```

### 3. フォーマッタの作成
こちら必須ではありませんが、ログ出力するときのフォーマットのカスタマイズ。
ついでなので作成しておく。

```CustomizeFormatter.php
<?php

namespace App\Logging;

use Monolog\Formatter\LineFormatter;

class CustomizeFormatter
{
    /**
     *
     * @param  \Illuminate\Log\Logger  $logger
     * @return void
     */
    public function __invoke($logger)
    {
        $date_format = 'Y-n-d H:i:s';
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(new LineFormatter(
                '[%datetime%] [%level_name%] %message% %context% %extra%' . PHP_EOL, $date_format, true, true
            ));
        }
    }
}

```

4. コンフィグファイルの修正
ロギングのconfigファイルを修正します。

```config/logging.php
<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

return [

    'default' => env('LOG_CHANNEL', 'custom'), // 変更


    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['single'],
            'ignore_exceptions' => false,
        ],

        // 中略

        // 以下追加
        'custom' => [
            'driver' => 'custom',
            'name' => 'test',
            'tap' => [App\Logging\CustomizeFormatter::class],
            'path' => storage_path('logs/laravel-' . date('Y-m-d') . '.log'),
            'level' => 'debug',
            'via' => App\Logging\CreateCustomLogger::class,
        ],
    ],
];
```

### 5. ログ出力メソッドを持つクラスの作成
Laravel標準ではLogファサードを使用して柔軟にログを埋め込むことができます。
チャンネルを指定することで、拡張したLoggerのログメソッドを出力することもできますが、せっかくなのでログ出力用のクラスも作成しておきます。

```CustomLog.php

<?php

namespace App\Logging;

use App\Constants\LogMessage;
use Illuminate\Support\Facades\Log;

class CustomLog
{
    const CUSTOM_CHANNEL = 'custom'; // チャンネル名を保持

    public static function major(string $msg, array $context = [])
    {
        Log::channel(self::CUSTOM_CHANNEL)
            ->major($msg, $context);
    }
}
```

後はLogファサードと同じ感覚でmajorメソッドを使用することができるようになります。

```php
CustomLog::major('ログ出力テスト');
```

## 既存のログレベルを拡張したログレベルに置き換える
続いて、既存のログレベルを新しく追加したログレベルに置き換える方法について。
自分でプログラムの中にログ出力処理を埋め込む場合には、出力したいログレベルに合わせたメソッドを使用すればよいですが、内部的にExceptionが投げられた場合には、Laravel側で自動でログレベルを判断してエラーログが出力されます。
このときに自動で出力されるログレベルを独自に追加したログレベルに変換します。
変換するには、Loggerクラスでメソッドをオーバーライドして、ログレベルを上書きします。
ここではログレベルERRORをMAJORに置き換えます。
こうすることで、内部でExceptionが発生して自動的に出力されたログでも、独自に追加したログレベルで出力されるようになります。

```Logger.php
<?php

namespace App\Logging;

class Logger extends \Monolog\Logger
{
    public const MAJOR = 360; // 数字は適当。ほかのログレベルと被らないように

    protected static $levels = [
        self::DEBUG     => 'DEBUG',
        self::INFO      => 'INFO',
        self::NOTICE    => 'NOTICE',
        self::WARNING   => 'WARNING',
        self::ERROR     => 'ERROR',
        self::CRITICAL  => 'CRITICAL',
        self::ALERT     => 'ALERT',
        self::EMERGENCY => 'EMERGENCY',
        self::MAJOR     => 'MAJOR', // 追加
    ];

    /**
     * @param string  $message The log message
     * @param mixed[] $context The log context
     */
    public function major($message, array $context = []): void
    {
        $this->addRecord(static::MAJOR, (string) $message, $context);
    }

    /* オーバーライド Laravel標準のログレベルを上書き */
    public function error($message, array $context = []): void
    {
        $this->addRecord(static::MAJOR, (string) $message, $context);
    }
}
```

### まとめ
ログレベルのカスタマイズは標準のLoggerを拡張すれば実現可能でしたが、ログのチャンネルをカスタムにする必要があって、他のチャンネルと組み合わせて使うことが難しかったです。
なのでできれば標準で用意されている8つのログレベルをそのまま活用して実装することをお勧めします。
そもそもログレベル追加の要望がある案件はそれほど多くはないと思いますが。。