upskill-event-manager/includes/class-hvac-query-monitor.php
bengizmo 5f240e4112 feat: Add Organization Headquarters dropdown fields to registration form
- 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>
2025-08-08 10:35:14 -03:00

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;
}
}