A proper WordPress debugging workflow goes beyond enabling WP_DEBUG — it involves structured logging, query monitoring, tracking slow hooks, and making debug output accessible only to developers without exposing it to users. Getting this right speeds up plugin development and catches bugs before they reach production.
Problem: WordPress debug logging requires manually toggling WP_DEBUG and WP_DEBUG_LOG in wp-config.php — there is no standard developer tooling workflow for capturing, filtering, and rotating WordPress-specific debug output in a local environment.
Solution: Define WP_DEBUG, WP_DEBUG_LOG, and WP_DEBUG_DISPLAY based on an environment variable (getenv('WP_ENV')) so production never accidentally enables debug output. Use Query Monitor for browser-side debugging, tail -f wp-content/debug.log | grep ERROR for terminal monitoring, and configure log rotation with logrotate to prevent unbounded growth.
The examples below configure wp-config.php for safe debug logging, add a structured logging helper, integrate with Query Monitor, and show how to debug a slow hook or action using timing.
queries (Query Monitor uses this)
// Set custom log file path (optional — default is wp-content/debug.log)
// ini_set( 'error_log', '/var/log/wp-debug.log' );
// Suppress specific notices if a library generates noise (use sparingly)
// error_reporting( E_ALL & ~E_DEPRECATED & ~E_NOTICE );
Structured logging helper and hook timing:
200, 'body' => $data ] );
*/
function myplugin_log( string $event, mixed $context = null, string $level = 'debug' ): void {
$entry = [
'time' => date( 'Y-m-d H:i:s' ),
'level' => $level,
'event' => $event,
'context' => $context,
'request' => $_SERVER['REQUEST_URI'] ?? 'cli',
];
error_log( wp_json_encode( $entry ) );
}
// Hook timing — find slow hooks in your plugin
function myplugin_time_hook( string $hook, callable $callback, int $priority = 10 ): void {
add_action( $hook, function() use ( $hook, $callback ) {
$start = microtime( true );
call_user_func_array( $callback, func_get_args() );
$elapsed = round( ( microtime( true ) - $start ) * 1000, 2 );
if ( $elapsed > 10 ) { // only log hooks taking > 10ms
myplugin_log( 'slow_hook', [ 'hook' => $hook, 'ms' => $elapsed ], 'warning' );
}
}, $priority );
}
// Debug backtrace to find who called a function
function myplugin_trace( string $label = '' ): void {
$trace = wp_debug_backtrace_summary( null, 2, false );
myplugin_log( $label ?: 'trace', $trace );
}
// Dump $wpdb->queries after page load (requires SAVEQUERIES = true)
add_action( 'shutdown', function() {
global $wpdb;
if ( empty( $wpdb->queries ) ) return;
$slow = array_filter( $wpdb->queries, fn( $q ) => $q[1] > 0.05 ); // > 50ms
if ( $slow ) {
myplugin_log( 'slow_queries', array_map(
fn( $q ) => [ 'sql' => substr( $q[0], 0, 200 ), 'time_ms' => round( $q[1] * 1000, 2 ) ],
$slow
), 'warning' );
}
} );
$data ] );
do_action( 'qm/warning', 'Slow operation detected: {ms}ms', [ 'ms' => $elapsed ] );
do_action( 'qm/error', 'API call failed: {code}', [ 'code' => $status ] );
// Add a custom QM data collector (advanced — shows your plugin data in a QM panel)
add_filter( 'qm/collectors', function( array $collectors, QM_Plugin $qm ): array {
require_once plugin_dir_path( __FILE__ ) . 'class-qm-collector-myplugin.php';
$collectors['myplugin'] = new QM_Collector_MyPlugin();
return $collectors;
}, 10, 2 );
// ── XDEBUG STEP DEBUGGING ──
// Install: pecl install xdebug
// /etc/php/8.3/fpm/conf.d/20-xdebug.ini:
// zend_extension=xdebug.so
// xdebug.mode=debug,develop
// xdebug.start_with_request=trigger <- only activate when XDEBUG_SESSION cookie set
// xdebug.client_host=127.0.0.1
// xdebug.client_port=9003
// Use the VS Code PHP Debug extension or PhpStorm to connect.
// Set a breakpoint, open the site with ?XDEBUG_SESSION_START=PHPSTORM in the URL.
NOTE: Never commit wp-config.php with WP_DEBUG_LOG = true to a shared repo — the debug log can contain passwords, API keys, and user data in stack traces. Use environment-specific configs by including a wp-config-local.php file and adding it to .gitignore. The Query Monitor plugin is the single best investment for WordPress development productivity — it makes invisible problems visible without any code changes.