m6web/monolog-extra-bundle
Symfony bundle that extends Monolog with extra handlers and processors, making it easier to route, format, and enrich application logs through standard bundle configuration. A practical add-on when built-in Monolog options need additional integrations without custom wiring.
First, the reality check. m6web/monolog-extra-bundle is a Symfony bundle, not a Laravel package. It requires symfony/framework-bundle and symfony/monolog-bundle, ships no service provider, and offers nothing you can composer require into a Laravel app without dragging half of Symfony's kernel in behind it. So "getting started" splits into two realistic paths:
report()).composer require m6web/monolog-extra-bundle
config/bundles.php — Flex does this automatically).monolog_extra root key to config/packages/ and enable the pieces you want.monolog.processor tag, so monolog.yaml wiring is usually just ordering).README.md — short, but it's the only doc.DependencyInjection/Configuration.php — the authoritative list of config keys. With 6 stars and 0 dependents, treat this file as the docs.Processor/ — the actual classes: request/context enrichment, tags, and the like.Resources/config/*.xml — real service ids (they'll look like monolog_extra.*), which is what you need for debug:container.Structured log enrichment: every log record carries request_id, uri, method, user_id, plus whatever domain tags you push, so a grep in Kibana/CloudWatch is enough to reconstruct a request. That's the 20% of the bundle that pays for itself.
A processor decorates every Monolog\LogRecord with request metadata. In a Symfony app you enable it and forget it; in Laravel you build the equivalent in a middleware plus a channel tap:
// Laravel equivalent
Log::withContext([
'request_id' => $requestId,
'uri' => $request->path(),
'method' => $request->method(),
]);
The bundle's real value in a microservice mesh is threading one ID from the ingress header through every log line — including the ones emitted by a queue worker hours later. In Symfony that means a kernel.request listener that reads X-Request-Id (or generates one) before anything logs. In Laravel:
// app/Http/Middleware/CorrelationId.php
$id = $request->header('X-Request-Id') ?? (string) Str::uuid();
Context::add('request_id', $id);
$response = $next($request);
return $response->header('X-Request-Id', $id);
And on jobs, push the ID onto the payload so the worker re-adds it.
The bundle's nicest idea: an exception implements a marker interface exposing its own log level and context, and a listener logs it automatically — controllers stop doing catch { $logger->error(...); throw $e; }. Laravel's analogue is report()/Handler::report() combined with a renderable-style convention:
interface LoggableException {
public function logLevel(): string;
public function logContext(): array;
}
// App\Exceptions\Handler
protected function shouldReport(Throwable $e): bool { /* ... */ }
Wire a small listener in Handler::report() that reads logLevel()/logContext() when the interface is present.
Rather than interpolating into the message ("order {$id} failed"), push order_id as a structured field. In Laravel this is Log::withContext() scoped to the request, or a channel tap that merges a Context repository:
// config/logging.php
'daily' => [
'driver' => 'daily',
'tap' => [App\Logging\AttachContext::class],
],
Processors attach arrays. LineFormatter will flatten or stringify them. Use monolog.formatter.json in Symfony, or 'formatter' => JsonFormatter::class per channel in Laravel, otherwise the enrichment is invisible.
| Bundle | Laravel equivalent |
|---|---|
| Loggable exception interface | Handler::report() / shouldntReport |
| Request processor | Middleware + Log::withContext |
| Tag/context processor | Context facade, channel tap |
| Logger service | Typed wrapper around Log::channel() |
| Log levels per exception | LogLevel::from($e) in report() |
symfony/framework-bundle, which conflicts with illuminate/* in subtle ways (duplicate EventDispatcherInterface implementations, kernel bootstrapping). The patterns port; the package does not.request_id disappears, something downstream is clobbering it — reorder in monolog.yaml / your tap array.nulls or throw on getCurrentRequest() === null. Guard on php_sapi_name() === 'cli' and rely on the job payload instead. This is the #1 source of "why are all my worker logs missing context."User entity into the context. It bloats every record, leaks PII into your log vendor, and blows up on lazy-loading proxies. Push user_id; join elsewhere.user_id, order_id, full URIs with IDs) are fine as fields in Datadog/ELK but ruinous if you key indexes or tag names off them. Normalize paths, keep IDs as attributes.Monolog\LogRecord. The bundle's last release is dated 2026-07-21, so verify the major you're on before assuming a processor works — a mismatched ProcessorInterface signature fails loudly at boot, which is at least easy to spot.bin/console debug:container monolog_extra (or debug:container --tag=monolog.processor) tells you what's actually registered; config/packages/monolog.yaml tells you what's actually attached. Most "it doesn't work" reports are a processor that's registered but not attached to the handler.Monolog\Processor\ProcessorInterface and tag it monolog.processor — no bundle config needed, and it's trivially ported to Laravel later by moving the class into app/Logging/.Configuration.php and the processors before committing. For a large Symfony codebase that's cheap; for a Laravel one, copy the ideas and skip the dependency.How can I help you explore Laravel packages today?