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

# なにがしたい？

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


# 結論

## どんなものができるか？

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/129854/a80eb925-e456-0e67-ecc7-a8f9ce42ec4f.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トランザクションミドルウェア」](https://qiita.com/kd9951/items/da7326f74475823de10f)を参照くださいませ。

```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

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

### マイグレーション

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

```php: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'));
    }

}
```

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

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

### Monologハンドラー

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

```php: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があれば一瞬で取得できるので。

```php: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画面作ってない…）