Логирование ошибок в PHP: как включить php log и где его найти

Логирование ошибок в PHP: как включить php log и где его найти Полезное

Лог ошибок PHP (php log) — это файл или поток, куда интерпретатор записывает предупреждения, фатальные ошибки и необработанные исключения, а также сообщения, которые вы передали функции error_log(). Он включается директивой log_errors = On, а место записи задает директива error_log: путь к файлу, syslog или пустое значение — тогда сообщения уходят в журнал того, кто запустил PHP (веб-сервер, PHP-FPM, консоль).

Ниже — рабочая настройка, где лог лежит в разных связках, как писать в него свои сообщения и когда переходить на Monolog. Все примеры прогнаны в PHP 8.4.25 (официальный Docker-образ) 24.09.2026; связка nginx + PHP-FPM проверена там же.

Четыре директивы, которые путают

Ошибка проходит два фильтра: сначала error_reporting решает, замечать ли ее вообще, потом две независимые директивы решают, куда ее отправить.

Директива За что отвечает Production Разработка
error_reporting Какие уровни ошибок PHP учитывает E_ALL & ~E_DEPRECATED E_ALL
display_errors Показывать ли ошибку в ответе (браузер, консоль) Off On
log_errors Записывать ли ошибку в лог On On
error_log Куда писать: файл, syslog или журнал SAPI путь к файлу путь к файлу

Значения в колонках — из шаблонов php.ini-production и php.ini-development, которые идут с PHP 8.4. Без php.ini вообще (так стартует официальный Docker-образ) проверка php -i показала log_errors => Off и display_errors => STDOUT: ошибки видны в выводе, но не записываются. Поэтому первым делом стоит проверить, какой php.ini реально загружен.

Минимальная рабочая настройка

Фрагмент php.ini для сервера:

error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/app-error.log

Каталог для лога создается заранее, писать в него должен пользователь, от которого работает PHP (в Debian и Ubuntu для Apache и PHP-FPM это обычно www-data), и он не должен лежать внутри document root, иначе лог можно скачать по URL:

sudo mkdir -p /var/log/php
sudo chown www-data:www-data /var/log/php
sudo chmod 750 /var/log/php

После правки php.ini перезапускают тот процесс, который читает этот файл: PHP-FPM (sudo systemctl restart php8.4-fpm) или Apache с mod_php (sudo systemctl restart apache2). Перезапуск nginx настройки PHP не перечитывает. Имена служб здесь — для Debian/Ubuntu с пакетами PHP 8.4, в других дистрибутивах они отличаются.

Проверим, что пишется в лог, на одном скрипте:

<?php
// demo.php: пишет в лог предупреждение, свое сообщение и необработанное исключение
echo "старт\n";
$user = ['name' => 'Анна'];
echo $user['email'] ?? 'нет email', "\n";   // без ошибки: оператор ??
echo $user['phone'], "\n";                  // Warning: Undefined array key
error_log('заказ 42: оплата не прошла');     // свое сообщение, тип 0
throw new RuntimeException('Нет связи с БД');

Запуск с настройками из командной строки: php -d display_errors=0 -d log_errors=1 -d error_log=/app/php-errors.log -d date.timezone=Europe/Moscow demo.php. На экран выводятся только «старт» и «нет email», код выхода 255, а в файле появляется:

[24-Sep-2026 15:25:01 Europe/Moscow] PHP Warning:  Undefined array key "phone" in /app/demo.php on line 6
[24-Sep-2026 15:25:01 Europe/Moscow] заказ 42: оплата не прошла
[24-Sep-2026 15:25:01 Europe/Moscow] PHP Fatal error:  Uncaught RuntimeException: Нет связи с БД in /app/demo.php:8
Stack trace:
#0 {main}
  thrown in /app/demo.php on line 8

Время PHP ставит сам, в поясе из date.timezone. Предупреждение не остановило скрипт, необработанное исключение записано как PHP Fatal error: Uncaught ... со стеком, а свое сообщение попало в тот же файл без префикса уровня.

Где лежит лог: Apache, nginx, PHP-FPM, консоль

Если error_log задан и файл доступен на запись, лог всегда там. Если директива пустая, место зависит от того, как запущен PHP:

Как запущен PHP Куда уходят ошибки при пустом error_log
Консоль (CLI) В stderr процесса, то есть в терминал
Apache + mod_php В журнал ошибок Apache (ErrorLog), в Debian по умолчанию /var/log/apache2/error.log
nginx + PHP-FPM В stderr FastCGI, nginx пишет их в свой error_log строкой FastCGI sent in stderr
Официальный Docker-образ php:*-fpm В лог PHP-FPM, то есть в вывод контейнера (docker logs)

В прогоне строка в журнале nginx начиналась так: [error] ... FastCGI sent in stderr: "PHP message: PHP Warning: Undefined variable $nope .... За это отвечает директива fastcgi.logging: по умолчанию она включена, а в официальном Docker-образе выключена файлом conf.d/docker-fpm.ini, поэтому там сообщения идут в лог FPM. Отдельный файл для пула PHP-FPM задают в конфиге пула (например, /etc/php/8.4/fpm/pool.d/www.conf):

php_admin_value[error_log] = /var/log/php/www-error.log
php_admin_flag[log_errors] = on

php_admin_* нельзя переопределить через ini_set() из кода сайта, в отличие от php_value. С такой настройкой в прогоне предупреждение и сообщение error_log() оказались в /var/log/php/www-error.log, а в журнал nginx не попали.

Чтобы не гадать, спросите у самого PHP. php --ini покажет, какой php.ini загружен, php -i | grep -E "^(error_log|log_errors)" — текущие значения. Важная граница: это настройки CLI. У PHP-FPM свой php.ini (в Debian — /etc/php/8.4/fpm/php.ini), поэтому для сайта значения надо смотреть из веб-запроса: временный скрипт с echo ini_get('error_log'); или phpinfo(), удаленный сразу после проверки.

Если нет доступа к php.ini

Директивы php_flag log_errors on и php_value error_log ... в .htaccess работают только при Apache с mod_php. На PHP-FPM или CGI такая строка приводит к ошибке 500, а при AllowOverride None игнорируется. Для FPM вместо этого есть файл .user.ini в каталоге сайта (имя задает директива user_ini.filename), куда пишут те же строки, что в php.ini. Он перечитывается не сразу, а раз в user_ini.cache_ttl секунд (по умолчанию 300).

Уровни ошибок и error_reporting

Для лога важны уровни: по ним фильтруют записи и ищут проблемы.

Константа Что означает Скрипт продолжается
E_ERROR Фатальная ошибка выполнения (нехватка памяти, превышен лимит времени) Нет
E_PARSE Синтаксическая ошибка при разборе файла Нет, файл не выполняется
E_WARNING Предупреждение: неизвестный ключ массива, неопределенная переменная (в PHP 8) Да
E_NOTICE Замечание о вероятной ошибке Да
E_DEPRECATED Использована возможность, которую уберут в будущих версиях Да
E_ALL Все уровни сразу —

Исключения (Exception, Error и их потомки) — отдельный механизм: их ловят через try/catch, а в лог они попадают, только если остались необработанными или вы записали их сами. Подробнее о видах ошибок и их обработке — в статье «Ошибки в PHP».

Маски собирают побитовыми операторами: E_ALL & ~E_DEPRECATED — все, кроме устаревших возможностей. Старые инструкции часто советуют E_ALL & ~E_STRICT, но в PHP 8.4 сама константа объявлена устаревшей, и такая строка дает Deprecated: Constant E_STRICT is deprecated.

Синтаксическую ошибку не поймать строкой ini_set('display_errors', '1') в том же файле: файл не проходит разбор, и ini_set() не выполняется. Скрипт с пропущенной точкой с запятой при display_errors=0 и выключенном логе молча завершился с кодом 255. Такие ошибки ищут в логе или проверкой php -l file.php:

Parse error: syntax error, unexpected token "echo", expecting "," or ";" in parse.php on line 4
Errors parsing parse.php

Функция error_log(): свои сообщения в лог

Сигнатура: error_log(string $message, int $message_type = 0, ?string $destination = null, ?string $additional_headers = null): bool.

message_type Куда пишет
0 В системный лог PHP, то есть туда же, куда error_log из php.ini
1 На e-mail из $destination через mail()
3 Дописывает в файл $destination
4 Прямо в обработчик журнала SAPI (например, журнал веб-сервера)

Частая ошибка с типом 3 — ожидать готовых строк лога. Два вызова error_log('без перевода строки', 3, $file) и error_log('вторая запись', 3, $file) дают в файле одну склеенную строку без даты: тип 3 не добавляет ни времени, ни перевода строки. Исправление — добавлять их самому: error_log(date('c') . " запись\n", 3, $file);.

Не пишите в лог сырые данные пользователя

Если в сообщение попадает строка из формы, перевод строки внутри нее создаст в логе поддельную запись:

<?php
$login = "bob\n[24-Sep-2026 12:00:00 UTC] вход администратора admin";   // пришло из формы
error_log('неудачный вход: ' . $login);
error_log('неудачный вход: ' . json_encode($login, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE));
[24-Sep-2026 12:26:40 UTC] неудачный вход: bob
[24-Sep-2026 12:00:00 UTC] вход администратора admin
[24-Sep-2026 12:26:40 UTC] неудачный вход: "bob\n[24-Sep-2026 12:00:00 UTC] вход администратора admin"

Первый вызов дал две строки, и вторая выглядит как настоящая запись. json_encode() экранирует управляющие символы (\n, \r, ESC), и запись остается одной строкой. Флаг JSON_INVALID_UTF8_SUBSTITUTE здесь обязателен: без него на строке с битым UTF-8 (например, байты "bob\xC3\x28" из формы) json_encode() возвращает false, и в лог молча уходит неудачный вход: без логина. С флагом битые байты заменяются на U+FFFD, запись сохраняется. Пароли, токены, номера карт и лишние персональные данные в лог не пишут вообще.

Monolog: когда error_log() уже мало

Когда нужны уровни, контекст в JSON, несколько каналов и ротация, берут Monolog — библиотеку, реализующую стандарт PSR-3 (ее используют Laravel и Symfony). Установка: composer require monolog/monolog. Пример проверен на Monolog 3.12.0:

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

use Monolog\Level;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\ErrorHandler;

$log = new Logger('shop');
$log->pushHandler(new StreamHandler(__DIR__ . '/var/log/app.log', Level::Info));
ErrorHandler::register($log); // warning и необработанные исключения PHP - тоже в этот лог

$log->info('Заказ создан', ['order_id' => 42, 'sum' => 1990]);
$log->debug('Не попадет в файл: уровень ниже Info');
$log->error('Оплата отклонена', ['order_id' => 42, 'code' => 'card_declined']);
echo $undefined;
[2026-09-24T12:26:34.709583+00:00] shop.INFO: Заказ создан {"order_id":42,"sum":1990} []
[2026-09-24T12:26:34.711923+00:00] shop.ERROR: Оплата отклонена {"order_id":42,"code":"card_declined"} []
[2026-09-24T12:26:34.712084+00:00] shop.WARNING: E_WARNING: Undefined variable $undefined {"code":2,"message":"Undefined variable $undefined","file":"/app/app.php","line":16} []

Запись debug отсечена порогом Level::Info, а предупреждение PHP перехватил ErrorHandler и записал с файлом и строкой. Каталог var/ здесь лежит рядом с vendor/, а не в публичной папке сайта: document root должен указывать на отдельный public/. Для ротации файлов есть RotatingFileHandler, но на сервере чаще настраивают системный logrotate.

Если лог не пишется

Симптом Вероятная причина Что сделать
Файл не появляется, ошибки видны в терминале или журнале сервера Нет прав на каталог: PHP молча переключается на журнал SAPI Проверить владельца и права каталога от пользователя PHP
В CLI пишется, на сайте нет У PHP-FPM свой php.ini Смотреть ini_get() из веб-запроса, править FPM-конфиг
Правка php.ini не действует Не перезапущен PHP-FPM или Apache Перезапустить службу PHP, а не nginx
Лог пуст, хотя ошибки есть log_errors = Off или узкий error_reporting Проверить обе директивы через php -i / phpinfo()
Ошибка 500 после правки .htaccess php_flag на PHP-FPM Убрать строку, перенести настройки в .user.ini
Лог растет без конца Нет ротации Настроить logrotate

Первая строка таблицы проверена: при error_log=/nonexistent/dir/x.log запись ушла в stderr, файл не создался, а сам PHP ни о чем не предупредил.

Выводы

  • Лог включают log_errors = On и путь в error_log; показ ошибок пользователю (display_errors) — отдельная настройка, на сервере ее выключают.
  • При пустом error_log ошибки уходят в журнал того, кто запустил PHP: терминал, ErrorLog Apache, журнал nginx через FastCGI или лог PHP-FPM.
  • У CLI и PHP-FPM разные php.ini: реальные значения для сайта смотрят из веб-запроса, после правки перезапускают службу PHP.
  • error_log() с типом 3 не добавляет дату и перевод строки, а пользовательский ввод перед записью нужно экранировать.
  • Для уровней, контекста и нескольких каналов подходит Monolog, а логи в любом варианте хранят вне document root.

Где применяется / связь с практикой

Освойте тему на практике

Настроенный лог — первое, что открывают при разборе инцидента: по нему находят упавший запрос, медленную интеграцию или попытку подбора пароля. В команде следом идут единый формат, централизованный сбор и алерты. Эти вещи вместе с архитектурой и отладкой PHP-приложений разбирают на курсе «PHP-разработчик. Продвинутый уровень». Отдельные темы бэкенд-разработки можно посмотреть на открытых уроках Otus.

FAQ

Можно ли писать лог PHP в syslog или journald?
Да: error_log = syslog отправляет сообщения в системный журнал, откуда их читает journalctl или rsyslog. Имя программы в записях задает директива syslog.ident (по умолчанию php).

Как логировать фатальные ошибки своим обработчиком?
set_error_handler() фатальные ошибки не перехватывает. Их ловят функцией, зарегистрированной через register_shutdown_function(), внутри нее проверяют error_get_last().

Нужно ли включать display_errors на время отладки на рабочем сервере?
Лучше нет: сообщения увидят посетители, а в тексте ошибок бывают пути и фрагменты запросов. Для отладки достаточно лога, а воспроизводить проблему удобнее на копии окружения.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)