Magento 2 Logging Best Practices: Performance, Structure & Debugging
Logging is the silent backbone of every Magento 2 store. When something breaks — a failed order, a stuck cron, a plugin throwing exceptions — logs are your first stop. But few developers treat logging as seriously as caching or indexing.
Poor logging habits cause real damage: debug logs growing to gigabytes in production, I/O bottlenecks from channel spam, critical errors buried in noise, and disk exhaustion taking down entire servers. This guide walks through how Magento 2 logging actually works, how to configure it properly, and how to build habits that keep logs useful without killing performance.
How Magento 2 Logging Works
Magento 2 uses Monolog — the PHP logging standard — as its PSR-3 logger implementation. Monolog sends log messages through channels (categories) and handlers (destinations). The entire system is configured in app/etc/di.xml and overridden per environment in env.php.
The core channels you'll find in a standard Magento 2 installation:
-
system— General system messages (var/log/system.log) -
exception— Uncaught exceptions and fatal errors (var/log/exception.log) -
debug— Debug-level output, disabled by default in production (var/log/debug.log) -
db— Database queries and connection issues (var/log/db.log) -
payment— Payment gateway communication (var/log/payment.log) -
checkout— Checkout flow events (var/log/checkout.log) -
setup— Installation and upgrade logs (var/log/setup.log) -
crontab— Cron job execution logs (var/log/cron.log) -
main— The catch-all channel used by\Magento\Framework\Logger\Monolog
Each channel maps to a handler that writes to var/log/<channel>.log. The magic happens in how you configure these channels per environment.
The Performance Cost of Logging
Here's the uncomfortable truth: every log line is a synchronous disk write. On a busy store with debug logging enabled, a single page request can generate 50+ log entries. Multiply that by thousands of requests per minute, and you're looking at:
- Disk I/O contention — Log writes compete with MySQL, Redis, and Varnish for disk throughput
- Disk space exhaustion — A debug log can grow 500MB+ per hour under moderate traffic
-
PHP execution time — Each
->info()or->debug()call adds latency, even if you think nobody reads it - GC pressure — Large log files slow down log rotation and archival
I've seen stores where disabling debug logging alone reduced response time by 200ms. That's a bigger win than most caching optimizations.
Configuring Log Levels Per Environment
The single most important rule: production should never log at debug level. Magento 2's deploy modes handle this partially, but you should be explicit.
Production (env.php)
'log' => [
'Monolog\Logger' => [
'handlers' => [
'system' => [
'type' => 'stream',
'path' => '/var/log/magento/system.log',
'level' => 'warning'
],
'exception' => [
'type' => 'stream',
'path' => '/var/log/magento/exception.log',
'level' => 'error'
],
'debug' => [
'type' => 'stream',
'path' => '/var/log/magento/debug.log',
'level' => 'emergency' // effectively disabled
]
]
]
]
By setting debug level to emergency, only the most critical messages pass through. The warning level on the system channel captures warnings, errors, and criticals — everything you need to investigate incidents.
Staging
Staging can afford info level on the system channel and debug level enabled — you need to reproduce issues before they hit production.
Development
In development, crank everything to debug or even info:
'log' => [
'Monolog\Logger' => [
'handlers' => [
'system' => [
'type' => 'stream',
'path' => 'var/log/system.log',
'level' => 'debug'
],
'debug' => [
'type' => 'stream',
'path' => 'var/log/debug.log',
'level' => 'debug'
]
]
]
]
Log Rotation: Non-Negotiable
If you're not rotating logs, you're one bad weekend away from a full disk. Magento 2 doesn't handle rotation itself — use logrotate at the OS level.
Create /etc/logrotate.d/magento:
/var/www/magento/var/log/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
Key settings:
-
copytruncate— Copies the log file, then truncates the original. This avoids the need to signal Magento to reopen file handles, which can cause issues with long-running PHP processes. -
rotate 14— Keep two weeks of logs. Adjust based on your compliance requirements. -
compress— Gzip-compress old logs to save space. -
daily— Run every day. For high-traffic stores, considerhourlyon the debug log specifically.
Check your rotation is actually running:
logrotate -d /etc/logrotate.d/magento
Writing Logs in Custom Modules
When you build custom modules, never use ObjectManager to grab a logger. Inject the proper logger interface instead:
use Magento\Framework\Logger\Monolog;
class MyService
{
private Monolog $logger;
public function __construct(Monolog $logger)
{
$this->logger = $logger;
}
public function processOrder($order)
{
try {
// ... logic ...
} catch (\Exception $e) {
$this->logger->error(
'Order processing failed for order #{orderId}: {message}',
[
'orderId' => $order->getIncrementId(),
'message' => $e->getMessage(),
'exception' => $e
]
);
}
}
}
Use PSR-3 Placeholder Interpolation
Always use curly-brace placeholders instead of string concatenation:
// GOOD — placeholder interpolation
$this->logger->info('Synced {count} products from ERP', ['count' => $productCount]);
// BAD — string concatenation evaluated even if log level is filtered
$this->logger->info('Synced ' . $productCount . ' products from ERP');
// BAD — sprintf inside log call
$this->logger->info(sprintf('Synced %d products', $productCount));
Placeholders are only interpolated if the log message actually passes the level filter. String concatenation is always evaluated, even when the message gets discarded. On high-traffic stores, this difference matters.
Avoid Logging in Loops
// BAD — writes 100,000 log lines during a full catalog sync
foreach ($products as $product) {
$this->logger->info('Processing product SKU: ' . $product->getSku());
$this->syncProduct($product);
}
// GOOD — log a summary, use a timer, flag outliers
$startTime = microtime(true);
$processed = 0;
$errors = [];
foreach ($products as $product) {
try {
$this->syncProduct($product);
$processed++;
} catch (\Exception $e) {
$errors[] = ['sku' => $product->getSku(), 'error' => $e->getMessage()];
}
}
$this->logger->info('Catalog sync completed: {processed} products in {seconds}s, {errors} errors', [
'processed' => $processed,
'seconds' => round(microtime(true) - $startTime, 2),
'errors' => count($errors)
]);
if (count($errors) > 0) {
$this->logger->error('Catalog sync had {count} errors. First few: {details}', [
'count' => count($errors),
'details' => json_encode(array_slice($errors, 0, 5))
]);
}
Magento 2's Hidden Log Spam
Several core subsystems log aggressively in debug mode. The worst offenders:
-
\Magento\Framework\DB\Adapter\Pdo\Mysql— Logs every query in debug mode. On a catalog page with 200 queries, this means 200 log lines per request. -
\Magento\Framework\Event\Manager— Logs every dispatched event. A single request can dispatch 50+ events. -
\Magento\Framework\App\Http— Logs plugin execution order and layout generation. -
\Magento\Framework\View\Layout— Logs every block render.
If you enable debug logging, you immediately get a firehose. The fix: create a custom handler that filters debug messages for specific channels only.
// app/code/Vendor/Logging/Model/SelectiveDebugHandler.php
namespace Vendor\Logging\Model;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class SelectiveDebugHandler extends StreamHandler
{
private array $allowedChannels = ['payment', 'checkout', 'custom_api'];
public function isHandling(array $record): bool
{
if ($record['level'] >= Logger::INFO) {
return parent::isHandling($record);
}
// Only allow debug-level for specific channels
return in_array($record['channel'] ?? '', $this->allowedChannels)
&& parent::isHandling($record);
}
}
Register this handler in di.xml and you get full debug logging for payment flows without the database query firehose.
Structured Logging for Serious Debugging
Plain text logs are fine for "what happened at 3 AM?" investigations, but for any store doing serious volume, structured logging (JSON format) is a game-changer. It lets you pipe logs into Elasticsearch, Loki, or Datadog and run actual queries.
Magento 2 supports custom Monolog formatters. Register a JsonFormatter handler:
// app/code/Vendor/Logging/etc/di.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Monolog\Logger">
<arguments>
<argument name="handlers" xsi:type="array">
<item name="json" xsi:type="object">
Vendor\Logging\Model\JsonLogHandler
</item>
</argument>
</argument>
</type>
</type>
// app/code/Vendor/Logging/Model/JsonLogHandler.php
namespace Vendor\Logging\Model;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
class JsonLogHandler extends StreamHandler
{
public function __construct()
{
parent::__construct('var/log/structured.log', Logger::WARNING);
$this->setFormatter(new JsonFormatter());
}
}
Now every log entry above the warning threshold is written as structured JSON:
{"message":"Order #100234 processing failed: Payment gateway timeout","level":400,"level_name":"ERROR","channel":"main","datetime":"2026-08-02T09:15:23.123+0000","context":{"orderId":"100234","reason":"timeout"}}
Centralized log viewers can then filter, search, and alert on structured fields.
Centralized Logging: The Next Step
For multi-server deployments, writing logs to local files is insufficient. You need centralized logging — shipping logs to a dedicated aggregator.
Option 1: Filebeat + Elasticsearch + Kibana (ELK)
Install Filebeat on each Magento server, tail the log files, and ship to Elasticsearch. Kibana gives you a dashboard to search and visualize errors across all servers.
# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
paths:
- /var/www/magento/var/log/*.log
fields:
environment: production
application: magento
fields_under_root: true
output.elasticsearch:
hosts: ["logstash.internal:9200"]
Option 2: Vector or Fluent Bit
Lighter than Filebeat and supports filtering, enrichment, and routing at the edge. For Magento stores running in containers, Fluent Bit as a DaemonSet is the standard choice.
Option 3: SaaS (Datadog, New Relic, Sentry)
If you're already using a monitoring platform, ship logs there directly. Datadog has a Monolog handler package that integrates cleanly:
composer require datadog/dd-trace
Configure the Monolog handler to send logs to Datadog's intake endpoint, and you get automatic correlation between APM traces, infrastructure metrics, and log entries.
Debugging Tips: Finding the Needle
When production is broken and you need to find the relevant log entry fast:
1. Filter by Time Window
# All errors in a 10-minute window
awk '/2026-08-02 11:0[0-9]/' var/log/system.log | grep -i error
# Errors containing a specific keyword
grep "payment" var/log/exception.log | tail -20
2. Correlate Across Channels
An exception might be logged in exception.log while the root cause appears in db.log or system.log seconds earlier:
# Find the timestamp range of an exception
head -1 var/log/exception.log
# Then search other logs in that window
grep "2026-08-02 11:03:" var/log/system.log var/log/db.log
3. Enable Temporary Targeted Debug Logging
Instead of enabling global debug mode, inject a targeted logger for just the subsystem you're investigating:
// In your di.xml (temporary)
<type name="Magento\Framework\DB\Adapter\Pdo\Mysql">
<arguments>
<argument name="logger" xsi:type="object">Vendor\Logging\Model\QueryDebugLogger</argument>
</argument>
</type>
Remove it after debugging — never commit temporary debug loggers.
A Practical Logging Checklist
Before going live with any Magento 2 store, run through this checklist:
-
Deploy mode is
production—bin/magento deploy:mode:set production -
Debug logging disabled — Check
env.phplevel andMAGE_DEBUG_SHOW_ARGS -
Logrotate configured and tested —
logrotate -d /etc/logrotate.d/magento -
Disk space monitoring — Monitor
/var/log/magento/disk usage - Custom module logging uses placeholders — No string concatenation or sprintf
- No debug logging inside loops — Summary > per-item
-
Exception log checked daily —
var/log/exception.logshould be empty or near-empty on a healthy store - Log directory permissions correct — Web server user needs write access, nobody else
- Payment and checkout channels writable — These are your most critical logs
- Centralized logging setup — For multi-server, ship logs to ELK/Datadog/Sentry
Conclusion
Logging in Magento 2 is deceptively simple — call a logger, write to a file, done. But at scale, the difference between good and bad logging habits is the difference between a healthy store and one that crumbles under load. By using the right log levels per environment, rotating aggressively, writing structured logs for searchability, and avoiding the most common performance traps, you build a foundation that keeps your store observable without sacrificing speed.
The best logs are the ones you never have to read — because the system is well-behaved. But when things go wrong, you'll be glad you set this up properly.
Top comments (0)