Laravel 5.6. Monolog хранения логов в базе данных.

Данная статья не является полноценным переводом, для меня она является руководством по размещению логов в базу данных. Надеюсь кому-то она еще поможет, решить данную проблему. Если будут вопросы, пишите в комментариях, будем вместе разбираться.

Итак проблема: запись логов в базу данных.

В Laravel в коробочном решении есть логгер «Monolog». Фреймворк и библиотека позволяет нам написать собственный обработчик логов, но информации, как всегда, в русскоязычной сети недостаточно. Поиск привел меня на статью, с помощью которой мне удалось решить данную проблему, и я решил поделиться с Вами.

В первую очередь создаем миграцию и модель

php artisan make:model Models/Log -m

Код миграции:

Schema::create('logs', function (Blueprint $t) {
    $t->increments('id');
    $t->text('description')->nullable();
    $t->string('origin', 200)->nullable();
    $t->enum('type', ['log', 'store', 'change', 'delete']);
    $t->enum('result', ['success', 'neutral', 'failure']);
    $t->enum('level', ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug']);
    $t->string('token', 100)->nullable();
    $t->ipAddress('ip');
    $t->string('user_agent', 200)->nullable();
    $t->string('session', 100)->nullable();
    $t->timestamps();
});

Далее настраиваем канал логов.

Переходим в настройки \config\logging.php

'database' => [
  'driver' => 'custom',
  'via' => App\Logging\Database\Log::class,
],

custom — указываем на то, что мы пишем свой обработчик
database — название канала
via — обработчик

Настройка обработчика Monolog

Создаем файл Log.php. Я сделал такой путь до файла \app\Logging\Database\Log.php. Вы можете сделать любой другой.

namespace App\Logging\Database;

use Monolog\Logger as Monolog;

class Log
{
  /**
   * Create a custom Monolog instance.
   *
   * @param  array $config
   *
   * @return \Monolog\Logger
   */
  public function __invoke(array $config)
  {
    $logger = new Monolog('database');
    $logger->pushHandler(new LogHandler());
    $logger->pushProcessor(new LogProcessor());

    return $logger;

  }
}

LogHandler

Находясь в том же пространстве имен, создаем обработчик LogHandler

namespace App\Logging\Database;

use App\Events\Logs\LogMonologEvent;
use Monolog\Logger as Monolog;
use Monolog\Handler\AbstractProcessingHandler;
use App\Models\Log;

class LogHandler extends AbstractProcessingHandler
{
  public function __construct($level = Monolog::DEBUG)
  {
    parent::__construct($level);
  }

  /**
   * Writes the record down to the log of the implementing handler
   *
   * @param  array $record
   *
   * @return void
   */
  protected function write(array $record)
  {
    // Simple store implementation
    $log = new Log();
    $log->fill($record['formatted']);
    $log->save();
// Queue implementation
     //event(new LogMonologEvent($record));
  }

  /**
   * {@inheritDoc}
   */
  protected function getDefaultFormatter()
  {
    return new LogFormatter();
  }
}

LogProcessor

Создаем LogProcessor

namespace App\Logging\Database;


class LogProcessor
{
  public function __invoke(array $record)
  {
    $record['extra'] = [
      'user_id'    => auth()->user() ? auth()->user()->id : null,
      'origin'     => request()->headers->get('origin'),
      'ip'         => request()->server('REMOTE_ADDR'),
      'user_agent' => request()->server('HTTP_USER_AGENT')
    ];

    return $record;
  }
}

При выводе $record будет массив, здесь есть два массива context и extra. Нам нужно еще level, description, result и др, чтобы собрать в кучу и отформатировать их, пишем LogFormatter.

array:7 [▼
  "message" => "monologTest database!"
  "context" => array:1 [▼
    "foo" => "a"
  ]
  "level" => 200
  "level_name" => "INFO"
  "channel" => "database"
  "datetime" => DateTime @1572337460 {#538 ▶}
  "extra" => array:4 [▼
    "user_id" => 1
    "origin" => null
    "ip" => "127.0.0.1"
    "user_agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"
  ]
]

 

LogFormatter

array:8 [▼
  "message" => "monologTest database!"
  "context" => array:1 [▶]
  "level" => 200
  "level_name" => "INFO"
  "channel" => "database"
  "datetime" => DateTime @1572337204 {#548 ▶}
  "extra" => array:4 [▶]
  "formatted" => array:10 [▼
    "foo" => "a"
    "user_id" => 1
    "origin" => null
    "ip" => "127.0.0.1"
    "user_agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"
    "level" => "info"
    "description" => "monologTest database!"
    "token" => "djGHAzWpZirbl4HysbVUUFWYpxiA3u"
    "type" => "log"
    "result" => "neutral"
  ]
]

Создаем обработчик

namespace App\Logging\Database;

use Monolog\Formatter\NormalizerFormatter;

class LogFormatter extends NormalizerFormatter
{
  /**
   * type
   */
  const LOG = 'log';
  const STORE = 'store';
  const CHANGE = 'change';
  const DELETE = 'delete';
  /**
   * result
   */
  const SUCCESS = 'success';
  const NEUTRAL = 'neutral';
  const FAILURE = 'failure';

  public function __construct()
  {
    parent::__construct();
  }

  /**
   * {@inheritdoc}
   */
  public function format(array $record)
  {
    $record = parent::format($record);

    return $this->getDocument($record);
  }

  /**
   * Convert a log message into an MariaDB Log entity
   *
   * @param array $record
   *
   * @return array
   */
  protected function getDocument(array $record)
  {
    $fills                = $record['extra'];
    $fills['level']       = mb_strtolower($record['level_name']);
    $fills['description'] = $record['message'];
    $fills['token']       = str_random(30);
    $context              = $record['context'];
    if (!empty($context))
    {
      $fills['type']   = array_has($context, 'type') ? $context['type'] : self::LOG;
      $fills['result'] = array_has($context, 'result') ? $context['result'] : self::NEUTRAL;
      $fills           = array_merge($record['context'], $fills);
    }

    return $fills;
  }
}

Сохранение в БД

Теперь при добавлении данного кода

Log::channel('database')->info('monologTest database!',['foo'=>'a']);

появиться ошибка Unknown column ‘foo’, произошло она из-за того что мы слили массив и этого поля нет в БД.

 

Сделаем исключение лишних данных(перед методом fill)ю

Реализуем Event-Listener

в обработчике раскомментируем строку event()

// App\Logging\Database\LogHandler

protected function write(array $record)
{
    event(new LogMonologEvent($record));
}

LogMonologEvent

Создаем Event

php artisan make:event LogMonologEvent
<?php

namespace App\Events\Logs;

use Illuminate\Queue\SerializesModels;

class LogMonologEvent
{
    use SerializesModels;

  /**
   * @var
   */
  public $records;
  /**
   * @param $model
   */
  public function __construct(array $records)
  {
    $this->records = $records;
}
}

Регистрируем слушателя

В файле EventServiceProvider

protected $subscribe = [
    \App\Listeners\LogMonologEventListener::class,
];

LogMonologEventListener

namespace App\Listeners;


use App\Events\Logs\LogMonologEvent;
use App\Models\Log;
use Illuminate\Contracts\Queue\ShouldQueue;

class LogMonologEventListener implements ShouldQueue
{
  public $queue = 'logs';
  protected $log;

  public function __construct(Log $log)
  {
    $this->log = $log;
  }

  /**
   * @param $event
   */
  public function onLog($event)
  {
    $log = new $this->log;
    $log->fill($event->records['formatted']);
    $log->save();
  }

  /**
   * Register the listeners for the subscriber.
   *
   * @param \Illuminate\Events\Dispatcher $events
   */
  public function subscribe($events)
  {
    $events->listen(
      LogMonologEvent::class,
      '\App\Listeners\LogMonologEventListener@onLog'
    );
  }
}

В модели Log

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Log extends Model
{
  /**
   * @var string $table
   */
  protected $table = 'logs';
  /**
   * @var array $guarded
   */
  protected $guarded = ['id'];

  protected $fillable = ['description', 'origin', 'type', 'result', 'level', 'token', 'ip', 'user_agent', 'session'];
}

 

Отдельный лог ошибок исключений

В файле \app\Exceptions\Handler.php делаем такой репорт.

public function report(Exception $exception)
{
    if ($this->shouldntReport($exception)) {
        return;
    }
    Log::channel('daily')->error(
        $exception->getMessage(),
        array_merge($this->context(), ['exception' => $exception])
    );
}

Вот и все.