refactor(database): enhance error logging and handling in Database and DatabaseDebugger classes

- Updated error handling in Database class to provide more meaningful error messages, including detailed PDO error information.
- Enhanced log_error method in DatabaseDebugger to accept exceptions, allowing for more reliable error logging with comprehensive context.
- Improved user-facing error messages while maintaining detailed logging for administrators and developers.
- Added checks for connection status and query context in error logs to aid in debugging.
This commit is contained in:
Yury Pikhtarev 2025-06-20 14:34:32 +04:00
commit 8d4723f64c
No known key found for this signature in database
3 changed files with 192 additions and 11 deletions

View file

@ -26,9 +26,9 @@ $posts_without_attach = $topics_without_attach = [];
DB()->query("
CREATE TEMPORARY TABLE $tmp_attach_tbl (
physical_filename VARCHAR(255) NOT NULL default '',
physical_filename VARCHAR(255) NOT NULL default '' COLLATE utf8mb4_unicode_ci,
KEY physical_filename (physical_filename(20))
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
");
DB()->add_shutdown_query("DROP TEMPORARY TABLE IF EXISTS $tmp_attach_tbl");

View file

@ -188,7 +188,7 @@ class Database
// Initialize affected rows to 0 (most queries don't affect rows)
$this->last_affected_rows = 0;
} catch (\Exception $e) {
$this->debugger->log_error();
$this->debugger->log_error($e);
$this->result = null;
$this->last_affected_rows = 0;
}
@ -549,9 +549,28 @@ class Database
if ($this->connection) {
try {
$pdo = $this->connection->getPdo();
$errorCode = $pdo->errorCode();
$errorInfo = $pdo->errorInfo();
// Filter out "no error" states - PDO returns '00000' when there's no error
if (!$errorCode || $errorCode === '00000') {
return ['code' => '', 'message' => ''];
}
// Build meaningful error message from errorInfo array
// errorInfo format: [SQLSTATE, driver-specific error code, driver-specific error message]
$message = '';
if (isset($errorInfo[2]) && $errorInfo[2]) {
$message = $errorInfo[2]; // Driver-specific error message is most informative
} elseif (isset($errorInfo[1]) && $errorInfo[1]) {
$message = "Error code: " . $errorInfo[1];
} else {
$message = "SQLSTATE: " . $errorCode;
}
return [
'code' => $pdo->errorCode(),
'message' => implode(': ', $pdo->errorInfo())
'code' => $errorCode,
'message' => $message
];
} catch (\Exception $e) {
return ['code' => $e->getCode(), 'message' => $e->getMessage()];
@ -753,7 +772,93 @@ class Database
public function trigger_error(string $msg = 'Database Error'): void
{
$error = $this->sql_error();
$error_msg = "$msg: " . $error['message'];
// Build a meaningful error message
if (!empty($error['message'])) {
$error_msg = "$msg: " . $error['message'];
if (!empty($error['code'])) {
$error_msg = "$msg ({$error['code']}): " . $error['message'];
}
} else {
// Base error message for all users
$error_msg = "$msg: Database operation failed";
// Only add detailed debugging information for administrators or in development mode
$is_admin = defined('IS_ADMIN') && IS_ADMIN;
$is_dev_mode = (defined('APP_ENV') && APP_ENV === 'local') || (defined('DBG_USER') && DBG_USER) || function_exists('dev');
if ($is_admin || $is_dev_mode) {
// Gather detailed debugging information - ONLY for admins/developers
$debug_info = [];
// Connection status
if ($this->connection) {
$debug_info[] = "Connection: Active";
try {
$pdo = $this->connection->getPdo();
if ($pdo) {
$debug_info[] = "PDO: Available";
$errorInfo = $pdo->errorInfo();
if ($errorInfo && count($errorInfo) >= 3) {
$debug_info[] = "PDO ErrorInfo: " . json_encode($errorInfo);
}
$debug_info[] = "PDO ErrorCode: " . $pdo->errorCode();
} else {
$debug_info[] = "PDO: Null";
}
} catch (\Exception $e) {
$debug_info[] = "PDO Check Failed: " . $e->getMessage();
}
} else {
$debug_info[] = "Connection: None";
}
// Query information
if ($this->cur_query) {
$debug_info[] = "Last Query: " . substr($this->cur_query, 0, 200) . (strlen($this->cur_query) > 200 ? '...' : '');
} else {
$debug_info[] = "Last Query: None";
}
// Database information
$debug_info[] = "Database: " . ($this->selected_db ?: 'None');
$debug_info[] = "Server: " . $this->db_server;
// Recent queries from debug log (if available)
if (isset($this->debugger->dbg) && !empty($this->debugger->dbg)) {
$recent_queries = array_slice($this->debugger->dbg, -3); // Last 3 queries
$debug_info[] = "Recent Queries Count: " . count($recent_queries);
foreach ($recent_queries as $i => $query_info) {
$debug_info[] = "Query " . ($i + 1) . ": " . substr($query_info['sql'] ?? 'Unknown', 0, 100) . (strlen($query_info['sql'] ?? '') > 100 ? '...' : '');
}
}
if ($debug_info) {
$error_msg .= " [DEBUG: " . implode("; ", $debug_info) . "]";
}
// Log this for investigation
if (function_exists('bb_log')) {
bb_log("Unknown Database Error Debug:\n" . implode("\n", $debug_info), 'unknown_db_errors');
}
} else {
// For regular users: generic message only + contact admin hint
$error_msg = "$msg: A database error occurred. Please contact the administrator if this problem persists.";
// Still log basic information for debugging
if (function_exists('bb_log')) {
bb_log("Database Error (User-facing): $error_msg\nRequest: " . ($_SERVER['REQUEST_URI'] ?? 'CLI'), 'user_db_errors');
}
}
}
// Add query context for debugging (but only for admins/developers)
$is_admin = defined('IS_ADMIN') && IS_ADMIN;
$is_dev_mode = (defined('APP_ENV') && APP_ENV === 'local') || (defined('DBG_USER') && DBG_USER) || function_exists('dev');
if ($this->cur_query && ($is_admin || $is_dev_mode)) {
$error_msg .= "\nQuery: " . $this->cur_query;
}
if (function_exists('bb_die')) {
bb_die($error_msg);
@ -797,9 +902,9 @@ class Database
/**
* Log error (delegated to debugger)
*/
public function log_error(): void
public function log_error(?\Exception $exception = null): void
{
$this->debugger->log_error();
$this->debugger->log_error($exception);
}
/**

View file

@ -278,11 +278,87 @@ class DatabaseDebugger
/**
* Log error
*
* NOTE: This method logs detailed information to FILES only (error_log, bb_log).
* Log files are not accessible to regular users, so detailed information is safe here.
* User-facing error display is handled separately with proper security checks.
*/
public function log_error(): void
public function log_error(?\Exception $exception = null): void
{
$error = $this->db->sql_error();
error_log("Database Error: " . $error['message'] . " Query: " . $this->db->cur_query);
$error_details = [];
$error_msg = '';
if ($exception) {
// Use the actual exception information which is more reliable
$error_msg = "Database Error: " . $exception->getMessage();
$error_code = $exception->getCode();
if ($error_code) {
$error_msg = "Database Error ({$error_code}): " . $exception->getMessage();
}
// Collect detailed error information
$error_details[] = "Exception: " . get_class($exception);
$error_details[] = "Message: " . $exception->getMessage();
$error_details[] = "Code: " . $exception->getCode();
$error_details[] = "File: " . $exception->getFile() . ":" . $exception->getLine();
// Add PDO-specific details if it's a PDO exception
if ($exception instanceof \PDOException) {
$error_details[] = "PDO Error Info: " . json_encode($exception->errorInfo ?? []);
}
} else {
// Fallback to PDO error state (legacy behavior)
$error = $this->db->sql_error();
// Only log if there's an actual error (not 00000 which means "no error")
if (!$error['code'] || $error['code'] === '00000' || !$error['message']) {
return; // Don't log empty or "no error" states
}
$error_msg = "Database Error ({$error['code']}): " . $error['message'];
$error_details[] = "PDO Error Code: " . $error['code'];
$error_details[] = "PDO Error Message: " . $error['message'];
}
// Add comprehensive context for debugging
$error_details[] = "Query: " . ($this->db->cur_query ?: 'None');
$error_details[] = "Source: " . $this->debug_find_source();
$error_details[] = "Database: " . ($this->db->selected_db ?: 'None');
$error_details[] = "Server: " . $this->db->db_server;
$error_details[] = "Timestamp: " . date('Y-m-d H:i:s');
$error_details[] = "Request URI: " . ($_SERVER['REQUEST_URI'] ?? 'CLI');
$error_details[] = "User IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'Unknown');
// Check connection status
try {
if ($this->db->connection) {
$error_details[] = "Connection Status: Active";
$pdo = $this->db->connection->getPdo();
$error_details[] = "PDO Connection: " . ($pdo ? 'Available' : 'Null');
if ($pdo) {
$errorInfo = $pdo->errorInfo();
$error_details[] = "Current PDO Error Info: " . json_encode($errorInfo);
}
} else {
$error_details[] = "Connection Status: No connection";
}
} catch (\Exception $e) {
$error_details[] = "Connection Check Failed: " . $e->getMessage();
}
// Build comprehensive log message
$log_message = $error_msg . "\n" . implode("\n", $error_details);
// Log to both error_log and TorrentPier's logging system
error_log($error_msg);
// Use TorrentPier's bb_log for better file management and organization
if (function_exists('bb_log')) {
bb_log($log_message, 'database_errors');
}
// Also log to PHP error log for immediate access
error_log("DETAILED: " . $log_message);
}
/**