Если вы видите это, значит, я еще не придумал, что написать.
При разработке проектов я придерживаюсь принципа подробного логирования критически важных частей приложения. Это позволяет по логам понять, что происходило, что пошло не так и почему.
Однако при активном использовании логирования в проектах на Laravel я регулярно сталкивался с рядом неудобств:
Чтобы решить эти (и не только) проблемы, я сначала добавил необходимые улучшения в одном из проектов, затем начал переносить их в другие, а в итоге оформил всё в отдельный пакет — faustoff/laravel-contextify. Им я и хочу с вами поделиться.
Контекстное логирование с встроенными уведомлениями для Laravel.
use Faustoff\Contextify\Facades\Contextify;
Contextify::notice('Updated', ['key' => 'value'])->notify(['mail']);
// [2025-01-01 12:00:00] local.NOTICE: Updated {"key":"value"} {"trace_id":"4f9c2a1b"}
Laravel Contextify расширяет возможности логирования Laravel двумя основными (но не единственными) функциями:
Предоставляет фасад Contextify, совместимый с фасадом Log от Laravel: те же методы (debug, info, notice, warning, error, critical, alert, emergency) с идентичными параметрами, плюс цепочный метод notify().
Происхождение названия: “Contextify” объединяет Context (контекст) и Notify (уведомлять), отражая двойное назначение — обогащать логи контекстными данными и отправлять уведомления о событиях логирования.
only и exceptУстановите пакет через Composer:
composer require faustoff/laravel-contextify
При необходимости опубликуйте файл конфигурации:
php artisan vendor:publish --tag=contextify-config
Это создаст config/contextify.php для настройки провайдеров контекста и уведомлений.
Добавьте в .env для настройки получателей уведомлений:
CONTEXTIFY_MAIL_ADDRESSES=admin@example.com,team@example.com
CONTEXTIFY_TELEGRAM_CHAT_ID=123456789
Примечание: Для уведомлений Telegram требуется установка пакета laravel-notification-channels/telegram вручную.
Используйте фасад Contextify так же, как фасад Log от Laravel. Логи автоматически включают дополнительный контекст из провайдеров контекста, настроенных для логирования:
<?php
use Faustoff\Contextify\Facades\Contextify;
Contextify::debug('Отладочное сообщение', ['key' => 'value']);
// [2025-01-01 12:00:00] local.DEBUG: Отладочное сообщение {"key":"value"} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Services/ExampleService.php:42","class":"App\\Services\\ExampleService"}
Contextify::info('Пользователь вошёл в систему', ['user_id' => 123]);
// [2025-01-01 12:00:00] local.INFO: Пользователь вошёл в систему {"user_id":123} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Http/Controllers/Auth/LoginController.php:55","class":"App\\Http\\Controllers\\Auth\\LoginController"}
Contextify::notice('Важное уведомление');
// [2025-01-01 12:00:00] local.NOTICE: Важное уведомление {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"routes/web.php:10","class":null}
// ... и то же самое для warning, error, critical, alert и emergency
Цепочкой вызовите notify() после любого метода логирования для отправки уведомлений. Уведомления включают сообщение лога, контекст и дополнительный контекст из провайдеров контекста, настроенных для уведомлений.
Фильтруйте каналы с помощью параметров only и except:
<?php
use Faustoff\Contextify\Facades\Contextify;
Contextify::error('Ошибка обработки платежа', ['order_id' => 456])->notify();
// [2025-01-01 12:00:00] local.ERROR: Ошибка обработки платежа {"order_id":456} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Http/Controllers/Api/OrderController.php:133","class":"App\\Http\\Controllers\\Api\\OrderController"}
// Уведомление с контекстом {"order_id":456} и дополнительным контекстом отправлено во все настроенные каналы уведомлений
Contextify::critical('Потеряно соединение с базой данных')->notify(only: ['mail']);
// [2025-01-01 12:00:00] local.CRITICAL: Потеряно соединение с базой данных {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/MonitorCommand.php:71","class":"App\\Console\\Commands\\MonitorCommand"}
// Уведомление с дополнительным контекстом отправлено только в канал почты
Contextify::alert('Обнаружена попытка взлома')->notify(except: ['telegram']);
// [2025-01-01 12:00:00] local.ALERT: Обнаружена попытка взлома {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Providers/AppServiceProvider.php:25","class":"App\\Providers\\AppServiceProvider"}
// Уведомление с дополнительным контекстом отправлено во все настроенные каналы уведомлений, кроме канала Telegram
По необходимости, вы можете переопределить стандартную реализацию уведомления LogNotification:
<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\LogNotification;
class CustomLogNotification extends LogNotification
{
// Переопределите методы или добавьте новые
}
Обновите конфигурацию:
'notifications' => [
'class' => \App\Notifications\CustomLogNotification::class,
// ... другие настройки уведомлений
],
Уведомления об исключениях отправляются автоматически (включено по умолчанию). Уведомления включают детали исключения (сообщение и трассировку стека) и дополнительный контекст из провайдеров контекста, настроенных для уведомлений.
По необходимости, вы можете переопределить стандартную реализацию уведомления ExceptionNotification:
<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\ExceptionNotification;
class CustomExceptionNotification extends ExceptionNotification
{
// Переопределите методы или добавьте новые
}
Обновите конфигурацию:
'notifications' => [
'exception_class' => \App\Notifications\CustomExceptionNotification::class,
// ... другие настройки уведомлений
],
Чтобы отключить автоматические уведомления об исключениях, установите reportable в null:
'notifications' => [
'reportable' => null,
// ... другие настройки уведомлений
],
Примечание:
ExceptionNotificationFailedExceptionпредотвращает бесконечные циклы при сбое уведомлений об исключениях.
Провайдеры контекста добавляют дополнительные контекстные данные в логи и уведомления, помогая вам сохранять сообщения в записях логов и уведомлениях короткими и чистыми, перемещая дополнительный контекст из самого сообщения в отдельную область. Контекстные данные по-прежнему присутствуют в записи лога или уведомлении, но они отделены от самого сообщения — оставляя сообщение в центре внимания, при этом сохраняя все дополнительные контекстные данные для поиска и анализа. Вам больше не нужно заботиться о том, чтобы каждый раз явно передавать нужный дополнительный контекст, так как он будет добавлен автоматически.
Статические провайдеры возвращают данные, которые остаются постоянными на протяжении жизненного цикла запроса/процесса. Они реализуют StaticContextProviderInterface.
Встроенные:
pid)trace_id) для распределённой трассировкиhostname)environment)Статический контекст кэшируется при загрузке приложения. Используйте touch() для ручного обновления, что полезно при форке процесса (например, для воркеров очередей) для генерации нового ID трассировки:
<?php
use Faustoff\Contextify\Facades\Contextify;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
// Обновить конкретный провайдер (например, сгенерировать новый ID трассировки)
Contextify::touch(TraceIdContextProvider::class);
// Обновить все статические провайдеры
Contextify::touch();
Динамические провайдеры обновляют данные при каждом вызове логирования. Они реализуют DynamicContextProviderInterface.
Встроенные:
file) и имя класса (class) вызывающего кодаpeak_memory_usage)datetime)Реализуйте StaticContextProviderInterface или DynamicContextProviderInterface:
<?php
namespace App\Context\Providers;
use Faustoff\Contextify\Context\Contracts\StaticContextProviderInterface;
class CustomContextProvider implements StaticContextProviderInterface
{
public function getContext(): array
{
return [
// реализация ...
];
}
}
Добавьте пользовательские провайдеры в config/contextify.php:
<?php
use App\Context\Providers\CustomContextProvider;
use Faustoff\Contextify\Context\Providers\CallContextProvider;
use Faustoff\Contextify\Context\Providers\EnvironmentContextProvider;
use Faustoff\Contextify\Context\Providers\HostnameContextProvider;
use Faustoff\Contextify\Context\Providers\ProcessIdContextProvider;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
return [
'logs' => [
'providers' => [
// Встроенные провайдеры
ProcessIdContextProvider::class,
TraceIdContextProvider::class,
CallContextProvider::class,
// Пользовательские провайдеры
CustomContextProvider::class,
],
// ... другие настройки логов
],
'notifications' => [
'providers' => [
// Встроенные провайдеры
HostnameContextProvider::class,
ProcessIdContextProvider::class,
TraceIdContextProvider::class,
EnvironmentContextProvider::class,
CallContextProvider::class,
// Пользовательские провайдеры
CustomContextProvider::class,
],
// ... другие настройки уведомлений
],
];
Определите отдельные провайдеры контекста для логов и уведомлений. Если провайдер присутствует в обоих наборах, те же контекстные данные используются для обоих случаев.
Настройте в config/contextify.php:
logs.providers — провайдеры для записей логовnotifications.providers — провайдеры для уведомленийПример:
<?php
use Faustoff\Contextify\Context\Providers\CallContextProvider;
use Faustoff\Contextify\Context\Providers\EnvironmentContextProvider;
use Faustoff\Contextify\Context\Providers\HostnameContextProvider;
use Faustoff\Contextify\Context\Providers\PeakMemoryUsageContextProvider;
use Faustoff\Contextify\Context\Providers\ProcessIdContextProvider;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
return [
'logs' => [
'providers' => [
ProcessIdContextProvider::class, // Общий
TraceIdContextProvider::class, // Общий
CallContextProvider::class, // Только для логов
PeakMemoryUsageContextProvider::class, // Только для логов
],
// ... другие настройки логов
],
'notifications' => [
'providers' => [
HostnameContextProvider::class, // Только для уведомлений
EnvironmentContextProvider::class, // Только для уведомлений
ProcessIdContextProvider::class, // Общий
TraceIdContextProvider::class, // Общий
],
// ... другие настройки уведомлений
],
];
Поддерживаются каналы mail и telegram из коробки. Почта работает сразу; для Telegram требуется пакет laravel-notification-channels/telegram.
Настройте каналы в config/contextify.php:
'notifications' => [
/*
* Используйте формат ассоциативного массива ['channel' => 'queue'] для указания
* очереди для каждого канала. При простом массиве ['channel'] будет использоваться очередь 'default'.
*/
'channels' => [
'mail' => 'mail-queue',
'telegram' => 'telegram-queue',
],
'mail_addresses' => explode(',', env('CONTEXTIFY_MAIL_ADDRESSES', '')),
// ... другие настройки уведомлений
],
Например, чтобы добавить уведомления Slack, необходимо:
toSlack() согласно документации:<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\LogNotification;
use Illuminate\Notifications\Messages\SlackMessage;
class CustomLogNotification extends LogNotification
{
public function toSlack($notifiable): SlackMessage
{
// См. https://laravel.com/docs/12.x/notifications#formatting-slack-notifications
return (new SlackMessage())
->content(ucfirst($this->level) . ': ' . $this->message);
}
}
routeNotificationForSlack() согласно документации:<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\Notifiable;
class CustomNotifiable extends Notifiable
{
public function routeNotificationForSlack($notification): string
{
// См. https://laravel.com/docs/12.x/notifications#routing-slack-notifications
return config('services.slack.notifications.channel');
}
}
Настроить Slack в config/services.php.
Обновить config/contextify.php:
'notifications' => [
'class' => \App\Notifications\CustomLogNotification::class,
'notifiable' => \App\Notifications\CustomNotifiable::class,
'channels' => [
'mail',
'telegram',
'slack'
],
// ... другие настройки уведомлений
],
Примечание: Для уведомлений об исключениях расширьте
ExceptionNotificationи добавьте методtoSlack()аналогичным образом.
Нужно больше каналов уведомлений? Добро пожаловать на Laravel Notifications Channels.
Используйте трейт Faustoff\Contextify\Console\Trackable для логирования начала, завершения и времени выполнения команды:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Faustoff\Contextify\Console\Trackable;
use Faustoff\Contextify\Facades\Contextify;
class SyncData extends Command
{
use Trackable;
protected $signature = 'data:sync';
public function handle(): int
{
// Ваша бизнес-логика здесь
Contextify::notice('Данные синхронизированы');
return self::SUCCESS;
}
}
Лог:
[2025-01-01 12:00:00] local.DEBUG: Run with arguments {"command":"data:sync"} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
[2025-01-01 12:00:00] local.NOTICE: Данные синхронизированы {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
[2025-01-01 12:00:00] local.DEBUG: Execution time: 1 second {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
Используйте трейт Faustoff\Contextify\Console\Outputable для перехвата вывода консоли Laravel из методов типа info() и сохранения его в логах:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Faustoff\Contextify\Console\Outputable;
class SyncData extends Command
{
use Outputable;
protected $signature = 'data:sync';
public function handle(): int
{
// Ваша бизнес-логика здесь
$this->info('Данные синхронизированы');
return self::SUCCESS;
}
}
Лог:
[2025-01-01 12:00:00] local.NOTICE: Данные синхронизированы {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
Обрабатывайте сигналы завершения (SIGQUIT, SIGINT, SIGTERM по умолчанию) для корректного завершения. Используйте соответствующий трейт с SignalableCommandInterface:
TerminatableV62 для symfony/console:<6.3 (Laravel 9, 10)TerminatableV63 для symfony/console:^6.3 (Laravel 9, 10)TerminatableV70 для symfony/console:^7.0 (Laravel 11+)<?php
namespace App\Console\Commands;
use Faustoff\Contextify\Console\TerminatableV62;
use Illuminate\Console\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
class ConsumeStats extends Command implements SignalableCommandInterface
{
use TerminatableV62;
protected $signature = 'stats:consume';
public function handle(): void
{
while (true) {
// ...
if ($this->shouldTerminate) {
// Выполнение прервано обработчиком сигнала завершения
break;
}
}
}
}
Лог:
[2025-01-01 12:00:00] local.WARNING: Received SIGTERM (15) shutdown signal {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/ConsumeStats.php:42","class":"App\\Console\\Commands\\ConsumeStats"}
{message}