511 lines
18 KiB
PHP
511 lines
18 KiB
PHP
|
|
<?php
|
|||
|
|
/**
|
|||
|
|
* Real-time SQL Query Logger with PRE-execution tracking
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
class RealtimeSqlLogger {
|
|||
|
|
private static $instance;
|
|||
|
|
private $log_file;
|
|||
|
|
private $request_id;
|
|||
|
|
private $start_time;
|
|||
|
|
private $pending_queries = [];
|
|||
|
|
private $log_enabled = true;
|
|||
|
|
private $current_location = 'unknown';
|
|||
|
|
|
|||
|
|
public static function get_instance() {
|
|||
|
|
if (null === self::$instance) {
|
|||
|
|
self::$instance = new self();
|
|||
|
|
}
|
|||
|
|
return self::$instance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function __construct() {
|
|||
|
|
$this->log_file = WP_CONTENT_DIR . '/sql-debug.log';
|
|||
|
|
$this->request_id = uniqid('req_', true);
|
|||
|
|
$this->start_time = microtime(true);
|
|||
|
|
|
|||
|
|
$this->init_log_file();
|
|||
|
|
$this->init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function init_log_file() {
|
|||
|
|
if (!is_writable(WP_CONTENT_DIR)) {
|
|||
|
|
$this->log_enabled = false;
|
|||
|
|
error_log('Директория wp-content недоступна для записи');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!file_exists($this->log_file)) {
|
|||
|
|
if (@touch($this->log_file)) {
|
|||
|
|
@chmod($this->log_file, 0644);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Очищаем файл при каждой инициализации
|
|||
|
|
@file_put_contents($this->log_file, '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function init() {
|
|||
|
|
if (!$this->log_enabled) return;
|
|||
|
|
|
|||
|
|
// Трекинг местоположения в WordPress
|
|||
|
|
$this->setup_location_tracking();
|
|||
|
|
|
|||
|
|
// Перехватываем запрос ДО выполнения
|
|||
|
|
add_filter('query', [$this, 'log_query_before_execution'], 1, 1);
|
|||
|
|
|
|||
|
|
// Логируем результат выполнения
|
|||
|
|
add_filter('query', [$this, 'log_query_after_execution'], 9999, 2);
|
|||
|
|
|
|||
|
|
// Логируем ошибки запросов
|
|||
|
|
add_action('wpdb_error', [$this, 'log_query_error'], 10, 2);
|
|||
|
|
|
|||
|
|
add_action('shutdown', [$this, 'log_pending_queries'], 9999);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Настраиваем трекинг местоположения в WordPress
|
|||
|
|
*/
|
|||
|
|
private function setup_location_tracking() {
|
|||
|
|
// Основные хуки WordPress для определения местоположения
|
|||
|
|
$location_hooks = [
|
|||
|
|
'plugins_loaded' => 'PLUGINS_LOADED',
|
|||
|
|
'setup_theme' => 'SETUP_THEME',
|
|||
|
|
'after_setup_theme' => 'AFTER_SETUP_THEME',
|
|||
|
|
'init' => 'INIT',
|
|||
|
|
'wp_loaded' => 'WP_LOADED',
|
|||
|
|
'parse_query' => 'PARSE_QUERY',
|
|||
|
|
'pre_get_posts' => 'PRE_GET_POSTS',
|
|||
|
|
'wp' => 'WP_MAIN_QUERY',
|
|||
|
|
'template_redirect' => 'TEMPLATE_REDIRECT',
|
|||
|
|
'get_header' => 'HEADER',
|
|||
|
|
'wp_head' => 'WP_HEAD',
|
|||
|
|
'loop_start' => 'LOOP_START',
|
|||
|
|
'the_post' => 'THE_POST',
|
|||
|
|
'loop_end' => 'LOOP_END',
|
|||
|
|
'get_sidebar' => 'SIDEBAR',
|
|||
|
|
'get_footer' => 'FOOTER',
|
|||
|
|
'wp_footer' => 'WP_FOOTER',
|
|||
|
|
'shutdown' => 'SHUTDOWN'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
foreach ($location_hooks as $hook => $location) {
|
|||
|
|
add_action($hook, function() use ($location) {
|
|||
|
|
$this->current_location = $location;
|
|||
|
|
$this->log_message("📍 LOCATION: {$location}", 'LOCATION', false);
|
|||
|
|
}, 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Специальные хуки для контента
|
|||
|
|
add_action('the_content', function($content) {
|
|||
|
|
$this->current_location = 'THE_CONTENT';
|
|||
|
|
return $content;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
add_action('the_title', function($title) {
|
|||
|
|
$this->current_location = 'THE_TITLE';
|
|||
|
|
return $title;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
add_action('the_excerpt', function($excerpt) {
|
|||
|
|
$this->current_location = 'THE_EXCERPT';
|
|||
|
|
return $excerpt;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Перехватываем запрос ДО выполнения
|
|||
|
|
*/
|
|||
|
|
public function log_query_before_execution($query) {
|
|||
|
|
if (!$this->log_enabled) return $query;
|
|||
|
|
|
|||
|
|
$trimmed = trim($query);
|
|||
|
|
if (empty($trimmed)) return $query;
|
|||
|
|
|
|||
|
|
$query_hash = md5($query . microtime(true));
|
|||
|
|
$backtrace_info = $this->get_detailed_caller_info();
|
|||
|
|
|
|||
|
|
// Сохраняем запрос как pending
|
|||
|
|
$this->pending_queries[$query_hash] = [
|
|||
|
|
'query' => $query,
|
|||
|
|
'start_time' => microtime(true),
|
|||
|
|
'backtrace' => $backtrace_info['short'],
|
|||
|
|
'detailed_backtrace' => $backtrace_info['full'],
|
|||
|
|
'component' => $this->get_component_info($backtrace_info['file']),
|
|||
|
|
'location' => $this->current_location,
|
|||
|
|
'status' => 'PENDING'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
"🚦 QUERY QUEUED: " . $this->shorten_query($query),
|
|||
|
|
'SQL-QUEUE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 📍 Source: " . $backtrace_info['short'],
|
|||
|
|
'SQL-QUEUE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏷️ Component: " . $this->pending_queries[$query_hash]['component'],
|
|||
|
|
'SQL-QUEUE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏠 Location: " . $this->current_location,
|
|||
|
|
'SQL-QUEUE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return $query;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Логируем запрос ПОСЛЕ выполнения
|
|||
|
|
*/
|
|||
|
|
public function log_query_after_execution($query, $result = null) {
|
|||
|
|
if (!$this->log_enabled) return $query;
|
|||
|
|
|
|||
|
|
$trimmed = trim($query);
|
|||
|
|
if (empty($trimmed)) return $query;
|
|||
|
|
|
|||
|
|
$query_hash = md5($query . microtime(true));
|
|||
|
|
$execution_time = 0;
|
|||
|
|
$caller_info = '';
|
|||
|
|
$component_info = '';
|
|||
|
|
$location_info = '';
|
|||
|
|
|
|||
|
|
// Находим соответствующий pending запрос
|
|||
|
|
foreach ($this->pending_queries as $hash => $pending) {
|
|||
|
|
if ($pending['query'] === $query) {
|
|||
|
|
$execution_time = microtime(true) - $pending['start_time'];
|
|||
|
|
$this->pending_queries[$hash]['status'] = 'COMPLETED';
|
|||
|
|
$this->pending_queries[$hash]['execution_time'] = $execution_time;
|
|||
|
|
$caller_info = $pending['backtrace'];
|
|||
|
|
$component_info = $pending['component'];
|
|||
|
|
$location_info = $pending['location'];
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$status = $result === false ? 'FAILED' : 'COMPLETED';
|
|||
|
|
$time_ms = round($execution_time * 1000, 2);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
"✅ QUERY {$status}: {$time_ms}ms - " . $this->shorten_query($query),
|
|||
|
|
$status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 📍 Source: " . $caller_info,
|
|||
|
|
$status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏷️ Component: " . $component_info,
|
|||
|
|
$status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏠 Location: " . $location_info,
|
|||
|
|
$status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Для медленных запросов добавляем дополнительную информацию
|
|||
|
|
if ($time_ms > 100 && $status !== 'FAILED') {
|
|||
|
|
$this->log_message(
|
|||
|
|
" ⚠️ SLOW QUERY: {$time_ms}ms - consider optimization",
|
|||
|
|
'SQL-SLOW',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $query;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Логируем ошибки запросов
|
|||
|
|
*/
|
|||
|
|
public function log_query_error($error, $query) {
|
|||
|
|
if (!$this->log_enabled) return;
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
"❌ QUERY ERROR: " . $error,
|
|||
|
|
'SQL-ERROR',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 💥 Failed query: " . $this->shorten_query($query),
|
|||
|
|
'SQL-ERROR',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Находим информацию о вызывающем коде для ошибочного запроса
|
|||
|
|
foreach ($this->pending_queries as $pending) {
|
|||
|
|
if ($pending['query'] === $query) {
|
|||
|
|
$this->log_message(
|
|||
|
|
" 📍 Source: " . $pending['backtrace'],
|
|||
|
|
'SQL-ERROR',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏷️ Component: " . $pending['component'],
|
|||
|
|
'SQL-ERROR',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏠 Location: " . $pending['location'],
|
|||
|
|
'SQL-ERROR',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Логируем зависшие запросы
|
|||
|
|
*/
|
|||
|
|
public function log_pending_queries() {
|
|||
|
|
if (!$this->log_enabled || empty($this->pending_queries)) return;
|
|||
|
|
|
|||
|
|
$current_time = microtime(true);
|
|||
|
|
$hung_queries = 0;
|
|||
|
|
|
|||
|
|
foreach ($this->pending_queries as $hash => $query_info) {
|
|||
|
|
if ($query_info['status'] === 'PENDING') {
|
|||
|
|
$hang_time = round(($current_time - $query_info['start_time']) * 1000, 2);
|
|||
|
|
$hung_queries++;
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
"⚠️ HUNG QUERY: {$hang_time}ms - " . $this->shorten_query($query_info['query']),
|
|||
|
|
'SQL-HUNG',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 📍 Source: " . $query_info['backtrace'],
|
|||
|
|
'SQL-HUNG',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏷️ Component: " . $query_info['component'],
|
|||
|
|
'SQL-HUNG',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🏠 Location: " . $query_info['location'],
|
|||
|
|
'SQL-HUNG',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
" 🔍 Full backtrace:\n" . $query_info['detailed_backtrace'],
|
|||
|
|
'SQL-HUNG',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($hung_queries > 0) {
|
|||
|
|
$this->log_message(
|
|||
|
|
"🔴 Found {$hung_queries} hung queries!",
|
|||
|
|
'SQL-HUNG',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Логируем статистику по всем запросам
|
|||
|
|
$this->log_query_statistics();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Логируем статистику выполнения запросов
|
|||
|
|
*/
|
|||
|
|
private function log_query_statistics() {
|
|||
|
|
$total_queries = count($this->pending_queries);
|
|||
|
|
$completed_queries = 0;
|
|||
|
|
$failed_queries = 0;
|
|||
|
|
$total_time = 0;
|
|||
|
|
|
|||
|
|
$queries_by_location = [];
|
|||
|
|
$queries_by_component = [];
|
|||
|
|
|
|||
|
|
foreach ($this->pending_queries as $query) {
|
|||
|
|
// Статистика по местоположению
|
|||
|
|
$location = $query['location'];
|
|||
|
|
if (!isset($queries_by_location[$location])) {
|
|||
|
|
$queries_by_location[$location] = 0;
|
|||
|
|
}
|
|||
|
|
$queries_by_location[$location]++;
|
|||
|
|
|
|||
|
|
// Статистика по компонентам
|
|||
|
|
$component = $query['component'];
|
|||
|
|
if (!isset($queries_by_component[$component])) {
|
|||
|
|
$queries_by_component[$component] = 0;
|
|||
|
|
}
|
|||
|
|
$queries_by_component[$component]++;
|
|||
|
|
|
|||
|
|
if ($query['status'] === 'COMPLETED') {
|
|||
|
|
$completed_queries++;
|
|||
|
|
$total_time += $query['execution_time'] ?? 0;
|
|||
|
|
} elseif ($query['status'] === 'FAILED') {
|
|||
|
|
$failed_queries++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$avg_time = $completed_queries > 0 ? round(($total_time / $completed_queries) * 1000, 2) : 0;
|
|||
|
|
|
|||
|
|
$this->log_message(
|
|||
|
|
"📊 STATS: Total: {$total_queries} | Completed: {$completed_queries} | " .
|
|||
|
|
"Failed: {$failed_queries} | Avg: {$avg_time}ms",
|
|||
|
|
'STATS',
|
|||
|
|
false
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Статистика по местоположению
|
|||
|
|
if (!empty($queries_by_location)) {
|
|||
|
|
$this->log_message("📊 QUERIES BY LOCATION:", 'STATS', false);
|
|||
|
|
arsort($queries_by_location);
|
|||
|
|
foreach ($queries_by_location as $location => $count) {
|
|||
|
|
$this->log_message(" {$location}: {$count} queries", 'STATS', false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Статистика по компонентам
|
|||
|
|
if (!empty($queries_by_component)) {
|
|||
|
|
$this->log_message("📊 QUERIES BY COMPONENT:", 'STATS', false);
|
|||
|
|
arsort($queries_by_component);
|
|||
|
|
foreach ($queries_by_component as $component => $count) {
|
|||
|
|
$this->log_message(" {$component}: {$count} queries", 'STATS', false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function log_message($message, $level = 'INFO', $check_enabled = true) {
|
|||
|
|
if ($check_enabled && !$this->log_enabled) return;
|
|||
|
|
|
|||
|
|
$timestamp = microtime(true);
|
|||
|
|
$elapsed = round(($timestamp - $this->start_time) * 1000, 2);
|
|||
|
|
|
|||
|
|
$log = sprintf(
|
|||
|
|
"[%s] %s | %6.2fms | %-12s | %s\n",
|
|||
|
|
date('H:i:s'),
|
|||
|
|
substr($this->request_id, -6),
|
|||
|
|
$elapsed,
|
|||
|
|
$level,
|
|||
|
|
$message
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
@file_put_contents($this->log_file, $log, FILE_APPEND | LOCK_EX);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Получаем детальную информацию о вызывающем коде
|
|||
|
|
*/
|
|||
|
|
private function get_detailed_caller_info() {
|
|||
|
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15);
|
|||
|
|
$short_info = 'unknown';
|
|||
|
|
$full_info = '';
|
|||
|
|
$file_path = '';
|
|||
|
|
|
|||
|
|
foreach ($backtrace as $index => $trace) {
|
|||
|
|
if (isset($trace['file']) &&
|
|||
|
|
!str_contains($trace['file'], 'wp-includes') &&
|
|||
|
|
!str_contains($trace['file'], 'wp-admin')) {
|
|||
|
|
|
|||
|
|
$file = basename($trace['file']);
|
|||
|
|
$line = $trace['line'] ?? 'unknown';
|
|||
|
|
$function = $trace['function'] ?? 'unknown';
|
|||
|
|
|
|||
|
|
if ($short_info === 'unknown') {
|
|||
|
|
$short_info = "{$file}:{$line} ({$function})";
|
|||
|
|
$file_path = $trace['file'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$full_info .= sprintf("#%d %s(%d): %s()\n",
|
|||
|
|
$index,
|
|||
|
|
$trace['file'],
|
|||
|
|
$trace['line'] ?? 0,
|
|||
|
|
$function
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
'short' => $short_info,
|
|||
|
|
'full' => $full_info ?: 'No backtrace available',
|
|||
|
|
'file' => $file_path
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Определяем компонент (плагин/тема) по пути файла
|
|||
|
|
*/
|
|||
|
|
private function get_component_info($file_path) {
|
|||
|
|
if (empty($file_path)) return 'unknown';
|
|||
|
|
|
|||
|
|
$abspath = ABSPATH;
|
|||
|
|
|
|||
|
|
// Определяем тип компонента
|
|||
|
|
if (str_contains($file_path, WP_PLUGIN_DIR)) {
|
|||
|
|
$relative_path = str_replace(WP_PLUGIN_DIR . '/', '', $file_path);
|
|||
|
|
$parts = explode('/', $relative_path);
|
|||
|
|
return 'Plugin: ' . $parts[0];
|
|||
|
|
} elseif (str_contains($file_path, get_template_directory())) {
|
|||
|
|
$theme = wp_get_theme();
|
|||
|
|
return 'Theme: ' . $theme->get('Name');
|
|||
|
|
} elseif (str_contains($file_path, get_stylesheet_directory())) {
|
|||
|
|
$theme = wp_get_theme();
|
|||
|
|
return 'Child Theme: ' . $theme->get('Name');
|
|||
|
|
} elseif (str_contains($file_path, $abspath . 'wp-content/mu-plugins')) {
|
|||
|
|
return 'MU-Plugin';
|
|||
|
|
} elseif (str_contains($file_path, $abspath . 'wp-content')) {
|
|||
|
|
return 'Other (wp-content)';
|
|||
|
|
} elseif (str_contains($file_path, $abspath)) {
|
|||
|
|
return 'WordPress Core';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 'External';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private function shorten_query($query, $length = 120) {
|
|||
|
|
$trimmed = trim($query);
|
|||
|
|
if (strlen($trimmed) <= $length) return $trimmed;
|
|||
|
|
|
|||
|
|
return substr($trimmed, 0, $length) . '...';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Инициализируем логгер
|
|||
|
|
//if (defined('WP_DEBUG') && WP_DEBUG) {
|
|||
|
|
RealtimeSqlLogger::get_instance();
|
|||
|
|
//}
|
|||
|
|
|
|||
|
|
// Функция для просмотра текущего состояния
|
|||
|
|
add_action('wp_footer', 'show_sql_debug_info', 9999);
|
|||
|
|
function show_sql_debug_info() {
|
|||
|
|
if (!current_user_can('administrator')) return;
|
|||
|
|
|
|||
|
|
$log_file = WP_CONTENT_DIR . '/sql-debug.log';
|
|||
|
|
|
|||
|
|
if (file_exists($log_file)) {
|
|||
|
|
$log_content = file_get_contents($log_file);
|
|||
|
|
echo '<div style="background:#1e1e1e;padding:20px;margin:20px;border:1px solid #444;font-family:monospace;font-size:11px;max-height:400px;overflow:auto;color:#fff;">';
|
|||
|
|
echo '<h3 style="color:#fff;">🎯 SQL Debug Log (Live)</h3>';
|
|||
|
|
echo '<pre style="color:#ccc;line-height:1.4;">' . htmlspecialchars($log_content) . '</pre>';
|
|||
|
|
echo '</div>';
|
|||
|
|
}
|
|||
|
|
}
|