Пакет · logger

Ментальная модель

Три идеи объясняют почти каждое правило в этой библиотеке. Держите их в голове — и API перестаёт казаться мешком методов и начинает выглядеть закономерным.

Идея 1 — Библиотека никогда не трогает вашу инфраструктуру

Winter Logger не читает env-переменные, не определяет Docker, не проверяет SAPI и не ищет Swoole. Всё это делает кто-то другой — фреймворк или ваш бутстрап: он определяет всё это и передаёт менеджеру готовый конфиг — уровни, форматы, цели вывода и объект хранилища контекста.

Думайте о логгере как о принтере, а не о коммутаторе. Он печатает то, что ему сказали, и так, как сказали. Решать, что за рантайм сегодня — FPM или Swoole — и куда должен идти stderr, — чужая работа.

Это осознанный компромисс:

  • Переносимость и тестируемость — нет глобалей, которые надо мокать, нет env, который надо подменять. Вы конструируете LoggerManager из обычных данных и проверяете вывод.
  • Взамен — вы (или ваш фреймворк) обязаны определить инфраструктуру до сборки менеджера. Библиотека не станет гадать за вас.

Вот почему

Каждая пометка «это передаёт фреймворк» в этих доках восходит сюда. LoggerManager принимает массив конфига и ContextStorage; он никогда не выводит их сам.

Идея 2 — Контекст изолируется под рантайм, а не под библиотеку

Вы хотите, чтобы request_id появлялся в каждой строке лога в течение запроса — но никогда не протекал в другой запрос, идущий в то же время. Безопасно это или нет — целиком зависит от рантайма, поэтому стратегия изоляции — это заменяемый объект: ContextStorage.

text
FPM  — один запрос = один процесс   → ProcessContext   (обычный массив на процесс)
CLI  — одна задача  = один процесс   → ProcessContext
Swoole — много корутин, один процесс → CoroutineContext (мешок на корутину)

Под FPM каждый запрос и так получает свежий процесс, поэтому обычный массив безопасен. Под Swoole один процесс жонглирует тысячами конкурентных корутин — обычный массив позволил бы user_id запроса A всплыть в логах запроса B. CoroutineContext вместо этого привязывает мешок к Swoole\Coroutine::getContext(), так что каждый запрос видит только свой.

Следствие, с которым вы столкнётесь везде: в долгоживущих процессах очищайте контекст в конце каждой единицы работы. FPM сносит процесс за вас; корутину Swoole убирает Swoole; но CLI-демон в цикле переиспользует один процесс вечно — поэтому вы обязаны clear() на каждой итерации, иначе поля протекут между задачами.

Полные внутренности — мешки по ссылке, фолбэк вне корутины — в Глубоком погружении → Изоляция контекста.

Идея 3 — Monolog — это бэкенд, и он опционален

Winter Logger — тонкий PSR-3-слой поверх Monolog. Он строит каналы Monolog, навешивает процессоры и обработчики и оборачивает их так, чтобы поклассовый контекст следовал за каждым вызовом. Но если Monolog не установлен, всё это молча вырождается в NullLogger.

text
monolog/monolog установлен  →  настоящий канал Monolog  →  форматированный вывод
monolog/monolog отсутствует →  Psr\Log\NullLogger      →  ничего, без ошибки

Так что логирование — это фича, которую можно выключить, просто не установив пакет: без флага в конфиге, без правки кода. Компромисс: если вы забудете поставить Monolog, логи исчезнут молча, а не выбросят исключение. Это сделано намеренно (см. Monolog опционален).

Как связаны части

text
LoggerManager   — строит и кэширует по каналу Monolog на запись конфига
    │            (каждый канал автоматически получает ContextInjectingProcessor)

LoggerFactory   — статический фасад: getLogger(Class), channel(), Log::*
    │            логгеры на класс кэшируются по "channel:FQCN"

Logger          — PSR-3-обёртка; вливает привязанный контекст в каждый вызов


Monolog  →  процессоры (контекст, маскирование)  →  форматтер  →  обработчик  →  вывод

Когда что брать

Логгер на класс getLogger(Class::class)Быстрый однострочник Log::info(…)Сырой канал LoggerFactory::channel(‘http’)
  • ✅ Используйте getLogger(self::class) в сервисах — имя класса окажется в каждой строке бесплатно.
  • ✅ Используйте Log::info(...) для быстрых, не привязанных к классу сообщений на канале по умолчанию.
  • ✅ Используйте channel('name'), когда осознанно нужен конкретный канал без метки класса.

Дальше