Любите загадки? Событие всё ещё доступно на сайте

Контекстное логирование с встроенными уведомлениями для 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 двумя основными (но не единственными) функциями:

  1. Встроенные уведомленияотправка уведомлений вместе с логированием в одну строку без необходимости разделения кода на несколько строк для логирования и отправки уведомления.
  2. Автоматическое добавление контекста — логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста (встроенные: 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, необходимо:

  1. Создать пользовательский класс уведомлений с реализованным методом 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);
    }
}
  1. Создать пользовательский класс 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');
    }
}
  1. Настроить Slack в config/services.php.

  2. Обновить 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"}

GitHub