Skip to content

Логирование

Rapira пишет всё в один поток: события жизненного цикла сервера, решения мастер-процесса, работа HTTP-фронта, диагностика PHP и то, что приложение пишет само, — всё это на stderr. Предупреждение PHP — это запись в том же логе, а не строка в отдельном файле error_log, и её уровень поднимается или опускается так же, как у любой другой записи.

Уровень по умолчанию — error, поэтому наружу выходят только ошибки, а исправный сервер не пишет в лог ничего. Поднять его — одна строчка в конфигурации или переменная окружения RUST_LOG, если конфигурацию править не хочется вовсе.

Уровни и формат

За логирование отвечает секция [log] в вашем rapira.toml:

toml
[log]
level = "error"   # error (default) | warn | info | debug | trace
format = "plain"  # plain (default) | json

level — общий порог сразу для всех целей: error пропускает только ошибки, warn добавляет к ним предупреждения и так далее вплоть до trace, который показывает вообще всё. format задаёт вид каждой записи — читаемые человеком строки или по одному JSON-объекту на строку.

Оба ключа необязательны, как и вся секция целиком. Про остальную часть файла — слушающий сокет, пул, супервизор — рассказывает раздел Конфигурация.

Уровни для отдельных целей

Одного общего уровня часто не хватает. Секция [log.targets] поднимает и опускает уровень отдельных целей поверх него, так что диагностика PHP может работать на debug, а внутренние подробности HTTP-стека вместе с ней не поднимаются:

toml
[log]
level = "error"

[log.targets]
php = "debug"
pingora_core = "warn"

Каждый ключ называет цель и поднимает или опускает уровень только для неё; всё остальное остаётся на level. Ключ сопоставляется по префиксу, поэтому php захватывает заодно php_sys и php_sys::callbacks: достаточно самого короткого подходящего префикса, а подмодули перечислять по отдельности не нужно.

Цели, под которыми пишет сама Rapira:

ЦельО чём пишет
rapiraжизненный цикл сервера: старт, жизненный цикл воркеров, остановка
masterнадзор: форки, уборка завершившихся процессов, пересоздание воркеров, перезагрузки, масштабирование пула
httpHTTP-фронт: слушающие сокеты, обработка полей запроса и ответа, мягкое завершение
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_ERRORerror
Предупреждения — E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNINGwarn
Уведомления — E_NOTICE, E_USER_NOTICEinfo
Сообщения об устаревании — E_DEPRECATED, E_USER_DEPRECATEDdebug

Сообщения об устаревании сидят на debug для того, чтобы пара тысяч таких сообщений из vendor не хоронила под собой предупреждения и ошибки, которые приходят вместе с ними.

Сообщение, которое не пропускает маска error_reporting скрипта, не исчезает — оно опускается до trace. Поэтому привычная маска делает ровно то, чего вы от неё ждёте:

php
<?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
<?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Уровень записи
Errorerror
Warningwarn
Infoinfo
Debugdebug
Tracetrace

Если уровень не указан, используется Info. Поскольку уровни те же самые, что и везде, [log.targets] и RUST_LOG фильтруют записи приложения ровно так же, как записи самого сервера: app = "debug" в [log.targets] поднимает записи приложения, не трогая ничего вокруг.

Массив контекста сериализуется в JSON и прикрепляется к записи полем context. Ключи сохраняются как написаны, вложенные массивы сохраняют структуру:

php
<?php

\Rapira\log('checkout failed', \Rapira\LogLevel::Error, [
    'order' => 41,
    'totals' => ['net' => 1250, 'tax' => 250],
]);

Throwable в контексте раскрывается до сериализации, потому что json_encode() видит исключение как пустой объект — его состояние лежит в приватных полях Exception и Error. Раскрытие несёт имя класса, сообщение, код, файл и строку и идёт по цепочке previous; стек вызовов в запись не попадает:

php
<?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 предназначен для чтения в терминале: отметка времени, уровень, цель, сообщение:

text
2026-07-30T09:12:34.567890Z ERROR php: …

Цвет появляется, когда stderr — терминал, и никогда не появляется при перенаправлении в файл, поэтому в сохранённом логе не будет управляющих последовательностей. Любое непустое значение NO_COLOR гасит цвет и в терминале.

json предназначен для сборщика логов: по одному объекту на строку:

text
{"timestamp":…,"level":"ERROR","message":…,"target":…}

timestamp — это RFC 3339 в UTC с миллисекундами. Переводы строк внутри сообщения экранируются, поэтому запись всегда занимает ровно одну строку, включая многострочную трассировку стека PHP. У записей от встроенного прокси-движка есть дополнительные поля log.* с местом вызова. JSON не раскрашивается никогда — ни в терминале, ни где-либо ещё.

RUST_LOG

RUST_LOG задаёт фильтр логов из окружения, поэтому разовая отладочная сессия обходится без правки конфигурации:

sh
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 она не влияет никогда.