
Introduction
According to software best practices, logging serves as the fundamental backbone of application development since it enables developers to identify events and address problems for health monitoring purposes. In the context of WordPress plugins, logging becomes indispensable for debugging API failures, database errors, and user actions. In this guide, you will learn how to build a secure, scalable Database Logging System for WordPress that writes to files or a database. It includes log rotation, security measures, and automated cleanup. By the end, you’ll have a production-ready logger tailored for WordPress.
Why Logging Matters in WordPress Plugins
- Debugging: Identify root causes of errors without relying on user reports.
- Auditing: Track user actions (e.g., form submissions, settings changes).
- Performance Monitoring: Detect bottlenecks in API calls or database queries.
- Compliance: Maintain records for regulatory requirements (e.g., GDPR).
Example Scenario:
A payment gateway plugin fails to process transactions. Without logs, it is nearly impossible to diagnose whether an API timeout, invalid credentials, or a database lock causes the issue.
Choosing Between File and Database Logging
File-Based Logging
- Pros: Fast, minimal database overhead, ideal for high-volume logging.
- Cons: Harder to query, risk of file permission issues.
Database Logging
- Pros: Structured data, easy to filter/search, integrates with WordPress admin.
- Cons: Adds database load, slower for large datasets.
Hybrid Approach: Use files for high-frequency logs (e.g., API requests) and databases for actionable errors (e.g., failed logins).
Step 1: Setting Up the Logging Class
Create a Logger class to encapsulate logging logic.
// PHP Code
class Plugin_Logger {
private $log_dir;
private $log_table;
private $log_levels = array('info', 'warning', 'error');
public function __construct() {
// Initialize log directory and database table
$this->setup_log_directory();
$this->setup_database_table();
}
private function setup_log_directory() {
$upload_dir = wp_upload_dir();
$this->log_dir = $upload_dir['basedir'] . '/plugin-logs/';
if (!file_exists($this->log_dir)) {
wp_mkdir_p($this->log_dir);
// Secure the directory
file_put_contents($this->log_dir . '.htaccess', 'Deny from all');
}
}
private function setup_database_table() {
global $wpdb;
$this->log_table = $wpdb->prefix . 'plugin_logs';
}
}
Step 2: File-Based Logging Implementation
Writing Logs to Files
// PHP Code
public function log_to_file($message, $level = 'info') {
$log_file = $this->log_dir . 'logs-' . date('Y-m-d') . '.log';
$timestamp = current_time('mysql');
$entry = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
if (wp_is_writable($this->log_dir)) {
file_put_contents($log_file, $entry, FILE_APPEND | LOCK_EX);
} else {
error_log("Plugin Logs: Directory not writable: {$this->log_dir}");
}
}
Security Measures:
- Both directories should obtain 755 permissions, while all files must receive 644 permissions.
- Block public access via .htaccess:
// Apache Code
<Files *.log>Â Â
 Order deny,allow Â
 Deny from all Â
</Files>
Log Rotation
Automatically delete logs older than 30 days.
// PHP Code
private function rotate_logs($days_to_keep = 30) {
$files = glob($this->log_dir . '*.log');
foreach ($files as $file) {
if (filemtime($file) < strtotime("-{$days_to_keep} days")) {
unlink($file);
}
}
}
Step 3: Database Logging Implementation
Creating the Log Table
Use dbDelta() during plugin activation.
// PHP Code
public function create_log_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$this->log_table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
timestamp DATETIME NOT NULL,
level VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
Activation Hook:
// PHP Code
register_activation_hook(__FILE__, array($this, 'create_log_table'));
Inserting Log Entries
// PHP Code
public function log_to_db($message, $level = 'info') {
global $wpdb;
$wpdb->insert(
$this->log_table,
array(
'timestamp' => current_time('mysql'),
'level' => $level,
'message' => $this->sanitize_message($message)
),
array('%s', '%s', '%s')
);
}
private function sanitize_message($message) {
return sanitize_text_field($message);
}
Step 4: Implementing Log Levels
Control log granularity using levels like info, warning, and error.
// PHP Code
public function log($message, $level = 'info', $mask_sensitive = true) {
if (!in_array($level, $this->log_levels)) {
return;
}
if ($mask_sensitive) {
$message = $this->mask_sensitive_data($message);
}
$this->log_to_file($message, $level);
$this->log_to_db($message, $level);
}
private function mask_sensitive_data($message) {
// Mask API keys, emails, etc.
$patterns = array(
'/\b[A-Za-z0-9]{32}\b/' => '***MASKED_API_KEY***',
'/\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/' => '***MASKED_EMAIL***'
);
return preg_replace(array_keys($patterns), array_values($patterns), $message);
}
Step 5: Automated Log Cleanup
Use WordPress cron to delete old entries.
Scheduling Cleanup
// PHP Code
public function schedule_cleanup() {
if (!wp_next_scheduled('plugin_logs_cleanup')) {
wp_schedule_event(time(), 'daily', 'plugin_logs_cleanup');
}
add_action('plugin_logs_cleanup', array($this, 'cleanup_old_logs'));
}
public function cleanup_old_logs() {
// Clean files
$this->rotate_logs(30);
// Clean database
global $wpdb;
$cutoff = date('Y-m-d H:i:s', strtotime('-30 days'));
$wpdb->query(
$wpdb->prepare("DELETE FROM {$this->log_table} WHERE timestamp < %s", $cutoff)
);
}
Step 6: Viewing and Managing Logs
File Logs Viewer
Create a shortcode to display logs in the WordPress admin:
// PHP Code
public function display_file_logs() {
  $log_file = $this->log_dir . 'logs-' . date('Y-m-d') . '.log';
  if (file_exists($log_file)) {
    echo '<pre>' . esc_html(file_get_contents($log_file)) . '</pre>';
  } else {
    echo 'No logs found.';
  }
}
10add_shortcode('display_logs', array($this, 'display_file_logs'));
Database Logs Viewer
Extend WP_List_Table for a paginated interface:
// PHP Code
class Logs_List_Table extends WP_List_Table {
public function prepare_items() {
global $wpdb;
$per_page = 20;
$current_page = $this->get_pagenum();
$offset = ($current_page - 1) * $per_page;
$this->items = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}plugin_logs
ORDER BY timestamp DESC
LIMIT %d OFFSET %d",
$per_page,
$offset
)
);
$total_items = $wpdb->get_var("SELECT COUNT(id) FROM {$wpdb->prefix}plugin_logs");
$this->set_pagination_args(array(
'total_items' => $total_items,
'per_page' => $per_page
));
}
public function get_columns() {
return array(
'timestamp' => 'Timestamp',
'level' => 'Level',
'message' => 'Message'
);
}
}
Step 7: Integrating the Logger
Use the logger in your plugin’s functions:
// PHP Code
$logger = new Plugin_Logger();
// Example: Log an API request
try {
$response = wp_remote_get('https://api.example.com/data');
if (is_wp_error($response)) {
$logger->log("API request failed: " . $response->get_error_message(), 'error');
} else {
$logger->log("API request successful: " . json_encode($response), 'info');
}
} catch (Exception $e) {
$logger->log("API exception: " . $e->getMessage(), 'error');
}
// Example: Log a database error
$result = $wpdb->query("INSERT INTO {$wpdb->prefix}some_table (column) VALUES ('value')");
if ($result === false) {
$logger->log("Database error: " . $wpdb->last_error, 'error');
}
Best Practices for Logging
- Use Appropriate Log Levels: Differentiate between informational messages, warnings, and errors to facilitate easier debugging.
- Avoid Verbose Logging in Production: Limit logging to essential information to reduce performance overhead and log file size.
- Regular Monitoring: Periodically review logs to identify patterns or recurring issues.
- Test the Logging System: Simulate various scenarios to ensure that the logger captures all necessary events and gracefully handles errors.
Security Considerations
- File Permissions: Ensure that log directories and files have appropriate permissions to prevent unauthorized access.
- Prevent Public Access: Use .htaccess rules to restrict access to log files.
- Sanitize Log Messages: Always sanitize messages before logging to prevent potential injection attacks.
- Database Security: Use prepared statements to prevent SQL injection when inserting log entries.
Troubleshooting Common Issues
- Logs Not Being Written: Check directory permissions and ensure the logging directory is writable.
- Permission Denied Errors: Verify that the web server user has the necessary permissions to write to the log directory.
- Database Connection Problems: Ensure that the database is accessible and that the log table exists.
Frequenlty Asked Questions
Conclusion
Implementing a robust logging system in your WordPress plugin is essential for maintaining application health, debugging issues, and ensuring compliance. By following the steps outlined in this guide, you can create a secure, efficient logging mechanism that meets your needs. Remember to regularly review and refine your logging practices and improve overall performance. With a well-implemented logging system, you can enhance the reliability and maintainability of your WordPress plugins, ultimately leading to a better user experience.
About the writer
Hassan Tahir wrote this article, drawing on his experience to clarify WordPress concepts and enhance developer understanding. Through his work, he aims to help both beginners and professionals refine their skills and tackle WordPress projects with greater confidence.