Лог ошибок 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: терминал,ErrorLogApache, журнал 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 на время отладки на рабочем сервере?
Лучше нет: сообщения увидят посетители, а в тексте ошибок бывают пути и фрагменты запросов. Для отладки достаточно лога, а воспроизводить проблему удобнее на копии окружения.



