Debugging in Production

What you can safely switch on a live store, how to turn a report id into a stack trace, and how to reproduce a bug without developer mode.

Syntaxcp pub/errors/local.xml.sample pub/errors/local.xml

Do not switch modes

bin/magento deploy:mode:set developer on a live store clears the compiled dependency injection configuration and the deployed static files. Every following request then generates them on demand, and the store is effectively down for as long as that takes. Treat a mode switch as a planned outage, never as a debugging step.

What is safe

Reading is always safe: var/log/exception.log, var/log/system.log, the files in var/report/, and bin/magento dev:di:info, which only inspects configuration. Cleaning one cache type with cache:clean is low risk. Your own logging is safest of all, because it shipped with the module and needs no switch.

Printing the real exception

The error page shows an id because exception printing is off by default. Copy pub/errors/local.xml.sample to pub/errors/local.xml and set <action>print</action> and the next error prints its trace instead. There is nothing to flush; the error handler reads that file directly. It also shows the trace to every visitor, so remove the file when you are done.

Reproducing

Copy the database to a staging install, keep it in production mode, and import the live configuration with bin/magento app:config:dump on live and app:config:import on staging. Bugs that only appear in production mode are usually caching or compiled-code bugs, and a developer-mode copy hides exactly those.

Example

Example · xml
<!-- pub/errors/local.xml  (copied from local.xml.sample) -->
<config>
    <skin>default</skin>
    <report>
        <!-- print shows the exception on screen; email sends it instead. -->
        <action>print</action>
        <subject>Store Debug Information</subject>
        <email_address></email_address>
        <!-- leave keeps the report file; delete removes it after handling. -->
        <trash>leave</trash>
        <!-- 0 to 32. Subdirectory depth used when storing reports. -->
        <dir_nesting_level>0</dir_nesting_level>
    </report>
</config>

When to use it

  • A customer reports an error id and the site must stay up while you investigate.
  • An order fails at payment only on the live store and never on staging.
  • Deciding whether a production-only bug is caused by caching or by compiled code.
  • Adding enough logging to a module that the next incident does not need a live debugging session.

More examples

From a customer's error id to a stack trace

This is the whole read-only path, and it answers most production incidents without changing anything on the server. The report file holds the serialised exception, including the message and trace. Its modification time gives you the moment the error happened, which lets you find the matching request in the web server access log and so the URL, the user agent and sometimes the customer. Do this before you consider enabling anything.

Example · bash
# The customer's screenshot: "Error log record number:
# 44ffb1087a44e61b018b3cdee72284d017f22e52755c24e5c85cbac1647ae7a7"
# The id is a 64 character hash of the message and trace, not a timestamp.
REPORT=44ffb1087a44e61b018b3cdee72284d017f22e52755c24e5c85cbac1647ae7a7

# dir_nesting_level may have put it in subdirectories.
find var/report -name "$REPORT"

php -r '$f = $argv[1]; print_r(json_decode(file_get_contents($f), true));' \
    "$(find var/report -name "$REPORT" | head -n 1)"

# The file's own timestamp is the moment of the error, and the id itself is
# logged as the report_id context of the CRITICAL record in system.log.
date -r "$(find var/report -name "$REPORT" | head -n 1)"
grep -n "$REPORT" var/log/system.log | tail -n 20

A narrow window, not a mode switch

maintenance:enable --ip exempts your own address, so the store shows the maintenance page to customers while you still reach it. That turns printing stack traces from a public disclosure into a controlled test. Everything in the last block is safe at any time: dev:di:info only reads merged configuration, and cache:status only reports. Note the webapi_rest area in that command; a bug that appears through the REST API and not on the storefront is often an area-specific di.xml, and the default global area would not show it.

Example · bash
bin/magento deploy:mode:show
# Current application mode: production.

# Keep real traffic out while you reproduce.
bin/magento maintenance:enable --ip=203.0.113.10

cp pub/errors/local.xml.sample pub/errors/local.xml
# edit <action> to print, then reproduce the error once

rm pub/errors/local.xml
bin/magento maintenance:disable

# Read-only inspection needs no window at all.
bin/magento dev:di:info 'Magento\Quote\Api\CartManagementInterface' webapi_rest
bin/magento cache:status

Ship the logging you will need next time

debug.log is not written in production mode, so calls to $logger->debug() leave no trace on a live store. The fix is to decide in advance what production should always record, log that at error or info level, and put the verbose detail behind your own system.xml flag that an admin can switch per store. Your module then arrives with its own switch, and the next incident needs a configuration change and a cache:clean config rather than a mode switch. Reach for this pattern in any integration that talks to a third party, where the failure you will have to explain happened hours ago.

Example · php
<?php
// app/code/Vendor/Pricing/Model/FeedGuard.php

declare(strict_types=1);

namespace Vendor\Pricing\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Psr\Log\LoggerInterface;

class FeedGuard
{
    private const XML_PATH_VERBOSE = 'vendor_pricing/debug/verbose';

    private LoggerInterface $logger;

    private ScopeConfigInterface $scopeConfig;

    public function __construct(
        LoggerInterface $logger,
        ScopeConfigInterface $scopeConfig
    ) {
        $this->logger = $logger;
        $this->scopeConfig = $scopeConfig;
    }

    public function record(string $sku, string $stage): void
    {
        // Always record failures, at a level production keeps.
        if ($stage === 'failed') {
            $this->logger->error('Feed stage failed', ['sku' => $sku]);

            return;
        }

        // Record the chatter only when an admin has asked for it.
        if ($this->scopeConfig->isSetFlag(self::XML_PATH_VERBOSE)) {
            $this->logger->info('Feed stage', ['sku' => $sku, 'stage' => $stage]);
        }
    }
}

Discussion

  • Be the first to comment on this lesson.