symfony/monolog-bundle
MonologBundle integrates the Monolog logging library into Symfony, giving applications a flexible, PSR-3-compatible logging system. Configure handlers, formatters, and channels through Symfony configuration to route logs to files, consoles, syslog, or external services with minimal setup.
symfony/monolog-bundle is not a logging library — it is the Symfony FrameworkBundle glue that turns a YAML/XML tree into Monolog handler, formatter and processor service definitions. If you are working in a Laravel codebase, you will almost never install it: illuminate/log already depends on Monolog and configures it from config/logging.php. Its value to you is twofold — you will meet it on any Symfony service you touch, and its Configuration.php is the most complete, battle-tested catalogue of Monolog handler options in existence. Read it like documentation.
Minimum steps in a Symfony app:
composer require symfony/monolog-bundle (Symfony Flex registers it in config/bundles.php; otherwise add Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true]).config/packages/monolog.yaml — this is where 95% of real work happens.DependencyInjection/Configuration.php for the per-type options.# config/packages/monolog.yaml
monolog:
handlers:
main:
type: fingers_crossed # buffers everything...
action_level: error # ...and only flushes when >= error
excluded_http_codes: [404, 405] # (v4 name; 3.x used excluded_404s)
handler: file
file:
type: rotating_file
path: '%kernel.logs_dir%/%kernel.environment%.log'
max_files: 14
level: !php/const Monolog\Level::Debug
formatter: monolog.formatter.line
console:
type: console
process_psr_3_messages: false # never duplicate what stderr shows
channels: ['!event', '!doctrine', '!console']
Quick mental mapping from Laravel's config/logging.php:
| Laravel driver | MonologBundle equivalent |
|---|---|
single |
type: stream |
daily |
type: rotating_file + max_files |
stack |
type: group (all handlers fire) |
errorlog |
type: error_log |
syslog |
type: syslog / syslogudp |
slack |
type: slack / slackwebhook / telegram |
custom driver via: |
type: service pointing at your own handler service |
Channel-per-domain, resolved by DI. A channel is just a named logger. You declare it by referencing it, or explicitly:
monolog:
channels: ['checkout', 'security']
handlers:
security:
type: stream
path: '%kernel.logs_dir%/security.log'
channels: ['security'] # only this channel
noise:
type: stream
path: '%kernel.logs_dir%/noise.log'
channels: ['!security', '!checkout'] # everything else
Consume it with a tag, or — on Monolog 3.5+ / bundle 3.9+ — with the attribute, which is the pattern to prefer because it survives service renames and works with autoconfiguration:
use Monolog\Attribute\WithMonologChannel;
#[WithMonologChannel('checkout')]
final class PaymentGateway { public function __construct(private LoggerInterface $logger) {} }
Manually, the equivalent is the monolog.logger tag with channel: checkout. Inside a class, type-hint LoggerInterface and let the tag do the wiring; never new Logger(...).
Handler stacks that model intent, not verbosity. The two dominant stacks:
fingers_crossed → leaf handler: near-zero I/O in production until a threshold fires, then the whole buffer is dumped with full context. This is the single highest-value pattern the bundle gives you and has no first-class Laravel equivalent.buffer (flush on overflow / flush_on_overflow) → batch shipping for Slack, Loggly, Gelf, Telegram handlers; wrap the network handler, never log directly through it.Use group when you genuinely want fan-out (file + Slack), fallback_group (3.8+) when you want "Slack unless Slack is down, then file", whatfailuregroup when a flaky remote handler must not break a request.
Processors: attribute first, tag second. Bundle 3.8 added autoconfiguration for #[AsMonologProcessor]; 3.4 added it for ProcessorInterface services. Tag attributes (channel, handler, method, priority — priority added 3.11) let you scope a processor to a single handler, which is how you add user_id/request_id to Slack messages but not to 500-line debug files.
#[AsMonologProcessor(channel: 'security', handler: 'security')]
final class RequestIdProcessor { public function __invoke(array $record): array { /* ... */ } }
Custom handlers stay custom. Anything the bundle does not model (your in-house audit sink, a Laravel-style "tap") is type: service with a service ID. Keep the class framework-agnostic; only the YAML knows about Symfony.
Env-aware levels. Since 3.6/3.9 level accepts parameters and env placeholders: level: '%env(LOG_LEVEL)%'. Convert once in config/services.yaml (!php/const Monolog\Level::Info or the int/string form) rather than hardcoding strings in every handler.
Separate CLI from FPM. console.interactive_only (3.11) plus a dedicated console handler prevents every bin/console run from polluting the web log, and keeps VERBOSITY_QUIET honoured.
Package/bundle authors. Log only through Psr\Log\LoggerInterface, never register handlers, and if you must add a processor/handler ship it as an autoconfigured tagged service so host apps can re-scope it with tag attributes.
Testing. Use monolog.handler.test (type: test) or Symfony's TestLogger; assert on records rather than file_get_contents. Because channels are just services, self::getContainer()->get('monolog.logger.checkout') gives you an isolated logger per test.
sentry, raven, elasticsearch and mongo handler types, excluded_404s, and the misspelled console_formater_options. Sentry now goes through sentry/sentry-symfony and type: service. If you are pasting config from a blog post, check which major it targets.fingers_crossed writes nothing below the threshold — by design. "My logs are empty" is almost always action_level too high, a missing leaf handler, or an act_level-vs-action_level typo. Remember the leaf handler is implicitly nested: true.nested: true means "not attached to the logger". If you list a nested handler in a channel's handler array you get puzzling double-handling; nesting is only for handlers referenced by another handler.fingers_crossed wrapper instead of its leaf.excluded_http_codes only works on fingers_crossed and only inspects the exception's HTTP status; 404 excluded does not silence a NotFoundHttpException thrown from a non-HTTP context.bin/console debug:config monolog, bin/console debug:container monolog.logger.checkout, bin/console debug:container --tag=monolog.processor, bin/console debug:container --tag=monolog.logger. Then dump the live stack with dump($logger) in a throwaway controller — service IDs follow monolog.handler.<name> and monolog.formatter.<name>, so debug:container monolog.handler.file shows you the exact resolved arguments.kernel.reset (3.7) and handlers are explicitly closed on kernel shutdown (4.1), but handlers you built yourself with buffers must implement ResetInterface or you will ship stale context and lose buffered records on restart.use_locking: true on rotating_file (3.7) matters the moment two PHP-FPM pools or a cron and a web request share a log path. Rotations racing each other silently truncate files.use_microseconds: false at the top level is a real perf win under high volume and makes timestamps diffable with grep.channels: are public services — legacy behaviour, but it means container compilation cost and a small leak of internals; prefer autowiring via the attribute/tag.exit/die before kernel shutdown. 4.1 closes handlers on terminate; a hard exit loses buffered records, which is exactly the logs you want at 3 a.m.config/logging.php — stack ≈ group, tap ≈ processors, and a custom driver for fingers_crossed/buffer if you truly need them — but the cleanest route for a Laravel app is to instantiate the Monolog handler class directly in a custom channel rather than installing this bundle. Steal the option names from Configuration.php; they map 1:1 onto handler constructor arguments.How can I help you explore Laravel packages today?