flytachi / winter-logger
Multi-runtime PSR-3 logger for Winter framework (FPM, Swoole, CLI)
Requires
- php: >=8.3
- psr/log: ^3.0
Requires (Dev)
- monolog/monolog: @stable
- phpunit/phpunit: @stable
- squizlabs/php_codesniffer: @stable
Suggests
- ext-openswoole: Alternative to ext-swoole
- ext-swoole: For Swoole runtime with coroutine-local context isolation
- monolog/monolog: Required for actual logging (^3.5). Without it all loggers silently become NullLogger.
README
A PSR-3 logger that stays correct when one process serves many requests at once. It wraps Monolog with per-unit-of-work context isolation, Spring Boot-style output and a Java-style static factory, so the same logging code behaves the same under FPM, CLI and Swoole.
The library is infrastructure-agnostic: it never reads env vars and never detects Docker, the SAPI or Swoole. The framework that boots it builds the channel config and passes it in โ which is what keeps the package testable and usable outside Winter.
๐ Documentation ยท Quick start ยท API reference ยท Channel config
Installation
composer require flytachi/winter-logger monolog/monolog
Requires PHP 8.3+ and psr/log ^3.0. monolog/monolog ^3.5 is a suggestion rather
than a dependency โ install without it and every channel resolves to a NullLogger instead
of failing. ext-swoole is needed only for CoroutineContext.
Quick start
Build a manager with your channels and hand it to the factory once at bootstrap:
use Flytachi\Winter\Logger\{LoggerFactory, LoggerManager}; use Flytachi\Winter\Logger\Context\ProcessContext; use Monolog\Level; LoggerFactory::setManager(new LoggerManager( contextStorage: new ProcessContext(), // CoroutineContext under Swoole channels: [ 'http' => ['level' => Level::Info, 'format' => 'line', 'output' => 'stderr'], 'cli' => ['level' => Level::Debug, 'format' => 'line', 'output' => 'stdout', 'color' => true], ], )); LoggerFactory::setDefaultChannel('http');
Then log from anywhere:
use Flytachi\Winter\Logger\{Log, LoggerFactory}; Log::info('user created', ['id' => 42]); // default channel LoggerFactory::getLogger(UserService::class)->info('cache warmed'); // named after the class LoggerFactory::channel('cli')->warning('rate limit hit'); // a specific channel
[2026-01-01 12:00:00] [INFO ] -http- [4821]: user created {"id":42}
[2026-01-01 12:00:00] [INFO ] -http- [4821] (UserService): cache warmed {"class":"UserService"}
[2026-01-01 12:00:00] [WARN ] -cli- [4821]: rate limit hit
The last line goes to cli, which sits at Debug โ a debug() call on the default http
channel above would have been filtered out, since that one is configured at Info.
What you get
- Context that cannot leak โ fields set once per request appear in every record, and live in the coroutine under Swoole, in the process under FPM and CLI.
- Per-class loggers โ
LoggerFactory::getLogger(self::class)names the record after the class that wrote it, cached per class. - Bound context โ
withContext([...])returns a logger carrying those fields into every later call, without mutating the original. - Five outputs โ
stdout,stderr,syslog, rotatingfile, andnullfor tests. - Two formats โ a readable line for humans, JSON for collectors, with optional ANSI colour on the line.
- Sensitive-value masking โ passwords and tokens are replaced before a record is written, not after it is read.
- Survives a closed pipe โ a
SIGPIPE-safe stream handler, so a reader that went away cannot take the process down. - Monolog optional โ absent, everything degrades to
NullLogger; logging never prevents a boot.
Request-scoped context
Set the fields once at the start of the unit of work and every record picks them up:
$storage = LoggerFactory::contextStorage(); $storage->set('request_id', $requestId); $storage->set('user_id', $userId); // ... anywhere downstream Log::info('processing'); // carries request_id and user_id $storage->clear(); // at the end โ mandatory in a long-running process
Under Swoole, pass CoroutineContext instead of ProcessContext and each concurrent
request gets its own bag โ see
Context isolation.
Documentation
The user-facing documentation lives at winterframe.net/packages/logger (the link picks your language; RU and EN are both complete).
Start here
| Page | What it answers |
|---|---|
| Introduction | What the package is, and what it adds to Monolog |
| Installation | Requirements, optional Monolog, Swoole |
| Quick start | Bootstrap, first channel, first record |
| Mental model | Storage, manager, factory, facade โ who does what |
Guides
| Page | What it answers |
|---|---|
| Framework integration | Wiring it into an application's entry point |
| Request context | Attaching fields to every record of a request |
| Swoole coroutines | Keeping context isolated under concurrency |
| Dynamic channels | Adding a channel after bootstrap |
| Masking sensitive data | Keeping secrets out of the log |
Reference
| Page | What it answers |
|---|---|
| API reference | Every class and method |
| Channel config | Each config key, output target and format |
| Log format | What each segment of a line means |
Deep dive
| Page | What it answers |
|---|---|
| Log record lifecycle | What happens between the call and the write |
| Context isolation | Why storage belongs to a unit of work |
| Output and broken pipe | Surviving a reader that went away |
| Monolog optional | What degrades without it, and how quietly |
Classes in this package carry an @link to their page, so the same documentation is one
click away from your IDE.
Contributing
Internal technical notes โ exact contracts, invariants, and the reasoning behind decisions
that are not obvious from the code โ live in docs/. Read that before
changing how a record is built.
composer test # phpunit composer test-detail # phpunit --testdox composer cs-check # phpcs composer cs-fix # phpcbf
License
MIT License. See LICENSE.