m6web/log-bridge-bundle
M6Web LogBridgeBundle provides a Symfony logging bridge that integrates PSR-3/Monolog-compatible loggers into the service container, making it easy to configure and route application logs consistently across services with minimal setup.
Install and register
composer require m6web/log-bridge-bundle
Symfony Flex auto-registers bundles; on older apps add it manually:
// config/bundles.php
M6Web\Bundle\LogBridgeBundle\M6WebLogBridgeBundle::class => ['all' => true],
Minimal configuration — one filter, one level, log the response too:
# config/packages/m6_web_log_bridge.yaml
m6_web_log_bridge:
logger:
filters:
api_errors:
options:
level: error
response: true
request: true
routes: ['app_api_orders_create', 'app_api_payments']
Then give the bundle a dedicated Monolog channel so records don't get lost among app logs:
# config/packages/monolog.yaml
monolog:
channels: ['log_bridge']
handlers:
log_bridge:
type: stream
path: '%kernel.logs_dir%/%kernel.environment%.log_bridge.log'
level: info
channels: ['log_bridge']
Where to look first
vendor/m6web/log-bridge-bundle/DependencyInjection/Configuration.php — this is the authoritative schema. Config keys here are cheaper to read than to debug via InvalidConfigurationException.EventListener/ / EventSubscriber/ — tells you when the log record is emitted (typically on kernel.terminate, after the response has been flushed to the client).First use case: don't log everything. Start by capturing exactly what you'd otherwise tail -f for — failed write operations and 5xx responses on your most critical endpoints — with full request/response context in one record. That's the bundle's actual value proposition: a complete request/response pair correlated with a single Monolog line, without hand-rolling a listener.
One filter per audience, not one filter for everything. Filters are matched by route name and carry their own options. Use that to split severity:
m6_web_log_bridge:
logger:
filters:
mutations:
options: { level: info, request: true, response: true }
routes: ['app_api_order_create', 'app_api_order_cancel']
server_errors:
options: { level: critical, response: true }
routes: ['app_api_.*'] # verify regex vs. exact-match in your version
Pair it with a correlation ID. The bundle logs the request, but the useful part is tying that record to your other logs and your error tracker. Add a Monolog processor that stamps request_id (from a UID header or generated in a kernel.request listener) and make sure it's applied to the log_bridge channel:
services:
App\Monolog\RequestIdProcessor:
tags: [{ name: monolog.processor, channel: log_bridge }]
Use it as a cheap black box instead of building one. For non-idempotent endpoints (payments, refunds, stock decrements) a kernel.terminate-time log of method + path + status + payload is often enough to reconstruct "what did the client actually send" during an incident, without an APM license.
Structured output. If you already ship JSON logs, don't fight the default formatter — implement the bundle's formatter interface (check Formatter/ for the exact contract and tag name; typically a service tagged for the log_bridge channel) and return a JSON-serializable array/string. Then point the handler at type: stream with formatter: monolog.formatter.json. This makes the records queryable in whatever you actually use to search logs.
Sample instead of mute. High-traffic read endpoints are the classic reason people rip this bundle out again. Either scope filters tightly or gate the logger service behind a small decorator that drops N% of records — cheaper than maintaining an exclusion list of forty route names.
Testing pattern: functional-test the contract, not the bundle. Boot a kernel, hit an endpoint with KernelBrowser, and assert a record landed on the channel via a Monolog test handler or a spy on the bundle's logger service. This catches the two regressions that actually happen: a route rename silently unmatching your filter, and a channel misconfiguration swallowing records.
Logging on kernel.terminate cuts both ways. You get zero latency added to the response — but the record appears after the client already has the response, so log ordering in your file is not request ordering. Don't build dashboards that assume adjacency.
Response body capture is the main footgun. Large payloads, StreamedResponse, BinaryFileResponse, and file downloads can serialize into megabytes of garbage or explode memory. Exclude download/export routes from any filter with response: true, and check whether your version offers a truncation/size option before you ship it.
Monolog drops records silently if the channel isn't configured. If nothing appears, the first thing to check is channels: on your handlers and that monolog.channels includes the bundle's channel. bin/console monolog:debug or a temporary type: test handler resolves this in one minute.
Sanitize before it reaches disk. request: true will happily capture Authorization headers, session cookies, API keys in query strings, and password fields in bodies. That is a GDPR/PCI problem, not a cosmetic one. Either extend the formatter to redact a known key list, or restrict body capture to explicitly safe routes. Never rely on "we'll clean the logs later."
Filters match on route name, not URL. Routes generated by annotations with empty names, or routes renamed during refactors, will silently stop matching — no error, no warning, just missing logs. Add a test that asserts each critical filter still matches. Also confirm behaviour for sub-requests (ESI/fragments): check whether the listener guards on master request, or you'll get several records per page view.
Level escalation is manual. A filter has one level. If you want warning for 4xx and critical for 5xx you need two filters with status-based conditions, or you handle the mapping in a custom formatter/logger. Don't assume per-status levels are automatic.
Config is cached. bin/console cache:clear after editing the bundle config, especially in dev when you're iterating on regexes.
Verify the wiring before debugging your own code:
bin/console debug:container --tag=<bundle tag> # confirm formatter/logger services
bin/console debug:event-dispatcher kernel.terminate
Adoption risk is real and should shape your usage. 18 stars and zero dependents means you are close to the only consumer — this is a BedrockStreaming-internal-shaped bundle, not a de-facto standard. Two mitigations: (1) pin the version explicitly, and (2) keep your customizations in a formatter service rather than forking the bundle, so replacing it with a 60-line kernel.terminate listener later is an afternoon rather than a quarter. The extension points — custom formatter, custom filter — are the parts worth designing around.
How can I help you explore Laravel packages today?