Данная статья не является полноценным переводом, для меня она является руководством по размещению логов в базу данных. Надеюсь кому-то она еще поможет, решить данную проблему. Если будут вопросы, пишите в комментариях, будем вместе разбираться.
Итак проблема: запись логов в базу данных.
В 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]) ); }
Вот и все.