Контекстное логирование с встроенными уведомлениями для Laravel
При разработке проектов я придерживаюсь принципа подробного логирования критически важных частей приложения. Это позволяет по логам понять, что происходило, что пошло не так и почему.
Однако при активном использовании логирования в проектах на Laravel я регулярно сталкивался с рядом неудобств:
- Когда требуется залогировать важное событие и одновременно отправить уведомление о нём, приходится писать несколько строк кода: сформировать сообщение, записать его в лог, отправить уведомление. Это громоздко и утомляет.
- Когда нужно отследить последовательность событий по логам, сузив контекст до конкретного пользователя, джобы, консольной команды или процесса, приходится заранее думать о структуре сообщений и постоянно добавлять в них дополнительные детали, чтобы потом найти их в логах. В итоге растёт когнитивная нагрузка, сообщения становятся слишком длинными.
- При возникновении непредвиденных исключений необходимо отправлять уведомления в разные каналы и передавать дополнительный контекст: для какого пользователя произошла ошибка, в каком процессе, на каком сервере и т.д.
Чтобы решить эти (и не только) проблемы, я сначала добавил необходимые улучшения в одном из проектов, затем начал переносить их в другие, а в итоге оформил всё в отдельный пакет — faustoff/laravel-contextify. Им я и хочу с вами поделиться.
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 двумя основными (но не единственными) функциями:
- Встроенные уведомления — отправка уведомлений вместе с логированием в одну строку без необходимости разделения кода на несколько строк для логирования и отправки уведомления.
- Автоматическое добавление контекста — логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста (встроенные: ID трассировки, ID процесса, Имя хоста, Файл и строка вызова и другие), помогая сохранять сообщения короткими и чистыми, перемещая дополнительный контекст из самого сообщения в отдельную область.
Предоставляет фасад Contextify, совместимый с фасадом Log от Laravel: те же методы (debug, info, notice, warning, error, critical, alert, emergency) с идентичными параметрами, плюс цепочный метод notify().
Происхождение названия: “Contextify” объединяет Context (контекст) и Notify (уведомлять), отражая двойное назначение — обогащать логи контекстными данными и отправлять уведомления о событиях логирования.
Возможности
- 📧 Поддержка уведомлений: Отправка уведомлений через почту, telegram или любой канал уведомлений Laravel
- 🔍 Автоматическое добавление контекста: Логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста
- 🔌 Пользовательские провайдеры контекста: Расширяйте встроенные провайдеры своими собственными
- 🔄 Статические и динамические провайдеры**: Статические (кэшируемые) и динамические (обновляемые при каждом вызове) провайдеры
- 🎯 Контекст на основе групп: Отдельные провайдеры контекста для логов и уведомлений
- 📊 Уровни логирования PSR-3: Все стандартные уровни логирования (debug, info, notice, warning, error, critical, alert, emergency)
- 🎨 Пользовательские уведомления: Расширяйте классы уведомлений и добавляйте пользовательские каналы
- 🔔 Фильтрация каналов: Фильтрация каналов с параметрами
onlyиexcept - 🔄 Удобный API: Цепочка методов для читаемого кода
- ⚡ Интеграция с Monolog: Интегрируется с логированием Laravel через процессоры Monolog
Требования
- PHP 8.0 или выше
- Laravel 8.0 или выше
- Monolog 2.0 или выше
Установка
Установите пакет через 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.
Встроенные:
- ProcessIdContextProvider: Добавляет ID текущего процесса PHP (
pid) - TraceIdContextProvider: Генерирует уникальный 16-символьный шестнадцатеричный ID трассировки (
trace_id) для распределённой трассировки - HostnameContextProvider: Добавляет имя хоста сервера (
hostname) - EnvironmentContextProvider: Добавляет окружение приложения (
environment)
Обновление статического контекста
Статический контекст кэшируется при загрузке приложения. Используйте touch() для ручного обновления, что полезно при форке процесса (например, для воркеров очередей) для генерации нового ID трассировки:
<?php
use Faustoff\Contextify\Facades\Contextify;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
// Обновить конкретный провайдер (например, сгенерировать новый ID трассировки)
Contextify::touch(TraceIdContextProvider::class);
// Обновить все статические провайдеры
Contextify::touch();
Динамические провайдеры контекста
Динамические провайдеры обновляют данные при каждом вызове логирования. Они реализуют DynamicContextProviderInterface.
Встроенные:
- CallContextProvider: Добавляет путь к файлу и номер строки (
file) и имя класса (class) вызывающего кода - PeakMemoryUsageContextProvider: Добавляет пиковое использование памяти в байтах (
peak_memory_usage) - DateTimeContextProvider: Добавляет текущую дату и время в формате логов Laravel (
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);
}
}
- Создать пользовательский класс notifiable с реализованным методом
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"}