Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Utils.log.psr.multi Logger Laravel Package

mouf/utils.log.psr.multi-logger

A PSR-3 compatible multi-logger for the Mouf framework that fans out each log message to several underlying loggers at once, letting you route logs to multiple destinations through a single PSR-3 interface, wired up via Mouf's dependency injection.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect

composer require mouf/utils.log.psr.multi-logger

Before writing any code, open src/MultiLogger.php in the repo. This package is a single small class (plus an interface or two); reading the whole thing takes two minutes and removes all guesswork about the exact method names (addLogger(), constructor signature, whether it implements LoggerAwareInterface). It depends on psr/log, so check which major version Composer resolved — that determines whether LoggerInterface::log() accepts string|\Stringable and how strict it is about level casing.

First use case: one logger, two sinks

The idiomatic entry point is "I already have two PSR-3 loggers and I want both to receive every record."

use Mouf\Utils\Log\Psr\MultiLogger\MultiLogger;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;

$file = new Logger('app');
$file->pushHandler(new StreamHandler(__DIR__.'/logs/app.log'));

$stderr = new Logger('app');
$stderr->pushHandler(new StreamHandler('php://stderr'));

$logger = new MultiLogger([$file, $stderr]);
// or: $logger = (new MultiLogger())->addLogger($file)->addLogger($stderr);

$logger->info('Boot complete');

Verify fan-out by writing one line at debug and one at error and confirming both sinks saw them (or didn't, if the child logger's own level threshold filtered them — see Gotchas).

The natural first production use is bridging: you have a legacy Mouf/LoggerInterface-based component that only knows about PSR-3, and a modern Monolog stack with handlers, processors, and formatters. Wrap the modern logger in a MultiLogger, keep the legacy sink attached for one release cycle, then delete it.

Implementation Patterns

1. Tee / fan-out for migration and dual-write

The core pattern is dual-write during a cutover. Attach the old and new backend, compare output for a week, then drop the old one:

$logger = new MultiLogger([
    $legacyMoufLogger,     // still used by the ops dashboard
    $monolog,              // new structured JSON sink
]);

Because MultiLogger itself is a LoggerInterface, it can be injected anywhere a logger is expected — nothing downstream knows it is a composition.

2. Environment-based routing

Keep the wiring in a container definition / service provider, not in business logic. Route per environment:

$loggers = [ $monolog ];                       // always
if ($env === 'prod') {
    $loggers[] = $slackLogger;                 // alerts channel
}
if ($env === 'local') {
    $loggers[] = $dumpLogger;                  // Symfony VarDumper / FirePHP
}
$container->set(LoggerInterface::class, new MultiLogger($loggers));

Register it against Psr\Log\LoggerInterface so every consumer — controllers, queue workers, console commands — gets the fan-out automatically. For classes using LoggerAwareInterface, call setLogger($multiLogger) in the container factory.

3. Per-channel instances, not one global

Do not build a single god-logger with a dozen children. Build one MultiLogger per channel (app, security, audit, billing) and give each its own children. This keeps level filtering and retention policies meaningful, and it means a noisy debug channel doesn't drag a pager-worthy one along with it. Cache them in the container keyed by channel name.

4. Decorator ordering matters

MultiLogger is a decorator, so decide deliberately whether it sits inside or outside your processors:

  • MultiLogger(Monolog with processors) → every sink gets the same enriched record. Usually what you want.
  • Monolog(MultiLogger as a handler) → not possible directly; Monolog handlers aren't PSR-3 loggers. If you need per-sink processing, give each child Monolog its own processor chain instead.

If you want a record ID or request ID on every line, add it as a Monolog processor on each child rather than bolting it on at the call site.

5. Fail-soft child loggers

Wrap unreliable sinks (HTTP, Slack, syslog over network) before handing them to the MultiLogger. Monolog's WhatFailureGroupHandler or a tiny try/catch decorator prevents one flaky sink from breaking the fan-out:

class SafeLogger implements LoggerInterface
{
    public function __construct(private LoggerInterface $inner) {}

    public function log($level, string|\Stringable $message, array $context = []): void
    {
        try { $this->inner->log($level, $message, $context); }
        catch (\Throwable) { /* never let logging kill the request */ }
    }
    // ...delegate remaining methods...
}

$logger = new MultiLogger([$monolog, new SafeLogger($slack)]);

6. Lazy registration

Constructing a Slack or Sentry logger can open sockets or spawn clients. If the package's API lets you pass closures/callables, use them; otherwise build the child list lazily in a factory and only pass it to MultiLogger on first log() call (a small LoggerInterface proxy in front of it). Registering eager services in a constructor is the usual source of "why is my container booting 40 services in CLI commands that never log?"

7. Testing the composition

MultiLogger is trivially testable. Use Psr\Log\Test\TestLogger (or a small in-memory recorder) as a child and assert fan-out:

$recorder = new TestLogger();
$null = new NullLogger();
$logger = new MultiLogger([$recorder, $null]);

$logger->warning('quota at {pct}%', ['pct' => 91]);

self::assertTrue($recorder->hasWarning('quota at {pct}%'));
self::assertSame(['pct' => 91], $recorder->records[0]['context']);

Note the assertion checks the raw PSR-3 template, not the interpolated string — that's where most people's tests get written wrong.

8. Health check / smoke test

Because a misconfigured MultiLogger silently writes nowhere, add a boot-time assertion in non-production environments: log a debug/info sentinel and assert at least one child recorded it, or log the child count and class names once at startup.

Gotchas and Tips

An exception in one child aborts the rest. There is no built-in isolation. If child #1 throws, children #2..n never see the record. Always wrap unreliable sinks (see pattern 5) — this is the single most important operational detail with a fan-out logger.

Duplicate output is easy to create. Registering the same logger twice (e.g., passing it in the constructor and calling the add method, or a container factory that runs twice) doubles every line. If you see duplicate log entries, look at registration, not at the logger. Some Monolog handlers (FingersCrossedHandler, buffers) will also look duplicated if you wrap one instance in two places.

The wrapper does not filter levels. MultiLogger delegates log() to every child; each child applies its own threshold. A debug call will hit the file sink but not the Slack sink (if configured at warning), which is usually what you want — but it means "I added a logger and got no output" is almost always a child-level configuration issue. Check each child's threshold and handler bubbling first, then the MultiLogger registration.

No context interpolation at the wrapper level. PSR-3 says interpolation is the emitter's job, and MultiLogger correctly punts to the children. If your test asserts on quota at 91% you will fail; assert on the template and the context array. Similarly, ['exception' => $e] handling is a child-specific convention (Monolog has one; a bare PSR-3 implementation may just stringify it in the context).

Watch psr/log major-version drift. The package's composer.json may allow ^1.0 || ^2.0 || ^3.0. If one of your children is still on psr/log v1 (log($level, $message, array $context = []) with no return type) while the wrapper was resolved against v3, you can hit signature incompatibilities or Stringable type errors when passing objects as messages. Run composer why psr/log and align.

Message objects vs. strings. Under psr/log v2/v3, string|\Stringable is accepted. If you pass a Stringable object, each child will cast it — if the child is an old v1-only implementation, you get a type error. Pass strings unless you control every child.

There is no flush() / close(). If a child buffers (Monolog BufferHandler, FingersCrossed), you must iterate your own list of children and flush them — the MultiLogger won't expose that, because LoggerInterface has no such method. Keep a reference to the child array in your container definition if you need shutdown flushing.

Empty child list = silent no-op. A MultiLogger with zero loggers swallows everything without complaint. Guard against this in your container factory (throw or fall back to NullLogger explicitly) so a misconfigured environment fails loudly at boot rather than by missing logs during an incident.

Performance on hot paths. Every log call is multiplied by child count. Three children on a per-request line is fine; five children including an HTTP sink on a loop over 10k rows is not. Either raise the child thresholds in production, buffer at the request level, or don't register heavy sinks on high-volume channels.

Extension points. Since the class is tiny, the cleanest extension is subclassing and overriding log() — e.g., to add a request ID into $context, enforce a minimum level before the fan-out, or emit a metric. That's cheaper than writing a separate decorator, and it's why vendoring the class is a defensible option here.

Low adoption means low maintenance risk — and low support. With a handful of stars and no dependents, expect no upstream fixes and no Stack Overflow answers. Pin the exact version in composer.json, read the source once, and consider copying the ~50 lines into your own namespace. A fan-out PSR-3 logger is small enough that owning it removes a supply-chain dependency and lets you add the fail-soft and flush behavior described above without forking.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json