chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user