flytachi/winter-logger

Multi-runtime PSR-3 logger for Winter framework (FPM, Swoole, CLI)

Maintainers

Package info

github.com/Flytachi/winter-logger

Homepage

Issues

Documentation

pkg:composer/flytachi/winter-logger

Transparency log

Statistics

Installs: 376

Dependents: 1

Suggesters: 0

Stars: 0

v1.1.2 2026-08-17 15:17 UTC

This package is auto-updated.

Last update: 2026-08-17 17:06:17 UTC


README

Latest Version on Packagist PHP Version Require Software License

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, rotating file, and null for 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.