Пакет · logger

Изоляция контекста под Swoole

Под Swoole один процесс обслуживает тысячи конкурентных запросов. Обычный массив на процесс позволил бы user_id одного запроса протечь в логи другого. CoroutineContext вместо этого привязывает контекст к корутине, так что каждый запрос видит только свой.

Какую проблему это решает

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

Требует ext-swoole

CoroutineContext требует ext-swoole (или ext-openswoole). Это единственная часть библиотеки, зависящая от расширения — всё остальное работает где угодно.

Подключение в точке входа

Соберите менеджер с CoroutineContext или подмените хранилище на существующем менеджере до начала приёма запросов:

php
use Flytachi\Winter\Logger\LoggerFactory;
use Flytachi\Winter\Logger\Context\CoroutineContext;

// Точка входа HTTP-сервера Swoole, до того как сервер начнёт принимать запросы:
LoggerFactory::setContextStorage(new CoroutineContext());
LoggerFactory::setDefaultChannel('http');

Сборка с нуля вместо этого:

php
use Flytachi\Winter\Logger\LoggerManager;
use Flytachi\Winter\Logger\Context\CoroutineContext;

$manager = new LoggerManager(
  contextStorage: new CoroutineContext(),
  channels: [ /* ... */ ],
);

stdout безопасен под Swoole

Для каналов Swoole используйте output: 'stdout' — Swoole управляет HTTP-ответами через собственный асинхронный I/O, поэтому запись в php://stdout никогда не вызывает проблему broken pipe, которая есть у FPM. Подробнее в Вывод и broken pipe.

Задавайте контекст на запрос

Задавайте поля внутри обработчика запроса — они видны только этой корутине:

php
$server->on('request', function ($request, $response) {
  $ctx = LoggerFactory::contextStorage();
  $ctx->set('request_id', uniqid('req_'));
  $ctx->set('method',     $request->server['request_method']);
  $ctx->set('path',       $request->server['request_uri']);

  // Обрабатываем запрос — каждая строка лога несёт request_id/method/path:
  LoggerFactory::getLogger('App\\Handler')->info('handling');
  $response->end('ok');

  // Опционально — Swoole и так очищает контекст корутины при её завершении,
  // но явный clear делает намерение очевидным:
  $ctx->clear();
});

Два конкурентных запроса получают каждый свой request_id — они никогда не видят полей друг друга.

Вне корутины

Часть кода выполняется до существования какой-либо корутины — например, onWorkerStart или бутстрап сервера. Там Swoole\Coroutine::getCid() не положителен, поэтому CoroutineContext откатывается к статическому массиву. Логгер продолжает работать во время бутстрапа, не выбрасывая исключений; эти поля просто не изолированы по корутинам (корутины ещё нет, чтобы изолировать).

php
$server->on('workerStart', function () {
  // Здесь нет корутины — CoroutineContext использует фолбэк-массив, всё равно работает:
  LoggerFactory::getLogger('App\\Server')->info('worker started');
});

WebSocket и другие события корутин

Та же изоляция применяется к любому колбэку Swoole, выполняющемуся в своей корутине — каждый onMessage, onConnect или task срабатывает в свежей корутине со своим мешком контекста.

Связанное