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.
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.

Hybrid Approach: Use files for high-frequency logs (e.g., API requests) and databases for actionable errors (e.g., failed logins).
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';
}
}
// 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:
// Apache Code
<Files *.log>
Order deny,allow
Deny from all
</Files>
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);
}
}
}
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'));
// 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);
}
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);
}
Use WordPress cron to delete old entries.
// 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)
);
}
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'));
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'
);
}
}
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');
}
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.

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.