Package · logger

Isolate context under Swoole

Under Swoole one process serves thousands of concurrent requests. A plain per-process array would let one request’s user_id leak into another’s logs. CoroutineContext binds context to the coroutine instead, so each request sees only its own.

The problem it solves

ProcessContext stores one array per process — safe under FPM (one request = one process), unsafe under Swoole (many coroutines share the process). CoroutineContext uses Swoole\Coroutine::getContext(), which returns a bag bound to the current coroutine. When the coroutine ends, Swoole destroys the bag automatically.

Requires ext-swoole

CoroutineContext needs ext-swoole (or ext-openswoole). It’s the only part of the library that depends on the extension — everything else runs anywhere.

Wire it at the entry point

Build the manager with CoroutineContext, or swap storage on an existing manager before accepting requests:

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

// Swoole HTTP server entry point, before the server starts accepting:
LoggerFactory::setContextStorage(new CoroutineContext());
LoggerFactory::setDefaultChannel('http');

Building it from scratch instead:

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

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

stdout is safe under Swoole

Use output: 'stdout' for Swoole channels — Swoole manages HTTP responses through its own async I/O, so writing to php://stdout never causes the broken-pipe problem that FPM has. More in Output & broken pipe.

Set context per request

Set fields inside the request handler — they’re visible only to that coroutine:

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']);

  // Handle request — every log line carries request_id/method/path:
  LoggerFactory::getLogger('App\\Handler')->info('handling');
  $response->end('ok');

  // Optional — Swoole clears the coroutine context when it ends anyway,
  // but an explicit clear makes the intent obvious:
  $ctx->clear();
});

Two concurrent requests each get their own request_id — they never see each other’s fields.

Outside a coroutine

Some code runs before any coroutine exists — e.g. onWorkerStart or server bootstrap. There, Swoole\Coroutine::getCid() is not positive, so CoroutineContext falls back to a static array. The logger keeps working during bootstrap without throwing; those fields simply aren’t coroutine-isolated (there’s no coroutine yet to isolate).

php
$server->on('workerStart', function () {
  // No coroutine here — CoroutineContext uses its fallback array, still works:
  LoggerFactory::getLogger('App\\Server')->info('worker started');
});

WebSocket & other coroutine events

The same isolation applies to any Swoole callback that runs in its own coroutine — each onMessage, onConnect, or task fires in a fresh coroutine with its own context bag.