Package · logger

Framework integration

The library is infrastructure-agnostic: it never reads env vars or detects the runtime. Your bootstrap does that, builds a config, and wires the logger. Here’s the split of responsibilities and the exact call order.

Who does what

Step Responsibility Who calls it
Resolve env → config (levels, output, paths) infrastructure framework kernel
Build the LoggerManager infrastructure framework kernel
setManager() register once framework kernel
setDefaultChannel() pick active channel entry point
setContextStorage() pick isolation strategy entry point (Swoole)

The kernel builds and registers the manager. The entry pointpublic/index.php, the Swoole runner, or a CLI script — knows the actual runtime, so it sets the default channel and (for Swoole) the context storage.

Bootstrap — register the manager once

bootstrap.php
<?php

use Monolog\Level;
use Flytachi\Winter\Logger\LoggerManager;
use Flytachi\Winter\Logger\LoggerFactory;
use Flytachi\Winter\Logger\Context\ProcessContext;

$manager = new LoggerManager(
  contextStorage: new ProcessContext(),   // default; swapped per-runtime below
  channels: [
      'sys'  => ['level' => Level::Warning, 'format' => 'line', 'output' => 'stderr',
                 'file_path' => null, 'file_max' => 30, 'syslog_ident' => 'winter'],
      'http' => ['level' => Level::Info,    'format' => 'line', 'output' => 'stderr',
                 'file_path' => null, 'file_max' => 30, 'syslog_ident' => 'winter'],
      'cli'  => ['level' => Level::Debug,   'format' => 'line', 'output' => 'stdout',
                 'file_path' => null, 'file_max' => 30, 'syslog_ident' => 'winter'],
  ],
);

LoggerFactory::setManager($manager);

Entry points — pick channel & storage per runtime

Each entry point requires the bootstrap, then declares its runtime:

public/index.php — FPM
<?php

require '../bootstrap.php';

// FPM: one request = one process, ProcessContext is already correct.
LoggerFactory::setDefaultChannel('http');
swoole_server.php — Swoole
<?php

require '../bootstrap.php';

use Flytachi\Winter\Logger\Context\CoroutineContext;

// Swoole: swap to coroutine-isolated context before accepting requests.
LoggerFactory::setContextStorage(new CoroutineContext());
LoggerFactory::setDefaultChannel('http');
cli runner
<?php

require 'bootstrap.php';

// CLI jobs / queue workers / daemons.
LoggerFactory::setDefaultChannel('cli');

Order matters

setManager() first, always. setContextStorage() and addChannel() operate on the current manager, so they must run after it. Each of these also clears the per-class logger cache, so set them at bootstrap — not mid-request.

Use it from application code

Once wired, application code never sees the manager:

php
use Flytachi\Winter\Logger\LoggerFactory;
use Flytachi\Winter\Logger\Log;

// Per-class logger on the default channel:
LoggerFactory::getLogger(self::class)->info('user created', ['id' => 42]);

// Quick one-liner:
Log::warning('rate limit hit');

A common convention (as used in winter-kernel):

Channel Typical use Default output
sys system events, fallback stderr
http controllers, services (FPM/Swoole) stderr (FPM) / stdout (Swoole)
cli jobs, queue workers, daemons stdout

stderr for FPM, stdout for Swoole

Under FPM, php://stdout is the FastCGI response stream — writing to it after a client disconnect risks a broken pipe. Prefer stderr there. Swoole handles responses via its own I/O, so stdout is safe. See Output & broken pipe.

Reset in tests

LoggerFactory holds static state. Rebuild it in setUp() so tests don’t share a manager:

php
protected function setUp(): void
{
  LoggerFactory::setManager($this->buildManager());
}