Drupalwoo ☉

ging Drupal effectively with Xdebug and site logs

Drupal projects have a way of hiding their problems behind layers of hooks, services, and plugins. A permissions error only surfaces for editorial users, a JSON:API endpoint chokes under load, or a contrib module breaks after a minor release. Two complementary instruments help track those failures: Xdebug for line-by-line interactive inspection, and the Drupal logging subsystem for the persistent paper trail left by every event the framework emits.

The combination of an interactive debugger and a robust event log is rarely taught as a single discipline. Most tutorials cover Xdebug as if it lived in isolation, or treat Watchdog as a glorified error_log. In real Drupal work the two are tightly coupled. The breakpoint catches the failure as it happens, the log explains the broader pattern that led to it, and together they shorten the distance between symptom and cause.

For developers in Sydney, Melbourne, or Brisbane running client sites through local providers such as Anchor Hosting or Servers Australia, the practical considerations stretch beyond code. The Privacy Act 1988 and the Australian Privacy Principles influence how long log data can be retained, particularly when entries contain user emails or IP addresses. Tuning Xdebug also affects latency for users served from the ap-southeast-2 AWS zone, where every millisecond matters during an interactive session. The rest of this article covers the technical setup and the workflow patterns that keep both sides working in harmony.

Installing and configuring Xdebug with Drupal

Xdebug installs as a PHP extension, and the configuration differs sharply between Xdebug 2 and Xdebug 3. Drupal 10 sites on PHP 8.1 or higher should default to Xdebug 3, which simplifies the directive set into modes such as develop, debug, coverage, and profile. Most local development environments such as DDEV, Lando, or Docksal ship with sensible defaults, but on a bare LAMP stack in a Brisbane data centre the manual setup is still common.

The minimum php.ini snippet for interactive debugging looks like this:

zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=trigger
xdebug.client_host=127.0.0.1
xdebug.client_port=9003

start_with_request=trigger is the right choice when you want debugging on demand. It tells Xdebug to remain dormant until a special cookie, query string, or IDE trigger activates it, which keeps page load times reasonable when you are not actively investigating. The trigger_value directive lets you set a secret token so random visitors cannot silently start a debug session against your staging environment.

Validation matters before chasing a missing debugger connection. Run php -m | grep Xdebug from the command line, and check the output of a phpinfo() page rendered through Drupal. The Xdebug section should report the mode and client port. If the section is missing entirely, the extension did not load, and the most common cause on a fresh Ubuntu 22.04 box is a missing php-xdebug package or a mismatch between the CLI and FPM SAPIs.

Quick reference for common configurations

A few configuration patterns cover most debugging sessions:

Connecting Xdebug to PhpStorm and VS Code

Once Xdebug is active, the next step is pairing it with an IDE that speaks the DBGp protocol. PhpStorm remains the most popular choice among Australian agency teams because of its deep Symfony integration, which extends naturally to Drupal services and plugins. VS Code, with the PHP Debug extension by Felix Becker, is the lighter alternative favoured by solo developers in Melbourne's co-working spaces.

In PhpStorm, the workflow begins with Run > Edit Configurations > PHP Remote Debug. Add a server entry that maps the local project root to the absolute path inside your DDEV container or VM. Path mappings are the single most common source of "the breakpoint is grey, not red" frustration, so they deserve a moment of attention. If your document root on the server is /var/www/html/web but your project root is ~/projects/client-x, the mapping should connect /var/www/html to the equivalent folder on disk.

VS Code requires a launch.json entry similar to:

{
  "name": "Listen for Xdebug",
  "type": "php",
  "request": "launch",
  "port": 9003,
  "pathMappings": {
    "/var/www/html": "${workspaceFolder}"
  }
}

Both IDEs support the cookie trigger through browser extensions such as Xdebug Helper for Chrome or the PhpStorm helper. Once the cookie is set, refreshing a page attaches the debugger, and any breakpoint in a Drupal hook, controller, or service method halts execution and opens the variable inspection panel.

Mastering the Drupal logging system

Drupal's logging layer, historically called Watchdog and now accessed through \Drupal\Core\Logger\LoggerChannelFactoryInterface, records events at eight severity levels ranging from Emergency to Debug. When the core Database logging module is enabled, every entry lands in the watchdog table and is exposed at /admin/reports/dblog. Production sites typically route the same channel to syslog, Monolog, or a SaaS aggregator such as Sentry or New Relic, and that is where Australian compliance considerations begin to bite.

Custom modules can push entries through a logger service:

$logger = \Drupal::logger('mymodule');
$logger->notice('User @uid attempted restricted action @action', [
  '@uid' => $account->id(),
  '@action' => $action,
]);

This pattern matters because Drupal sanitises the placeholders before writing them, which prevents log injection from user-supplied input. Many developers fall into the habit of passing raw strings, only to discover that a newline from a form field has split a single log entry into several. The placeholder syntax is not decorative, it is a security control.

Filtering is where the Watchdog view becomes useful for day-to-day work. The /admin/reports/dblog screen accepts severity, type, and user filters, and the underlying query supports date ranges. For deeper investigation, exporting the table to CSV or running direct queries through drush sqlq "SELECT * FROM watchdog WHERE type = 'php' ORDER BY wid DESC LIMIT 50" is often faster than clicking through the UI.

Logging levels worth memorising

When reading Watchdog output, the severity column tells you what kind of problem you are looking at:

Profiling performance with Xdebug

Step debugging answers "what is the code doing", but performance debugging needs a different mode. Xdebug can record every function call, argument, and return value into a Cachegrind-compatible file, which is then visualised by tools such as Webgrind or KCachegrind. For a slow checkout flow on a Drupal Commerce site, this view quickly surfaces the recursive helper or under-cached view that nobody noticed during feature work.

To enable profiling, set xdebug.mode=profile and xdebug.output_dir=/tmp/xdebug-profiles in php.ini, then add XDEBUG_PROFILE=1 to the request. The resulting cachegrind.out.* file opens in Webgrind, a PHP application that runs locally and renders a call graph. Functions are sorted by inclusive and exclusive time, and clicking a node reveals the calling chain. For a developer in Adelaide working on a regional government tender, this kind of evidence is often the difference between "the site feels slow" and a documented, fixable bottleneck.

The trap with profiling is the overhead. Profile only the requests you actually want to investigate, and never leave the profiler active in production. Most teams use environment-specific settings.php includes to switch Xdebug modes between local and live servers, and a simple if (extension_loaded('xdebug')) block keeps the configuration sane when the extension is absent.

Bringing Xdebug and Watchdog together

The two tools stop being separate once a team treats them as parts of one workflow. A useful pattern is to leave a permanent Watchdog hook at the boundary of any module you maintain, then attach an Xdebug session only when the logged event is unexpected. This keeps the everyday code path fast and reserves the heavy debugger for genuine mysteries, which matters for users on slower regional connections in places like Hobart or Cairns.

The same workflow has to survive a move off the laptop. Once the site runs on a staging environment behind Cloudflare or a corporate firewall, the direct DBGp connection breaks, and the practical fix is an SSH tunnel such as ssh -R 9003:localhost:9003 staging.example.com. Pair that with a Watchdog exporter pointed at a Sydney-based aggregator, and the development loop stays tight even for distributed teams across Perth and Canberra.

Studios that prefer a fully Australian route can use ISPs such as Aussie Broadband or Telstra Business to obtain a static IP, which makes it easier to allow-list incoming connections from their own office without exposing port 9003 to the public internet.

Compliance shapes the storage side. When Watchdog entries contain personal information covered by the Australian Privacy Principles, the rotation policy should match the retention needs of the project. Many agencies keep diagnostic logs for 30 days, redact user IDs, and ship the rest to long-term storage. Xdebug traces, by contrast, should never leave the developer's machine. The table below summarises the differences at a glance.

Aspect Xdebug Drupal Watchdog
Primary use Interactive step debugging and profiling Persistent event recording across requests
Storage In-memory during the request Database table, syslog, or external aggregator
Persistence None after the request ends Retained according to the configured rotation policy
Output IDE panels showing variables and call stack Structured rows with timestamp, severity, type, and message
Best suited for Tracing a specific code path that misbehaves Spotting patterns across many requests or users

Combining both tools turns debugging from guesswork into a measurable workflow. Xdebug finds the immediate cause, Watchdog confirms it is not an isolated incident, and the combination respects the retention and privacy rules that apply to sites operated under Australian law.