Перейти к содержимому
Поиск
Php Разработка

Monolog в PHP: логирование, сравнение с error_log и 12 примеров

Уровни логов, контекст, JSON, исключения, ротация файлов и ошибки PHP. Сравниваем Monolog с error_log на одном событии и разбираем 12 запускаемых примеров.

Александр Некрасов
Monolog — логи и ошибки PHP: журнал событий с выделенной ошибкой и лупой

Пользователь увидел ошибку, задача в очереди остановилась, внешний API ответил неожиданно — разработчику нужно восстановить ход событий. Для этого приложение записывает журнал: что произошло, когда, насколько это серьёзно и какие безопасные данные помогут разобраться. Monolog позволяет организовать такую запись единообразно для всего PHP-проекта.

Записать ошибку и обработать её — разные действия. Логирование сохраняет сведения. Код приложения решает, что делать дальше: вернуть понятный ответ, отменить операцию, повторить запрос или завершить задачу с ошибкой. Вызов $logger->error() сам по себе не исправляет сбой и не останавливает программу.
Выберите свой маршрут

Статья посвящена Monolog 3. Учебный проект рассчитан на PHP 8.2+, запускается в терминале и пишет только в свою папку var/log/. Внешние сервисы не нужны. Если Composer ещё не установлен, начните с руководства по Composer: установка, зависимости и автозагрузка.

1. Установка и первая запись

В существующем проекте установите библиотеку:

composer require "monolog/monolog:3.*"
composer show monolog/monolog

Для практики по статье скачайте готовый ZIP, распакуйте его и перейдите в папку с composer.json. В архиве есть lock-файл с проверенными версиями; папки vendor/ нет, поэтому при первой установке потребуется интернет:

composer install
composer check-platform-reqs
composer first

Полный файл first.php:

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;

$logDir = __DIR__ . '/var/log';
if (!is_dir($logDir) && !mkdir($logDir, 0770, true) && !is_dir($logDir)) {
    throw new RuntimeException('Не удалось создать папку журнала.');
}
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler($logDir . '/app.log', Level::Info));
$logger->pushProcessor(new PsrLogMessageProcessor());

$logger->debug('Отладочная запись: ниже порога Info.');
$logger->info('Заказ {order_id} создан', ['order_id' => 1042]);
$logger->warning('Ответ API задерживается', ['duration_ms' => 1800]);
$logger->close();

echo "В var/log/app.log добавлены INFO и WARNING; DEBUG отфильтрован." . PHP_EOL;

Команда composer first запускает этот файл; можно выполнить и php first.php. Смотрите созданный файл var/log/app.log. В нём появятся INFO и WARNING с временем, каналом, сообщением и контекстом; DEBUG отфильтруется. При повторном запуске записи добавляются в конец.

После метки времени строка INFO будет выглядеть так: app.INFO: Заказ 1042 создан {"order_id":1042} []. Время зависит от момента запуска и настроек PHP, app — имя канала, order_id — полезное поле контекста. Последние [] означают, что дополнительных полей extra пока нет.

Просмотр последних строк:

PowerShell:

Get-Content ./var/log/app.log -Tail 10

Linux/macOS:

tail -n 10 var/log/app.log
Как запускать следующие фрагменты?

Создайте рядом с composer.json отдельный experiment.php с подготовкой ниже. После неё добавляйте один выбранный фрагмент и выполняйте php experiment.php. Примеры используют полные имена классов, поэтому дополнительные use не нужны. Исходный first.php оставьте для повторной проверки.

<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';

$logDir = __DIR__ . '/var/log';
if (!is_dir($logDir) && !mkdir($logDir, 0775, true) && !is_dir($logDir)) {
    throw new RuntimeException('Не удалось создать каталог логов.');
}

// Ниже добавьте один фрагмент из статьи.

Все дальнейшие PHP-фрагменты предполагают эту подготовку, если рядом не указано иное. Каждый пример создаёт нужный ему $logger.

Подробнее: Monolog: установка и требования.

2. Monolog и стандартный error_log

error_log() доступна в самом PHP и подходит для простой диагностической записи. Monolog — библиотека, которая добавляет единый интерфейс уровней, контекст, настройку форматов, обработчики и правила маршрутизации. Он также умеет писать через error_log() с помощью ErrorLogHandler: эти инструменты могут использоваться вместе.

Задача error_log() Monolog
Записать простое сообщение Один вызов без установки пакетов Один вызов после настройки логгера и обработчика
Разделить важность событий Метки и фильтрацию организуете сами Восемь уровней и порог у обработчика
Передать данные события Собираете строку или JSON самостоятельно Передаёте массив context
Выбрать формат Готовите текст перед вызовом Назначаете formatter обработчику
Писать в несколько мест Добавляете вызовы и условия Настраиваете стек handlers
Добавить общие поля Собственная функция или обёртка Processor дополняет записи
Тестировать код приложения Обычно нужна своя обёртка LoggerInterface, TestHandler или NullLogger

Одно событие двумя способами. Файлы ниже будут содержать сведения об одном учебном заказе. Для сравнения используйте следующую подготовку вместо предыдущего начала experiment.php: она уже содержит <?php и автозагрузку. После неё добавьте один или оба варианта записи. Не вставляйте второй открывающий тег PHP в середину файла.

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

$logDir = __DIR__ . '/var/log';
if (!is_dir($logDir) && !mkdir($logDir, 0770, true) && !is_dir($logDir)) {
    throw new RuntimeException('Не удалось создать папку журнала.');
}
$orderId = 1042;
$context = ['order_id' => $orderId, 'request_id' => 'demo-001'];

Нативный PHP:

$record = [
    'datetime' => (new DateTimeImmutable())->format(DATE_ATOM),
    'channel' => 'orders',
    'level_name' => 'INFO',
    'message' => 'Заказ ' . $orderId . ' создан',
    'context' => $context,
];
$line = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL;
if (!error_log($line, 3, $logDir . '/native.jsonl')) {
    throw new RuntimeException('Не удалось записать журнал.');
}

Monolog:

$handler = new Monolog\Handler\StreamHandler($logDir . '/monolog.jsonl', Monolog\Level::Info);
$handler->setFormatter(new Monolog\Formatter\JsonFormatter());

$logger = new Monolog\Logger('orders');
$logger->pushHandler($handler);
$logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor());

$logger->info('Заказ {order_id} создан', $context);
$logger->close();

Полный вариант находится в error-log-vs-monolog.php. Запустите composer compare: он запишет событие обоими способами и проверит поля. Monolog берёт на себя оформление времени, канала и уровня; в варианте error_log() эти данные формирует ваш код. JSON можно получить в обоих случаях.

Не путайте тип сообщения с уровнем. Число 3 во втором аргументе error_log($message, 3, $file) означает добавление в указанный файл, а не уровень ERROR. Для этого режима перевод строки добавляют самостоятельно. В режиме 0 место записи зависит от конфигурации PHP, а возвращённое true не доказывает доставку сообщения.

Когда что выбрать? Для короткого разового скрипта error_log() может быть достаточно. Если нужны единые события в нескольких модулях, разные уровни, JSON, ротация и тестирование, Monolog помогает сосредоточить правила в одной настройке. Ради одного сообщения добавлять библиотеку необязательно.

Подробнее: PHP: error_log, типы записи и результат.

3. Восемь уровней и порог записи

Уровень отвечает на вопрос «насколько событие серьёзно?». Договоритесь о значениях внутри команды: важно, чтобы похожие события получали одинаковую оценку. Ниже — практические ориентиры, а не обязательная классификация любого приложения.

Уровень Значение Monolog Пример
DEBUG 100 Промежуточный этап обработки для диагностики
INFO 200 Фоновая задача успешно завершилась
NOTICE 250 Значимое штатное событие: изменена конфигурация
WARNING 300 Сервис замедлился, но операция пока выполнена
ERROR 400 Одна операция не выполнена
CRITICAL 500 Важный компонент недоступен
ALERT 550 Сбой требует немедленного вмешательства
EMERGENCY 600 Система в целом неработоспособна

Числа в таблице — шкала Monolog. Это не PHP-константы E_WARNING/E_ERROR. В коде удобнее использовать Monolog\Level, а для записи — методы debug(), info(), warning(), error() и остальные.

$logger = new Monolog\Logger('levels');
$logger->pushHandler(new Monolog\Handler\StreamHandler(
    $logDir . '/levels.log',
    Monolog\Level::Warning
));

$logger->debug('Промежуточный расчёт');
$logger->info('Операция завершена');
$logger->warning('Ответ сервиса занял больше обычного');
$logger->error('Не удалось завершить операцию');
$logger->close();

Результат: добавятся только WARNING и ERROR. Порог Warning пропускает этот уровень и все более серьёзные; он не означает «записывать только warning». Порог Debug пропустит все восемь уровней.

Почему ничего не случилось после critical()?

Уровень помечает запись. Он сам не отправляет уведомление, не меняет HTTP-статус и не завершает процесс. Для оповещения нужен отдельный настроенный обработчик или система мониторинга, а поведение приложения задаётся кодом.

Подробнее: PSR-3: общий интерфейс логирования.

4. Logger, handler, formatter и processor

Чтобы настройка не выглядела набором непонятных классов, разделим обязанности:

Часть На какой вопрос отвечает Пример
Logger Какое событие записать и от какого канала? new Logger('orders')
Processor Какие общие поля добавить или преобразовать? request_id, имя приложения
Handler Куда направить запись и какие уровни принять? Файл с порогом Info
Formatter Как представить запись? Одна строка текста или JSON

В типичной настройке путь такой: вызов logger → подготовка записи и processors → подходящий handler → его formatter → место назначения. Processors также можно назначать конкретному handler. Несколько обработчиков могут получить одно событие, каждый со своим порогом и форматом.

Канал — имя вроде orders, payments или worker. Он помогает определить источник сообщения. Одно имя канала не создаёт автоматически отдельный файл: куда писать, решает handler.

5. Контекст и подстановка значений

Оставляйте сообщение понятным и стабильным, а переменные значения передавайте отдельно. Тогда по тексту события проще искать, а по полям — фильтровать.

$logger = new Monolog\Logger('orders');
$logger->pushHandler(new Monolog\Handler\StreamHandler(
    $logDir . '/context.log', Monolog\Level::Info
));
$logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor());

$logger->info('Заказ {order_id} создан', [
    'order_id' => 1042,
    'items_count' => 3,
    'delivery' => 'pickup',
]);
$logger->close();

Сообщение станет Заказ 1042 создан; дополнительные данные останутся в контексте при обычных настройках этого processor. Без PsrLogMessageProcessor не стоит ожидать автоматической замены {order_id} в таком логгере. Подстановка в сообщение и сохранение структурированных полей — разные операции.

Общее поле для всех событий. Собственный processor в Monolog 3 получает LogRecord и возвращает запись:

$logger = new Monolog\Logger('worker');
$logger->pushHandler(new Monolog\Handler\StreamHandler(
    $logDir . '/processor.log', Monolog\Level::Info
));
$logger->pushProcessor(function (Monolog\LogRecord $record): Monolog\LogRecord {
    $record->extra['application'] = 'demo-worker';
    $record->extra['request_id'] = 'demo-request-001';
    return $record;
});
$logger->info('Начата обработка задания', ['job_id' => 42]);
$logger->close();

context описывает конкретное событие, extra удобно использовать для полей, добавленных processor. В реальном приложении request_id создаётся или проверяется на входе запроса; учебная строка здесь фиксирована для воспроизводимости.

Выбирайте поля явно. Не записывайте пароли, токены, cookies, полные платёжные реквизиты и всё содержимое запроса «на всякий случай». Исключение тоже может содержать чувствительные данные в сообщении или адресе. Маскирование контекста не очищает автоматически текст и stack trace.

Подробнее: Реализация подстановки PsrLogMessageProcessor.

6. Обычный текст и JSON

Строковый журнал удобно читать в терминале, JSON — разбирать программой и отправлять в систему поиска. Формат назначается обработчику.

$handler = new Monolog\Handler\StreamHandler(
    $logDir . '/events.jsonl', Monolog\Level::Info
);
$handler->setFormatter(new Monolog\Formatter\JsonFormatter());
$logger = new Monolog\Logger('orders', [$handler]);

$logger->info('Заказ создан', ['order_id' => 1042, 'amount' => 1500]);
$logger->close();

Каждое событие записывается отдельной JSON-строкой. В записи есть message, context, level, level_name, channel, datetime и extra. Это формат JSON Lines: весь файл из нескольких строк не является одним JSON-массивом. Читайте и декодируйте его построчно.

Как настроить короткую текстовую строку?

После создания $handler, до записи события, назначьте formatter. Не убирайте context из шаблона, если он нужен для диагностики:

$handler->setFormatter(new Monolog\Formatter\LineFormatter(
    "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
    'Y-m-d H:i:s'
));

Один формат не обязательно подходит всем назначениям. Один handler может писать JSON, другой — короткие строки в консоль.

7. Исключения и решение, что делать дальше

Передавайте объект исключения в ключе exception. Тогда formatter сможет извлечь класс, сообщение и другие диагностические сведения. Пример ниже — для локального CLI, ошибка создаётся намеренно:

$handler = new Monolog\Handler\StreamHandler(
    $logDir . '/exceptions.jsonl', Monolog\Level::Error
);
$formatter = new Monolog\Formatter\JsonFormatter();
$formatter->includeStacktraces(true);
$handler->setFormatter($formatter);
$logger = new Monolog\Logger('worker', [$handler]);

$processOrder = static function (): void {
    throw new RuntimeException('Учебный сбой обработки заказа');
};

try {
    $processOrder();
} catch (Throwable $e) {
    $logger->error('Обработка заказа не выполнена', [
        'order_id' => 1042,
        'exception' => $e,
    ]);
    $logger->close();
    fwrite(STDERR, "Не удалось обработать заказ. Подробности записаны в журнал.\n");
    exit(1);
}

Здесь три отдельных действия: catch перехватывает сбой, Monolog записывает сведения, а exit(1) сообщает оболочке о неуспешном выполнении. Если убрать exit, программа сможет продолжить работу после блока — иногда это нужно, иногда приведёт к неверному результату.

Учебная функция вызвана отдельно, чтобы в trace было видно место её вызова. Если бросить исключение прямо на верхнем уровне файла, стек вызовов может оказаться пустым даже при включённом includeStacktraces.

Для веб-приложения вместо CLI-вывода сформируйте корректный HTTP-ответ и понятное пользователю сообщение. Детальный trace оставьте в защищённом журнале. Не записывайте одно и то же исключение в каждом слое перед повторным throw: выберите ответственную границу, чтобы не получить несколько одинаковых событий.

Exception или Throwable?

В PHP Throwable охватывает и Exception, и ошибки типа Error/TypeError. Общий перехват у границы CLI-команды или HTTP-запроса полезен для завершения операции, но внутри бизнес-логики лучше ловить ожидаемые конкретные исключения. Перехват не означает, что после любого сбоя можно безопасно продолжить.

Подробнее: PHP: исключения и распространение ошибки.

8. Предупреждения PHP и непойманные исключения

Вызовы $logger->warning() и $logger->error() записывают ваши события. Чтобы подключить Monolog к обработке диагностических событий самого PHP, существует ErrorHandler. В отдельном учебном скрипте можно зарегистрировать его так:

$logger = new Monolog\Logger('php');
$logger->pushHandler(new Monolog\Handler\StreamHandler(
    $logDir . '/php-errors.log', Monolog\Level::Debug
));
Monolog\ErrorHandler::register($logger);

trigger_error('Учебное предупреждение', E_USER_WARNING);
echo "Код после предупреждения выполнен.\n";

Обычное предупреждение не останавливает исполнение: следующая строка выполнится. В зависимости от настроек и предыдущего обработчика предупреждение также может попасть в штатный вывод/журнал PHP. ErrorHandler не превращает автоматически каждое предупреждение в выброшенное исключение.

Непойманное исключение — другой сценарий. В копии того же примера замените последние две строки следующими:

throw new RuntimeException('Учебное непойманное исключение');
echo "Эта строка не выполнится.\n";

Глобальный обработчик запишет исключение, но выполнение не вернётся к строке после throw. В архиве сценарии php-warning и uncaught выполняются отдельными процессами: проверяются и журнал, и факт продолжения/завершения.

Регистрируйте обработчики в ранней точке входа приложения. Они не смогут исправить синтаксическую ошибку, из-за которой этот файл вообще не начал выполняться, и не заменяют штатный журнал PHP/FPM/веб-сервера. В готовом фреймворке сначала используйте его механизм логирования: повторная глобальная регистрация может изменить существующую обработку ошибок.

Подробнее: Monolog ErrorHandler.

Подробнее: PHP: возможности и ограничения error handler.

9. Файлы, консоль и ротация

StreamHandler записывает в файл или PHP-поток. В контейнере и CLI может быть удобнее отправлять журнал в стандартный поток ошибок, а хранение и сбор поручить окружению:

$logger = new Monolog\Logger('cli');
$logger->pushHandler(new Monolog\Handler\StreamHandler(
    'php://stderr', Monolog\Level::Info
));
$logger->info('Начата обработка задания', ['job_id' => 42]);

Сообщение появится в stderr; оно не смешивается со stdout, куда скрипт может выводить данные. Кто хранит этот поток и как долго — отдельная настройка процесса, контейнера или платформы.

Файлы по датам. Для небольшого приложения можно использовать RotatingFileHandler:

$handler = new Monolog\Handler\RotatingFileHandler(
    $logDir . '/daily.log',
    7,
    Monolog\Level::Info
);
$logger = new Monolog\Logger('app', [$handler]);
$logger->info('Ежедневная проверка завершена');
$logger->close();

При обычных настройках имя будет вида daily-YYYY-MM-DD.log. Число 7 задаёт предел количества сохраняемых файлов этой ротации, а не размер файла в мегабайтах и не отдельный таймер удаления ровно через семь суток. Очистка выполняется при работе обработчика; файл за день всё равно может вырасти большим.

Для нагруженного сервиса заранее определите управление объёмом, сбор, срок хранения и наблюдение за заполнением диска. Можно использовать внешнюю ротацию или централизованный сбор. Не включайте две независимые схемы ротации одного файла без согласования.

Место хранения имеет значение. В веб-приложении размещайте логи вне публичного каталога сайта. Пользователь PHP-процесса должен иметь право записи, а доступ к чтению — только те, кому журнал нужен. Учебная папка var/log не должна становиться доступной по URL.

Подробнее: RotatingFileHandler: ротация и maxFiles.

10. Несколько обработчиков и каналы

Запишем INFO и выше в общий журнал, а ERROR и выше — ещё и в отдельный файл:

$all = new Monolog\Handler\StreamHandler(
    $logDir . '/all.log', Monolog\Level::Info
);
$errors = new Monolog\Handler\StreamHandler(
    $logDir . '/errors.log', Monolog\Level::Error
);
$logger = new Monolog\Logger('orders');
$logger->pushHandler($all);
$logger->pushHandler($errors);

$logger->info('Заказ создан', ['order_id' => 1042]);
$logger->error('Заказ не обработан', ['order_id' => 1043]);
$logger->close();

Ожидается: в all.log две записи, в errors.log одна — если смотреть только этот запуск. Последний добавленный через pushHandler() обработчик вызывается первым. По умолчанию обработанная запись может идти дальше по стеку.

Что изменит bubble=false?

Если при создании $errors передать третьим аргументом false, принятый им ERROR не дойдёт до следующего $all. INFO он не принимает, поэтому INFO попадёт в общий журнал. Так можно разделить назначения, но для копии ошибок в общем журнале оставляйте обычный bubble=true.

Если handlers переданы массивом в конструктор?

В массиве конструктора они перечисляются в порядке вызова. Это отличается от последовательных pushHandler(), каждый из которых кладёт обработчик наверх стека.

Два канала в одном файле:

$handler = new Monolog\Handler\StreamHandler(
    $logDir . '/channels.log', Monolog\Level::Info
);
$orders = new Monolog\Logger('orders', [$handler]);
$worker = new Monolog\Logger('worker', [$handler]);

$orders->info('Заказ поставлен в очередь', ['order_id' => 1042]);
$worker->info('Задание получено', ['job_id' => 42]);
$handler->close();

Обе записи находятся в channels.log, но у них разные поля channel. Не создавайте новый Logger на каждую строку: настройте нужные каналы при запуске приложения и передавайте их компонентам.

Подробнее: Порядок обработчиков и каналы.

11. Предыстория ошибки: FingersCrossedHandler

Иногда полный DEBUG-журнал слишком шумный, но при сбое нужны предшествующие шаги. FingersCrossedHandler может сначала собирать записи в памяти, а при достижении выбранной серьёзности передать накопленное вложенному обработчику.

$file = new Monolog\Handler\StreamHandler(
    $logDir . '/incident.log', Monolog\Level::Debug
);
$buffer = new Monolog\Handler\FingersCrossedHandler(
    $file,
    Monolog\Level::Error,
    50
);
$logger = new Monolog\Logger('checkout', [$buffer]);

$logger->debug('Начинаем проверку заказа');
$logger->info('Товары найдены', ['items_count' => 3]);
$logger->error('Не удалось завершить заказ');
$logger->close();

В incident.log попадут все три записи: ERROR активирует буфер и раскрывает предысторию. Если убрать ERROR, эти DEBUG/INFO в данной конфигурации не будут записаны. Внутренний handler настроен на Debug специально: если поставить ему Error, он отфильтрует полезную предысторию.

Параметр 50 ограничивает буфер количеством записей, не байтами. Не кладите в context огромные объекты. Для долгоживущего обработчика очереди завершайте цикл после задания: $logger->reset() сбрасывает состояние поддерживающих это handlers/processors. Это не удаляет уже записанные файлы и не заменяет установку нового request_id для следующей задачи.

Подробнее: FingersCrossedHandler: активация и буфер.

12. Как подключать логирование к своему коду

Классу приложения полезнее зависеть от Psr\Log\LoggerInterface, чем самому создавать путь к файлу. Тогда формат и место записи задаются снаружи, а в тесте можно использовать другой logger.

final class OrderService
{
    public function __construct(private Psr\Log\LoggerInterface $logger)
    {
    }

    public function prepare(int $orderId): void
    {
        if ($orderId <= 0) {
            throw new InvalidArgumentException('Некорректный номер заказа.');
        }
        $this->logger->info('Заказ подготовлен', ['order_id' => $orderId]);
    }
}

$logger = new Monolog\Logger('orders', [
    new Monolog\Handler\StreamHandler($logDir . '/service.log', Monolog\Level::Info),
]);
$service = new OrderService($logger);
$service->prepare(1042);
$logger->close();

Пример показывает передачу зависимости; метод не создаёт заказ в базе. Ошибку некорректного аргумента должен обработать вызывающий код на выбранной границе. Для теста самого класса можно передать Psr\Log\NullLogger, если журнал не проверяется, либо Monolog с TestHandler, если нужно проверить событие.

Где ставить записи в интеграции: перед важным этапом — безопасный идентификатор операции; при успехе — результат и длительность; при отказе — понятное событие и диагностические поля. Если используете HTTP-клиент, рядом пригодится статья о Guzzle, запросах и исключениях. Секретные заголовки и полное тело ответа не должны автоматически становиться контекстом лога.

Что делать, если само логирование не сработало?

Запись на диск или во внешний сервис тоже может завершиться ошибкой. Продумайте резервное направление и отдельный контроль состояния журнала. У Monolog есть групповые обработчики для разных политик отказа, но подавление исключения handler не доказывает, что событие сохранено. Не вызывайте тот же сломанный logger рекурсивно из обработчика его собственной ошибки.

Нужно ли создавать handlers в каждом контроллере?

Обычно нет. Соберите настройки один раз в bootstrap или контейнере зависимостей. Это помогает избежать повторных handlers и случайного дублирования каждой записи.

Подробнее: Обработчики, форматы и processors.

13. Готовый проект: 12 сценариев

Скачать примеры Monolog — ZIP

Архив содержит исходники, README, lock-файл и сведения о проверке. Внешних уведомлений и подключения к серверу не требуется. После composer install запустите:

composer first
composer compare
composer demo -- list
composer demo -- all
Сценарий Команда Что проверяет
Уровни composer demo -- levels Порог и прохождение более серьёзных событий
Файл composer demo -- stream Запись через StreamHandler
JSON composer demo -- json Разбор JSON-строки и полей
Контекст composer demo -- context Подстановку placeholder и сохранение данных
Исключение composer demo -- exception Класс и сообщение в exception context
Каналы composer demo -- channels Разные источники в одном журнале
Ротация composer demo -- rotating Создание непустого файла с датой текущего дня
Буфер composer demo -- fingers-crossed Сохранение предыстории при ERROR
Обработчики composer demo -- handlers Пороги, порядок и bubble
Processor composer demo -- processor Общие поля extra
Предупреждение PHP composer demo -- php-warning Запись и продолжение выполнения
Непойманное исключение composer demo -- uncaught Запись и завершение дочернего процесса

Где смотреть результат? first дописывает в var/log/app.log. Каждый запуск compare создаёт новую папку var/log/compare-…, а запуск самопроверки — var/log/run-…; сценарии одного запуска пишут разные файлы в этой общей папке. Конкретный путь выводится в терминале. README объясняет ожидаемые записи и файлы. Проект не меняет системный php.ini.

Код 0 в сценарии ошибки — это успех теста. Например, uncaught запускает отдельный PHP-процесс и проверяет, что тот записал исключение и завершился ожидаемым образом. Родительская самопроверка возвращает 0, если всё совпало. Сам скрипт с непойманным исключением продолжить работу не сможет.

Проверено 6 ноября 2026 года: PHP 8.3.33, Composer 2.10.3, Monolog 3.12.1 и psr/log 3.0.2, Windows. Прошли 12 из 12 сценариев, первый пример и сравнение с error_log. Журналы действительно записывались в файлы; JSON и ожидаемые поля проверялись программно. Предупреждение PHP и непойманное исключение проверены в отдельных процессах: после warning выполнение продолжилось, после uncaught процесс завершился с кодом 255. Исходные PHP-файлы прошли проверку синтаксиса, а зависимости — composer validate --strict, composer check-platform-reqs и composer audit --locked. Известных advisories для зафиксированных зависимостей на момент проверки не обнаружено.

Дополнительно все 19 PHP-блоков статьи прошли синтаксическую проверку и локальный запуск с указанной подготовкой. Проверены содержимое файлов, пороги, context/extra, JSON, настоящий перевод строки LineFormatter, непустой trace, маршрутизация по handlers, каналы и буферизация. Для примера с явным exit(1) ожидается код 1, для непойманного исключения — 255; это подтверждённое поведение примеров, а не провал проверки.

Стенд не проверяет нагрузку, одновременную запись разных процессов, доставку во внешние сервисы, смену суток и очистку старых файлов ротации, а также работу под другими версиями PHP и веб-серверами. Эти границы не мешают повторить локальные примеры, но важны при переносе настроек в рабочее приложение.

14. Диагностика и частые ошибки

Файл не появился или в нём нет нужного события

Проверьте абсолютный путь, пользователя процесса, права каталога, уровень handler, порядок и bubble. Возможно, запись находится в другом файле с датой или пока удерживается буфером. Каталог CLI и окружение веб-сервера могут различаться.

Вместо номера осталось {order_id}

Проверьте наличие PsrLogMessageProcessor и точное совпадение ключа в context. Наличие context само по себе не включает подстановку. В JSON полезно сохранять поле отдельно даже при подстановке в message.

Одно событие появляется несколько раз

Посмотрите, не добавлен ли один handler повторно и не вызывается ли настройка logger при каждом обращении. Для исключений проверьте сочетание «записали и перебросили выше»; для PHP-ошибок — параллельную работу штатного и пользовательского обработчиков.

Лог растёт слишком быстро

Проверьте пороги, повторяющиеся события и размеры context. Ротация по датам не ограничивает объём одного дня. Установите срок хранения, сбор и контроль диска; не используйте бесконечный Debug-журнал как замену продуманной диагностике.

В JSON нет stack trace исключения

Проверьте передачу объекта в exception и настройку formatter. Для приведённого JsonFormatter явно вызвано includeStacktraces(true). Наличие trace полезно для диагностики, но требует контроля доступа и содержимого.

Старый пример processor не работает с Monolog 3

В новых примерах используйте Monolog\LogRecord. Старые материалы могут показывать другой контракт, например тип array в параметре processor. Сверяйте код с установленной основной версией библиотеки.

Нужно ли включить display_errors на рабочем сайте?

Подробные ошибки направляйте в защищённый журнал, пользователю отдавайте понятный ответ без внутренних путей и trace. Показ и запись — разные настройки PHP. Для локальной разработки подробный вывод может помогать, но это не причина показывать его посетителям сайта.

Команды, с которых удобно начать проверку окружения:

php --version
php --ini
composer show monolog/monolog
composer check-platform-reqs
composer diagnose
composer audit
Задача Что использовать
Обычное событие $logger->info('Событие', $context)
Ошибка операции $logger->error('Не выполнено', ['exception' => $e])
Оставить Warning и выше Level::Warning у handler
Подставить {id} в сообщение PsrLogMessageProcessor
Получить JSON Lines JsonFormatter у handler
Сохранить предысторию сбоя FingersCrossedHandler
Сбросить состояние между заданиями $logger->reset()

15. Источники и дальнейшая практика

Для закрепления поменяйте порог с Info на Warning, добавьте своё безопасное поле context и сравните текстовый журнал с JSON. Затем выберите одну операцию своего приложения и опишите три события: начало, успешное завершение и отказ. Для каждого заранее определите полезные поля и дальнейшее действие программы.