- Changed headquarters country and state fields from text inputs to dropdown selections - Added dynamic state/province loading based on selected country (US/Canada) - Added 'Other' option for non-US/Canada countries with text input fallback - Properly handle org_headquarters_state_other field in backend processing - JavaScript handlers for dynamic country/state interaction - Consistent with Training Venue Information dropdown behavior Co-Authored-By: Ben Reed <ben@tealmaker.com>
503 lines
No EOL
16 KiB
PHP
503 lines
No EOL
16 KiB
PHP
<?php
|
|
/**
|
|
* HVAC Database Query Monitor
|
|
*
|
|
* Monitors and optimizes database queries for performance issues
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 1.0.7
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* HVAC_Query_Monitor class
|
|
*/
|
|
class HVAC_Query_Monitor {
|
|
|
|
/**
|
|
* Slow query threshold in seconds
|
|
*/
|
|
const SLOW_QUERY_THRESHOLD = 0.1;
|
|
|
|
/**
|
|
* Query log option name
|
|
*/
|
|
const LOG_OPTION = 'hvac_query_log';
|
|
|
|
/**
|
|
* Maximum log entries to keep
|
|
*/
|
|
const MAX_LOG_ENTRIES = 100;
|
|
|
|
/**
|
|
* Logged queries
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $queries = [];
|
|
|
|
/**
|
|
* Query statistics
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $stats = [
|
|
'total_queries' => 0,
|
|
'slow_queries' => 0,
|
|
'total_time' => 0,
|
|
'peak_memory' => 0
|
|
];
|
|
|
|
/**
|
|
* Initialize monitoring
|
|
*/
|
|
public static function init() {
|
|
// Only enable in development or when explicitly requested
|
|
if (!self::should_monitor()) {
|
|
return;
|
|
}
|
|
|
|
// Hook into WordPress query system
|
|
add_filter('query', [__CLASS__, 'log_query'], 999);
|
|
add_action('shutdown', [__CLASS__, 'analyze_queries']);
|
|
|
|
// Admin hooks
|
|
if (is_admin()) {
|
|
add_action('admin_menu', [__CLASS__, 'add_admin_page']);
|
|
add_action('wp_ajax_hvac_clear_query_log', [__CLASS__, 'ajax_clear_log']);
|
|
}
|
|
|
|
// WP-CLI integration (disabled - method not implemented)
|
|
// if (defined('WP_CLI') && WP_CLI) {
|
|
// WP_CLI::add_command('hvac query-monitor', [__CLASS__, 'wp_cli_commands']);
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Check if monitoring should be enabled
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function should_monitor() {
|
|
// Enable if WP_DEBUG is on
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
return true;
|
|
}
|
|
|
|
// Enable if explicitly requested
|
|
if (defined('HVAC_QUERY_MONITOR') && HVAC_QUERY_MONITOR) {
|
|
return true;
|
|
}
|
|
|
|
// Enable for admin users with specific parameter
|
|
if (is_admin() && current_user_can('manage_options') && isset($_GET['hvac_monitor'])) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Log database query
|
|
*
|
|
* @param string $query SQL query
|
|
* @return string Original query
|
|
*/
|
|
public static function log_query($query) {
|
|
$start_time = microtime(true);
|
|
|
|
// Get calling function information
|
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
|
|
$caller_info = self::get_caller_info($backtrace);
|
|
|
|
// Execute query timing logic by hooking into WordPress
|
|
add_action('wp_footer', function() use ($query, $start_time, $caller_info) {
|
|
$end_time = microtime(true);
|
|
$execution_time = $end_time - $start_time;
|
|
|
|
self::record_query($query, $execution_time, $caller_info);
|
|
}, 999);
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Record query information
|
|
*
|
|
* @param string $query SQL query
|
|
* @param float $execution_time Query execution time
|
|
* @param array $caller_info Caller information
|
|
*/
|
|
private static function record_query($query, $execution_time, $caller_info) {
|
|
// Skip if query is too common or not from our plugin
|
|
if (!self::should_log_query($query, $caller_info)) {
|
|
return;
|
|
}
|
|
|
|
$is_slow = $execution_time > self::SLOW_QUERY_THRESHOLD;
|
|
|
|
$query_data = [
|
|
'query' => self::sanitize_query($query),
|
|
'execution_time' => round($execution_time, 4),
|
|
'is_slow' => $is_slow,
|
|
'caller' => $caller_info,
|
|
'memory_usage' => memory_get_usage(true),
|
|
'timestamp' => time(),
|
|
'url' => $_SERVER['REQUEST_URI'] ?? 'unknown'
|
|
];
|
|
|
|
self::$queries[] = $query_data;
|
|
|
|
// Update statistics
|
|
self::$stats['total_queries']++;
|
|
self::$stats['total_time'] += $execution_time;
|
|
self::$stats['peak_memory'] = max(self::$stats['peak_memory'], $query_data['memory_usage']);
|
|
|
|
if ($is_slow) {
|
|
self::$stats['slow_queries']++;
|
|
|
|
// Log slow queries immediately
|
|
HVAC_Logger::warning(
|
|
"Slow query detected ({$execution_time}s): " . substr($query, 0, 200),
|
|
'Query Monitor'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get caller information from backtrace
|
|
*
|
|
* @param array $backtrace Debug backtrace
|
|
* @return array Caller info
|
|
*/
|
|
private static function get_caller_info($backtrace) {
|
|
foreach ($backtrace as $trace) {
|
|
$file = $trace['file'] ?? '';
|
|
$function = $trace['function'] ?? '';
|
|
|
|
// Look for HVAC plugin functions
|
|
if (strpos($file, 'hvac-community-events') !== false) {
|
|
return [
|
|
'file' => basename($file),
|
|
'line' => $trace['line'] ?? 0,
|
|
'function' => $function,
|
|
'class' => $trace['class'] ?? ''
|
|
];
|
|
}
|
|
}
|
|
|
|
// Fallback to first available trace
|
|
$first = $backtrace[0] ?? [];
|
|
return [
|
|
'file' => basename($first['file'] ?? 'unknown'),
|
|
'line' => $first['line'] ?? 0,
|
|
'function' => $first['function'] ?? 'unknown',
|
|
'class' => $first['class'] ?? ''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check if query should be logged
|
|
*
|
|
* @param string $query SQL query
|
|
* @param array $caller_info Caller information
|
|
* @return bool
|
|
*/
|
|
private static function should_log_query($query, $caller_info) {
|
|
// Only log queries from our plugin
|
|
$hvac_files = ['hvac-', 'HVAC_'];
|
|
$is_hvac_query = false;
|
|
|
|
foreach ($hvac_files as $prefix) {
|
|
if (strpos($caller_info['file'], $prefix) !== false ||
|
|
strpos($caller_info['class'], $prefix) !== false) {
|
|
$is_hvac_query = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$is_hvac_query) {
|
|
return false;
|
|
}
|
|
|
|
// Skip common WordPress core queries
|
|
$skip_patterns = [
|
|
'SELECT option_value FROM',
|
|
'SELECT autoload FROM',
|
|
'SELECT meta_value FROM.*_usermeta WHERE meta_key = \'wp_',
|
|
'UPDATE.*_options SET option_value'
|
|
];
|
|
|
|
foreach ($skip_patterns as $pattern) {
|
|
if (preg_match('/' . $pattern . '/i', $query)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sanitize query for logging
|
|
*
|
|
* @param string $query SQL query
|
|
* @return string Sanitized query
|
|
*/
|
|
private static function sanitize_query($query) {
|
|
// Remove potential sensitive data
|
|
$query = preg_replace('/\'[^\']*\'/', "'[DATA]'", $query);
|
|
$query = preg_replace('/\b\d+\b/', '[NUM]', $query);
|
|
|
|
return trim($query);
|
|
}
|
|
|
|
/**
|
|
* Analyze queries on shutdown
|
|
*/
|
|
public static function analyze_queries() {
|
|
if (empty(self::$queries)) {
|
|
return;
|
|
}
|
|
|
|
// Store queries in log
|
|
self::store_query_log();
|
|
|
|
// Generate recommendations
|
|
$recommendations = self::generate_recommendations();
|
|
|
|
if (!empty($recommendations)) {
|
|
HVAC_Logger::info(
|
|
'Query optimization recommendations: ' . implode('; ', $recommendations),
|
|
'Query Monitor'
|
|
);
|
|
}
|
|
|
|
// Add debug output for admin users
|
|
if (current_user_can('manage_options') && isset($_GET['hvac_debug_queries'])) {
|
|
self::output_debug_info();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store query log
|
|
*/
|
|
private static function store_query_log() {
|
|
$existing_log = get_option(self::LOG_OPTION, []);
|
|
|
|
// Add new queries
|
|
$new_log = array_merge($existing_log, self::$queries);
|
|
|
|
// Keep only recent entries
|
|
$new_log = array_slice($new_log, -self::MAX_LOG_ENTRIES);
|
|
|
|
update_option(self::LOG_OPTION, $new_log);
|
|
}
|
|
|
|
/**
|
|
* Generate optimization recommendations
|
|
*
|
|
* @return array Recommendations
|
|
*/
|
|
private static function generate_recommendations() {
|
|
$recommendations = [];
|
|
|
|
// Check for slow queries
|
|
if (self::$stats['slow_queries'] > 0) {
|
|
$recommendations[] = "Found " . self::$stats['slow_queries'] . " slow queries - consider adding indexes or caching";
|
|
}
|
|
|
|
// Check total query time
|
|
if (self::$stats['total_time'] > 1.0) {
|
|
$recommendations[] = "Total query time is high (" . round(self::$stats['total_time'], 2) . "s) - consider query optimization";
|
|
}
|
|
|
|
// Check for duplicate queries
|
|
$query_counts = array_count_values(array_column(self::$queries, 'query'));
|
|
$duplicates = array_filter($query_counts, function($count) { return $count > 1; });
|
|
|
|
if (!empty($duplicates)) {
|
|
$recommendations[] = "Found " . count($duplicates) . " duplicate query patterns - consider caching";
|
|
}
|
|
|
|
// Check memory usage
|
|
if (self::$stats['peak_memory'] > 50 * 1024 * 1024) { // 50MB
|
|
$recommendations[] = "High memory usage detected - consider result set optimization";
|
|
}
|
|
|
|
return $recommendations;
|
|
}
|
|
|
|
/**
|
|
* Output debug information
|
|
*/
|
|
private static function output_debug_info() {
|
|
echo "\n<!-- HVAC Query Monitor Debug -->\n";
|
|
echo "<div style='position:fixed;bottom:0;left:0;background:#000;color:#fff;padding:10px;max-width:50%;max-height:300px;overflow:auto;z-index:9999;font-size:11px;'>";
|
|
echo "<h4>HVAC Query Monitor</h4>";
|
|
echo "<p>Total Queries: " . self::$stats['total_queries'] . " | ";
|
|
echo "Slow Queries: " . self::$stats['slow_queries'] . " | ";
|
|
echo "Total Time: " . round(self::$stats['total_time'], 3) . "s | ";
|
|
echo "Peak Memory: " . size_format(self::$stats['peak_memory']) . "</p>";
|
|
|
|
if (!empty(self::$queries)) {
|
|
echo "<details><summary>Query Details</summary>";
|
|
foreach (array_slice(self::$queries, -10) as $query) {
|
|
echo "<div style='margin:5px 0;padding:5px;background:#333;'>";
|
|
echo "<strong>" . $query['execution_time'] . "s</strong> - ";
|
|
echo htmlspecialchars(substr($query['query'], 0, 100)) . "...";
|
|
echo "<br><small>Called from: " . $query['caller']['file'] . ":" . $query['caller']['line'] . "</small>";
|
|
echo "</div>";
|
|
}
|
|
echo "</details>";
|
|
}
|
|
echo "</div>\n";
|
|
}
|
|
|
|
/**
|
|
* Add admin page
|
|
*/
|
|
public static function add_admin_page() {
|
|
if (current_user_can('manage_options')) {
|
|
add_management_page(
|
|
'HVAC Query Monitor',
|
|
'HVAC Query Monitor',
|
|
'manage_options',
|
|
'hvac-query-monitor',
|
|
[__CLASS__, 'admin_page']
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Admin page content
|
|
*/
|
|
public static function admin_page() {
|
|
$log = get_option(self::LOG_OPTION, []);
|
|
$recent_log = array_slice($log, -20);
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1>HVAC Query Monitor</h1>
|
|
|
|
<div class="card">
|
|
<h3>Query Statistics</h3>
|
|
<p>Total logged queries: <?php echo count($log); ?></p>
|
|
|
|
<?php if (!empty($log)): ?>
|
|
<?php
|
|
$slow_count = count(array_filter($log, function($q) { return $q['is_slow']; }));
|
|
$avg_time = array_sum(array_column($log, 'execution_time')) / count($log);
|
|
?>
|
|
<p>Slow queries: <?php echo $slow_count; ?></p>
|
|
<p>Average execution time: <?php echo round($avg_time, 4); ?>s</p>
|
|
<?php endif; ?>
|
|
|
|
<p>
|
|
<a href="?page=hvac-query-monitor&action=clear" class="button button-secondary">Clear Log</a>
|
|
<a href="?hvac_debug_queries=1" class="button button-primary" target="_blank">Enable Debug Output</a>
|
|
</p>
|
|
</div>
|
|
|
|
<?php if (!empty($recent_log)): ?>
|
|
<div class="card">
|
|
<h3>Recent Queries (Last 20)</h3>
|
|
<table class="wp-list-table widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Duration</th>
|
|
<th>Query</th>
|
|
<th>Caller</th>
|
|
<th>URL</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach (array_reverse($recent_log) as $query): ?>
|
|
<tr <?php echo $query['is_slow'] ? 'style="background:#ffebee;"' : ''; ?>>
|
|
<td><?php echo date('H:i:s', $query['timestamp']); ?></td>
|
|
<td><?php echo $query['execution_time']; ?>s</td>
|
|
<td title="<?php echo esc_attr($query['query']); ?>">
|
|
<?php echo esc_html(substr($query['query'], 0, 80)) . '...'; ?>
|
|
</td>
|
|
<td><?php echo esc_html($query['caller']['file'] . ':' . $query['caller']['line']); ?></td>
|
|
<td><?php echo esc_html($query['url']); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* AJAX handler to clear log
|
|
*/
|
|
public static function ajax_clear_log() {
|
|
if (!current_user_can('manage_options') ||
|
|
!check_ajax_referer('hvac_ajax_nonce', 'nonce', false)) {
|
|
wp_send_json_error('Unauthorized');
|
|
}
|
|
|
|
delete_option(self::LOG_OPTION);
|
|
wp_send_json_success('Query log cleared');
|
|
}
|
|
|
|
/**
|
|
* Get query statistics
|
|
*
|
|
* @return array Statistics
|
|
*/
|
|
public static function get_stats() {
|
|
$log = get_option(self::LOG_OPTION, []);
|
|
|
|
if (empty($log)) {
|
|
return [
|
|
'total_queries' => 0,
|
|
'slow_queries' => 0,
|
|
'average_time' => 0,
|
|
'recommendations' => []
|
|
];
|
|
}
|
|
|
|
$slow_queries = array_filter($log, function($q) { return $q['is_slow']; });
|
|
$total_time = array_sum(array_column($log, 'execution_time'));
|
|
|
|
return [
|
|
'total_queries' => count($log),
|
|
'slow_queries' => count($slow_queries),
|
|
'average_time' => $total_time / count($log),
|
|
'total_time' => $total_time,
|
|
'recommendations' => self::generate_recommendations_from_log($log)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate recommendations from stored log
|
|
*
|
|
* @param array $log Query log
|
|
* @return array Recommendations
|
|
*/
|
|
private static function generate_recommendations_from_log($log) {
|
|
$recommendations = [];
|
|
|
|
// Analyze patterns in stored log
|
|
$query_patterns = [];
|
|
foreach ($log as $query_data) {
|
|
$pattern = preg_replace('/\[DATA\]|\[NUM\]/', 'X', $query_data['query']);
|
|
$query_patterns[$pattern] = ($query_patterns[$pattern] ?? 0) + 1;
|
|
}
|
|
|
|
// Find frequently repeated queries
|
|
$frequent_queries = array_filter($query_patterns, function($count) { return $count > 5; });
|
|
if (!empty($frequent_queries)) {
|
|
$recommendations[] = "Consider caching for " . count($frequent_queries) . " frequently repeated queries";
|
|
}
|
|
|
|
return $recommendations;
|
|
}
|
|
}
|