Логи PHP — один из главных источников правды о том, что реально происходит с вашим приложением в продакшене. Но по умолчанию они часто настроены абы как: всё сваливается в один файл, ротации нет, диск внезапно забивается за ночь, а нужная ошибка теряется среди вороха notice и случайных var_dump в проде.
Ниже разберёмся, как организовать логирование в PHP так, чтобы:
- ошибки и предупреждения не терялись;
- логи не съедали весь диск;
- можно было быстро понять, что сломалось — и где;
- настройки были предсказуемы как на shared-хостинге, так и на VDS.
Базовая модель логирования в PHP
В PHP есть две основные составляющие логирования:
- настройки движка (через
php.ini,.user.ini, конфиг PHP-FPM или директивы веб-сервера); - вызовы функции
error_log()и других логирующих функций в самом коде.
Начнём с настроек, потому что от них зависит, будут ли вообще записываться ошибки и куда они пойдут.
Ключевые директивы логирования в php.ini
Минимальный набор директив, которые нужно знать:
display_errors— показывать ли ошибки в браузер;log_errors— логировать ли ошибки в файл/журнал;error_log— путь к файлу логов (или специальное значение);error_reporting— какие уровни ошибок учитывать;html_errors— форматировать ли вывод ошибок HTML-разметкой (актуально в dev);ignore_repeated_errorsиignore_repeated_source— бороться с «шумными» повторяющимися ошибками.
Пример типичной продакшен-конфигурации в php.ini или в конфиге пула PHP-FPM:
; Не светим стек-трейсы пользователю
display_errors = Off
; Но обязательно логируем
log_errors = On
; Логируем все, кроме notice и deprecated (пример)
error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT
; Путь до лог-файла (о ротации поговорим отдельно)
error_log = /var/log/php/app-error.log
; Убираем HTML-форматирование
html_errors = Off
; Немного снижаем шум
ignore_repeated_errors = On
ignore_repeated_source = On
Для dev-среды настройки обычно другие:
display_errors = On(или хотя быOnтолько для CLI и локальной разработки);error_reporting = E_ALL;error_logможно оставить тем же, но стоит учитывать объём логов и ротацию.
Если вы на shared-хостинге, полезно знать приёмы из статьи по настройке PHP через .user.ini и php_value — там можно локально управлять частью логирующих директив.
Где задавать настройки: php.ini, FPM, .user.ini, .htaccess
Важно понимать приоритеты, особенно на shared-хостинге:
- глобальный
php.ini— общие настройки всей установки PHP; - конфиг PHP-FPM для пула (
php_admin_value,php_value) — переопределяет глобальныйphp.iniдля пула; .user.ini— для директив уровняPHP_INI_PERDIR, действует в пределах каталога и вложенных;.htaccess(Apache с mod_php или проксирование на FPM) — может переопределять часть директив черезphp_value/php_flag.
На связке Nginx + PHP-FPM параметры чаще всего задаются в конфиге пула:
; pool.d/www.conf или отдельный пул
php_admin_value[error_log] = /var/log/php/app-error.log
php_admin_flag[log_errors] = on
php_admin_value[error_reporting] = E_ALL & ~E_NOTICE & ~E_DEPRECATED
На shared-хостинге обычно дают возможность управлять логами через .user.ini или панель управления:
; .user.ini в корне сайта
log_errors = On
error_log = /home/user/logs/site-php-error.log
Функция error_log(): возможности и подводные камни
Большинство разработчиков знают про error_log('что-то случилось'), но у функции есть несколько режимов работы, которые сильно расширяют её применимость.
Сигнатура и типы логирования
Сигнатура в актуальных версиях PHP:
bool error_log(
string $message,
int $message_type = 0,
?string $destination = null,
?string $additional_headers = null
)
Нас интересуют значения $message_type:
0— по умолчанию, записать вerror_log, заданный в конфиге;1— отправить email (обычно не стоит злоупотреблять в проде);3— дописать в файл, указанный в$destination;4— отправить напрямую вsyslog(если поддерживается).
В повседневной практике чаще всего используются:
error_log('что-то пошло не так')— пишет в общийerror_logPHP;error_log('debug info: ' . json_encode($data), 3, '/var/log/php/app-debug.log')— отдельный лог-файл приложения.
При использовании параметра
$message_type = 3PHP сам открывает файл в режиме append перед каждой записью. Убедитесь, что у процесса PHP-FPM есть права на запись в каталог и файл, иначе получите тихий провал без записи.
Стиль сообщений и контекст
PHP не добавляет структуру к пользовательским сообщениям, поэтому имеет смысл выработать формат:
- фиксированный префикс (имя приложения, окружение);
- уровень (
INFO,WARN,ERROR,DEBUG); - идентификатор запроса/пользователя, если есть.
Например:
$requestId = $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8));
error_log('[myapp][prod][' . $requestId . '][ERROR] Failed to save order: ' . $e->getMessage());
Так потом значительно проще фильтровать логи grep'ом или в лог-агрегаторе.
Куда писать логи: файл или syslog
У PHP есть два основных направления для ошибок и логов:
- обычный файл (через директиву
error_logилиerror_log(..., 3, ...)); syslog(через директивуerror_log = syslogилиerror_log(..., 4)).
Лог в файл: просто, понятно и… забивает диск
Самый распространённый вариант — путь к файлу:
error_log = /var/log/php/app-error.log
Плюсы файловых логов:
- их легко смотреть локально (
tail -f,less); - просто подключить
logrotateили аналогичные утилиты; - прозрачно и понятно, что где лежит.
Минусы:
- при высокой нагрузке несколько процессов PHP пишут в один файл — возможны lock-и и повышенная нагрузка на IO;
- при отсутствии ротации легко забить диск до отказа;
- если у каждого виртуального хоста свой файл, их может стать очень много.
Лог в syslog: когда это уместно
Альтернатива — отправить логи в системный журнал:
; В php.ini или FPM-пуле
error_log = syslog
При этом сообщения PHP попадают в syslog или journald и дальше уже обрабатываются системой логирования (rsyslog, journald, syslog-ng и т.п.).
Плюсы:
- централизованный сбор (можно форвардить на удалённый лог-сервер);
- системные инструменты ротации уже настроены;
- легче интегрироваться с SIEM, ELK, Loki, Promtail и т.п.
Минусы:
- сложнее локально отлаживать (нужно фильтровать по тегам/процессам);
- при неправильно настроенном syslog можно получить узкое место;
- требуется понимание, как именно ваша ОС хранит и ротирует системные логи.
Использование syslog в error_log() напрямую:
// Тип 4 - отправка в системный журнал
error_log('Payment service unavailable', 4);
На реальных продакшен-проектах часто комбинируют оба подхода:
- ошибки движка PHP (warning/fatal) — в системный журнал или выделенный файл с ротацией;
- бизнес-логи приложения — в отдельный файл или через библиотеку в syslog/JSON/удалённый лог-сервис.

Ротация логов PHP: logrotate и не только
Теперь самое болезненное: ротация. Оставить гигабайтный error_log без ротации — гарантированный путь к неожиданным проблемам. Рассмотрим типовую схему на Linux с logrotate.
Простой logrotate-конфиг для файла error_log
Допустим, у нас есть файл /var/log/php/app-error.log. Добавляем конфиг /etc/logrotate.d/php-app:
/var/log/php/app-error.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
create 0640 www-data www-data
sharedscripts
postrotate
# Перезапуск не всегда обязателен, зависит от способа открытия файла
# Для PHP-FPM часто не требуется, но если вы видите, что лог продолжает
# писаться в старый inode после ротации, добавьте reload.
# systemctl reload php-fpm
endscript
}
Кратко по опциям:
daily— ротация раз в день;rotate 14— хранить 14 архивов (примерно две недели);compress+delaycompress— старые логи архивируются в.gzсо сдвигом на один цикл;create 0640 www-data www-data— создавать новый файл с нужными правами;notifempty— не ротировать пустые файлы;postrotate— блок с действиями после ротации (опционален, зависит от способа логирования).
Нужно ли перезапускать PHP-FPM после ротации?
Классический вопрос: если logrotate переименовал файл, а PHP продолжает держать старый дескриптор (inode), попадут ли новые записи в новый файл?
Для классического варианта с прямым логом в файл:
- обычный сценарий:
mv app-error.log app-error.log.1и создание нового файла. Если PHP держит открытым старый дескриптор, он продолжит писать в «старый» файл, уже переименованный, пока процесс не перезапустится или не переоткроет файл; - поэтому иногда используют опцию
copytruncate— logrotate копирует данные в новый файл и очищает старый, не меняя дескриптор; это убирает необходимость в перезапуске, но даёт риск потерять несколько строк на границе.
Пример с copytruncate:
/var/log/php/app-error.log {
daily
rotate 7
missingok
notifempty
compress
delaycompress
copytruncate
}
Подходы на практике:
- на высоконагруженных проектах часто используют
copytruncate, чтобы избежать лишних перезапусков/перезагрузок пула; - либо логируют в syslog/journald и отдают ротацию на откуп системным сервисам;
- либо перезапускают FPM по расписанию вместе с ротацией (но это влияет на активные запросы).
Разделение логов по пулам/виртуальным хостам
Хорошей практикой считается разделение логов хотя бы по пулам PHP-FPM или по сайтам:
; pool.d/site1.conf
php_admin_value[error_log] = /var/log/php/site1-error.log
; pool.d/site2.conf
php_admin_value[error_log] = /var/log/php/site2-error.log
И соответствующий блок в logrotate:
/var/log/php/site1-error.log /var/log/php/site2-error.log {
daily
rotate 10
missingok
notifempty
compress
delaycompress
copytruncate
}
Так проще:
- искать ошибки конкретного проекта;
- задавать разные политики хранения (для теста можно хранить меньше);
- избежать ситуации, когда один шумный сайт заполняет общий лог.
Работа с syslog: facility, тег и фильтрация
Если вы используете error_log = syslog или тип 4 в error_log(), полезно понимать, что дальше происходит с сообщениями.
Facility и идентификатор
PHP позволяет задать facility и идентификатор (program name) через директивы:
syslog.facility— например,LOG_USER,LOG_LOCAL0;syslog.ident— строка-идентификатор приложения.
Пример:
syslog.facility = LOG_LOCAL0
syslog.ident = php-app
error_log = syslog
После этого в rsyslog или journald можно повесить отдельное правило и писать такие сообщения в свой файл, пересылать на удалённый сервер, фильтровать по уровню и т.п.
Пример правила в rsyslog
Предположим, вы хотите писать все сообщения от php-app в отдельный файл /var/log/php/php-app.log. В /etc/rsyslog.d/php-app.conf можно добавить:
if $programname == "php-app" then /var/log/php/php-app.log
& stop
После перезапуска rsyslog вы получите централизованный файл с логами PHP, в который пишет и сам движок, и ваши вызовы error_log(..., 4).
Уровни ошибок и шум в логах
Неправильный error_reporting легко превращает логи в свалку: тонны notice и deprecated-сообщений скрывают реальную проблему. Тут важен баланс между информативностью и шумом.
Типы ошибок
Наиболее часто встречающиеся уровни:
E_ERROR,E_CORE_ERROR,E_COMPILE_ERROR— фатальные ошибки;E_WARNING,E_CORE_WARNING,E_COMPILE_WARNING— предупреждения;E_PARSE— ошибки синтаксиса;E_NOTICE— уведомления (часто о неинициализированных переменных и прочем);E_STRICT,E_DEPRECATED,E_USER_DEPRECATED— устаревшее поведение, несовместимости;E_USER_ERROR,E_USER_WARNING,E_USER_NOTICE— пользовательские уровни изtrigger_error().
Для продакшена разумно логировать всё, но не всё превращать в «красную тревогу» в алёртинге.
Рекомендации по настройке
Вариант для стабильного продакшена:
error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED & ~E_USER_DEPRECATED
Замечания:
- не стоит полностью вырубать
E_WARNING— там часто живут реальные проблемы; - но стоит серьёзно относиться к
E_DEPRECATED— они подскажут, что пора готовиться к новой мажорной версии PHP; - на dev-окружении лучше включать
E_ALL, чтобы видеть всё и сразу чинить.
PHP и логирование в окружении с Nginx/Apache
Ещё один частый вопрос: чем отличается лог PHP от логов веб-сервера, нужно ли всё валить в один файл и как потом коррелировать события?
Логи веб-сервера ≠ логи PHP
У Nginx и Apache есть свои access- и error-логи. PHP пишет отдельный error_log. Это разные сущности:
- access-лог веб-сервера — список запросов, кодов ответа, времени обработки, размеров ответа;
- error-лог веб-сервера — его внутренние ошибки (proxy, timeout, upstream failed и т.п.);
- PHP error_log — ошибки интерпретатора и ваши
error_log().
Смешивать всё в один файл — плохая идея. Лучше:
- держать отдельные логи Nginx/Apache по виртуальным хостам (особенно если используете виртуальный хостинг);
- для каждого пула PHP-FPM свой
error_log; - коррелировать события по времени и по идентификатору запроса (
X-Request-ID,trace_idи подобное).
Связка через X-Request-ID
Полезный паттерн: на входе в приложение генерировать (или принимать от фронтовика/балансировщика) X-Request-ID и писать его и в access-лог сервера, и в логи PHP.
Например, в Nginx:
map $http_x_request_id $request_id_final {
default $http_x_request_id;
"" $request_id;
}
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$request_id_final"';
access_log /var/log/nginx/access.log main;
proxy_set_header X-Request-ID $request_id_final;
И в PHP:
$requestId = $_SERVER['HTTP_X_REQUEST_ID'] ?? 'no-request-id';
error_log('[req:' . $requestId . '][ERROR] Something happened');
Так любой инцидент можно проследить от клиента до PHP по одному идентификатору.

Практические паттерны логирования в PHP
Соберём всё в набор практических рекомендаций, которые можно применить почти в любом проекте.
1. Разделите окружения: dev/stage/prod
Для каждого окружения стоит задать отдельные настройки:
- dev:
display_errors = On,error_reporting = E_ALL, подробные stack trace; - stage: как прод, но можно включить больше уровней логирования и дополнительный debug-лог;
- prod:
display_errors = Off,log_errors = On, адекватныйerror_reporting, обязательная ротация логов.
Часто это делается через отдельные php.ini или pool.d/*.conf для каждого пула, либо через переменные окружения и bootstrap-конфиг приложения.
2. Отдельные файлы для error и debug
Не смешивайте критичные ошибки и подробный debug в один файл. Типовая схема:
error_logдвижка PHP — только ошибки и предупреждения интерпретатора;- отдельный
app-error.log— бизнес-ошибки приложения (ERROR,WARNING); - отдельный
app-debug.log— подробный debug (в проде включается ограниченно и на время).
В коде это может выглядеть так:
function app_log_error(string $message): void {
error_log('[ERROR] ' . $message, 3, '/var/log/php/app-error.log');
}
function app_log_debug(string $message): void {
if (!getenv('APP_DEBUG')) {
return;
}
error_log('[DEBUG] ' . $message, 3, '/var/log/php/app-debug.log');
}
Не забывайте про отдельные блоки в logrotate для этих файлов и мониторинг их размера.
3. Не логируйте чувствительные данные
Классический антипаттерн: error_log(print_r($_POST, true)) в продакшене. В логах оказываются пароли, токены, персональные данные.
Рекомендации:
- маскируйте значения полей вроде
password,token,card_numberперед логированием; - не логируйте полные запросы и ответы внешних API, если в них есть секреты;
- периодически пересматривайте, что именно попадает в логи и кто к ним имеет доступ.
4. Следите за размером логов и лимитами диска
Даже при наличии logrotate можно попасть в ситуацию, когда приложение внезапно начинает спамить ошибками (например, из-за падения внешнего сервиса), и лог за несколько часов вырастает до гигабайтов.
Практичные меры:
- мониторинг размера критичных файлов логов (простые скрипты или готовые метрики в Prometheus/Netdata);
- лимиты на размер логов на уровне файловой системы (quota, project quotas) или отдельного раздела под логи;
- алерты на резкий рост скорости записи в логи (можно считать по разнице размеров между запусками).
Если вы масштабируете проект и переносите его на более мощный VDS, сразу планируйте отдельный раздел или диск под логи.
Когда стоит перейти на структурированные логи
До этого момента речь шла в основном о классическом текстовом формате. Но на определённом масштабе grep по plain-text перестаёт быть удобным: хочется фильтровать по полям, строить графики, связывать события.
Если у вас уже есть централизованный лог-агрегатор (ELK, Loki, OpenSearch, SaaS-сервис и т.п.), есть смысл:
- либо писать в syslog/journald и там парсить;
- либо писать JSON-строки в отдельный файл, который подбирает агент (Filebeat, Promtail, Fluent Bit и т.п.).
Простейший пример JSON-лога в PHP:
$entry = [
'ts' => date('c'),
'level' => 'error',
'request_id' => $requestId,
'message' => 'Order save failed',
'exception' => [
'class' => get_class($e),
'msg' => $e->getMessage(),
],
];
error_log(json_encode($entry, JSON_UNESCAPED_UNICODE), 3, '/var/log/php/app-json.log');
Такой лог легко парсится и в Kibana, и в Loki/Grafana, и в других системах.
Итоги
Грамотная организация логирования в PHP — это не про «куда-то пишет и ладно», а про предсказуемость и управляемость:
- чётко разделяйте окружения и настройки (
display_errors,log_errors,error_reporting); - осознанно выбирайте направление логов: файл или syslog (или оба);
- обязательно настраивайте ротацию — через
logrotateили системный журнал; - разделяйте логи по приложениям и пулам, а также по типу (error/debug);
- думайте о безопасности: не утекают ли в логи пароли и токены;
- встраивайте идентификаторы запроса и структурированные записи там, где это окупается.
Если всё сделать однажды аккуратно, дальше логи PHP перестают быть источником боли и превращаются в удобный инструмент: и для отладки, и для расследования инцидентов, и для понимания реальной жизни вашего проекта на боевом сервере.


