Жизненный цикл записи лога
Проследим единственный $log->info(…) от места вызова до байта в потоке. Знание
порядка — особенно того, где выполняются процессоры — объясняет поведение, которое иначе
удивляет, например, какие поля маскируются.
Путь
ваш вызов $log->info('msg', ['id' => 42])
│
▼
Winter Logger::log() влить boundContext (['class' => FQCN]) под context вызова
│
▼
Monolog Logger::addRecord проверка уровня → построить LogRecord(context, extra={})
│
▼
процессоры (LIFO) выполняются в обратном порядке добавления:
│ 1. SensitiveMaskingProcessor (если добавлен приложением)
│ 2. ContextInjectingProcessor (добавлен менеджером)
▼
обработчик (проверка уровня) SafeStreamHandler / Syslog / RotatingFile / Null
│
▼
форматтер SpringLineFormatter или JsonFormatter
│
▼
запись @fwrite(stream, formatted) — никогда не бросаетШаг 1 — Привязанный контекст вливается первым
Logger::log() из Winter вливает свой
boundContext в context вызова прежде, чем передать управление Monolog. Для логгера на класс
boundContext — это ['class' => FQCN]; withContext() добавляет ещё. При конфликте ключей
побеждает context самого вызова:
public function log(mixed $level, string|Stringable $message, array $context = []): void
{
$this->monolog->log(
$level,
(string) $message,
empty($this->boundContext) ? $context : array_merge($this->boundContext, $context),
);
}Так что к моменту, когда Monolog видит запись, context уже несёт class (и любые привязанные
поля). extra пока пуст — его заполняет процессор следующим шагом.
Шаг 2 — Процессоры выполняются в порядке LIFO
Вот часть, которую стоит усвоить. pushProcessor() из Monolog добавляет в начало списка
процессоров (array_unshift), а addRecord() обходит этот список от начала к концу. Итоговый
эффект: последний добавленный процессор выполняется первым.
Менеджер добавляет ContextInjectingProcessor
при построении канала. Если ваше приложение позже добавит
SensitiveMaskingProcessor через
monolog()->pushProcessor(), он был добавлен позже — поэтому выполняется первым:
порядок добавления: ContextInjecting (менеджером) → Masking (приложением)
порядок выполнения: Masking → ContextInjectingSensitiveMaskingProcessorскрывает чувствительные ключи вcontextиextra.ContextInjectingProcessorзатем вливает снимок текущего хранилища контекста вextra(с ранним завершением, когда хранилище пусто).
Маскирование выполняется до инъекции контекста
Поскольку маскирование выполняется первым, оно видит context вызова (уже заполненный), но
не поля контекста запроса — те вливаются в extra позже. Секреты, переданные напрямую в
вызов лога, маскируются; секрет, заданный через contextStorage()->set(), будет влит после
маскирования и проскользнёт. Держите секреты вне хранилища контекста запроса.
Шаг 3 — Фильтрация по уровню происходит дважды
Уровень проверяется у логгера Monolog (ни против чего — Monolog передаёт всё обработчикам) и
затем у обработчика, построенного с level канала. Запись ниже минимального уровня канала
отбрасывается обработчиком до форматирования. Вот почему level живёт в конфиге канала, а не в
вызове.
Шаг 4 — Форматирование
Форматтер обработчика превращает LogRecord в строку.
SpringLineFormatter объединяет context + extra в
JSON-хвост, берёт короткое имя класса из context['class'] и штампует PID через getmypid().
JsonFormatter вместо этого выдаёт один JSON-объект на строку.
Шаг 5 — Безопасная запись
Для потоковых выводов обработчик — это
SafeStreamHandler, чей streamWrite()
использует @fwrite и никогда не бросает — так что broken pipe стоит одной строки, а не
процесса. Обработчики syslog, file и null делают свой финальный шаг.
Почему порядок важен на практике
classвсегда присутствует для логгеров на класс — он вливается на шаге 1, прежде чем что-либо ещё сможет его коснуться.- Контекст запроса — построчный, живой — он читается заново на шаге 2 на каждой записи, так что изменение его посреди запроса меняет последующие строки.
- Маскируйте то, что передаёте, а не то, что вливаете — порядок LIFO означает, что маскирование и инъекция не компонуются так, как вы могли бы предположить.
Смотрите также
- Изоляция контекста — что содержит влитый снимок
- Формат логов — шаг форматирования в деталях
- Вывод и broken pipe — финальная запись