Monolog使ってバッチのログをDBに出力する


あらすじ

業務でLaravel使っていたある日のこと。
元々ログはMonologを使って、ログ用のテーブルに出力していました。

バッチで出したログもそのテーブルに出す予定だったのですが、バッチログ用のテーブルを新たに作ってそっちに出力したいとのこと。

というわけで、調査のまとめとして投稿することにしました。

環境

  • Laravel7
  • MariaDB

新規作成・編集するもの

Monologはインストール済みとして考えます。
なかったらComposer使って入れてください。

マイグレーションファイル

Artisanコマンドを使ってサクっと作ります。
カラムは一先ず次のようにしていますが、各自の好きに設定して下さい。

  • batch_name
    • 動いたバッチの名前
  • level
    • ログレベル。成功なら200, エラーなら400が入る(その他の番号は分かんないです)
  • level_name
    • DEBUG, INFO, ERRORと言ったログレベルの区分
  • start_time
    • バッチ処理が始まった時間
  • end_time
    • バッチ処理が終わった時間
  • message
    • ログメッセージ
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateBatchLogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('batch_logs', function (Blueprint $table) {
            $table->id();
            $table->string('batch_name');
            $table->string('level');
            $table->string('level_name');
            $table->datetime('start_time');
            $table->datetime('end_time');
            $table->string('message');
        });
    }

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

.env

DB_BATCH_LOG_TABLE 以外は最初からある項目です。環境に合わせて記述します。

# いつものDB設定
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=hogeeeeee
DB_USERNAME=root
DB_PASSWORD=1234

# バッチログの出力先テーブル
DB_BATCH_LOG_TABLE=batch_logs

config/logging.php

configファイルにバッチログ用の設定を追加
handler の部分のクラスはこれから新しく作ります。

また、下記の参考記事のように env()の記述はconfig以外に書かないようするため、handler_withにenvの情報を書きます。
handler_withで書かれた情報は、handlerで指定したクラスのコンストラクタで受け取ることができます。

Laravel では env() を config 系ファイル以外の場所に書いてはいけない

        'batch' => [
            'driver' => 'monolog',
            'handler' => BatchMonologHandler::class,  # ← これから新しく作成します
            'handler_with' => [    # ← 'handler'で指定したクラスのコンストラクラで受け取り可
                'table' => env('DB_BATCH_LOG_TABLE', 'batch_logs'),
                'connection' => env('DB_LOG_CONNECTION', env('DB_CONNECTION', 'mysql')),
                'ratio' => env('DB_LOG_FLUSH_RATIO', 100),
                'limit' => env('DB_LOG_PRESERVE_DAYS', 30)
            ],
        ],

Loggers/BatchMonologHandler

下記の記事を参考にしました。
Laravel5.6でログをデータベースに記録する

namespace App\Loggers;


use Illuminate\Support\Facades\DB;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;

class BatchMonologHandler extends AbstractProcessingHandler
{
    protected $table;
    protected $connection;
    protected $ratio;
    protected $limit;

    public function __construct(string $table, string $connection, string $ratio, int $limit, $level = Logger::DEBUG, $bubble = true)
    {
        $this->table = $table;
        $this->connection = $connection;
        $this->ratio = $ratio;
        $this->limit = $limit;
        parent::__construct($level, $bubble);
    }

    protected function write(array $record): void
    {
        $data = [
            'batch_name' => $record['context']['batch_name'],
            'start_time' => $record['context']['start_time']->format('Y-m-d H:i:s'),
            'end_time' => $record['context']['end_time']->format('Y-m-d H:i:s'),
            'message' => $record['message'],
            'level' => $record['level'],
            'level_name' => $record['level_name'],
        ];
        DB::connection($this->connection)->table($this->table)->insert($data);

        $ratio = $this->ratio;
        if (rand(0, $ratio - 1) == 0) {
            $this->deleteOld();
        }

    }

    protected function deleteOld()
    {
        $limit = $this->limit;
        $date = date('Y-m-d H:i:s', strtotime("-$limit days"));
        DB::connection($this->connection)->table($this->table)->where('created_at', '<', $date)->delete();
    }
}

BatchTest

バッチ処理を書くクラスです。Artisanコマンドで作成します。

Logファサードを使ってログを書いていますが、普通に書いてしまうと.envのLOG_CHANNELで設定されているものが適用されてしまいます。
そのためLog::channel('batch');で新しく記述した設定を使うようにしています。

また、ログを残すコードの第1引数はmessageです。他の情報は第2引数の配列で追加しています。

namespace App\Console\Commands;

use App\Models\Logs;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'batch log test command';

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

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        // config/logging.phpで設定したものを使用する
        $batch_log = Log::channel('batch');

        $start_time = Carbon::now();
        sleep(5);
        $end_time = Carbon::now();

        // 第1引数はmessage。他は第2引数の配列にする
        $batch_log->info('test batch start', [
            'batch_name' => 'test_batch',
            'start_time' => $start_time,
            'end_time' => $end_time,
        ]);

        return 0;
    }
}

結果

batch_logsテーブルには画像のようなレコードが追加されていました。

悩んだとこ

Log::info('hoge') のようなログ出力はよくやりますが、第2引数に配列を指定をするのはやったことなかったです。
なので、BatchMonologHandlerクラスのwtiteメソッドで「batch_nameなんてねーよ」って怒られた時は悩みました。配列で指定した内容をどこから取得するのは分かってなかったんです。

print_r($record)で中身を見るとcontextキーの中に含まれていたので解決できました。

まとめ

Monologを使って、バッチのログを指定したテーブルに出力することができました。
テーブルカラムや、BatchMonologHandlerクラスのwtiteメソッドをいじればいろんな形で出力できそうですね。
バッチファイルが増えてきたら、ログ出力する前のLog::channel('batch');を書くのが忘れそうっていうのが気になりますね。何かいい方法ないかな?