How to Log MySQL Errors in WordPress

MySQL query errors can be difficult to diagnose and correct.

For performance reasons, most MySQL installations fail to retain an error log for posterity; instead, if an error occurs, the reason is passed back to the application during runtime. If that error isn’t captured then and there, it is lost forever.

Development Environments

WordPress contains a special development mode that can be enabled by adding WP_DEBUG, WP_DEBUG_LOG, and/or WP_DEBUG_DISPLAY constants to your configuration. When enabled, PHP errors/warnings/notices and MySQL errors will be logged to wp-content/debug.log and/or printed to the screen.

WordPress’s $wpdb object also provides some debugging functions in case you wanted to debug MySQL issues independently of PHP ones:

  1. $wpdb->show_errors(): this causes MySQL error information to be printed to the screen as it happens. You would call this function before the queries you are looking to debug.
  2. $wpdb->hide_errors(): this reverses the behavior of show_errors() and returns WordPress to its default configuration. You can call this function anytime after executing the questionable queries.
  3. $wpdb->print_error(): this prints the error, if any, from the most recent query.

Production Environments

These tools are probably all you need when developing a new theme or plugin, but you shouldn’t use these under production environments. For one thing, printing random, technical-looking strings in the middle of a document will break the layout and confuse your users. It can also provide interesting information to any bad actors who might be poking around your site. But even if you’re just logging the information, WP_DEBUG_LOG isn’t a great idea: it degrades your site performance and, under most server configurations, exposes way too much information to anyone who knows where to look.

Of course, by the time a site is live, you should have thoroughly debugged everything, so there’s no need to log query failures, right? Well… maybe.

There are a lot of ways to mess up a MySQL query. Chances are, no matter how many times you tested your code during development, you’ll have missed some highly obscure edge case. Even if you didn’t, and everything was coded perfectly, sometimes an update to the WordPress core can subtly change the way a query is structured.

Such a change occurred recently with the release of WordPress 4.4. In prior versions, Null values passed via $wpdb->insert() or the like were typecast according to the type specified. %s would convert a Null value to '', %d to 0, etc. Now, however, Null values are passed as-is to MySQL. For columns with NOT NULL attributes, this can create problems where previously none existed.

So what to do?

Though we were unable to find any documentation, investigation into the WordPress source code revealed that MySQL errors from the current page request are collected in an obscure global variable during runtime, $EZSQL_ERROR.

We can access this variable in a custom PHP function that we then trigger through one of WordPress’ action hooks. Since we want to capture all errors for a given page request, the shutdown action is the best candidate as it triggers just before PHP terminates.

The following example code block does just that. At the end of every WordPress page execution, the function looks to see if any MySQL errors were encountered. If there were any, it combines some basic runtime information (date, page, etc.) with the error details and emails it to the site administrator.

//-------------------------------------------------
// Database logging - query errors
//
// email database query errors to the contact
// specified
//
// @param n/a
// @return n/a

function db_debug_log(){

	//WP already stores query errors in this obscure
	//global variable, so we can see what we've ended
	//up with just before shutdown
	global $EZSQL_ERROR;

	try {
		//proceed if there were MySQL errors during runtime
		if(is_array($EZSQL_ERROR) && count($EZSQL_ERROR)) {
			//build a log entry
			$xout = array();

			//let's start with some environmental information
			$xout[] = "DATE: " . current_time('r');
			$xout[] = "SITE: " . site_url();
			$xout[] = "IP: " . $_SERVER['REMOTE_ADDR'];
			$xout[] = "UA: " . $_SERVER['HTTP_USER_AGENT'];
			$xout[] = "SCRIPT: " . $_SERVER['SCRIPT_NAME'];
			$xout[] = "REQUEST: " . $_SERVER['REQUEST_URI'];
			$xout[] = "\n\n\n\n";

			//and lastly, add the error messages with some line separations for readability
			foreach($EZSQL_ERROR AS $e) {
				$xout[] = str_repeat('-', 50) . "\n" . implode("\n", $e) . "\n" . str_repeat('-', 50);
				$xout[] = "\n\n\n\n";
			}

			//email it!
			//if a plugin overrides the content-type header for outbound emails, change the message body
			//below to nl2br(esc_html(implode("\n", $xout)))
			wp_mail(get_bloginfo('admin_email'), '[' . get_bloginfo('name') . '] DB Error', implode(“\n”, $xout));
		}
	} catch(Exception $e){ }

	return;
}
add_action('shutdown', 'db_debug_log');

If email isn’t desirable, whether for reasons of security or practicality, the general idea could be altered to push data via error_log() or write the contents to any arbitrary log file (preferably in a non-web-accessible location).

These techniques can help make hunting down elusive MySQL errors easier. With a proper record in place, developers can see what went wrong and where, and find a solution more quickly.

Posted By
Josh Stoik

Share Post