Ментальная модель
Три идеи объясняют почти каждое правило в этой библиотеке. Держите их в голове — и API перестаёт казаться мешком методов и начинает выглядеть закономерным.
Идея 1 — Библиотека никогда не трогает вашу инфраструктуру
Winter Logger не читает env-переменные, не определяет Docker, не проверяет SAPI и не ищет Swoole. Всё это делает кто-то другой — фреймворк или ваш бутстрап: он определяет всё это и передаёт менеджеру готовый конфиг — уровни, форматы, цели вывода и объект хранилища контекста.
Думайте о логгере как о принтере, а не о коммутаторе. Он печатает то, что ему сказали, и так, как сказали. Решать, что за рантайм сегодня — FPM или Swoole — и куда должен идти stderr, — чужая работа.
Это осознанный компромисс:
- Переносимость и тестируемость — нет глобалей, которые надо мокать, нет env, который надо
подменять. Вы конструируете
LoggerManagerиз обычных данных и проверяете вывод. - Взамен — вы (или ваш фреймворк) обязаны определить инфраструктуру до сборки менеджера. Библиотека не станет гадать за вас.
Вот почему
Каждая пометка «это передаёт фреймворк» в этих доках восходит сюда. LoggerManager принимает
массив конфига и ContextStorage; он никогда не выводит их сам.
Идея 2 — Контекст изолируется под рантайм, а не под библиотеку
Вы хотите, чтобы request_id появлялся в каждой строке лога в течение запроса — но никогда не
протекал в другой запрос, идущий в то же время. Безопасно это или нет — целиком зависит от
рантайма, поэтому стратегия изоляции — это заменяемый объект:
ContextStorage.
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.
monolog/monolog установлен → настоящий канал Monolog → форматированный вывод
monolog/monolog отсутствует → Psr\Log\NullLogger → ничего, без ошибкиТак что логирование — это фича, которую можно выключить, просто не установив пакет: без флага в конфиге, без правки кода. Компромисс: если вы забудете поставить Monolog, логи исчезнут молча, а не выбросят исключение. Это сделано намеренно (см. Monolog опционален).
Как связаны части
LoggerManager — строит и кэширует по каналу Monolog на запись конфига
│ (каждый канал автоматически получает ContextInjectingProcessor)
▼
LoggerFactory — статический фасад: getLogger(Class), channel(), Log::*
│ логгеры на класс кэшируются по "channel:FQCN"
▼
Logger — PSR-3-обёртка; вливает привязанный контекст в каждый вызов
│
▼
Monolog → процессоры (контекст, маскирование) → форматтер → обработчик → выводКогда что брать
- ✅ Используйте
getLogger(self::class)в сервисах — имя класса окажется в каждой строке бесплатно. - ✅ Используйте
Log::info(...)для быстрых, не привязанных к классу сообщений на канале по умолчанию. - ✅ Используйте
channel('name'), когда осознанно нужен конкретный канал без метки класса.
Дальше
- Руками: Быстрый старт
- Рецепты задач: Контекст запроса · Динамические каналы
- Авторитетная поверхность: Справочник API