OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

PHP: логирование, ротация логов, error_log и syslog на практике

Разбираем, как грамотно настроить логирование в PHP: какие директивы включать в php.ini и пуле PHP-FPM, как использовать функцию error_log, чем отличается запись в файл от syslog, как через logrotate настроить ротацию логов и не убить диск бесконечным debug-выводом в продакшене.
PHP: логирование, ротация логов, error_log и syslog на практике

Логи 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_log PHP;
  • error_log('debug info: ' . json_encode($data), 3, '/var/log/php/app-debug.log') — отдельный лог-файл приложения.

При использовании параметра $message_type = 3 PHP сам открывает файл в режиме 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/удалённый лог-сервис.

Консоль Linux с файлом error_log PHP и конфигом logrotate

Ротация логов 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 по одному идентификатору.

Корреляция логов Nginx и PHP-FPM по идентификатору запроса

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

Поделиться статьей

Вам будет интересно

Нагрузочное тестирование staging и prod: практический гид для админов OpenAI Статья написана AI (GPT 5)

Нагрузочное тестирование staging и prod: практический гид для админов

Разберем, как системно подойти к нагрузочному тестированию веб‑проектов: чем реально отличается staging от prod, как строить профи ...
VDS: шифрование диска с LUKS2 и autounlock без ручного ввода пароля OpenAI Статья написана AI (GPT 5)

VDS: шифрование диска с LUKS2 и autounlock без ручного ввода пароля

Разберём, как включить шифрование диска с LUKS2 на VDS и не вводить пароль после каждой перезагрузки. Пошагово создадим LUKS2-том, ...
cron vs systemd timers: что выбрать для задач и healthcheck OpenAI Статья написана AI (GPT 5)

cron vs systemd timers: что выбрать для задач и healthcheck

Cron до сих пор жив на большинстве серверов, но в современных Linux-дистрибутивах с systemd таймеры дают более управляемый и наблю ...