From e9a622f454bd7430aceda460cdd57b4949ba991a Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Mon, 8 Nov 2021 16:21:06 -0300 Subject: [PATCH 1/8] Remove unnecessary setup() calls Singleton instantiator already calls setup() --- 10up-experience.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/10up-experience.php b/10up-experience.php index c614160..fcb0272 100644 --- a/10up-experience.php +++ b/10up-experience.php @@ -70,19 +70,19 @@ function( $plugin_info, $http_response = null ) { define( 'TENUP_EXPERIENCE_IS_NETWORK', (bool) $network_activated ); if ( ! defined( 'TENUP_DISABLE_BRANDING' ) || ! TENUP_DISABLE_BRANDING ) { - AdminCustomizations\Customizations::instance()->setup(); + AdminCustomizations\Customizations::instance(); } -API\API::instance()->setup(); -Authentication\Usernames::instance()->setup(); -Authors\Authors::instance()->setup(); -Gutenberg\Gutenberg::instance()->setup(); -Headers\Headers::instance()->setup(); -Plugins\Plugins::instance()->setup(); -PostPasswords\PostPasswords::instance()->setup(); -SupportMonitor\Monitor::instance()->setup(); -SupportMonitor\Debug::instance()->setup(); -Notifications\Welcome::instance()->setup(); +API\API::instance(); +Authentication\Usernames::instance(); +Authors\Authors::instance(); +Gutenberg\Gutenberg::instance(); +Headers\Headers::instance(); +Plugins\Plugins::instance(); +PostPasswords\PostPasswords::instance(); +SupportMonitor\Monitor::instance(); +SupportMonitor\Debug::instance(); +Notifications\Welcome::instance(); /** * We load this later to make sure there are no conflicts with other plugins. @@ -90,7 +90,7 @@ function( $plugin_info, $http_response = null ) { add_action( 'plugins_loaded', function() { - Authentication\Passwords::instance()->setup(); + Authentication\Passwords::instance(); } ); From 6da59cedab116071178dda8a710c30c7e541cf5c Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Mon, 8 Nov 2021 16:37:57 -0300 Subject: [PATCH 2/8] Initial version of DBQueryMonitor --- .../classes/SupportMonitor/DBQueryMonitor.php | 173 ++++++++++++++++++ includes/classes/SupportMonitor/Monitor.php | 11 ++ 2 files changed, 184 insertions(+) create mode 100644 includes/classes/SupportMonitor/DBQueryMonitor.php diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php new file mode 100644 index 0000000..f9ee7ca --- /dev/null +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -0,0 +1,173 @@ +get_setting( 'production_environment' ); + if ( 'no' === $production_environment || apply_filters( 'tenup_experience_log_heavy_queries', false ) ) { + add_filter( 'query', [ $this, 'maybe_log_query' ] ); + } + } + + /** + * Conditionally log a query + * + * Get all potential heavy queries (CREATE, ALTER, etc.) and store it, + * ignoring transients and options by default. + * + * @param string $query The SQL query. + * @return string + */ + public function maybe_log_query( $query ) { + global $wpdb; + + if ( ! preg_match( '/^\s*(create|alter|truncate|drop|insert|delete|update|replace)\s/i', $query ) ) { + return $query; + } + + if ( false !== strpos( $query, 'transient_' ) ) { + return $query; + } + + if ( false !== strpos( $query, $wpdb->options ) ) { + return $query; + } + + if ( apply_filters( 'tenup_experience_log_query', true, $query ) ) { + $this->log_query( $query ); + } + + return $query; + } + + /** + * Get the logged queries. Also removes from the transient all queries logged for more than 7 days. + * + * @return array + */ + public function get_report() { + $date_limit = strtotime( '7 days ago' ); + + $stored_queries = array_filter( + (array) get_transient( self::TRANSIENT_NAME ), + function ( $query_date ) use ( $date_limit ) { + return strtotime( $query_date ) > $date_limit; + }, + ARRAY_FILTER_USE_KEY + ); + + set_transient( self::TRANSIENT_NAME, $stored_queries ); + + return $stored_queries; + } + + /** + * Log/store a SQL query + * + * Queries are stored like: + * + * 'YYYY-MM-DD' => [ + * 'a5be998feee8968155052c4d332a7223' => [ // md5 of file:line:query + * 'query' => 'ALTER TABLE wp_my_db_heavy_plugin CHANGE COLUMN `id` id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT', + * 'file' => '/var/www/html/wp-content/plugins/my-db-heavy-plugin/my-db-heavy-plugin.php', + * 'line' => 53, + * 'count' => 1, + * ] + * ] + * + * @param string $query The SQL query. + */ + protected function log_query( $query ) { + static $stored_queries; + if ( empty( $stored_queries ) ) { + $stored_queries = get_transient( self::TRANSIENT_NAME ) ?? []; + } + + $current_date = date_i18n( 'Y-m-d' ); + + if ( ! isset( $stored_queries[ $current_date ] ) ) { + $stored_queries[ $current_date ] = []; + } + + $main_caller = $this->find_main_caller(); + + $key = md5( + $main_caller['file'] . ':' . + $main_caller['line'] . ':' . + $query + ); + + if ( isset( $stored_queries[ $current_date ][ $key ] ) ) { + $stored_queries[ $current_date ][ $key ]['count']++; + } else { + $stored_queries[ $current_date ][ $key ] = [ + 'query' => $query, + 'file' => $main_caller['file'], + 'line' => $main_caller['line'], + 'count' => 1, + ]; + } + + set_transient( self::TRANSIENT_NAME, $stored_queries ); + } + + /** + * Based on the debug backtrace, try to find the main caller, i.e., the plugin/theme + * that fired the query. + * + * Simple SQL queries generally come from wp-db.php. dbDelta calls come from upgrade.php. + * We are usually interested in the caller immediately before those. + * + * @return array + */ + protected function find_main_caller() { + $debug_backtrace = debug_backtrace(); // phpcs:ignore + + // Remove this plugin references of the backtrace. + array_shift( $debug_backtrace ); + array_shift( $debug_backtrace ); + + $main_caller = null; + + $wp_db_found = false; + foreach ( $debug_backtrace as $caller ) { + $is_wp_db_file = ( false !== strpos( $caller['file'], 'wp-db.php' ) || false !== strpos( $caller['file'], 'upgrade.php' ) ); + if ( $is_wp_db_file ) { + $wp_db_found = true; + } + if ( ! $wp_db_found || $is_wp_db_file ) { + continue; + } + $main_caller = $caller; + break; + } + + // If a caller was not found simply get the first item of the backtrace. + if ( ! $main_caller ) { + $main_caller = array_shift( $debug_backtrace ); + } + + return $main_caller; + } +} diff --git a/includes/classes/SupportMonitor/Monitor.php b/includes/classes/SupportMonitor/Monitor.php index 35fb200..b1761e6 100644 --- a/includes/classes/SupportMonitor/Monitor.php +++ b/includes/classes/SupportMonitor/Monitor.php @@ -15,12 +15,22 @@ * Monitor class */ class Monitor extends Singleton { + /** + * The DBQueryMonitor instance. + * + * @since x.x + * @var DBQueryMonitor + */ + protected $db_query_monitor; + /** * Setup module * * @since 1.7 */ public function setup() { + $this->db_query_monitor = new DBQueryMonitor(); + $this->db_query_monitor->setup(); if ( TENUP_EXPERIENCE_IS_NETWORK ) { add_action( 'wpmu_options', [ $this, 'ms_settings' ] ); @@ -461,6 +471,7 @@ public function send_daily_report() { $this->format_message( [ 'php_version' => $this->get_php_version(), + 'db_queries' => $this->db_query_monitor->get_report(), ], 'notice', 'system' From 0bec10e2e5a0e07e249fdd5d29849128b86791fa Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Wed, 10 Nov 2021 13:29:59 -0300 Subject: [PATCH 3/8] Escape query to avoid breaking json_decode later --- includes/classes/SupportMonitor/DBQueryMonitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php index f9ee7ca..b5c6918 100644 --- a/includes/classes/SupportMonitor/DBQueryMonitor.php +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -122,7 +122,7 @@ protected function log_query( $query ) { $stored_queries[ $current_date ][ $key ]['count']++; } else { $stored_queries[ $current_date ][ $key ] = [ - 'query' => $query, + 'query' => stripslashes( $query ), 'file' => $main_caller['file'], 'line' => $main_caller['line'], 'count' => 1, From 07cd95245658adda4027a402c208e6fe3ca54071 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 11 Nov 2021 17:48:01 -0300 Subject: [PATCH 4/8] New queries page, reduce Support Monitor message + docs --- .../classes/SupportMonitor/DBQueryMonitor.php | 216 +++++++++++++++++- includes/classes/SupportMonitor/Monitor.php | 4 +- 2 files changed, 207 insertions(+), 13 deletions(-) diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php index b5c6918..bf3132e 100644 --- a/includes/classes/SupportMonitor/DBQueryMonitor.php +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -2,6 +2,17 @@ /** * Database Queries Monitor. A submodule of Support Monitor to report heavy SQL queries executed on staging. * + * This feature is turned off in production environments but can be enabled + * using `add_filter( 'tenup_experience_enable_query_monitor', '__return_true' );` + * + * The original purpose of this feature is to log any heavy SQL query performed, for example, + * during a plugins upgrade. These are the queries logged: + * - All create, alter, truncate, drop queries + * - Insert, delete, update, and replace queries without a nested select and bigger than 20000 chars + * - Transients and options operations are ignored by default + * + * Additional checks can be created using the `tenup_experience_log_query` filter. + * * @since x.x * @package 10up-experience */ @@ -20,13 +31,131 @@ class DBQueryMonitor { */ const TRANSIENT_NAME = 'tenup_experience_db_queries'; + /** + * Minimum size of insert, delete, update, and replace queries to be logged. + * Queries with a nested select in it will always be logged. + * + * CUD instead of CRUD because select/(R)ead queries are not logged. + */ + const CUD_QUERY_SIZE = 20000; + /** * Setup module */ public function setup() { $production_environment = Monitor::instance()->get_setting( 'production_environment' ); - if ( 'no' === $production_environment || apply_filters( 'tenup_experience_log_heavy_queries', false ) ) { - add_filter( 'query', [ $this, 'maybe_log_query' ] ); + + /** + * Filter if the Query Monitor should be enabled. Defaults to true on non-production environments. + * + * Having it as true does not mean all queries will be logged, as they will be checked as being + * heavy or not first. + * + * @since x.x + * @hook tenup_experience_enable_query_monitor + * @param {bool} $should_log Whether Query Monitor should be enabled. + * @return {bool} New value + */ + if ( ! apply_filters( 'tenup_experience_enable_query_monitor', 'no' === $production_environment ) ) { + return; + } + + if ( TENUP_EXPERIENCE_IS_NETWORK ) { + add_action( 'network_admin_menu', [ $this, 'register_network_menu' ] ); + } else { + add_action( 'admin_menu', [ $this, 'register_menu' ] ); + } + + add_action( 'admin_init', [ $this, 'empty_queries' ] ); + + add_filter( 'query', [ $this, 'maybe_log_query' ] ); + } + + /** + * Registers the Query Monitor link under the 'Tools' menu + * + * @since x.x + */ + public function register_menu() { + add_submenu_page( + 'tools.php', + esc_html__( '10up Query Monitor', 'tenup' ), + esc_html__( '10up Query Monitor', 'tenup' ), + 'manage_options', + 'tenup_query_monitor', + [ $this, 'queries_list_screen' ] + ); + } + + /** + * Registers the Query Monitor link under the network settings + * + * @since x.x + */ + public function register_network_menu() { + add_submenu_page( + 'settings.php', + esc_html__( '10up Query Monitor', 'tenup' ), + esc_html__( '10up Query Monitor', 'tenup' ), + 'manage_network_options', + 'tenup_query_monitor', + [ $this, 'queries_list_screen' ] + ); + } + + /** + * Output the queries screen + * + * @since x.x + */ + public function queries_list_screen() { + $queries_per_date = $this->get_transient(); + ?> + +
+

+ +

+ +

+ + + $queries ) : ?> +

+ +
+
+
+
+

+
+ + + +

+ +
+ log_query( $query ); } @@ -62,24 +209,39 @@ public function maybe_log_query( $query ) { } /** - * Get the logged queries. Also removes from the transient all queries logged for more than 7 days. + * Generate the response for the Support Monitor. * - * @return array + * Also, as this is called periodically, removes from the transient all old queries. + * + * @see cleanup_queries() + * + * @return bool Whether queries were logged recently or not. */ public function get_report() { + $this->cleanup_queries(); + + $queries_per_date = $this->get_transient(); + + return ( ! empty( $queries_per_date ) ); + } + + /** + * Remove old queries from the transient. + * + * @return void + */ + protected function cleanup_queries() { $date_limit = strtotime( '7 days ago' ); $stored_queries = array_filter( - (array) get_transient( self::TRANSIENT_NAME ), + $this->get_transient(), function ( $query_date ) use ( $date_limit ) { return strtotime( $query_date ) > $date_limit; }, ARRAY_FILTER_USE_KEY ); - set_transient( self::TRANSIENT_NAME, $stored_queries ); - - return $stored_queries; + $this->set_transient( $stored_queries ); } /** @@ -92,7 +254,7 @@ function ( $query_date ) use ( $date_limit ) { * 'query' => 'ALTER TABLE wp_my_db_heavy_plugin CHANGE COLUMN `id` id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT', * 'file' => '/var/www/html/wp-content/plugins/my-db-heavy-plugin/my-db-heavy-plugin.php', * 'line' => 53, - * 'count' => 1, + * 'count' => 3, // how many times the same query was sent in a day * ] * ] * @@ -122,14 +284,14 @@ protected function log_query( $query ) { $stored_queries[ $current_date ][ $key ]['count']++; } else { $stored_queries[ $current_date ][ $key ] = [ - 'query' => stripslashes( $query ), + 'query' => $query, 'file' => $main_caller['file'], 'line' => $main_caller['line'], 'count' => 1, ]; } - set_transient( self::TRANSIENT_NAME, $stored_queries ); + $this->set_transient( $stored_queries ); } /** @@ -170,4 +332,36 @@ protected function find_main_caller() { return $main_caller; } + + /** + * Get the transient value. + * + * @return array + */ + protected function get_transient() { + if ( TENUP_EXPERIENCE_IS_NETWORK ) { + $transient = get_site_transient( self::TRANSIENT_NAME ); + } else { + $transient = get_transient( self::TRANSIENT_NAME ); + } + + if ( ! is_array( $transient ) ) { + $transient = []; + } + + return $transient; + } + + /** + * Set the transient value. + * + * @param array $queries Queries to be stored. + */ + protected function set_transient( $queries ) { + if ( TENUP_EXPERIENCE_IS_NETWORK ) { + set_site_transient( self::TRANSIENT_NAME, $queries ); + } else { + set_transient( self::TRANSIENT_NAME, $queries ); + } + } } diff --git a/includes/classes/SupportMonitor/Monitor.php b/includes/classes/SupportMonitor/Monitor.php index b1761e6..d9d3464 100644 --- a/includes/classes/SupportMonitor/Monitor.php +++ b/includes/classes/SupportMonitor/Monitor.php @@ -470,8 +470,8 @@ public function send_daily_report() { ), $this->format_message( [ - 'php_version' => $this->get_php_version(), - 'db_queries' => $this->db_query_monitor->get_report(), + 'php_version' => $this->get_php_version(), + 'had_heavy_db_queries' => $this->db_query_monitor->get_report(), ], 'notice', 'system' From 21f2ab6bc9caca6a1dbdb199a5080e51c7c44acf Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 9 Dec 2021 10:34:00 -0300 Subject: [PATCH 5/8] Change DB Query Monitor to be Opt In Add a field in the dashboard so admins can enable it --- .../classes/SupportMonitor/DBQueryMonitor.php | 45 +++++++++++------- includes/classes/SupportMonitor/Monitor.php | 47 +++++++++++++++++-- 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php index bf3132e..cce8ee0 100644 --- a/includes/classes/SupportMonitor/DBQueryMonitor.php +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -2,8 +2,9 @@ /** * Database Queries Monitor. A submodule of Support Monitor to report heavy SQL queries executed on staging. * - * This feature is turned off in production environments but can be enabled - * using `add_filter( 'tenup_experience_enable_query_monitor', '__return_true' );` + * This feature is turned off by default. + * For performance reasons, it is only available in production environments by default, but that + * can be changed using `add_filter( 'tenup_experience_disable_query_monitor', '__return_false' );` * * The original purpose of this feature is to log any heavy SQL query performed, for example, * during a plugins upgrade. These are the queries logged: @@ -43,20 +44,9 @@ class DBQueryMonitor { * Setup module */ public function setup() { - $production_environment = Monitor::instance()->get_setting( 'production_environment' ); + $is_enabled = 'yes' === Monitor::instance()->get_setting( 'enable_db_query_monitor' ); - /** - * Filter if the Query Monitor should be enabled. Defaults to true on non-production environments. - * - * Having it as true does not mean all queries will be logged, as they will be checked as being - * heavy or not first. - * - * @since x.x - * @hook tenup_experience_enable_query_monitor - * @param {bool} $should_log Whether Query Monitor should be enabled. - * @return {bool} New value - */ - if ( ! apply_filters( 'tenup_experience_enable_query_monitor', 'no' === $production_environment ) ) { + if ( ! $this->is_available() || ! $is_enabled ) { return; } @@ -71,6 +61,29 @@ public function setup() { add_filter( 'query', [ $this, 'maybe_log_query' ] ); } + /** + * Utilitary function to check if the feature is available to be enabled or not. + * + * @return boolean + */ + public function is_available() { + $is_production = 'no' === Monitor::instance()->get_setting( 'production_environment' ); + + /** + * Filter if the Query Monitor should be available. Defaults to false on non-production environments. + * + * If it is available, it's still needed to enable the feature in the dashboard. Having it enabled + * does not mean all queries will be logged, as they will be checked as being + * heavy or not first. + * + * @since x.x + * @hook tenup_experience_disable_query_monitor + * @param {bool} $should_log Whether Query Monitor should be enabled. + * @return {bool} New value + */ + return apply_filters( 'tenup_experience_disable_query_monitor', $is_production ); + } + /** * Registers the Query Monitor link under the 'Tools' menu * @@ -263,7 +276,7 @@ function ( $query_date ) use ( $date_limit ) { protected function log_query( $query ) { static $stored_queries; if ( empty( $stored_queries ) ) { - $stored_queries = get_transient( self::TRANSIENT_NAME ) ?? []; + $stored_queries = $this->get_transient( self::TRANSIENT_NAME ) ?? []; } $current_date = date_i18n( 'Y-m-d' ); diff --git a/includes/classes/SupportMonitor/Monitor.php b/includes/classes/SupportMonitor/Monitor.php index d9d3464..705891d 100644 --- a/includes/classes/SupportMonitor/Monitor.php +++ b/includes/classes/SupportMonitor/Monitor.php @@ -89,6 +89,12 @@ public function ms_save_settings() { $setting['server_url'] = sanitize_text_field( $_POST['tenup_support_monitor_settings']['server_url'] ); } + if ( ! $this->db_query_monitor->is_available() || ! isset( $_POST['tenup_support_monitor_settings']['enable_db_query_monitor'] ) ) { + $setting['enable_db_query_monitor'] = 'no'; + } else { + $setting['enable_db_query_monitor'] = sanitize_text_field( $_POST['tenup_support_monitor_settings']['enable_db_query_monitor'] ); + } + update_site_option( 'tenup_support_monitor_settings', $setting ); } @@ -134,6 +140,14 @@ public function ms_settings() { + db_query_monitor->is_available() ) : ?> + + + + enable_db_query_monitor_field(); ?> + + + 'no', - 'api_key' => '', - 'server_url' => 'https://supportmonitor.10up.com', - 'production_environment' => 'no', + 'enable_support_monitor' => 'no', + 'api_key' => '', + 'server_url' => 'https://supportmonitor.10up.com', + 'production_environment' => 'no', + 'enable_db_query_monitor' => 'no', ]; $settings = ( TENUP_EXPERIENCE_IS_NETWORK ) ? get_site_option( 'tenup_support_monitor_settings', [] ) : get_option( 'tenup_support_monitor_settings', [] ); @@ -277,6 +292,15 @@ public function register_settings() { ); } + if ( $this->db_query_monitor->is_available() ) { + add_settings_field( + 'enable_db_query_monitor', + esc_html__( 'DB Query Monitor', 'tenup' ), + [ $this, 'enable_db_query_monitor_field' ], + 'general', + 'tenup_support_monitor' + ); + } } /** @@ -345,6 +369,21 @@ public function api_server_field() { get_setting( 'enable_db_query_monitor' ); + ?> + type="radio" id="tenup_enable_db_query_monitor_yes" value="yes">
+ type="radio" id="tenup_enable_db_query_monitor_no" value="no"> +

+ +

+ Date: Fri, 10 Dec 2021 13:27:51 -0300 Subject: [PATCH 6/8] Escape INSERT and UPDATE queries --- .../classes/SupportMonitor/DBQueryMonitor.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php index cce8ee0..2579270 100644 --- a/includes/classes/SupportMonitor/DBQueryMonitor.php +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -297,7 +297,7 @@ protected function log_query( $query ) { $stored_queries[ $current_date ][ $key ]['count']++; } else { $stored_queries[ $current_date ][ $key ] = [ - 'query' => $query, + 'query' => $this->escape_query( $query ), 'file' => $main_caller['file'], 'line' => $main_caller['line'], 'count' => 1, @@ -346,6 +346,25 @@ protected function find_main_caller() { return $main_caller; } + /** + * Escape queries to avoid storing sensitive info. + * + * This function takes INSERTs and UPDATEs and replace all parameter values + * between the `'` and `"` chars with `?` + * + * @param string $query The SQL query. + * @return string + */ + protected function escape_query( $query ) { + if ( ! preg_match( '/UPDATE|INSERT/', $query ) ) { + return $query; + } + + $query = preg_replace( "/[\"'](.*?)[\"']/", '?', $query ); + + return $query; + } + /** * Get the transient value. * From ef6bea07c5ac300666b2c19f3c26dbc162b67ffd Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Mon, 31 Jan 2022 14:34:11 -0300 Subject: [PATCH 7/8] Apply data size restrictions and move to its own message --- .../classes/SupportMonitor/DBQueryMonitor.php | 62 ++++++++++++++++--- includes/classes/SupportMonitor/Monitor.php | 11 +++- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php index 2579270..a794f3f 100644 --- a/includes/classes/SupportMonitor/DBQueryMonitor.php +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -40,13 +40,21 @@ class DBQueryMonitor { */ const CUD_QUERY_SIZE = 20000; + /** + * Max. size of the data stored. + */ + const STORE_MAX_SIZE = MB_IN_BYTES; + + /** + * Max. size of the data to be sent to the API. + */ + const SEND_API_MAX_SIZE = MB_IN_BYTES; + /** * Setup module */ public function setup() { - $is_enabled = 'yes' === Monitor::instance()->get_setting( 'enable_db_query_monitor' ); - - if ( ! $this->is_available() || ! $is_enabled ) { + if ( ! $this->is_enabled() ) { return; } @@ -84,6 +92,17 @@ public function is_available() { return apply_filters( 'tenup_experience_disable_query_monitor', $is_production ); } + /** + * Return whether the submodule is enabled or not. + * + * @return boolean + */ + public function is_enabled() { + $is_enabled = 'yes' === Monitor::instance()->get_setting( 'enable_db_query_monitor' ); + + return $this->is_available() && $is_enabled; + } + /** * Registers the Query Monitor link under the 'Tools' menu * @@ -233,9 +252,23 @@ public function maybe_log_query( $query ) { public function get_report() { $this->cleanup_queries(); - $queries_per_date = $this->get_transient(); + $all_queries_stored = $this->get_transient(); + $queries_to_send = $all_queries_stored; + $response = [ + 'trimmed' => false, + 'queries' => [], + ]; + + $queries_str = wp_json_encode( $all_queries_stored ); + while ( mb_strlen( $queries_str ) > self::SEND_API_MAX_SIZE ) { + array_shift( $queries_to_send ); + $response['trimmed'] = true; + $queries_str = wp_json_encode( $queries_to_send ); + } - return ( ! empty( $queries_per_date ) ); + $response['queries'] = $all_queries_stored; + + return $response; } /** @@ -377,7 +410,9 @@ protected function get_transient() { $transient = get_transient( self::TRANSIENT_NAME ); } - if ( ! is_array( $transient ) ) { + $transient = json_decode( $transient, true ); + + if ( ! is_array( $transient ) || JSON_ERROR_NONE !== json_last_error() ) { $transient = []; } @@ -390,10 +425,21 @@ protected function get_transient() { * @param array $queries Queries to be stored. */ protected function set_transient( $queries ) { + // Order the array, so older entries will be at the start. + ksort( $queries ); + + // JSON objects will be smaller than PHP serialized() ones. + $queries_str = wp_json_encode( $queries ); + + while ( mb_strlen( $queries_str ) > self::STORE_MAX_SIZE ) { + array_shift( $queries ); + $queries_str = wp_json_encode( $queries ); + } + if ( TENUP_EXPERIENCE_IS_NETWORK ) { - set_site_transient( self::TRANSIENT_NAME, $queries ); + set_site_transient( self::TRANSIENT_NAME, $queries_str ); } else { - set_transient( self::TRANSIENT_NAME, $queries ); + set_transient( self::TRANSIENT_NAME, $queries_str ); } } } diff --git a/includes/classes/SupportMonitor/Monitor.php b/includes/classes/SupportMonitor/Monitor.php index 705891d..7ee8590 100644 --- a/includes/classes/SupportMonitor/Monitor.php +++ b/includes/classes/SupportMonitor/Monitor.php @@ -509,8 +509,7 @@ public function send_daily_report() { ), $this->format_message( [ - 'php_version' => $this->get_php_version(), - 'had_heavy_db_queries' => $this->db_query_monitor->get_report(), + 'php_version' => $this->get_php_version(), ], 'notice', 'system' @@ -533,6 +532,14 @@ public function send_daily_report() { ); } + if ( $this->db_query_monitor->is_enabled() ) { + $messages[] = $this->format_message( + $this->db_query_monitor->get_report(), + 'notice', + 'db_queries' + ); + } + $this->send_request( $messages ); } From f2b84cc90d47dd58c07299c49b29e671b82c791b Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Tue, 8 Feb 2022 11:25:54 -0300 Subject: [PATCH 8/8] Add query categorization --- .../classes/SupportMonitor/DBQueryMonitor.php | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/includes/classes/SupportMonitor/DBQueryMonitor.php b/includes/classes/SupportMonitor/DBQueryMonitor.php index a794f3f..e22c582 100644 --- a/includes/classes/SupportMonitor/DBQueryMonitor.php +++ b/includes/classes/SupportMonitor/DBQueryMonitor.php @@ -50,6 +50,15 @@ class DBQueryMonitor { */ const SEND_API_MAX_SIZE = MB_IN_BYTES; + /** + * Category/score of a query. + */ + const QUERY_CATEGORIES = [ + 'high' => 1, + 'medium' => 2, + 'low' => 3, + ]; + /** * Setup module */ @@ -141,7 +150,8 @@ public function register_network_menu() { * @since x.x */ public function queries_list_screen() { - $queries_per_date = $this->get_transient(); + $queries_per_date = $this->get_categorized_queries(); + $status_names = array_flip( self::QUERY_CATEGORIES ); ?>
@@ -156,6 +166,7 @@ public function queries_list_screen() {

+



@@ -252,7 +263,7 @@ public function maybe_log_query( $query ) { public function get_report() { $this->cleanup_queries(); - $all_queries_stored = $this->get_transient(); + $all_queries_stored = $this->get_categorized_queries(); $queries_to_send = $all_queries_stored; $response = [ 'trimmed' => false, @@ -398,6 +409,46 @@ protected function escape_query( $query ) { return $query; } + /** + * Get the categorized and ordered queries list. + * + * Depending on the query, it may be more or less resource expensive. This function gets all + * queries and return them ordered from the more expensive to the less, and then from the ones + * with higher count to the fewer ones. + * + * @return string + */ + protected function get_categorized_queries() { + $queries_per_date = $this->get_transient(); + foreach ( $queries_per_date as &$queries_in_day ) { + array_walk( + $queries_in_day, + function( &$query ) { + if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query['query'] ) ) { + $query['status'] = self::QUERY_CATEGORIES['medium']; + + if ( $query['count'] > 1 ) { + $query['status'] = self::QUERY_CATEGORIES['high']; + } + } else { + $query['status'] = self::QUERY_CATEGORIES['low']; + } + } + ); + uasort( + $queries_in_day, + function ( $a, $b ) { + if ( $a['status'] === $b['status'] ) { + return $a['count'] >= $b['count'] ? -1 : 1; + } + return $a['status'] <= $b['status'] ? -1 : 1; + } + ); + } + + return $queries_per_date; + } + /** * Get the transient value. *