18
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravel5.6でログをデータベースに記録する

Last updated at Posted at 2018-08-08

なにがしたい?

Laravel5.6 からログハンドラの追加方法が変わったんですね!
そのまま移植したら動かなくて 無駄に時間を費やしました 改めて勉強になりました。
ひとまず記録のために、結論だけ置いておきます。

結論

どんなものができるか?

標準の storage/logs/laravel.log の代わりに、DBにこんな情報が書き込まれていきます。
ロードバランシングしているマルチインスタンス環境で動かしていることを想定しています(でなければDBじゃなくてローカルファイルでも大丈夫ですし)。
14日間で破棄される(と思う)。

image.png

設置方法

config

これが5.6から新しく導入されたっぽいログ設定ファイル。確かに今までは bootstrap/app.php という根元のファイルに直書きしていたのでキモチワルかった。

config/logging.php
        // ...
        'errorlog' => [
            'driver' => 'errorlog',
            'level' => 'debug',
        ],
        // 以下を追加
        'database' => [
            'driver' => 'monolog',
            'handler' => \App\Loggers\DatabaseMonologHandler::class,
            // 'handler_with' => [
            //     'host' => 'my.logentries.internal.datahubhost.company.com',
            //     'port' => '10000',
            // ],
        ],
    ],

次はオプションですが推奨設定。Logをコネクションで分けます。なぜこうするのか、詳しくは「DBトランザクションミドルウェア」を参照くださいませ。

config/database.php
        // 追加 内容は mysql と全く同じです
        'mysql_log' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

.env

.env
# 以下を追記
LOG_CHANNEL=database
DB_LOG_TABLE=logs
DB_LOG_CONNECTION=mysql_log # 上記コネクションを増やしてない場合は mysql で
DB_LOG_FLUSH_RATIO=100
DB_LOG_PRESERVE_DAYS=14

マイグレーション

もちろんファイル名は任意です。

2018_08_08_085700_create_log_table.php
<?php

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

class CreateLogTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create(
            env('DB_LOG_TABLE', 'logs'),
            function (Blueprint $table) {
                $table->engine = 'InnoDB';
                $table->bigIncrements('id');
                $table->string('instance')->index();
                $table->string('channel')->index();
                $table->string('level')->index();
                $table->string('level_name');
                $table->text('message');
                $table->text('context');
                $table->integer('remote_addr')->nullable()->unsigned();
                $table->string('user_agent')->nullable();
                $table->integer('created_by')->nullable()->index();
                $table->dateTime('created_at');

                // インデックス
                $table->index(['created_at']);
                $table->index(['level','channel','created_at']);
            }
        );
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop(env('DB_LOG_TABLE', 'logs'));
    }

}

マイグレーションを実行しておきます。

$ php artisan migrate
Migrating: 2018_08_08_085700_create_log_table
Migrated:  2018_08_08_085700_create_log_table

Monologハンドラー

ログインユーザーを拾ったりしているのでLaravel依存です。
そういうのを取り除けば、他のフレームワークでも使用できると思います。

app\Loggers\DatabaseMonologHandler
<?php
namespace App\Loggers;

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

class DatabaseMonologHandler extends AbstractProcessingHandler
{
    protected $table;
    protected $connection;
    public function __construct($level = Logger::DEBUG, $bubble = true)
    {
        $this->table = env('DB_LOG_TABLE', 'logs');
        $this->connection = env('DB_LOG_CONNECTION', env('DB_CONNECTION', 'mysql'));
        parent::__construct($level, $bubble);
    }

    protected function write(array $record)
    {
        $data = [
            'instance' => gethostname(),
            'message' => $record['message'],
            'channel' => $record['channel'],
            'level' => $record['level'],
            'level_name' => $record['level_name'],
            'context' => json_encode($record['context']),
            'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? ip2long($_SERVER['REMOTE_ADDR']) : null,
            'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null,
            'created_by' => Auth::id() > 0 ? Auth::id() : null,
            'created_at' => $record['datetime']->format('Y-m-d H:i:s'),
        ];
        DB::connection($this->connection)->table($this->table)->insert($data);

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

    }

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

モデル

以下もオプションです。ログをWEB上から一覧したい場合は、Modelがあれば一瞬で取得できるので。

app\Logger\Log.php
<?php
namespace App\Loggers;

use Illuminate\Database\Eloquent\Model;

class Log extends Model
{
    const UPDATED_AT = null;

}

ハマりポイント

この「Loggerの切り替え」で何らかのエラーがあると Use Emergency Logger といって、デフォルトのファイルLoggerを使います。そのとき、ちゃんと「何が原因で切り替えが失敗したのか」をエラーリポートしてくれなくて困りました。実際には、上記の Handler のパスが通ってなかった、といった簡単な理由だったのに。

TODO

とりあえずガガガーっと書いただけなので、また動作テストを重ねたりしてから更新しようと思っています。

  • DatabaseMonologHandler の中で env() しているけど、これは、コメントアウトしている handle_with でコンストラクタに注入したほうが良さそうです。
  • リレーションするわけでもないし、消えても大した問題はないし、こういうのは NoSQL の格好の適用例。いつかRedisに切り替えよう…。
  • remote_addr がintegerなのは、たぶん Laravel の仕様です。192.168... と10進数連結の文字列に変換したいところです。
  • インデックスが2つ用意していますが、WEB画面で日時順に取得したり、ログレベルでフィルタリングしたりするためのものです。こういうのは scopeXxxx で検索条件を固めておくと良いと思うので、実装したら書き足します(まだWEB画面作ってない…)
18
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?