WordPress Log Analysis Deep Dive – Complete Guide to Debug, Error & Access Logs

Table of Contents

Master WordPress logging systems: debug logs, error logs, access logs, and security logs. Learn professional log analysis techniques for troubleshooting, performance optimization, and security monitoring.

5+
Types of WordPress Logs
85%
Issues Found in Logs
24/7
Continuous Monitoring
10min
Average Debug Time
🛡️ ESSENTIAL GUIDE: WordPress Log Analysis Best Practices
WordPress logging is critical for debugging, security monitoring, and performance optimization. Proper log analysis can reduce troubleshooting time by 70% and catch security incidents before they escalate. This guide provides production-ready code examples following WordPress Coding Standards (WPCS) and security best practices.

01 WordPress Logging Fundamentals

WordPress supports multiple logging mechanisms, each serving different purposes. Understanding when and how to use each type is essential for effective debugging and monitoring.

Types of WordPress Logs

Log Type Purpose Location When to Use
Debug Log WordPress application errors and debug messages wp-content/debug.log Development, troubleshooting
PHP Error Log PHP runtime errors and warnings Varies by server config Critical errors, fatal issues
Access Log HTTP requests and responses /var/log/apache2/ or /var/log/nginx/ Traffic analysis, security monitoring
Security Log Login attempts, failed auth, suspicious activity Custom implementation Security auditing, intrusion detection
Database Log SQL queries and performance Custom implementation Performance tuning, query optimization

Log Levels & Severity

⚠️ EMERGENCY

System unusable, requires immediate action. Examples: database connection failure, core file corruption.

🔴 CRITICAL

Critical conditions requiring immediate attention. Examples: authentication bypass, SQL injection attempt.

🔶 ERROR

Runtime errors that don’t require immediate action but should be monitored and logged.

⚡ WARNING

Exceptional occurrences that are not errors. Examples: deprecated functions, high memory usage.

ℹ️ INFO

Interesting events for tracking application flow. Examples: user login, cache clearing.

🔍 DEBUG

Detailed information for debugging purposes. Should only be enabled in development.

02 Debug Log Configuration & Analysis

The WordPress debug log is your primary tool for troubleshooting plugin conflicts, theme issues, and code errors. Proper configuration is critical for effective debugging.

Enabling Debug Logging

Configure debugging in your wp-config.php file. Never enable debug mode on production sites without proper access restrictions.

PHP
<?php
/**
 * WordPress Debug Configuration
 *
 * Enable debug logging with security considerations.
 * Place this before the "That's all, stop editing!" line in wp-config.php
 *
 * @package WordPress
 * @since 1.0.0
 */

// Enable debug mode.
define( 'WP_DEBUG', true );

// Log errors to file instead of displaying them.
define( 'WP_DEBUG_LOG', true );

// Disable error display on frontend.
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

// Enable script and style debugging (unminified versions).
define( 'SCRIPT_DEBUG', true );

// Enable the query monitor.
define( 'SAVEQUERIES', true );

/**
 * Security: Restrict access to debug.log file.
 *
 * Add to .htaccess (Apache) or nginx config.
 */
// Apache .htaccess:
// <Files "debug.log">
//     Order allow,deny
//     Deny from all
// </Files>

/**
 * Production-safe debug logging.
 *
 * Only enable debug for specific IP addresses.
 */
$allowed_debug_ips = array(
    '123.456.789.000', // Your IP address.
    '::1',             // Localhost IPv6.
    '127.0.0.1',       // Localhost IPv4.
);

if ( in_array( $_SERVER['REMOTE_ADDR'] ?? '', $allowed_debug_ips, true ) ) {
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );
    define( 'WP_DEBUG_DISPLAY', true );
} else {
    define( 'WP_DEBUG', false );
    define( 'WP_DEBUG_LOG', false );
    define( 'WP_DEBUG_DISPLAY', false );
}
🔴 CRITICAL SECURITY WARNING
Never enable WP_DEBUG_DISPLAY on production sites. Displaying errors exposes sensitive information like file paths, database credentials, and server configuration to potential attackers. Always log to file and restrict access to the debug log.

Custom Debug Logger Implementation

Create a custom logger class for more control over what and how you log.

PHP
<?php
/**
 * Custom WordPress Debug Logger
 *
 * Provides enhanced logging with severity levels, context, and formatting.
 *
 * @package    WordPress_Logging
 * @subpackage Debug
 * @since      1.0.0
 */

if ( ! class_exists( 'WP_Custom_Logger' ) ) {
    /**
     * Custom WordPress Logger Class
     */
    class WP_Custom_Logger {
        /**
         * Log severity levels.
         *
         * @var array
         */
        const EMERGENCY = 'EMERGENCY';
        const CRITICAL  = 'CRITICAL';
        const ERROR     = 'ERROR';
        const WARNING   = 'WARNING';
        const INFO      = 'INFO';
        const DEBUG     = 'DEBUG';

        /**
         * Log file path.
         *
         * @var string
         */
        private $log_file;

        /**
         * Minimum log level to record.
         *
         * @var string
         */
        private $min_level;

        /**
         * Constructor.
         *
         * @param string $log_file  Path to log file.
         * @param string $min_level Minimum severity level to log.
         */
        public function __construct( $log_file = '', $min_level = self::DEBUG ) {
            $this->log_file = $log_file ? $log_file : WP_CONTENT_DIR . '/custom-debug.log';
            $this->min_level = $min_level;

            // Ensure log directory exists and is writable.
            $this->ensure_log_directory();
        }

        /**
         * Ensure log directory exists with proper permissions.
         *
         * @return bool True on success, false on failure.
         */
        private function ensure_log_directory() {
            $log_dir = dirname( $this->log_file );

            if ( ! file_exists( $log_dir ) ) {
                if ( ! \wp_mkdir_p( $log_dir ) ) {
                    return false;
                }
            }

            // Set secure permissions (owner read/write only).
            if ( file_exists( $log_dir ) ) {
                chmod( $log_dir, 0755 );
            }

            return true;
        }

        /**
         * Log a message with specified severity.
         *
         * @param string $level   Severity level.
         * @param string $message Log message.
         * @param array  $context Additional context data.
         *
         * @return bool True on success, false on failure.
         */
        public function log( $level, $message, $context = array() ) {
            // Check if level should be logged.
            if ( ! $this->should_log( $level ) ) {
                return false;
            }

            // Format the log entry.
            $formatted_message = $this->format_message( $level, $message, $context );

            // Write to log file with locking to prevent race conditions.
            $result = file_put_contents(
                $this->log_file,
                $formatted_message . PHP_EOL,
                FILE_APPEND | LOCK_EX
            );

            // Rotate log if it exceeds size limit.
            $this->rotate_log_if_needed();

            return false !== $result;
        }

        /**
         * Check if a level should be logged.
         *
         * @param string $level Severity level to check.
         *
         * @return bool True if should log, false otherwise.
         */
        private function should_log( $level ) {
            $levels = array(
                self::EMERGENCY => 0,
                self::CRITICAL  => 1,
                self::ERROR     => 2,
                self::WARNING   => 3,
                self::INFO      => 4,
                self::DEBUG     => 5,
            );

            $level_priority = $levels[ $level ] ?? 5;
            $min_priority   = $levels[ $this->min_level ] ?? 5;

            return $level_priority <= $min_priority;
        }

        /**
         * Format log message with timestamp and context.
         *
         * @param string $level   Severity level.
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return string Formatted message.
         */
        private function format_message( $level, $message, $context ) {
            $timestamp = current_time( 'Y-m-d H:i:s' );
            
            // Get backtrace for file and line information.
            $backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 3 );
            $caller    = $backtrace[2] ?? array();
            
            $file = isset( $caller['file'] ) ? basename( $caller['file'] ) : 'unknown';
            $line = $caller['line'] ?? '0';

            // Start with base format.
            $formatted = sprintf(
                '[%s] %s: %s (in %s:%s)',
                $timestamp,
                $level,
                $message,
                $file,
                $line
            );

            // Add context if provided.
            if ( ! empty( $context ) ) {
                $formatted .= ' | Context: ' . \wp_json_encode( $context );
            }

            return $formatted;
        }

        /**
         * Rotate log file if it exceeds size limit.
         *
         * @param int $max_size Maximum file size in bytes (default 10MB).
         *
         * @return void
         */
        private function rotate_log_if_needed( $max_size = 10485760 ) {
            if ( ! file_exists( $this->log_file ) ) {
                return;
            }

            clearstatcache( true, $this->log_file );
            $file_size = filesize( $this->log_file );

            if ( $file_size > $max_size ) {
                $backup_file = $this->log_file . '.' . time() . '.bak';
                rename( $this->log_file, $backup_file );

                // Keep only last 5 backup files.
                $this->cleanup_old_backups();
            }
        }

        /**
         * Clean up old backup log files.
         *
         * @param int $keep_count Number of backups to keep.
         *
         * @return void
         */
        private function cleanup_old_backups( $keep_count = 5 ) {
            $log_dir = dirname( $this->log_file );
            $pattern = basename( $this->log_file ) . '.*.bak';
            
            $backups = glob( $log_dir . '/' . $pattern );
            
            if ( count( $backups ) > $keep_count ) {
                // Sort by modification time, oldest first.
                usort( $backups, function( $a, $b ) {
                    return filemtime( $a ) - filemtime( $b );
                });

                // Delete oldest files.
                $to_delete = array_slice( $backups, 0, count( $backups ) - $keep_count );
                
                foreach ( $to_delete as $file ) {
                    unlink( $file );
                }
            }
        }

        /**
         * Log emergency message.
         *
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return bool Success status.
         */
        public function emergency( $message, $context = array() ) {
            return $this->log( self::EMERGENCY, $message, $context );
        }

        /**
         * Log critical message.
         *
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return bool Success status.
         */
        public function critical( $message, $context = array() ) {
            return $this->log( self::CRITICAL, $message, $context );
        }

        /**
         * Log error message.
         *
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return bool Success status.
         */
        public function error( $message, $context = array() ) {
            return $this->log( self::ERROR, $message, $context );
        }

        /**
         * Log warning message.
         *
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return bool Success status.
         */
        public function warning( $message, $context = array() ) {
            return $this->log( self::WARNING, $message, $context );
        }

        /**
         * Log info message.
         *
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return bool Success status.
         */
        public function info( $message, $context = array() ) {
            return $this->log( self::INFO, $message, $context );
        }

        /**
         * Log debug message.
         *
         * @param string $message Log message.
         * @param array  $context Additional context.
         *
         * @return bool Success status.
         */
        public function debug( $message, $context = array() ) {
            return $this->log( self::DEBUG, $message, $context );
        }
    }
}

/**
 * Get global logger instance.
 *
 * @return WP_Custom_Logger Logger instance.
 */
function wp_get_logger() {
    static $logger = null;

    if ( null === $logger ) {
        $logger = new WP_Custom_Logger();
    }

    return $logger;
}

// Usage examples:
// wp_get_logger()->error( 'Database connection failed', array( 'db_host' => DB_HOST ) );
// wp_get_logger()->warning( 'High memory usage detected', array( 'usage' => memory_get_usage() ) );
// wp_get_logger()->info( 'User logged in', array( 'user_id' => \get_current_user_id() ) );

Reading & Parsing Debug Logs

Create a WP-CLI command to analyze debug logs efficiently.

PHP
<?php
/**
 * WP-CLI Debug Log Analyzer
 *
 * Parses and analyzes WordPress debug logs.
 *
 * Usage: wp debug-log analyze --lines=100 --level=ERROR
 *
 * @package WordPress_CLI
 * @since   1.0.0
 */

if ( defined( 'WP_CLI' ) && WP_CLI ) {
    /**
     * Debug log analysis commands.
     */
    class Debug_Log_CLI {
        /**
         * Analyze debug log file.
         *
         * ## OPTIONS
         *
         * [--lines=<number>]
         * : Number of recent lines to analyze (default: 100)
         *
         * [--level=<severity>]
         * : Filter by severity level (ERROR, WARNING, etc.)
         *
         * [--search=<term>]
         * : Search for specific term in logs
         *
         * ## EXAMPLES
         *
         *     wp debug-log analyze --lines=50 --level=ERROR
         *     wp debug-log analyze --search="deprecated"
         *
         * @param array $args       Positional arguments.
         * @param array $assoc_args Associative arguments.
         *
         * @return void
         */
        public function analyze( $args, $assoc_args ) {
            $log_file = WP_CONTENT_DIR . '/debug.log';

            if ( ! file_exists( $log_file ) ) {
                WP_CLI::error( 'Debug log file not found. Enable WP_DEBUG_LOG first.' );
                return;
            }

            $lines_to_read = $assoc_args['lines'] ?? 100;
            $filter_level  = $assoc_args['level'] ?? '';
            $search_term   = $assoc_args['search'] ?? '';

            WP_CLI::log( sprintf( 'Analyzing debug log: %s', $log_file ) );
            WP_CLI::log( sprintf( 'File size: %s', size_format( filesize( $log_file ) ) ) );

            // Read last N lines.
            $log_lines = $this->read_last_lines( $log_file, $lines_to_read );

            if ( empty( $log_lines ) ) {
                WP_CLI::warning( 'Log file is empty' );
                return;
            }

            // Parse and filter log entries.
            $entries = $this->parse_log_entries( $log_lines );

            if ( $filter_level ) {
                $entries = array_filter( $entries, function( $entry ) use ( $filter_level ) {
                    return false !== stripos( $entry['level'], $filter_level );
                });
            }

            if ( $search_term ) {
                $entries = array_filter( $entries, function( $entry ) use ( $search_term ) {
                    return false !== stripos( $entry['message'], $search_term );
                });
            }

            // Display statistics.
            $this->display_statistics( $entries );

            // Display recent entries.
            $this->display_entries( $entries );
        }

        /**
         * Read last N lines from file efficiently.
         *
         * @param string $file  File path.
         * @param int    $lines Number of lines to read.
         *
         * @return array Array of lines.
         */
        private function read_last_lines( $file, $lines ) {
            $handle = fopen( $file, 'r' );
            
            if ( ! $handle ) {
                return array();
            }

            $line_buffer = array();
            
            while ( ! feof( $handle ) ) {
                $line = fgets( $handle );
                
                if ( false !== $line ) {
                    $line_buffer[] = $line;
                    
                    if ( count( $line_buffer ) > $lines ) {
                        array_shift( $line_buffer );
                    }
                }
            }

            fclose( $handle );

            return $line_buffer;
        }

        /**
         * Parse log entries into structured data.
         *
         * @param array $lines Log lines.
         *
         * @return array Parsed entries.
         */
        private function parse_log_entries( $lines ) {
            $entries = array();

            foreach ( $lines as $line ) {
                // Parse WordPress debug log format: [DD-MMM-YYYY HH:MM:SS UTC] Message
                if ( preg_match( '/^\[([^\]]+)\]\s+(.+)$/i', $line, $matches ) ) {
                    $timestamp = $matches[1];
                    $message   = trim( $matches[2] );

                    // Try to extract severity level.
                    $level = 'INFO';
                    
                    if ( stripos( $message, 'fatal' ) !== false || stripos( $message, 'error' ) !== false ) {
                        $level = 'ERROR';
                    } elseif ( stripos( $message, 'warning' ) !== false ) {
                        $level = 'WARNING';
                    } elseif ( stripos( $message, 'deprecated' ) !== false ) {
                        $level = 'DEPRECATED';
                    }

                    $entries[] = array(
                        'timestamp' => $timestamp,
                        'level'     => $level,
                        'message'   => $message,
                    );
                }
            }

            return $entries;
        }

        /**
         * Display log statistics.
         *
         * @param array $entries Log entries.
         *
         * @return void
         */
        private function display_statistics( $entries ) {
            $stats = array(
                'total'      => count( $entries ),
                'ERROR'      => 0,
                'WARNING'    => 0,
                'DEPRECATED' => 0,
                'INFO'       => 0,
            );

            foreach ( $entries as $entry ) {
                if ( isset( $stats[ $entry['level'] ] ) ) {
                    $stats[ $entry['level'] ]++;
                }
            }

            WP_CLI::log( "\n=== LOG STATISTICS ===" );
            WP_CLI::log( sprintf( 'Total Entries: %d', $stats['total'] ) );
            
            if ( $stats['ERROR'] > 0 ) {
                WP_CLI::error( sprintf( 'Errors: %d', $stats['ERROR'] ), false );
            }
            
            if ( $stats['WARNING'] > 0 ) {
                WP_CLI::warning( sprintf( 'Warnings: %d', $stats['WARNING'] ) );
            }
            
            if ( $stats['DEPRECATED'] > 0 ) {
                WP_CLI::log( sprintf( 'Deprecated: %d', $stats['DEPRECATED'] ) );
            }
        }

        /**
         * Display log entries.
         *
         * @param array $entries Log entries.
         *
         * @return void
         */
        private function display_entries( $entries ) {
            WP_CLI::log( "\n=== RECENT LOG ENTRIES ===" );

            $recent_entries = array_slice( $entries, -20 );

            foreach ( $recent_entries as $entry ) {
                $color = '';
                
                switch ( $entry['level'] ) {
                    case 'ERROR':
                        $color = '%r'; // Red.
                        break;
                    case 'WARNING':
                        $color = '%y'; // Yellow.
                        break;
                }

                WP_CLI::log( sprintf(
                    '%s[%s] %s: %s%s',
                    $color,
                    $entry['timestamp'],
                    $entry['level'],
                    substr( $entry['message'], 0, 100 ),
                    $color ? '%n' : ''
                ) );
            }
        }

        /**
         * Clear debug log file.
         *
         * ## EXAMPLES
         *
         *     wp debug-log clear
         *
         * @return void
         */
        public function clear() {
            $log_file = WP_CONTENT_DIR . '/debug.log';

            if ( ! file_exists( $log_file ) ) {
                WP_CLI::warning( 'Debug log file does not exist' );
                return;
            }

            $file_size = filesize( $log_file );

            WP_CLI::confirm(
                sprintf(
                    'Are you sure you want to clear the debug log? Current size: %s',
                    size_format( $file_size )
                )
            );

            if ( file_put_contents( $log_file, '' ) !== false ) {
                WP_CLI::success( 'Debug log cleared successfully' );
            } else {
                WP_CLI::error( 'Failed to clear debug log' );
            }
        }
    }

    WP_CLI::add_command( 'debug-log', 'Debug_Log_CLI' );
}
✅ USAGE EXAMPLE
Run the debug log analyzer from command line:
wp debug-log analyze --lines=100 --level=ERROR
wp debug-log analyze --search="deprecated"
wp debug-log clear

03 PHP Error Log Management

PHP error logs capture runtime errors, warnings, and notices. These logs are separate from WordPress debug logs and are configured at the PHP level.

PHP Error Logging Configuration

PHP
<?php
/**
 * PHP Error Logging Configuration
 *
 * Add to wp-config.php for production-safe error logging.
 *
 * @package WordPress
 * @since   1.0.0
 */

// Configure PHP error logging.
ini_set( 'log_errors', 1 );
ini_set( 'error_log', WP_CONTENT_DIR . '/php-errors.log' );

// Set error reporting level.
// E_ALL & ~E_DEPRECATED & ~E_STRICT excludes deprecated and strict notices.
error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT );

// Never display errors on production.
ini_set( 'display_errors', 0 );
ini_set( 'display_startup_errors', 0 );

/**
 * Custom error handler for WordPress.
 *
 * Captures and logs all PHP errors with context.
 *
 * @param int    $errno      Error level.
 * @param string $errstr     Error message.
 * @param string $errfile    File where error occurred.
 * @param int    $errline    Line number where error occurred.
 * @param array  $errcontext Variables in scope (deprecated in PHP 7.2+).
 *
 * @return bool False to continue with default error handler.
 */
function wp_custom_error_handler( $errno, $errstr, $errfile = '', $errline = 0, $errcontext = array() ) {
    // Don't process errors if error reporting is disabled.
    if ( 0 === error_reporting() ) {
        return false;
    }

    // Map error levels to severity.
    $error_types = array(
        E_ERROR             => 'ERROR',
        E_WARNING           => 'WARNING',
        E_PARSE             => 'PARSE ERROR',
        E_NOTICE            => 'NOTICE',
        E_CORE_ERROR        => 'CORE ERROR',
        E_CORE_WARNING      => 'CORE WARNING',
        E_COMPILE_ERROR     => 'COMPILE ERROR',
        E_COMPILE_WARNING   => 'COMPILE WARNING',
        E_USER_ERROR        => 'USER ERROR',
        E_USER_WARNING      => 'USER WARNING',
        E_USER_NOTICE       => 'USER NOTICE',
        E_STRICT            => 'STRICT',
        E_RECOVERABLE_ERROR => 'RECOVERABLE ERROR',
        E_DEPRECATED        => 'DEPRECATED',
        E_USER_DEPRECATED   => 'USER DEPRECATED',
    );

    $error_type = $error_types[ $errno ] ?? 'UNKNOWN';

    // Format error message with context.
    $log_message = sprintf(
        '[%s] PHP %s: %s in %s on line %d',
        current_time( 'Y-m-d H:i:s' ),
        $error_type,
        $errstr,
        $errfile,
        $errline
    );

    // Add memory usage for critical errors.
    if ( in_array( $errno, array( E_ERROR, E_USER_ERROR, E_CORE_ERROR ), true ) ) {
        $log_message .= sprintf(
            ' | Memory: %s / %s',
            size_format( memory_get_usage( true ) ),
            ini_get( 'memory_limit' )
        );
    }

    // Write to custom error log.
    error_log( $log_message );

    // For critical errors, also send email notification.
    if ( in_array( $errno, array( E_ERROR, E_USER_ERROR, E_CORE_ERROR ), true ) ) {
        \wp_mail(
            \get_option( 'admin_email' ),
            sprintf( 'Critical PHP Error on %s', \get_bloginfo( 'name' ) ),
            $log_message
        );
    }

    // Don't execute PHP internal error handler.
    return true;
}

// Set custom error handler.
set_error_handler( 'wp_custom_error_handler' );

/**
 * Custom exception handler.
 *
 * Catches uncaught exceptions and logs them.
 *
 * @param Exception $exception Uncaught exception.
 *
 * @return void
 */
function wp_custom_exception_handler( $exception ) {
    $log_message = sprintf(
        '[%s] Uncaught Exception: %s in %s on line %d | Stack trace: %s',
        current_time( 'Y-m-d H:i:s' ),
        $exception->getMessage(),
        $exception->getFile(),
        $exception->getLine(),
        $exception->getTraceAsString()
    );

    error_log( $log_message );

    // Send critical alert.
    \wp_mail(
        \get_option( 'admin_email' ),
        sprintf( 'Uncaught Exception on %s', \get_bloginfo( 'name' ) ),
        $log_message
    );

    // Show generic error page to user.
    if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
        \wp_die(
            'An error occurred. The site administrator has been notified.',
            'Error',
            array( 'response' => 500 )
        );
    }
}

// Set custom exception handler.
set_exception_handler( 'wp_custom_exception_handler' );

/**
 * Shutdown function to catch fatal errors.
 *
 * @return void
 */
function wp_fatal_error_handler() {
    $error = error_get_last();

    if ( null !== $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ), true ) ) {
        $log_message = sprintf(
            '[%s] FATAL ERROR: %s in %s on line %d',
            current_time( 'Y-m-d H:i:s' ),
            $error['message'],
            $error['file'],
            $error['line']
        );

        error_log( $log_message );

        \wp_mail(
            \get_option( 'admin_email' ),
            sprintf( 'FATAL ERROR on %s', \get_bloginfo( 'name' ) ),
            $log_message
        );
    }
}

// Register shutdown function.
register_shutdown_function( 'wp_fatal_error_handler' );

04 Access Log Analysis

Server access logs track every HTTP request to your WordPress site. Analyzing these logs helps identify traffic patterns, potential attacks, and performance issues.

Common Access Log Formats

Format Server Example
Apache Combined Apache 192.168.1.1 - - [01/Jan/2024:12:00:00 +0000] "GET /wp-admin/ HTTP/1.1" 200 1234
Nginx Access Nginx 192.168.1.1 - - [01/Jan/2024:12:00:00 +0000] "GET /wp-admin/" 200 1234
IIS W3C IIS 2024-01-01 12:00:00 GET /wp-admin/ - 200 0 0

Access Log Parser Implementation

PHP
<?php
/**
 * WordPress Access Log Analyzer
 *
 * Parse and analyze Apache/Nginx access logs for WordPress sites.
 *
 * @package WordPress_Logging
 * @since   1.0.0
 */

class WP_Access_Log_Analyzer {
    /**
     * Log file path.
     *
     * @var string
     */
    private $log_file;

    /**
     * Parsed log entries.
     *
     * @var array
     */
    private $entries = array();

    /**
     * Constructor.
     *
     * @param string $log_file Path to access log file.
     */
    public function __construct( $log_file ) {
        $this->log_file = $log_file;
    }

    /**
     * Parse Apache combined log format.
     *
     * Format: %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"
     *
     * @param int $max_lines Maximum lines to parse (0 = all).
     *
     * @return array Parsed entries.
     */
    public function parse_apache_log( $max_lines = 1000 ) {
        if ( ! file_exists( $this->log_file ) ) {
            return array();
        }

        $handle = fopen( $this->log_file, 'r' );
        
        if ( ! $handle ) {
            return array();
        }

        $count = 0;
        $pattern = '/^(\S+) \S+ \S+ \[([^\]]+)\] "([^"]+)" (\d+) (\S+) "([^"]*)" "([^"]*)"/';

        while ( ! feof( $handle ) && ( 0 === $max_lines || $count < $max_lines ) ) {
            $line = fgets( $handle );

            if ( preg_match( $pattern, $line, $matches ) ) {
                $this->entries[] = array(
                    'ip'         => $matches[1],
                    'timestamp'  => $matches[2],
                    'request'    => $matches[3],
                    'status'     => (int) $matches[4],
                    'size'       => '-' === $matches[5] ? 0 : (int) $matches[5],
                    'referrer'   => $matches[6],
                    'user_agent' => $matches[7],
                );

                $count++;
            }
        }

        fclose( $handle );

        return $this->entries;
    }

    /**
     * Detect potential security threats.
     *
     * @return array Suspicious entries.
     */
    public function detect_threats() {
        $threats = array();

        $suspicious_patterns = array(
            'sql_injection'    => "/(union|select|insert|update|delete|drop|';|--|\/\*)/i",
            'xss_attempt'      => '/(<script|javascript:|onerror=|onload=)/i',
            'path_traversal'   => '/(\.\.\/|\.\.\\\\)/i',
            'shell_injection'  => '/(;|\||`|&|\$\()/i',
            'wp_enumeration'   => '/wp-(admin|content|includes)\//',
        );

        foreach ( $this->entries as $entry ) {
            $request = $entry['request'];

            foreach ( $suspicious_patterns as $threat_type => $pattern ) {
                if ( preg_match( $pattern, $request ) ) {
                    $threats[] = array(
                        'type'       => $threat_type,
                        'ip'         => $entry['ip'],
                        'timestamp'  => $entry['timestamp'],
                        'request'    => $entry['request'],
                        'status'     => $entry['status'],
                    );
                }
            }
        }

        return $threats;
    }

    /**
     * Analyze traffic patterns.
     *
     * @return array Traffic statistics.
     */
    public function analyze_traffic() {
        $stats = array(
            'total_requests'   => count( $this->entries ),
            'unique_ips'       => array(),
            'status_codes'     => array(),
            'top_pages'        => array(),
            'top_user_agents'  => array(),
            'failed_logins'    => 0,
            'successful_logins' => 0,
        );

        foreach ( $this->entries as $entry ) {
            // Count unique IPs.
            $stats['unique_ips'][ $entry['ip'] ] = true;

            // Count status codes.
            $status = $entry['status'];
            
            if ( ! isset( $stats['status_codes'][ $status ] ) ) {
                $stats['status_codes'][ $status ] = 0;
            }
            
            $stats['status_codes'][ $status ]++;

            // Extract page from request.
            if ( preg_match( '/^(GET|POST|PUT|DELETE)\s+(\S+)/', $entry['request'], $matches ) ) {
                $page = $matches[2];
                
                if ( ! isset( $stats['top_pages'][ $page ] ) ) {
                    $stats['top_pages'][ $page ] = 0;
                }
                
                $stats['top_pages'][ $page ]++;

                // Count login attempts.
                if ( false !== strpos( $page, 'wp-login.php' ) ) {
                    if ( 200 === $status ) {
                        $stats['successful_logins']++;
                    } elseif ( in_array( $status, array( 401, 403 ), true ) ) {
                        $stats['failed_logins']++;
                    }
                }
            }

            // Count user agents.
            $ua = $entry['user_agent'];
            
            if ( ! isset( $stats['top_user_agents'][ $ua ] ) ) {
                $stats['top_user_agents'][ $ua ] = 0;
            }
            
            $stats['top_user_agents'][ $ua ]++;
        }

        // Convert unique IPs to count.
        $stats['unique_ips'] = count( $stats['unique_ips'] );

        // Sort top pages and user agents.
        arsort( $stats['top_pages'] );
        arsort( $stats['top_user_agents'] );

        // Keep only top 10.
        $stats['top_pages'] = array_slice( $stats['top_pages'], 0, 10, true );
        $stats['top_user_agents'] = array_slice( $stats['top_user_agents'], 0, 10, true );

        return $stats;
    }

    /**
     * Detect brute force attacks.
     *
     * @param int $threshold Failed login threshold per IP.
     * @param int $timeframe Time window in seconds.
     *
     * @return array IPs with suspicious login attempts.
     */
    public function detect_brute_force( $threshold = 10, $timeframe = 300 ) {
        $login_attempts = array();

        foreach ( $this->entries as $entry ) {
            // Check for wp-login.php requests.
            if ( false === strpos( $entry['request'], 'wp-login.php' ) ) {
                continue;
            }

            // Only count failed attempts.
            if ( ! in_array( $entry['status'], array( 401, 403 ), true ) ) {
                continue;
            }

            $ip = $entry['ip'];
            $timestamp = strtotime( $entry['timestamp'] );

            if ( ! isset( $login_attempts[ $ip ] ) ) {
                $login_attempts[ $ip ] = array();
            }

            $login_attempts[ $ip ][] = $timestamp;
        }

        // Find IPs exceeding threshold.
        $suspicious_ips = array();

        foreach ( $login_attempts as $ip => $timestamps ) {
            sort( $timestamps );

            // Check for threshold failures within timeframe.
            $window_count = 0;

            for ( $i = 0; $i < count( $timestamps ); $i++ ) {
                $window_end = $timestamps[ $i ] + $timeframe;
                $window_count = 1;

                for ( $j = $i + 1; $j < count( $timestamps ); $j++ ) {
                    if ( $timestamps[ $j ] <= $window_end ) {
                        $window_count++;
                    } else {
                        break;
                    }
                }

                if ( $window_count >= $threshold ) {
                    $suspicious_ips[ $ip ] = array(
                        'attempts'      => $window_count,
                        'first_attempt' => date( 'Y-m-d H:i:s', $timestamps[ $i ] ),
                        'last_attempt'  => date( 'Y-m-d H:i:s', $timestamps[ $j - 1 ] ),
                    );
                    break;
                }
            }
        }

        return $suspicious_ips;
    }
}

/**
 * WP-CLI command for access log analysis.
 *
 * Usage: wp access-log analyze /var/log/apache2/access.log
 */
if ( defined( 'WP_CLI' ) && WP_CLI ) {
    /**
     * Access log analysis CLI.
     */
    class Access_Log_CLI {
        /**
         * Analyze access log file.
         *
         * ## OPTIONS
         *
         * <file>
         * : Path to access log file
         *
         * [--lines=<number>]
         * : Number of lines to parse (default: 1000)
         *
         * ## EXAMPLES
         *
         *     wp access-log analyze /var/log/apache2/access.log
         *     wp access-log analyze /var/log/nginx/access.log --lines=5000
         *
         * @param array $args       Positional arguments.
         * @param array $assoc_args Associative arguments.
         */
        public function analyze( $args, $assoc_args ) {
            $log_file = $args[0] ?? '';
            $max_lines = $assoc_args['lines'] ?? 1000;

            if ( ! $log_file || ! file_exists( $log_file ) ) {
                WP_CLI::error( 'Log file not found' );
                return;
            }

            WP_CLI::log( sprintf( 'Analyzing access log: %s', $log_file ) );

            $analyzer = new WP_Access_Log_Analyzer( $log_file );
            $analyzer->parse_apache_log( $max_lines );

            // Traffic analysis.
            $stats = $analyzer->analyze_traffic();

            WP_CLI::log( "\n=== TRAFFIC STATISTICS ===" );
            WP_CLI::log( sprintf( 'Total Requests: %d', $stats['total_requests'] ) );
            WP_CLI::log( sprintf( 'Unique IPs: %d', $stats['unique_ips'] ) );
            WP_CLI::log( sprintf( 'Successful Logins: %d', $stats['successful_logins'] ) );
            
            if ( $stats['failed_logins'] > 0 ) {
                WP_CLI::warning( sprintf( 'Failed Logins: %d', $stats['failed_logins'] ) );
            }

            // Brute force detection.
            $brute_force = $analyzer->detect_brute_force( 10, 300 );

            if ( ! empty( $brute_force ) ) {
                WP_CLI::error( sprintf( "\nBrute Force Attacks Detected: %d IPs", count( $brute_force ) ), false );

                foreach ( $brute_force as $ip => $data ) {
                    WP_CLI::log( sprintf(
                        '  - %s: %d attempts (%s to %s)',
                        $ip,
                        $data['attempts'],
                        $data['first_attempt'],
                        $data['last_attempt']
                    ) );
                }
            }

            // Threat detection.
            $threats = $analyzer->detect_threats();

            if ( ! empty( $threats ) ) {
                WP_CLI::error( sprintf( "\nSecurity Threats Detected: %d", count( $threats ) ), false );

                foreach ( array_slice( $threats, 0, 10 ) as $threat ) {
                    WP_CLI::log( sprintf(
                        '  - [%s] %s: %s',
                        $threat['type'],
                        $threat['ip'],
                        substr( $threat['request'], 0, 80 )
                    ) );
                }
            }
        }
    }

    WP_CLI::add_command( 'access-log', 'Access_Log_CLI' );
}
⚠️ PERFORMANCE CONSIDERATION
Parsing large access logs can be resource-intensive. For production environments:

  • Use --lines parameter to limit parsing
  • Run analysis during low-traffic periods
  • Consider using log aggregation tools (ELK Stack, Graylog) for real-time analysis
  • Implement log rotation to prevent files from becoming too large

05 Security Event Logging

Security logging tracks authentication events, permission changes, and suspicious activities. This is critical for compliance, forensics, and intrusion detection.

Comprehensive Security Logger

PHP
<?php
/**
 * WordPress Security Event Logger
 *
 * Tracks security-relevant events for audit and compliance.
 *
 * @package WordPress_Security
 * @since   1.0.0
 */

class WP_Security_Logger {
    /**
     * Security log file path.
     *
     * @var string
     */
    private $log_file;

    /**
     * Events to log.
     *
     * @var array
     */
    private $logged_events = array(
        'user_login',
        'user_login_failed',
        'user_logout',
        'user_register',
        'profile_update',
        'password_reset',
        'delete_user',
        'added_user_role',
        'removed_user_role',
        'wp_login_failed',
    );

    /**
     * Constructor.
     */
    public function __construct() {
        $this->log_file = WP_CONTENT_DIR . '/security-events.log';
        $this->init_hooks();
    }

    /**
     * Initialize WordPress hooks.
     *
     * @return void
     */
    private function init_hooks() {
        // Login events.
        \add_action( 'wp_login', array( $this, 'log_successful_login' ), 10, 2 );
        \add_action( 'wp_login_failed', array( $this, 'log_failed_login' ), 10, 2 );
        \add_action( 'wp_logout', array( $this, 'log_logout' ) );

        // User events.
        \add_action( 'user_register', array( $this, 'log_user_registration' ) );
        \add_action( 'profile_update', array( $this, 'log_profile_update' ), 10, 2 );
        \add_action( 'delete_user', array( $this, 'log_user_deletion' ) );

        // Role changes.
        \add_action( 'add_user_role', array( $this, 'log_role_addition' ), 10, 2 );
        \add_action( 'remove_user_role', array( $this, 'log_role_removal' ), 10, 2 );

        // Password resets.
        \add_action( 'after_password_reset', array( $this, 'log_password_reset' ) );

        // Plugin/theme changes.
        \add_action( 'activated_plugin', array( $this, 'log_plugin_activation' ) );
        \add_action( 'deactivated_plugin', array( $this, 'log_plugin_deactivation' ) );
        \add_action( 'switch_theme', array( $this, 'log_theme_switch' ), 10, 3 );

        // Options changes.
        \add_action( 'update_option', array( $this, 'log_critical_option_update' ), 10, 3 );
    }

    /**
     * Write security event to log.
     *
     * @param string $event_type Event type.
     * @param array  $data       Event data.
     *
     * @return bool Success status.
     */
    private function write_log( $event_type, $data = array() ) {
        $log_entry = array(
            'timestamp'  => current_time( 'Y-m-d H:i:s' ),
            'event_type' => $event_type,
            'user_id'    => \get_current_user_id(),
            'user_ip'    => $this->get_client_ip(),
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
            'data'       => $data,
        );

        $formatted_entry = sprintf(
            '[%s] %s | User: %d | IP: %s | Data: %s',
            $log_entry['timestamp'],
            $log_entry['event_type'],
            $log_entry['user_id'],
            $log_entry['user_ip'],
            \wp_json_encode( $log_entry['data'] )
        );

        return false !== file_put_contents(
            $this->log_file,
            $formatted_entry . PHP_EOL,
            FILE_APPEND | LOCK_EX
        );
    }

    /**
     * Get client IP address safely.
     *
     * @return string IP address.
     */
    private function get_client_ip() {
        $ip = '';

        // Check for proxy headers (validate to prevent spoofing).
        $headers = array(
            'HTTP_CF_CONNECTING_IP', // Cloudflare.
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR',
        );

        foreach ( $headers as $header ) {
            if ( ! empty( $_SERVER[ $header ] ) ) {
                $ip = sanitize_text_field( \wp_unslash( $_SERVER[ $header ] ) );
                
                // For X-Forwarded-For, get the first IP.
                if ( false !== strpos( $ip, ',' ) ) {
                    $ips = explode( ',', $ip );
                    $ip = trim( $ips[0] );
                }
                
                // Validate IP address.
                if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
                    break;
                }
            }
        }

        return $ip;
    }

    /**
     * Log successful login.
     *
     * @param string  $user_login Username.
     * @param WP_User $user       User object.
     *
     * @return void
     */
    public function log_successful_login( $user_login, $user ) {
        $this->write_log(
            'user_login_success',
            array(
                'username' => $user_login,
                'user_id'  => $user->ID,
                'roles'    => $user->roles,
            )
        );
    }

    /**
     * Log failed login attempt.
     *
     * @param string $username Username or email.
     * @param WP_Error $error Error object.
     *
     * @return void
     */
    public function log_failed_login( $username, $error ) {
        $this->write_log(
            'user_login_failed',
            array(
                'username'     => $username,
                'error_code'   => $error->get_error_code(),
                'error_message' => $error->get_error_message(),
            )
        );

        // Check for brute force attempt.
        $this->check_brute_force( $this->get_client_ip() );
    }

    /**
     * Check for brute force attack.
     *
     * @param string $ip IP address.
     *
     * @return void
     */
    private function check_brute_force( $ip ) {
        $transient_key = 'failed_login_' . md5( $ip );
        $failed_attempts = \get_transient( $transient_key );

        if ( false === $failed_attempts ) {
            $failed_attempts = 1;
        } else {
            $failed_attempts++;
        }

        // Store for 15 minutes.
        \set_transient( $transient_key, $failed_attempts, 15 * MINUTE_IN_SECONDS );

        // Alert after 5 failed attempts.
        if ( 5 === $failed_attempts ) {
            $this->write_log(
                'brute_force_detected',
                array(
                    'ip'              => $ip,
                    'failed_attempts' => $failed_attempts,
                )
            );

            // Send admin notification.
            \wp_mail(
                \get_option( 'admin_email' ),
                sprintf( 'Brute Force Attack Detected on %s', \get_bloginfo( 'name' ) ),
                sprintf(
                    'Multiple failed login attempts detected from IP: %s\n\nAttempts: %d',
                    $ip,
                    $failed_attempts
                )
            );
        }

        // Block after 10 attempts.
        if ( $failed_attempts >= 10 ) {
            $this->write_log(
                'ip_blocked',
                array(
                    'ip'     => $ip,
                    'reason' => 'Too many failed login attempts',
                )
            );

            \wp_die(
                'Too many failed login attempts. Your IP has been temporarily blocked.',
                'Access Denied',
                array( 'response' => 403 )
            );
        }
    }

    /**
     * Log user logout.
     *
     * @return void
     */
    public function log_logout() {
        $user = \wp_get_current_user();

        $this->write_log(
            'user_logout',
            array(
                'user_id'  => $user->ID,
                'username' => $user->user_login,
            )
        );
    }

    /**
     * Log user registration.
     *
     * @param int $user_id User ID.
     *
     * @return void
     */
    public function log_user_registration( $user_id ) {
        $user = \get_userdata( $user_id );

        $this->write_log(
            'user_registered',
            array(
                'user_id'  => $user_id,
                'username' => $user->user_login,
                'email'    => $user->user_email,
                'roles'    => $user->roles,
            )
        );
    }

    /**
     * Log profile updates.
     *
     * @param int    $user_id       User ID.
     * @param object $old_user_data Old user data.
     *
     * @return void
     */
    public function log_profile_update( $user_id, $old_user_data ) {
        $new_user_data = \get_userdata( $user_id );

        $changes = array();

        // Check for email change.
        if ( $old_user_data->user_email !== $new_user_data->user_email ) {
            $changes['email'] = array(
                'old' => $old_user_data->user_email,
                'new' => $new_user_data->user_email,
            );
        }

        // Check for role changes.
        if ( $old_user_data->roles !== $new_user_data->roles ) {
            $changes['roles'] = array(
                'old' => $old_user_data->roles,
                'new' => $new_user_data->roles,
            );
        }

        if ( ! empty( $changes ) ) {
            $this->write_log(
                'profile_updated',
                array(
                    'user_id'  => $user_id,
                    'username' => $new_user_data->user_login,
                    'changes'  => $changes,
                )
            );
        }
    }

    /**
     * Log user deletion.
     *
     * @param int $user_id User ID.
     *
     * @return void
     */
    public function log_user_deletion( $user_id ) {
        $user = \get_userdata( $user_id );

        $this->write_log(
            'user_deleted',
            array(
                'user_id'  => $user_id,
                'username' => $user->user_login,
                'email'    => $user->user_email,
                'roles'    => $user->roles,
            )
        );
    }

    /**
     * Log role addition.
     *
     * @param int    $user_id User ID.
     * @param string $role    Role added.
     *
     * @return void
     */
    public function log_role_addition( $user_id, $role ) {
        $user = \get_userdata( $user_id );

        $this->write_log(
            'role_added',
            array(
                'user_id'  => $user_id,
                'username' => $user->user_login,
                'role'     => $role,
            )
        );
    }

    /**
     * Log role removal.
     *
     * @param int    $user_id User ID.
     * @param string $role    Role removed.
     *
     * @return void
     */
    public function log_role_removal( $user_id, $role ) {
        $user = \get_userdata( $user_id );

        $this->write_log(
            'role_removed',
            array(
                'user_id'  => $user_id,
                'username' => $user->user_login,
                'role'     => $role,
            )
        );
    }

    /**
     * Log password reset.
     *
     * @param WP_User $user User object.
     *
     * @return void
     */
    public function log_password_reset( $user ) {
        $this->write_log(
            'password_reset',
            array(
                'user_id'  => $user->ID,
                'username' => $user->user_login,
            )
        );
    }

    /**
     * Log plugin activation.
     *
     * @param string $plugin Plugin file.
     *
     * @return void
     */
    public function log_plugin_activation( $plugin ) {
        $this->write_log(
            'plugin_activated',
            array(
                'plugin' => $plugin,
            )
        );
    }

    /**
     * Log plugin deactivation.
     *
     * @param string $plugin Plugin file.
     *
     * @return void
     */
    public function log_plugin_deactivation( $plugin ) {
        $this->write_log(
            'plugin_deactivated',
            array(
                'plugin' => $plugin,
            )
        );
    }

    /**
     * Log theme switch.
     *
     * @param string   $new_name  New theme name.
     * @param WP_Theme $new_theme New theme object.
     * @param WP_Theme $old_theme Old theme object.
     *
     * @return void
     */
    public function log_theme_switch( $new_name, $new_theme, $old_theme ) {
        $this->write_log(
            'theme_switched',
            array(
                'old_theme' => $old_theme->get( 'Name' ),
                'new_theme' => $new_theme->get( 'Name' ),
            )
        );
    }

    /**
     * Log critical option updates.
     *
     * @param string $option    Option name.
     * @param mixed  $old_value Old value.
     * @param mixed  $new_value New value.
     *
     * @return void
     */
    public function log_critical_option_update( $option, $old_value, $new_value ) {
        // Only log critical options.
        $critical_options = array(
            'siteurl',
            'home',
            'admin_email',
            'users_can_register',
            'default_role',
        );

        if ( ! in_array( $option, $critical_options, true ) ) {
            return;
        }

        $this->write_log(
            'critical_option_updated',
            array(
                'option'    => $option,
                'old_value' => $old_value,
                'new_value' => $new_value,
            )
        );
    }
}

// Initialize security logger.
new WP_Security_Logger();
💡 COMPLIANCE TIP
Security event logging is required for many compliance standards including:

  • PCI DSS: Requirement 10 – Track and monitor all access to network resources
  • GDPR: Article 32 – Security of processing requires audit logs
  • HIPAA: 164.312(b) – Audit controls for protected health information
  • SOC 2: CC7.2 – System operations monitoring and logging

06 Database Query Logging

Database query logging helps identify slow queries, optimize performance, and detect SQL injection attempts.

PHP
<?php
/**
 * WordPress Database Query Logger
 *
 * Logs all database queries with execution time and context.
 *
 * @package WordPress_Performance
 * @since   1.0.0
 */

class WP_Query_Logger {
    /**
     * Query log file.
     *
     * @var string
     */
    private $log_file;

    /**
     * Slow query threshold in seconds.
     *
     * @var float
     */
    private $slow_query_threshold = 1.0;

    /**
     * Constructor.
     */
    public function __construct() {
        $this->log_file = WP_CONTENT_DIR . '/query-log.log';
        
        // Enable query saving.
        if ( ! defined( 'SAVEQUERIES' ) ) {
            define( 'SAVEQUERIES', true );
        }

        \add_action( 'shutdown', array( $this, 'log_queries' ) );
    }

    /**
     * Log all queries executed during request.
     *
     * @return void
     */
    public function log_queries() {
        global $wpdb;

        if ( empty( $wpdb->queries ) ) {
            return;
        }

        $total_time = 0;
        $slow_queries = array();
        $query_count = count( $wpdb->queries );

        foreach ( $wpdb->queries as $query_data ) {
            $query = $query_data[0];
            $execution_time = $query_data[1];
            $stack_trace = $query_data[2];

            $total_time += $execution_time;

            // Log slow queries separately.
            if ( $execution_time > $this->slow_query_threshold ) {
                $slow_queries[] = array(
                    'query'          => $query,
                    'execution_time' => $execution_time,
                    'stack_trace'    => $stack_trace,
                );
            }
        }

        // Create log entry.
        $log_entry = sprintf(
            "[%s] Request: %s | Queries: %d | Total Time: %.4fs | Slow Queries: %d\n",
            current_time( 'Y-m-d H:i:s' ),
            $_SERVER['REQUEST_URI'] ?? 'CLI',
            $query_count,
            $total_time,
            count( $slow_queries )
        );

        // Add slow query details.
        if ( ! empty( $slow_queries ) ) {
            $log_entry .= "=== SLOW QUERIES ===\n";
            
            foreach ( $slow_queries as $idx => $slow_query ) {
                $log_entry .= sprintf(
                    "  [%d] Time: %.4fs\n  Query: %s\n  Stack: %s\n\n",
                    $idx + 1,
                    $slow_query['execution_time'],
                    $slow_query['query'],
                    $slow_query['stack_trace']
                );
            }
        }

        // Write to log.
        file_put_contents(
            $this->log_file,
            $log_entry . "\n",
            FILE_APPEND | LOCK_EX
        );

        // Alert if too many queries.
        if ( $query_count > 100 ) {
            $this->alert_excessive_queries( $query_count, $total_time );
        }
    }

    /**
     * Send alert for excessive queries.
     *
     * @param int   $count Total query count.
     * @param float $time  Total execution time.
     *
     * @return void
     */
    private function alert_excessive_queries( $count, $time ) {
        // Only alert once per hour.
        $transient_key = 'query_alert_sent';
        
        if ( \get_transient( $transient_key ) ) {
            return;
        }

        \set_transient( $transient_key, true, HOUR_IN_SECONDS );

        \wp_mail(
            \get_option( 'admin_email' ),
            sprintf( 'Excessive Database Queries on %s', \get_bloginfo( 'name' ) ),
            sprintf(
                "Request: %s\nQuery Count: %d\nTotal Time: %.4fs\n\nThis may indicate a performance issue.",
                $_SERVER['REQUEST_URI'] ?? 'Unknown',
                $count,
                $time
            )
        );
    }
}

// Initialize query logger (only in development or when explicitly enabled).
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'ENABLE_QUERY_LOGGING' ) && ENABLE_QUERY_LOGGING ) {
    new WP_Query_Logger();
}

07 Custom Logging Implementation

Implement custom logging for specific business requirements and application-specific events.

PHP
<?php
/**
 * Custom Event Logger for WordPress
 *
 * Flexible logging system for custom application events.
 *
 * @package WordPress_Logging
 * @since   1.0.0
 */

class WP_Custom_Event_Logger {
    /**
     * Log events to database table.
     *
     * @var bool
     */
    private $use_database = true;

    /**
     * Log table name.
     *
     * @var string
     */
    private $table_name;

    /**
     * Constructor.
     *
     * @param bool $use_database Whether to use database storage.
     */
    public function __construct( $use_database = true ) {
        global $wpdb;

        $this->use_database = $use_database;
        $this->table_name = $wpdb->prefix . 'custom_event_logs';

        if ( $this->use_database ) {
            $this->create_log_table();
        }
    }

    /**
     * Create log table if it doesn't exist.
     *
     * @return void
     */
    private function create_log_table() {
        global $wpdb;

        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            event_time datetime NOT NULL,
            event_type varchar(100) NOT NULL,
            severity varchar(20) NOT NULL,
            user_id bigint(20) unsigned DEFAULT NULL,
            ip_address varchar(45) DEFAULT NULL,
            message text,
            context longtext,
            created_at timestamp DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY  (id),
            KEY event_type (event_type),
            KEY severity (severity),
            KEY user_id (user_id),
            KEY event_time (event_time)
        ) $charset_collate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
    }

    /**
     * Log custom event.
     *
     * @param string $event_type Event type identifier.
     * @param string $message    Log message.
     * @param string $severity   Severity level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
     * @param array  $context    Additional context data.
     *
     * @return bool Success status.
     */
    public function log_event( $event_type, $message, $severity = 'INFO', $context = array() ) {
        global $wpdb;

        if ( ! $this->use_database ) {
            return $this->log_to_file( $event_type, $message, $severity, $context );
        }

        $result = $wpdb->insert(
            $this->table_name,
            array(
                'event_time'  => current_time( 'mysql' ),
                'event_type'  => sanitize_text_field( $event_type ),
                'severity'    => sanitize_text_field( $severity ),
                'user_id'     => \get_current_user_id(),
                'ip_address'  => $this->get_client_ip(),
                'message'     => sanitize_textarea_field( $message ),
                'context'     => \wp_json_encode( $context ),
            ),
            array( '%s', '%s', '%s', '%d', '%s', '%s', '%s' )
        );

        return false !== $result;
    }

    /**
     * Log to file as fallback.
     *
     * @param string $event_type Event type.
     * @param string $message    Message.
     * @param string $severity   Severity.
     * @param array  $context    Context.
     *
     * @return bool Success status.
     */
    private function log_to_file( $event_type, $message, $severity, $context ) {
        $log_file = WP_CONTENT_DIR . '/custom-events.log';

        $log_entry = sprintf(
            "[%s] %s - %s: %s | Context: %s\n",
            current_time( 'Y-m-d H:i:s' ),
            $severity,
            $event_type,
            $message,
            \wp_json_encode( $context )
        );

        return false !== file_put_contents( $log_file, $log_entry, FILE_APPEND | LOCK_EX );
    }

    /**
     * Get client IP address.
     *
     * @return string IP address.
     */
    private function get_client_ip() {
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';

        // Check for proxy headers.
        if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
            $ips = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
            $ip = trim( $ips[0] );
        }

        return sanitize_text_field( $ip );
    }

    /**
     * Query event logs.
     *
     * @param array $args Query arguments.
     *
     * @return array Log entries.
     */
    public function query_logs( $args = array() ) {
        global $wpdb;

        $defaults = array(
            'event_type' => '',
            'severity'   => '',
            'user_id'    => 0,
            'date_from'  => '',
            'date_to'    => '',
            'limit'      => 100,
            'offset'     => 0,
        );

        $args = \wp_parse_args( $args, $defaults );

        $where = array( '1=1' );
        $prepare_args = array();

        if ( ! empty( $args['event_type'] ) ) {
            $where[] = 'event_type = %s';
            $prepare_args[] = $args['event_type'];
        }

        if ( ! empty( $args['severity'] ) ) {
            $where[] = 'severity = %s';
            $prepare_args[] = $args['severity'];
        }

        if ( ! empty( $args['user_id'] ) ) {
            $where[] = 'user_id = %d';
            $prepare_args[] = $args['user_id'];
        }

        if ( ! empty( $args['date_from'] ) ) {
            $where[] = 'event_time >= %s';
            $prepare_args[] = $args['date_from'];
        }

        if ( ! empty( $args['date_to'] ) ) {
            $where[] = 'event_time <= %s';
            $prepare_args[] = $args['date_to'];
        }

        $where_clause = implode( ' AND ', $where );

        $sql = "SELECT * FROM {$this->table_name} WHERE {$where_clause} ORDER BY event_time DESC LIMIT %d OFFSET %d";
        $prepare_args[] = $args['limit'];
        $prepare_args[] = $args['offset'];

        return $wpdb->get_results( $wpdb->prepare( $sql, $prepare_args ) );
    }

    /**
     * Get event statistics.
     *
     * @param string $date_from Start date.
     * @param string $date_to   End date.
     *
     * @return array Statistics.
     */
    public function get_statistics( $date_from = '', $date_to = '' ) {
        global $wpdb;

        $where = array();
        $prepare_args = array();

        if ( $date_from ) {
            $where[] = 'event_time >= %s';
            $prepare_args[] = $date_from;
        }

        if ( $date_to ) {
            $where[] = 'event_time <= %s';
            $prepare_args[] = $date_to;
        }

        $where_clause = ! empty( $where ) ? 'WHERE ' . implode( ' AND ', $where ) : '';

        // Get counts by severity.
        $sql = "SELECT severity, COUNT(*) as count FROM {$this->table_name} {$where_clause} GROUP BY severity";
        
        if ( ! empty( $prepare_args ) ) {
            $sql = $wpdb->prepare( $sql, $prepare_args );
        }
        
        $severity_counts = $wpdb->get_results( $sql, ARRAY_A );

        // Get counts by event type.
        $sql = "SELECT event_type, COUNT(*) as count FROM {$this->table_name} {$where_clause} GROUP BY event_type ORDER BY count DESC LIMIT 10";
        
        if ( ! empty( $prepare_args ) ) {
            $sql = $wpdb->prepare( $sql, $prepare_args );
        }
        
        $event_type_counts = $wpdb->get_results( $sql, ARRAY_A );

        return array(
            'by_severity'   => $severity_counts,
            'by_event_type' => $event_type_counts,
        );
    }

    /**
     * Clean old log entries.
     *
     * @param int $days Number of days to keep.
     *
     * @return int Number of deleted rows.
     */
    public function cleanup_old_logs( $days = 30 ) {
        global $wpdb;

        $date_threshold = date( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );

        return $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$this->table_name} WHERE event_time < %s",
                $date_threshold
            )
        );
    }
}

// Usage example:
// $logger = new WP_Custom_Event_Logger();
// $logger->log_event(
//     'order_completed',
//     'Order #12345 completed successfully',
//     'INFO',
//     array( 'order_id' => 12345, 'total' => 99.99 )
// );

08 Log Parsing & Analysis Techniques

Advanced log parsing techniques for extracting actionable insights from large log files.

Log Analysis Tools

🔍 GoAccess

Real-time web log analyzer. Generates visual HTML reports from access logs with minimal configuration.

📊 ELK Stack

Elasticsearch, Logstash, Kibana. Enterprise-grade log aggregation, analysis, and visualization platform.

🔧 AWK/Grep

Command-line text processing tools. Perfect for quick analysis and filtering of log files.

📈 Splunk

Commercial log analysis platform with powerful search, reporting, and alerting capabilities.

Common Log Analysis Patterns

Shell
# Extract all 404 errors from access log
grep " 404 " /var/log/apache2/access.log

# Count requests by IP address
awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -20

# Find all wp-login.php attempts
grep "wp-login.php" /var/log/apache2/access.log

# Extract slow queries from debug log
grep "slow query" /var/www/html/wp-content/debug.log

# Monitor logs in real-time
tail -f /var/log/apache2/error.log

# Search for specific error in date range
sed -n '/2024-01-01/,/2024-01-31/p' error.log | grep "Fatal error"

# Count errors by type
grep -o "PHP [A-Za-z ]*:" error.log | sort | uniq -c | sort -rn

09 Automated Log Monitoring

Set up automated monitoring to detect and alert on critical log events in real-time.

PHP
<?php
/**
 * Automated Log Monitoring System
 *
 * Monitors logs and sends alerts for critical events.
 *
 * @package WordPress_Monitoring
 * @since   1.0.0
 */

class WP_Log_Monitor {
    /**
     * Monitored log files.
     *
     * @var array
     */
    private $log_files = array();

    /**
     * Alert thresholds.
     *
     * @var array
     */
    private $thresholds = array(
        'errors_per_minute'   => 10,
        'failed_logins_per_hour' => 20,
        'slow_queries_per_hour' => 50,
    );

    /**
     * Constructor.
     */
    public function __construct() {
        $this->log_files = array(
            'debug'    => WP_CONTENT_DIR . '/debug.log',
            'security' => WP_CONTENT_DIR . '/security-events.log',
            'error'    => WP_CONTENT_DIR . '/php-errors.log',
        );

        // Schedule monitoring checks.
        if ( ! \wp_next_scheduled( 'wp_log_monitor_check' ) ) {
            \wp_schedule_event( time(), 'hourly', 'wp_log_monitor_check' );
        }

        \add_action( 'wp_log_monitor_check', array( $this, 'run_monitoring_checks' ) );
    }

    /**
     * Run all monitoring checks.
     *
     * @return void
     */
    public function run_monitoring_checks() {
        $alerts = array();

        // Check error rate.
        $error_rate = $this->check_error_rate();
        
        if ( $error_rate['count'] > $this->thresholds['errors_per_minute'] ) {
            $alerts[] = sprintf(
                'High error rate detected: %d errors in the last minute',
                $error_rate['count']
            );
        }

        // Check failed login attempts.
        $failed_logins = $this->check_failed_login_rate();
        
        if ( $failed_logins['count'] > $this->thresholds['failed_logins_per_hour'] ) {
            $alerts[] = sprintf(
                'High number of failed login attempts: %d in the last hour',
                $failed_logins['count']
            );
        }

        // Check disk space for logs.
        $disk_usage = $this->check_log_disk_usage();
        
        if ( $disk_usage > 1073741824 ) { // 1GB
            $alerts[] = sprintf(
                'Log files consuming excessive disk space: %s',
                size_format( $disk_usage )
            );
        }

        // Send alerts if any.
        if ( ! empty( $alerts ) ) {
            $this->send_alert( $alerts );
        }
    }

    /**
     * Check error rate in debug log.
     *
     * @return array Error count and details.
     */
    private function check_error_rate() {
        $log_file = $this->log_files['debug'] ?? '';

        if ( ! file_exists( $log_file ) ) {
            return array( 'count' => 0 );
        }

        $one_minute_ago = time() - 60;
        $error_count = 0;

        $handle = fopen( $log_file, 'r' );
        
        if ( ! $handle ) {
            return array( 'count' => 0 );
        }

        while ( ! feof( $handle ) ) {
            $line = fgets( $handle );

            if ( preg_match( '/\[([^\]]+)\]/', $line, $matches ) ) {
                $log_time = strtotime( $matches[1] );

                if ( $log_time >= $one_minute_ago && false !== stripos( $line, 'error' ) ) {
                    $error_count++;
                }
            }
        }

        fclose( $handle );

        return array(
            'count' => $error_count,
            'timeframe' => '1 minute',
        );
    }

    /**
     * Check failed login rate.
     *
     * @return array Failed login count.
     */
    private function check_failed_login_rate() {
        $log_file = $this->log_files['security'] ?? '';

        if ( ! file_exists( $log_file ) ) {
            return array( 'count' => 0 );
        }

        $one_hour_ago = time() - 3600;
        $failed_count = 0;

        $handle = fopen( $log_file, 'r' );
        
        if ( ! $handle ) {
            return array( 'count' => 0 );
        }

        while ( ! feof( $handle ) ) {
            $line = fgets( $handle );

            if ( preg_match( '/\[([^\]]+)\]/', $line, $matches ) ) {
                $log_time = strtotime( $matches[1] );

                if ( $log_time >= $one_hour_ago && false !== stripos( $line, 'login_failed' ) ) {
                    $failed_count++;
                }
            }
        }

        fclose( $handle );

        return array(
            'count'     => $failed_count,
            'timeframe' => '1 hour',
        );
    }

    /**
     * Check total disk usage of log files.
     *
     * @return int Total size in bytes.
     */
    private function check_log_disk_usage() {
        $total_size = 0;

        foreach ( $this->log_files as $log_file ) {
            if ( file_exists( $log_file ) ) {
                $total_size += filesize( $log_file );
            }
        }

        return $total_size;
    }

    /**
     * Send monitoring alert.
     *
     * @param array $alerts Alert messages.
     *
     * @return void
     */
    private function send_alert( $alerts ) {
        $subject = sprintf(
            'Log Monitoring Alert: %s',
            \get_bloginfo( 'name' )
        );

        $message = "The following issues were detected during log monitoring:\n\n";
        $message .= implode( "\n", array_map( function( $alert ) {
            return "• " . $alert;
        }, $alerts ) );

        $message .= "\n\nTimestamp: " . current_time( 'Y-m-d H:i:s' );

        \wp_mail( \get_option( 'admin_email' ), $subject, $message );
    }
}

// Initialize log monitor.
new WP_Log_Monitor();

10 Log Management Best Practices


Enable logging only when needed: Production sites should have minimal logging to avoid performance impact

Protect log files: Restrict access via .htaccess, move outside web root, or use proper file permissions (0600)

Implement log rotation: Automatically rotate logs to prevent disk space issues

Set retention policies: Delete old logs after 30-90 days based on compliance requirements

Monitor log size: Alert when log files exceed reasonable size thresholds

Centralize logs: Use log aggregation for multi-server environments

Filter sensitive data: Never log passwords, API keys, or PII

Use structured logging: JSON format enables better parsing and analysis

Set up alerting: Configure real-time alerts for critical events

Regular log review: Schedule periodic manual review of logs

Log Security Best Practices

Security Measure Implementation Priority
Access Control File permissions 0600, .htaccess deny rules Critical
Encryption at Rest Encrypt sensitive log data on disk High
Secure Transmission Use TLS for remote log shipping Critical
Data Sanitization Filter PII and credentials before logging Critical
Tamper Detection Use checksums or digital signatures Medium
Backup Logs Regular backups with secure storage High
✅ ACTION ITEMS – GET STARTED

  1. Enable debug logging in development environment with IP restrictions
  2. Implement custom logger using the WP_Custom_Logger class
  3. Set up security event logging for login attempts and user changes
  4. Configure log rotation to prevent disk space issues
  5. Create WP-CLI commands for log analysis and monitoring
  6. Set up automated alerts for critical events
  7. Protect log files with proper permissions and .htaccess rules
  8. Schedule regular log reviews and implement retention policies

🔰 Beginner

  • WordPress debug.log
  • PHP error logging
  • Manual log review
  • Basic WP-CLI analysis

⚙️ Intermediate

  • Custom logger class
  • Security event logging
  • Automated monitoring
  • Email alerts

🚀 Advanced

  • Database-backed logging
  • ELK Stack integration
  • Real-time dashboards
  • SIEM integration

🏢 Enterprise

  • Centralized log aggregation
  • Splunk/Datadog
  • Compliance logging
  • Advanced analytics

Comments (0)

← How to Scan Your WordPress Site for 0-Day Vulnerabilities (Before They Go Public) Building Your Own WP Security Scanner - Complete Guide →
Share this page
Back to top