【Laravel6】ログをDBに出力（Monologを使う）

Laravel6でArtisanコマンド作成し、そのArtisanコマンドをバッチで実行するような実装をしていました。
ログ出力をlaravel.logではなく、DBに出力する必要があったため、Laravelに標準でインストールされているMonologというパッケージを利用しました。

バージョン：Laravel 6.20.32

## 作業内容
- マイグレーションファイル、モデルの作成
- app/Loggers/DbMonologHandler.phpの作成
- config/logging.phpにチャネルを追加
- .envに追記
- Artisanコマンドにログを記述

## マイグレーションファイル、モデルの作成
ログ出力先のテーブルを作成します。
今回は`d_logs`というテーブルを作成。DLogモデルを作成し、マイグレーションも一緒に作成します。

```
php artisan make:model DLog --migration
```

### モデルの記述
使用するテーブルを記載します。（省略した場合、モデル名DLogの場合、`d_logs`テーブルが使用されるので、省略可能。）
すべてのカラムに値の代入可能とするので、`protected $guarded = [];`とします。

```
<?php

namespace App\models;

use Illuminate\Database\Eloquent\Model;

class DLog extends Model
{
    protected $table = 'd_logs';
    protected $guarded = [];
}
```

### マイグレーションの記述

- id
- level（debugなら100、errorなら400など）
- level_name（INFOやERRORなど）
- message（エラーメッセージなど）

他に必要なカラムがあれば適宜追加してください。

```
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class DLogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('d_logs', function(Blueprint $table)
		{
            $table->bigIncrements('id');
            $table->text('level')->nullable();
            $table->text('level_name')->nullable();
            $table->text('message')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('d_logs');
    }
}
```

## app/Loggers/DbMonologHandler.phpの作成
今回出力する'level', 'level_name', 'message'を配列$dataに入れてあげます。

```
<?php

namespace App\Loggers;

use Illuminate\Support\Facades\DB;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use App\Models\DLog;
use Log;

class DbMonologHandler extends AbstractProcessingHandler
{
    public function __construct(string $table, string $connection, $level = Logger::DEBUG, $bubble = true)
    {
        $this->table = $table;
        $this->connection = $connection;
        parent::__construct($level, $bubble);
    }

    protected function write(array $record): void
    {
        $data = [
            'message' => $record['message'],
            'level' => $record['level'],
            'level_name' => $record['level_name'],
        ];
        DLog::insert($data);
    }
}
```

## config/logging.phpにチャネルを追加
既存の他のチャネルと同様の記述で、チャネルを追加します。（今回は'db'という名前で追加）

`::class`は、クラス名を名前空間と一緒に返しています。

'handler_with'で接続先などを指定します。env関数の第二引数には、デフォルト値を記述しているので、.envファイルに記述がない場合、この第二引数の値が使われます。

```
db' => [
    'driver' => 'monolog',
    'handler' => DbMonologHandler::class,
    'handler_with' => [
        'table' => env('DB_LOG_TABLE', 'd_logs'),
        'connection' => env('DB_LOG_CONNECTION', env('DB_CONNECTION', 'mysql')),
    ],
],
```

チャネルの一番上、`stack`に記載のチャネルにログが出力されるので、作成した'db'チャネルを追記します。

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

## .envに追記
.envファイルに下記を追記。（書かない場合は、logging.phpのenv関数の第二引数の値が使われる。）

```
DB_LOG_TABLE=d_logs
DB_LOG_CONNECTION=mysql
```

## Artisanコマンドにログを記述
今回は、特定のArtisanコマンドを実行した際に、ログがDBに出力されるようにしたいので、ArtisanコマンドにLogファサードで記述します。今回はバッチでデータをインポートしているので、

- インポート開始
- インポート完了
- エラーメッセージ
- ロールバック完了

などのログを出力しています。

```
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\UpsertAllByCsvService;
use Log;
use DB;

class ImportOrderDataSetByCsv extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ImportOrderDataSetByCsv';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Upsert tables concerned about orders';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        Log::info('インポート開始');
        DB::beginTransaction();
        try {
            UpsertAllByCsvService::getOrdersDataByCsv('test');
            UpsertAllByCsvService::getOrderDetailsDataByCsv('test');
            UpsertAllByCsvService::getShippingsDataByCsv('test');
            UpsertAllByCsvService::getShipmentItemsDataByCsv('test');
            DB::commit();
            Log::info('インポート完了');
        } catch (\Exception $e) {
            DB::rollback();
            Log::error($e->getMessage());
            Log::info('ロールバック完了');
        }
    }
}

```

このArtisanコマンドをTinkerで実行すると、`d_logs`テーブルにレコードが追加されました！

ロールバックのタイミングに注意。ログテーブルのidが飛んでいる場合、ログは生成されているのに、そのログ出力も含めてロールバックされている可能性があります。

この記述だと、DbMonologHandlerで出力されるログは、laravel.logにも出力されます。

DBにのみ出力したい場合は、logging.phpのチャネル設定にて、`stack`には'db'を記載せず、
ログファサードを使う時に、`Log::channel('db')->info('インポート開始');`のように記述してください。

## 参考
https://qiita.com/kd9951/items/35b0a0c4e16353078b70
https://qiita.com/hirorin/items/409a66e63bb3b16245eb