Xdebug and Logs

Attach Xdebug on port 9003, turn on the printed error page, and read var/log and var/report instead of guessing.

Syntaxxdebug.mode=debug xdebug.client_host=host.docker.internal xdebug.client_port=9003

Magento generates code, intercepts method calls and merges XML from every module, so reading an error is usually cheaper than reasoning about one. Two things remove the guesswork: a debugger that stops inside a real request, and knowing which file the error was already written to.

Xdebug

Your IDE listens and Xdebug connects out to it. The port is xdebug.client_port, 9003 since Xdebug 3; Xdebug 2 used 9000, which is why old guides mislead. From a container, xdebug.client_host must point back at the host — host.docker.internal on Docker Desktop. Set xdebug.start_with_request=trigger so only requests carrying XDEBUG_TRIGGER are debugged, and the rest of the site stays fast.

Logs and reports

Three files in var/log: system.log for warnings and notices, exception.log for exceptions and traces, debug.log for debug-level messages. In Magento 2.4, debug.log is written in developer and default mode and is off in production mode, unless dev/debug/debug_logging in app/etc/env.php says otherwise. An uncaught exception outside developer mode also writes var/report/<id> and shows the visitor only that id; the file holds the message and the trace.

The printed error page

Copy pub/errors/local.xml.sample to pub/errors/local.xml. With <action>print</action> the exception prints instead of the generic page. No cache command is needed, because pub/errors is read on the request that failed.

What trips people up

Loading Xdebug in debug mode for every process makes setup:di:compile and setup:static-content:deploy several times slower.

Example

Example · ini
; docker/php/conf.d/xdebug.ini - mounted at /usr/local/etc/php/conf.d/xdebug.ini
zend_extension=xdebug

xdebug.mode=debug
xdebug.start_with_request=trigger
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=PHPSTORM
xdebug.log=/tmp/xdebug.log

; start_with_request=trigger means a request is debugged only when it carries
; XDEBUG_TRIGGER as a GET or POST parameter, a cookie, or an environment variable.
;
; Longhand: the settings above, loaded for every process in this container.
; Shorthand for a single command, same settings, nothing else affected:
;
;   php -dxdebug.mode=debug -dxdebug.start_with_request=yes \
;       bin/magento setup:upgrade
;
; Both set the same two options. The -d form is the one to use for CLI work,
; because it leaves di:compile and static-content:deploy running at full speed.

When to use it

  • Stop inside a controller to see the request Magento actually received.
  • Turn a 500 page with a report id into the exception message and trace behind it.
  • Step through a bin/magento command without slowing down every other PHP process.
  • Find out which module logged the warning a client sent you a screenshot of.

More examples

Reading what Magento already logged

Start here before attaching a debugger, because a 500 has usually already written its own explanation. The report id is the file name, so there is nothing to search for. Reach for setup:config:set when you need debug.log on a production-mode server for a short investigation, and turn it off afterwards; it writes a deployment configuration value, not a store setting.

Example · bash
# all three log files at once, from the Magento root
tail -f var/log/system.log var/log/exception.log var/log/debug.log

# the error page prints a report id; the report is the file with that name
ls -t var/report | head -5
cat var/report/44ffb1087a44e61b018b3cdee72284d017f22e52755c24e5c85cbac1647ae7a7

# turn debug logging on explicitly - this writes dev/debug/debug_logging
# into app/etc/env.php
bin/magento setup:config:set --enable-debug-logging=1

The developer error page

With this file in place, a failing request prints the exception and trace rather than the generic page with a report id. On the storefront that is only acceptable on a machine nobody else can reach, so keep local.xml out of the deployed artifact for anything public. dir_nesting_level above 0 spreads reports into subdirectories named after the first characters of the id, which you will want on a site generating thousands of them.

Example · xml
<?xml version="1.0"?>
<!-- pub/errors/local.xml - copied from pub/errors/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 trace in var/report; delete removes it -->
        <trash>leave</trash>
        <!-- 0 writes reports straight into var/report -->
        <dir_nesting_level>0</dir_nesting_level>
    </report>
</config>

Logging from your own code

Magento's logger is PSR-3, so the type to ask for is Psr\Log\LoggerInterface and the level you call decides the file. The second argument is a context array, which keeps the message greppable instead of interpolating values into it. The promoted and unpromoted constructors shown here are interchangeable; how Magento supplies that argument is the subject of the dependency injection stage.

Example · php
<?php
// app/code/Vendor/Diagnostics/Service/CheckoutAudit.php
declare(strict_types=1);

namespace Vendor\Diagnostics\Service;

use Psr\Log\LoggerInterface;

class CheckoutAudit
{
    private LoggerInterface $logger;

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

    // PHP 8.4 constructor property promotion is the same constructor signature,
    // so Magento resolves the dependency identically:
    //
    // public function __construct(private LoggerInterface $logger)
    // {
    // }

    public function recordAttempt(string $reference, string $method): void
    {
        $this->logger->debug('checkout attempt', ['reference' => $reference, 'method' => $method]);
        $this->logger->error('checkout declined', ['reference' => $reference]);
    }
}

// debug() lands in var/log/debug.log, error() in var/log/system.log.

Discussion

  • Be the first to comment on this lesson.