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