Map of metric_key => metric_value collected. */ public static function collect( bool $dry_run = false ): array { global $wpdb; $now = TMDO_DB::now(); $metrics = array(); // ── EAV row counts ──────────────────────────────────────────────── $eav_tables = array( 'eav.postmeta_rows' => $wpdb->postmeta, 'eav.usermeta_rows' => $wpdb->usermeta, 'eav.termmeta_rows' => $wpdb->termmeta, 'eav.commentmeta_rows' => $wpdb->commentmeta, ); foreach ( $eav_tables as $key => $table ) { $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB $metrics[ $key ] = $count; } // ── Flat-table row counts (hot + cold dynamic tables, warm) ─────── $hot_rows = 0; $cold_rows = 0; if ( class_exists( 'TMDO_Schema_Registry' ) ) { $registry = TMDO_Schema_Registry::instance(); foreach ( $registry->get_hot_post_types() as $pt ) { $ht = $wpdb->prefix . 'wpdo_hot_' . sanitize_key( $pt ); $exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $ht ) ); // phpcs:ignore WordPress.DB if ( $exists ) { $hot_rows += (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$ht}`" ); // phpcs:ignore WordPress.DB } } foreach ( $registry->get_cold_post_types() as $pt ) { $ct = $wpdb->prefix . 'wpdo_cold_' . sanitize_key( $pt ); $exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $ct ) ); // phpcs:ignore WordPress.DB if ( $exists ) { $cold_rows += (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$ct}`" ); // phpcs:ignore WordPress.DB } } } $warm_table = $wpdb->prefix . 'wpdo_warm'; $metrics['flat.hot_rows'] = $hot_rows; $metrics['flat.cold_rows'] = $cold_rows; $metrics['flat.warm_rows'] = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$warm_table}`" ); // phpcs:ignore WordPress.DB // ── Custom table row counts ─────────────────────────────────────── $custom_rows = 0; $custom_count = 0; if ( class_exists( 'TMDO_Custom_Table_Registry' ) ) { foreach ( TMDO_Custom_Table_Registry::instance()->all() as $cfg ) { $tbl = $wpdb->prefix . $cfg['table_name']; $exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $tbl ) ); // phpcs:ignore WordPress.DB if ( $exists ) { $custom_rows += (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$tbl}`" ); // phpcs:ignore WordPress.DB ++$custom_count; } } } $metrics['custom_tables.total_rows'] = $custom_rows; $metrics['custom_tables.table_count'] = $custom_count; // ── Error / shadow-diff activity (last 24 h) ───────────────────── $errors_table = $wpdb->prefix . 'wpdo_errors'; $metrics['errors.last_24h'] = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$errors_table}` WHERE created_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 24 HOUR)" // phpcs:ignore WordPress.DB ); $shadow_table = $wpdb->prefix . 'wpdo_shadow_diffs'; $shadow_exists = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $shadow_table ) ); // phpcs:ignore WordPress.DB $metrics['shadow_diffs.last_24h'] = $shadow_exists ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$shadow_table}` WHERE ts >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 24 HOUR)" ) // phpcs:ignore WordPress.DB : 0; if ( $dry_run ) { return $metrics; } // ── Persist each metric to wpdo_site_metrics ───────────────────── $dest = TMDO_DB::table( 'wpdo_site_metrics' ); foreach ( $metrics as $key => $value ) { $wpdb->insert( $dest, array( 'collected_at' => $now, 'metric_key' => $key, 'metric_value' => $value, 'context' => null, ), array( '%s', '%s', '%d', '%s' ) ); } // ── Prune old rows beyond retention window ──────────────────────── $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-' . self::RETENTION_DAYS . ' days' ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$dest} WHERE collected_at < %s", $cutoff ) ); // phpcs:ignore WordPress.DB if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'site_metrics_collected', array( 'metric_count' => count( $metrics ), 'postmeta_rows' => $metrics['eav.postmeta_rows'] ?? 0, 'hot_rows' => $metrics['flat.hot_rows'] ?? 0, 'custom_rows' => $metrics['custom_tables.total_rows'] ?? 0, ) ); } return $metrics; } /** * Get the most recent snapshot (latest collected_at timestamp). * * @return array metric_key => metric_value, or empty on miss. */ public static function get_latest_snapshot(): array { global $wpdb; $dest = TMDO_DB::table( 'wpdo_site_metrics' ); $latest_ts = $wpdb->get_var( "SELECT MAX(collected_at) FROM `{$dest}`" ); // phpcs:ignore WordPress.DB if ( ! $latest_ts ) { return array(); } $rows = (array) $wpdb->get_results( $wpdb->prepare( "SELECT metric_key, metric_value FROM `{$dest}` WHERE collected_at = %s", // phpcs:ignore WordPress.DB $latest_ts ), ARRAY_A ); $out = array(); foreach ( $rows as $r ) { $out[ (string) $r['metric_key'] ] = (int) $r['metric_value']; } return $out; } /** * Get daily metric history for a single key over the past N days. * * @param string $metric_key Metric key (e.g. 'eav.postmeta_rows'). * @param int $days Number of days of history to return (default 30). * @return array Oldest-first. */ public static function get_history( string $metric_key, int $days = 30 ): array { global $wpdb; $dest = TMDO_DB::table( 'wpdo_site_metrics' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $dest is a validated table name from TMDO_DB::table(). $rows = (array) $wpdb->get_results( $wpdb->prepare( "SELECT collected_at, metric_value AS value FROM `{$dest}` WHERE metric_key = %s AND collected_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) ORDER BY collected_at ASC", $metric_key, $days ), ARRAY_A ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared return array_map( static fn( $r ) => array( 'collected_at' => (string) $r['collected_at'], 'value' => (int) $r['value'], ), $rows ); } }