Логирование
Rapira пишет всё в один поток: события жизненного цикла сервера, решения мастер-процесса, работа HTTP-фронта, диагностика PHP и то, что приложение пишет само, — всё это на stderr. Предупреждение PHP — это запись в том же логе, а не строка в отдельном файле error_log, и её уровень поднимается или опускается так же, как у любой другой записи.
Уровень по умолчанию — error, поэтому наружу выходят только ошибки, а исправный сервер не пишет в лог ничего. Поднять его — одна строчка в конфигурации или переменная окружения RUST_LOG, если конфигурацию править не хочется вовсе.
Уровни и формат
За логирование отвечает секция [log] в вашем rapira.toml:
[log]
level = "error" # error (default) | warn | info | debug | trace
format = "plain" # plain (default) | jsonlevel — общий порог сразу для всех целей: error пропускает только ошибки, warn добавляет к ним предупреждения и так далее вплоть до trace, который показывает вообще всё. format задаёт вид каждой записи — читаемые человеком строки или по одному JSON-объекту на строку.
Оба ключа необязательны, как и вся секция целиком. Про остальную часть файла — слушающий сокет, пул, супервизор — рассказывает раздел Конфигурация.
Уровни для отдельных целей
Одного общего уровня часто не хватает. Секция [log.targets] поднимает и опускает уровень отдельных целей поверх него, так что диагностика PHP может работать на debug, а внутренние подробности HTTP-стека вместе с ней не поднимаются:
[log]
level = "error"
[log.targets]
php = "debug"
pingora_core = "warn"Каждый ключ называет цель и поднимает или опускает уровень только для неё; всё остальное остаётся на level. Ключ сопоставляется по префиксу, поэтому php захватывает заодно php_sys и php_sys::callbacks: достаточно самого короткого подходящего префикса, а подмодули перечислять по отдельности не нужно.
Цели, под которыми пишет сама Rapira:
| Цель | О чём пишет |
|---|---|
rapira | жизненный цикл сервера: старт, жизненный цикл воркеров, остановка |
master | надзор: форки, уборка завершившихся процессов, пересоздание воркеров, перезагрузки, масштабирование пула |
http | HTTP-фронт: слушающие сокеты, обработка полей запроса и ответа, мягкое завершение |
ext | чем закончились задачи расширений |
php | вывод и диагностика от самого PHP |
app | записи, которые приложение пишет через \Rapira\log() |
Access-лога нет: Rapira не пишет по строке на каждый запрос. О том, что цель http сообщает про поля запроса и ответа, рассказывает раздел Запросы и ответы HTTP.
Зависимости пишут под своими путями модулей — pingora_core, tokio и так далее — и фильтруются ровно тем же способом. Имя цели стоит в каждой записи, поэтому шумную зависимость можно приглушить, скопировав это имя в [log.targets].
Когда хочется понять, почему пул ведёт себя именно так, смотрите цель master: пересоздание воркеров, перезагрузки и масштабирование пула пишутся именно туда. Что означает каждое из этих событий, разбирает Модель процессов.
Диагностика PHP
Всё, о чём сообщает PHP, попадает в цель php, а уровень каждого сообщения берётся из типа ошибки — так что тем же фильтром, что управляет сервером, вы регулируете и то, сколько сообщений PHP попадает в лог:
| Тип сообщения | Уровень |
|---|---|
Фатальные ошибки — E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR | error |
Предупреждения — E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING | warn |
Уведомления — E_NOTICE, E_USER_NOTICE | info |
Сообщения об устаревании — E_DEPRECATED, E_USER_DEPRECATED | debug |
Сообщения об устаревании сидят на debug для того, чтобы пара тысяч таких сообщений из vendor не хоронила под собой предупреждения и ошибки, которые приходят вместе с ними.
Сообщение, которое не пропускает маска error_reporting скрипта, не исчезает — оно опускается до trace. Поэтому привычная маска делает ровно то, чего вы от неё ждёте:
<?php
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);На любом нормальном уровне сообщений об устаревании из vendor в логе не будет, а level = "trace" вернёт их обратно, когда захочется посмотреть, что именно заглушили. Исключений два. Фатальные ошибки не понижаются никогда, что бы ни говорила маска, потому что только по ним и можно понять, почему воркер пошёл на перезапуск: error_reporting(0) в каталоге vendor их не скроет. E_CORE_ERROR и E_CORE_WARNING возникают ещё до того, как скрипт вообще успевает задать маску, так что к ним она тоже не применяется.
Диагностика уходит в лог, а не в ответы: Rapira по умолчанию ставит display_errors в 0, а log_errors — в 1. Это именно значения по умолчанию, а не принудительная установка: php.ini, который задаёт любое из них, побеждает.
Логирование из приложения
\Rapira\log() пишет запись из PHP в цель app. Функция принимает сообщение, необязательный уровень и необязательный массив контекста и доступна во всех режимах выполнения:
<?php
\Rapira\log('order placed');
\Rapira\log('payment declined', \Rapira\LogLevel::Warning);
\Rapira\log('cache miss', \Rapira\LogLevel::Debug, ['key' => 'user:42', 'ttl' => 300]);Уровень — это случай перечисления \Rapira\LogLevel, и каждый случай отображается на тот же уровень, которым пользуется остальной лог:
Случай LogLevel | Уровень записи |
|---|---|
Error | error |
Warning | warn |
Info | info |
Debug | debug |
Trace | trace |
Если уровень не указан, используется Info. Поскольку уровни те же самые, что и везде, [log.targets] и RUST_LOG фильтруют записи приложения ровно так же, как записи самого сервера: app = "debug" в [log.targets] поднимает записи приложения, не трогая ничего вокруг.
Массив контекста сериализуется в JSON и прикрепляется к записи полем context. Ключи сохраняются как написаны, вложенные массивы сохраняют структуру:
<?php
\Rapira\log('checkout failed', \Rapira\LogLevel::Error, [
'order' => 41,
'totals' => ['net' => 1250, 'tax' => 250],
]);Throwable в контексте раскрывается до сериализации, потому что json_encode() видит исключение как пустой объект — его состояние лежит в приватных полях Exception и Error. Раскрытие несёт имя класса, сообщение, код, файл и строку и идёт по цепочке previous; стек вызовов в запись не попадает:
<?php
try {
$gateway->charge($order);
} catch (\Throwable $e) {
\Rapira\log('charge failed', \Rapira\LogLevel::Error, ['exception' => $e]);
}Про два ограничения стоит знать, решая, что класть в контекст. Значение, которое JSON выразить не может — ресурс, замыкание, NAN или INF, строка не в UTF-8, — заменяется заглушкой, а не стоит вам всей записи, так что соседние ключи доходят. И размер контекста ничем не ограничен: большой массив или длинная строка сериализуются целиком и дают соответственно большую запись, поэтому передавайте идентификаторы, а не объекты, которые они обозначают.
Форматы
Оба формата пишутся в stderr, по одной операции записи на запись лога. Именно это правило и не даёт мастеру с десятком воркеров, которые пишут в один файловый дескриптор, перемешаться посреди строки: запись уходит целиком, а не собирается из кусков.
Больше Rapira никуда не пишет, поэтому лог оказывается в файле при перенаправлении stderr процесса, а менеджер служб собирает его без какой-либо настройки. Подробнее — в разделе Запуск в продакшене.
plain предназначен для чтения в терминале: отметка времени, уровень, цель, сообщение:
2026-07-30T09:12:34.567890Z ERROR php: …Цвет появляется, когда stderr — терминал, и никогда не появляется при перенаправлении в файл, поэтому в сохранённом логе не будет управляющих последовательностей. Любое непустое значение NO_COLOR гасит цвет и в терминале.
json предназначен для сборщика логов: по одному объекту на строку:
{"timestamp":…,"level":"ERROR","message":…,"target":…}timestamp — это RFC 3339 в UTC с миллисекундами. Переводы строк внутри сообщения экранируются, поэтому запись всегда занимает ровно одну строку, включая многострочную трассировку стека PHP. У записей от встроенного прокси-движка есть дополнительные поля log.* с местом вызова. JSON не раскрашивается никогда — ни в терминале, ни где-либо ещё.
RUST_LOG
RUST_LOG задаёт фильтр логов из окружения, поэтому разовая отладочная сессия обходится без правки конфигурации:
RUST_LOG=info rapira serve worker.php
RUST_LOG=rapira=debug,php=info rapira serve worker.php
RUST_LOG=warn,rapira=trace rapira serve worker.phpПервая команда поднимает до info вообще всё. Вторая бьёт точечно: цель rapira на debug, PHP на info. Третья приглушает зависимости до warn и поднимает до trace цель rapira — старт, жизненный цикл воркеров, остановку. Остальные цели называются своими именами, так что дописывайте их, когда вопрос в другом месте: RUST_LOG=warn,rapira=trace,master=trace.
Если у RUST_LOG непустое значение, она полностью заменяет и level, и [log.targets] — фильтр берётся целиком, слияния не происходит. Ваши записи из [log.targets] не подкладываются под неё, их просто никто не читает. Чтобы вернуться к конфигурации, оставьте переменную незаданной или пустой. На format она не влияет никогда.