Logging is the heartbeat of a production application. In the early days of a project, a simple dev.log tail is sufficient. But as your Symfony application scales to handle payments, asynchronous workers and high-concurrency traffic, “writing to a file” becomes a liability rather than an asset.
The symfony/monolog-bundle offers sophisticated tools to transform logs from simple text streams into structured, actionable observability data.
This guide explores 10 advanced logging patterns that go beyond the defaults. We will use strict typing, PHP Attributes and modern YAML configuration.
Prerequisites
- Symfony: 7.4+
- PHP: 8.3+
- symfony/monolog-bundle
- monolog/monolog
- symfony/notifier (for alerting examples)
Scenation 1. The “Black Box” Recorder: FingersCrossed Handler
You want detailed debug logs when an error occurs to understand the sequence of events leading up to it, but you can’t afford the disk I/O to log debug messages for every successful request in production.
The FingersCrossedHandler buffers all logs in memory during the request. If the request finishes successfully, the buffer is discarded. If an error (or a specific threshold) is reached, the entire buffer (including previous debug logs) is flushed to the persistence handler.
Configuration
config/packages/prod/monolog.yaml:
monolog:
handlers:
main:
type: fingers_crossed
# The strategy: "error" means if an ERROR occurs, dump everything.
action_level: error
# Where to dump the logs if the threshold is met
handler: nested
# Optional: Keep a small buffer size to prevent memory leaks in long processes
buffer_size: 50
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
You’ll get the forensic detail of debug level logging exactly when you need it — during a crash — without filling your disk with noise during normal operations.
Scenario 2. Segregated Channels: The “Payment” Log
Your app.log is a mix of Doctrine queries, router matching and critical business logic. You need a dedicated file for financial transactions that can be audited separately.
Create a custom Monolog Channel.
Configuration
config/packages/monolog.yaml:
monolog:
channels: ['payment'] # Register the channel
handlers:
payment:
type: stream
path: "%kernel.logs_dir%/payment.log"
level: info
channels: ["payment"] # Only listen to this channel
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!payment"] # Exclude payment logs from the main file
Implementation
Inject the logger specifically for this channel using the Target attribute (available since Symfony 5.3+).
namespace App\Command;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
#[AsCommand(name: 'app:process-payments', description: 'Processes pending payments')]
class ProcessPaymentsCommand extends Command
{
public function __construct(
#[Target('payment.logger')]
private readonly LoggerInterface $paymentLogger,
private readonly LoggerInterface $mainLogger
) { parent::__construct(); }
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->mainLogger->info('Cron job app:process-payments started.');
$amounts = [10.50, 99.99, 45.00];
foreach ($amounts as $amount) {
$this->paymentLogger->info('Processing payment', ['amount' => $amount, 'status' => 'success']);
}
$this->mainLogger->info('Cron job finished.');
return Command::SUCCESS;
}
}
Run the Command. You will see payment.log created in var/log/ containing only these specific entries.
Scenario 3. Context Enrichment: The #[AsMonologProcessor] Attribute
Logs are useless if you can’t correlate them to a specific user or request ID. You find yourself manually adding [‘user_id’ => $user->getId()] to every single log statement.
A global Processor can automatically injects context into every log record.
Implementation
namespace App\Log;
use Monolog\Attribute\AsMonologProcessor;
use Monolog\LogRecord;
#[AsMonologProcessor]
class RequestContextProcessor
{
public function __invoke(LogRecord $record): LogRecord
{
// Simulated context since CLI commands don't have HTTP Requests
$extra = [
'pid' => getmypid(),
'user' => get_current_user(),
];
return $record->with(extra: array_merge($record->extra, $extra));
}
}
In Monolog 3, LogRecord is immutable. We use with() to return a modified copy.
Scenario 4. GDPR Compliance: Sensitive Data Redaction
A developer accidentally logs a user object, dumping PII (Personally Identifiable Information) or credit card numbers into the logs, violating GDPR/PCI-DSS.
A specialized processor can scans the context array and masks sensitive keys.
Implementation
namespace App\Log;
use Monolog\Attribute\AsMonologProcessor;
use Monolog\LogRecord;
#[AsMonologProcessor]
class SensitiveDataProcessor
{
private const array SENSITIVE_KEYS = ['password', 'credit_card', 'cvv', 'token'];
public function __invoke(LogRecord $record): LogRecord
{
$context = $record->context;
foreach ($context as $key => $value) {
if (in_array($key, self::SENSITIVE_KEYS, true)) {
$context[$key] = '***REDACTED***';
}
}
return $record->with(context: $context);
}
}
Verification:
$logger->info('User login', ['password' => 'secret123']);
// Output in log: "User login" {"password": "***REDACTED***"}
Scenario 5. Structured Logging: JSON for ELK/Datadog
Parsing multi-line text logs (like stack traces) in Kibana or Datadog is painful. Regex parsers break easily.
You can output logs as JSON lines. This allows log aggregators to natively index fields like context.order_id or extra.req_id.
Configuration
config/packages/monolog.yaml:
monolog:
handlers:
json_report:
type: stream
path: "%kernel.logs_dir%/app.json"
level: info
formatter: monolog.formatter.json
channels: ["!payment", "!event"]
Open var/log/app.json. The output should look like:
{"message":"Order created","context":{"id":123},"level":200,"channel":"app","datetime":"..."}
Scenario 6. Spam Prevention: The Deduplication Handler
Your database goes down. Your application receives 5,000 requests in a minute. Your “Email on Error” handler sends you 5,000 emails, getting your SMTP server blacklisted and flooding your inbox.
The DeduplicationHandler can aggregates identical log records and sends a single summary.
Configuration
config/packages/monolog.yaml:
monolog:
handlers:
deduplication:
type: deduplication
handler: nested_dedup
buffer_size: 60
time: 60
level: error
channels: ["!console"]
If the DB crashes, you receive one email every 60 seconds listing all occurrences, rather than one email per request.
Scenario 7. Dynamic Log Levels (Runtime Debugging)
A specific customer is reporting an issue in production. You can’t reproduce it and you can’t switch the entire production server to DEBUG level because of the performance hit.
Use an ActivationStrategy to switch the log level dynamically based on a request header.
Implementation
Create a custom strategy:
namespace App\Command;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:dynamic-debug', description: 'Tests dynamic log level activation')]
class DynamicDebugCommand extends Command
{
public function __construct(private readonly LoggerInterface $logger) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption('force-debug', null, InputOption::VALUE_NONE, 'Force debug logging for this run');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('force-debug')) {
$output->writeln('Debug mode forced via option. (Simulated, as Monolog ActivationStrategy relies on Http/Request state typically. But you can add processors/handlers dynamically in real apps based on this flag).');
}
$this->logger->debug('This detailed trace only appears if --force-debug is passed or an error occurs.');
$this->logger->info('Standard processing information.');
return Command::SUCCESS;
}
}
Scenario 8. Messenger Logging: Worker Context
Logs from messenger:consume are hard to trace. You see “Handling message,” but you don’t know which message ID caused the error because workers run as long-running processes.
Use Symfony’s EventListener to inject the Message ID into the Monolog context specifically for the worker process.
Implementation
namespace App\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
readonly class WorkerLogContextListener
{
public function __construct(private LoggerInterface $logger) {}
#[AsEventListener]
public function onMessageHandling(WorkerMessageReceivedEvent $event): void
{
$this->logger->info('Worker started message', [
'message_class' => $event->getEnvelope()->getMessage()::class,
]);
}
}
Scenario 9. Excluding 404s from Error Logs
Bots scanning your site for .env or wp-login.php generate thousands of 404 NotFoundHttpException logs. These clog your error monitoring tool (Sentry/Slack) with false positives.
Use the channels exclusion or a specific configuration to ignore bounced logs or better - configure the NotFoundHttpException to be ignored by the main error handler.
Configuration
config/packages/monolog.yaml:
monolog:
handlers:
fingers_crossed:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50
Scenario 10. Notifier Bridge: ChatOps
Email alerts are slow and often ignored. You want critical infrastructure failures to ping a Slack channel immediately.
Use symfony/notifier bridged with Monolog.
Prerequisites
composer require symfony/notifier symfony/slack-notifier
Configuration
config/packages/monolog.yaml:
monolog:
handlers:
slack_alerts:
type: service
id: Symfony\Bridge\Monolog\Handler\NotifierHandler
level: critical
Then configure the notifier chatter in config/packages/notifier.yaml and your DSN in .env.
framework:
notifier:
chatter_transports:
slack: '%env(SLACK_DSN)%'
texter_transports:
channel_policy:
urgent: ['chat/slack']
high: ['chat/slack']
medium: ['chat/slack']
low: ['chat/slack']
admin_recipients:
- { email: admin@example.com }
NotifierHandler maps log levels to Notifier importance. A critical log becomes a High Priority Slack notification automatically.
Conclusion
Logging is not a byproduct of code - it is a feature of your infrastructure.
In a junior developer’s mindset, logging is a safety net — something to check only when things break. But as you scale to Senior and Lead roles, your perspective must shift. You stop looking at logs as text files and start treating them as a stream of structured events.
By moving to Symfony 7.4 and leveraging the full power of Monolog 3, we transition from “logging” to “observability.”
Structured JSON turns your logs into a queryable database.
FingersCrossed handlers solve the “signal-to-noise” ratio, saving you gigabytes of storage while preserving critical context.
Processors ensuring every log entry carries the DNA of the request (User ID, Request ID) turn hours of debugging into minutes of verification.
Deduplication protects your inbox and your sanity.
Implementation of these patterns distinguishes a fragile application from a robust, enterprise-grade system. When your production environment faces a traffic spike or a silent data corruption issue, these configurations will be the difference between a stressful all-nighter and a quick, precise hotfix.
Source Code: You can find the full implementation and follow the project’s progress on GitHub: [https://github.com/mattleads/MonologPatterns]
Let’s Connect!
If you found this helpful or have questions about the implementation, I’d love to hear from you. Let’s stay in touch and keep the conversation going across these platforms:
- LinkedIn: [https://www.linkedin.com/in/matthew-mochalkin/]
- X (Twitter): [https://x.com/MattLeads]
- Telegram: [https://t.me/MattLeads]
- GitHub: [https://github.com/mattleads]
Top comments (0)