Логирование и мониторинг Laravel: рабочие приёмы
Дефолтный single-канал в storage/logs забивает диск и не помогает искать причину сбоя. Разбираем каналы, контекст, интеграцию с внешними системами и что реально отдавать в мониторинг.

Приложение отдаёт 500, а в логе — сотни строк production.ERROR без единой зацепки: ни ID пользователя, ни идентификатора запроса, ни того, какой из четырёх воркеров очереди упал. Дефолтная связка Laravel (single-канал, всё валится в один storage/logs/laravel.log) годится ровно до первого инцидента в проде. Дальше начинается разбор: как раскидать логи по каналам, как прицепить к каждой записи контекст запроса и как отдать наружу то, на что стоит настроить алерт. Материал для Laravel 11.x, большинство приёмов работает и в 10.x — расхождения отмечаю.
Каналы: почему single — плохая идея на проде
Laravel настраивает логирование через config/logging.php. По умолчанию активен канал stack, который оборачивает single — один файл, растущий бесконечно. Три проблемы разом: файл не ротируется и съедает диск, все уровни логов вперемешку, и при нескольких процессах записи конкурируют за один файл.
Первое, что стоит поменять — переключиться на daily или отдавать логи в stdout, если приложение живёт в контейнере. Логика проста: в Docker/Kubernetes файл на диске контейнера — это данные, которые исчезнут при рестарте пода, поэтому логи пишут в поток, а сбором занимается платформа.
# .env для контейнерного окружения
LOG_CHANNEL=stderr
LOG_LEVEL=info
# .env для классического сервера с ротацией
LOG_CHANNEL=daily
LOG_LEVEL=warning
Канал daily держит N дневных файлов и удаляет старые. Число дней задаётся параметром days в конфиге канала:
// config/logging.php
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14, // хранить две недели
'replace_placeholders' => true,
],
Разные каналы под разные задачи
Смешивать в одном файле платёжные события и отладку HTTP-клиента — значит потом искать иголку в стоге. Заведите отдельные каналы и пишите в них явно:
// config/logging.php
'channels' => [
'payments' => [
'driver' => 'daily',
'path' => storage_path('logs/payments.log'),
'level' => 'info',
'days' => 90, // платежи храним дольше
],
],
use Illuminate\Support\Facades\Log;
// пишем в конкретный канал
Log::channel('payments')->info('Платёж проведён', [
'order_id' => $order->id,
'amount' => $order->total,
'provider' => 'yookassa',
]);
Контекст: чтобы запись говорила, а не молчала
Голое сообщение «Ошибка при списании» бесполезно. Второй аргумент методов Log — массив контекста, и именно он превращает лог в инструмент. Но добавлять order_id вручную в каждый вызов — путь к пропущенным полям.
С Laravel 8 есть Log::withContext() — контекст навешивается на все последующие записи в рамках запроса. Удобное место для этого — middleware, где вы кладёте идентификатор запроса и пользователя один раз:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class AttachLogContext
{
public function handle(Request $request, Closure $next)
{
// единый идентификатор запроса для сквозной трассировки
$requestId = $request->header('X-Request-Id', (string) Str::uuid());
Log::withContext([
'request_id' => $requestId,
'user_id' => optional($request->user())->id,
'ip' => $request->ip(),
]);
$response = $next($request);
$response->headers->set('X-Request-Id', $requestId);
return $response;
}
}
Теперь любой Log::error() внутри запроса автоматически несёт request_id — и вы сшиваете разрозненные строки одного запроса в цепочку. Для очередей контекст withContext не переносится между задачами автоматически: там навешивайте его в начале handle() джоба.
Лог без контекста запроса — это показания свидетеля, который видел аварию, но не запомнил ни машины, ни времени, ни улицы. Формально запись есть, разобрать по ней нечего.
Структурированный JSON вместо строк
Если логи уходят в систему сбора (Loki, ELK, Datadog), парсить их построчными регулярками — боль. Laravel умеет писать через собственный форматтер Monolog. Подключите JsonFormatter к каналу через опцию formatter в конфиге канала — тогда каждая запись станет одной JSON-строкой с полями message, level, context и datetime. Точный синтаксис подключения форматтера и его аргументов сверьте в разделе документации Laravel «Customizing Monolog for Channels» — он менялся между мажорными версиями.
Куда отдавать логи и на что смотреть
Файл на диске — это архив, а не мониторинг. Мониторинг — это когда вам приходит уведомление до звонка клиента. Здесь два слоя: агрегация логов и отдельно перехват исключений.
| Инструмент | Что делает | Когда брать |
|---|---|---|
| Laravel Pulse | Метрики приложения: медленные запросы, очереди, исключения, нагрузка | Нужна встроенная панель по самому приложению, без внешних сервисов |
| Sentry / Bugsnag | Перехват исключений с трейсом, дедупликация, алерты | Нужно ловить и группировать ошибки в реальном времени |
| Grafana + Loki | Централизованный сбор и поиск по логам из stdout | Контейнеры, несколько сервисов, нужен единый поиск |
| Telegram/Slack-канал | Критические события через logging-канал | Мало ошибок, нужен быстрый пинг команде |
Laravel Pulse — официальный пакет от команды фреймворка, ставится через Composer и даёт дашборд по slow queries, исключениям и очередям. Он не замена трассировке ошибок, а обзор состояния. Sentry ловит именно исключения: интеграция ставится официальным пакетом sentry/sentry-laravel, дальше он сам подхватывает необработанные исключения. Ключ DSN держите в .env, не в репозитории.
Алерт в мессенджер через отдельный канал
Для критичных событий (упала оплата, отвалилась внешняя интеграция) удобно завести отдельный канал с высоким уровнем и отправкой в Slack. Laravel поддерживает Slack-драйвер из коробки:
// config/logging.php
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'App Log',
'emoji' => ':boom:',
'level' => 'critical', // только critical и выше
],
// в коде — только для того, что реально требует реакции ночью
Log::channel('slack')->critical('Платёжный провайдер недоступен', [
'provider' => 'yookassa',
'response_code' => $response->status(),
]);
Главная ошибка здесь — слать в мессенджер всё подряд. Через неделю канал заглушают, и настоящий инцидент проходит незамеченным. В Slack-канал уходит только critical и emergency — то, ради чего человека можно разбудить.
Что реально ломается
- Логи с чувствительными данными. Пароли, токены, номера карт в контексте — прямая утечка. Не логируйте
$request->all()целиком. В Laravel 11 вbootstrap/app.phpможно указать поля, которые фреймворк скроет при логировании исключений валидации через$exceptions->dontFlash()— проверьте актуальный синтаксис в документации Error Handling своей версии. - Диск забился логами. Классика на single-канале. Ротация
dailyплюс мониторинг свободного места. В контейнере — пишите в stdout, диск не ваша забота. - Уровень LOG_LEVEL=debug на проде. Debug пишет всё, включая SQL-запросы и тело ответов. Диск и производительность страдают, в логи попадает лишнее. На проде —
warningилиinfo, не ниже. - Логи очередей без контекста. Джоб упал, а какой заказ обрабатывал — неизвестно. Навешивайте контекст в начале
handle()и логируйтеfailed()-метод джоба явно.
Что настроить дальше
Минимальный набор для прода: канал daily или stderr вместо single, LOG_LEVEL не ниже warning, middleware с request_id, перехват исключений в Sentry и отдельный critical-канал в мессенджер. После этого смотрите на два показателя в проде: частоту исключений по группам (в Sentry видно, что растёт) и долю медленных запросов (Pulse покажет, где узкое место). Следующий большой шаг — распределённая трассировка через OpenTelemetry, если у вас несколько сервисов и request_id в логах перестаёт хватать для сшивания запроса через границы приложений. Это отдельная тема, здесь мы её не разворачиваем.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: Laravel Documentation — Logging, Laravel Pulse — официальная документация
Частые вопросы
Разница между single и daily — стоит ли всегда брать daily?
У меня Laravel 10, а не 11 — приёмы сработают?
Логирую, но в логах нет request_id — что не так?
Не опасно ли слать логи в Sentry или Slack — там же могут оказаться приватные данные?
Sentry и Laravel Pulse — что-то одно или оба сразу?
Какой LOG_LEVEL ставить на проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.