76c01e44df
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入 並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
239 lines
8.4 KiB
PHP
239 lines
8.4 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Site_Metrics_Collector — daily EAV health snapshot writer.
|
|
*
|
|
* Writes structured rows to `wpdo_site_metrics` once per day (via
|
|
* `wpdo_collect_site_metrics` cron action, scheduled at 05:00 UTC).
|
|
*
|
|
* Metric keys written per run:
|
|
* eav.postmeta_rows / eav.usermeta_rows / eav.termmeta_rows / eav.commentmeta_rows
|
|
* flat.hot_rows / flat.cold_rows / flat.warm_rows
|
|
* custom_tables.total_rows / custom_tables.table_count
|
|
* errors.last_24h / shadow_diffs.last_24h
|
|
*
|
|
* Monthly Summary (`TMDO_Monthly_Summary`) reads these rows for the
|
|
* `zone_growth` section instead of querying live tables, so the monthly
|
|
* rollup is fast even on large databases.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.6.2
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Collects site-wide EAV health metrics and writes to wpdo_site_metrics.
|
|
*/
|
|
class TMDO_Site_Metrics_Collector {
|
|
|
|
/**
|
|
* Cron hook name.
|
|
*
|
|
* @var string
|
|
*/
|
|
public const CRON_HOOK = 'wpdo_collect_site_metrics';
|
|
|
|
/**
|
|
* How many days of daily rows to retain before pruning.
|
|
*
|
|
* @var int
|
|
*/
|
|
private const RETENTION_DAYS = 90;
|
|
|
|
/**
|
|
* Register the cron handler and return the instance for chaining.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function register(): void {
|
|
add_action( self::CRON_HOOK, array( __CLASS__, 'collect' ) );
|
|
}
|
|
|
|
/**
|
|
* Collect all site metrics and persist them to wpdo_site_metrics.
|
|
*
|
|
* Called by the daily cron. Safe to call manually (e.g., via WP-CLI).
|
|
*
|
|
* @param bool $dry_run When true, collect but do not write to the DB.
|
|
* @return array<string,int> 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<string,int> 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<array{collected_at:string,value:int}> 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
|
|
);
|
|
}
|
|
}
|