You've already forked wp-prometheus
feat: Add WooCommerce, cron, and transient metrics (v0.2.0)
All checks were successful
Create Release Package / build-release (push) Successful in 56s
All checks were successful
Create Release Package / build-release (push) Successful in 56s
- WooCommerce integration metrics (products, orders, revenue, customers) - Cron job metrics (events by hook, overdue count, next run timestamp) - Transient cache metrics (total, expiring, expired) - Support for WooCommerce HPOS storage - Updated settings page with new metric categories - Updated translations and documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -447,6 +447,46 @@ class Settings {
|
||||
<td><?php esc_html_e( 'Counter', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Database queries by endpoint', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_cron_events_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Scheduled cron events by hook', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_cron_overdue_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Number of overdue cron events', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_cron_next_run_timestamp</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Unix timestamp of next scheduled cron', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_transients_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Total transients by type', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_woocommerce_products_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'WooCommerce products by status and type', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_woocommerce_orders_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'WooCommerce orders by status', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_woocommerce_revenue_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'WooCommerce revenue by period', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_woocommerce_customers_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'WooCommerce customers by type', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -514,6 +554,8 @@ class Settings {
|
||||
'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ),
|
||||
'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ),
|
||||
'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ),
|
||||
'wordpress_cron_events_total' => __( 'Cron Events (scheduled tasks, overdue, next run)', 'wp-prometheus' ),
|
||||
'wordpress_transients_total' => __( 'Transients (total, expiring, expired)', 'wp-prometheus' ),
|
||||
);
|
||||
|
||||
$runtime_metrics = array(
|
||||
@@ -522,6 +564,13 @@ class Settings {
|
||||
'wordpress_db_queries_total' => __( 'Database Queries Total (by endpoint)', 'wp-prometheus' ),
|
||||
);
|
||||
|
||||
$woocommerce_metrics = array(
|
||||
'wordpress_woocommerce_products_total' => __( 'WooCommerce Products (by status and type)', 'wp-prometheus' ),
|
||||
'wordpress_woocommerce_orders_total' => __( 'WooCommerce Orders (by status)', 'wp-prometheus' ),
|
||||
'wordpress_woocommerce_revenue_total' => __( 'WooCommerce Revenue (all time, today, month)', 'wp-prometheus' ),
|
||||
'wordpress_woocommerce_customers_total' => __( 'WooCommerce Customers (registered, guest)', 'wp-prometheus' ),
|
||||
);
|
||||
|
||||
echo '<fieldset>';
|
||||
echo '<legend class="screen-reader-text"><span>' . esc_html__( 'Static Metrics', 'wp-prometheus' ) . '</span></legend>';
|
||||
echo '<p><strong>' . esc_html__( 'Static Metrics', 'wp-prometheus' ) . '</strong></p>';
|
||||
@@ -551,6 +600,23 @@ class Settings {
|
||||
<?php
|
||||
}
|
||||
|
||||
// WooCommerce metrics (only show if WooCommerce is active).
|
||||
if ( class_exists( 'WooCommerce' ) ) {
|
||||
echo '<br><p><strong>' . esc_html__( 'WooCommerce Metrics', 'wp-prometheus' ) . '</strong></p>';
|
||||
echo '<p class="description" style="margin-bottom: 10px;">' . esc_html__( 'Metrics specific to WooCommerce stores. Only available when WooCommerce is active.', 'wp-prometheus' ) . '</p>';
|
||||
|
||||
foreach ( $woocommerce_metrics as $key => $label ) {
|
||||
?>
|
||||
<label style="display: block; margin-bottom: 5px;">
|
||||
<input type="checkbox" name="wp_prometheus_enabled_metrics[]"
|
||||
value="<?php echo esc_attr( $key ); ?>"
|
||||
<?php checked( in_array( $key, $enabled, true ) ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</label>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
echo '</fieldset>';
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,21 @@ class Collector {
|
||||
$this->collect_plugins_total();
|
||||
}
|
||||
|
||||
// Collect cron metrics.
|
||||
if ( in_array( 'wordpress_cron_events_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_cron_metrics();
|
||||
}
|
||||
|
||||
// Collect transient metrics.
|
||||
if ( in_array( 'wordpress_transients_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_transient_metrics();
|
||||
}
|
||||
|
||||
// Collect WooCommerce metrics (if WooCommerce is active).
|
||||
if ( $this->is_woocommerce_active() ) {
|
||||
$this->collect_woocommerce_metrics( $enabled_metrics );
|
||||
}
|
||||
|
||||
// Collect runtime metrics (HTTP requests, DB queries).
|
||||
$this->collect_runtime_metrics( $enabled_metrics );
|
||||
|
||||
@@ -236,6 +251,372 @@ class Collector {
|
||||
$gauge->set( count( $all_plugins ) - count( $active_plugins ), array( 'inactive' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect cron metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_cron_metrics(): void {
|
||||
$cron_array = _get_cron_array();
|
||||
|
||||
if ( ! is_array( $cron_array ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Events total gauge.
|
||||
$events_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'cron_events_total',
|
||||
'Total number of scheduled cron events',
|
||||
array( 'hook' )
|
||||
);
|
||||
|
||||
// Count events by hook.
|
||||
$hook_counts = array();
|
||||
$total_events = 0;
|
||||
$overdue_count = 0;
|
||||
$current_time = time();
|
||||
$next_run = PHP_INT_MAX;
|
||||
|
||||
foreach ( $cron_array as $timestamp => $cron ) {
|
||||
if ( $timestamp < $next_run ) {
|
||||
$next_run = $timestamp;
|
||||
}
|
||||
|
||||
foreach ( $cron as $hook => $events ) {
|
||||
$event_count = count( $events );
|
||||
$total_events += $event_count;
|
||||
|
||||
if ( ! isset( $hook_counts[ $hook ] ) ) {
|
||||
$hook_counts[ $hook ] = 0;
|
||||
}
|
||||
$hook_counts[ $hook ] += $event_count;
|
||||
|
||||
// Check if overdue.
|
||||
if ( $timestamp < $current_time ) {
|
||||
$overdue_count += $event_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set events by hook (limit to top 20 to avoid cardinality explosion).
|
||||
arsort( $hook_counts );
|
||||
$hook_counts = array_slice( $hook_counts, 0, 20, true );
|
||||
foreach ( $hook_counts as $hook => $count ) {
|
||||
$events_gauge->set( $count, array( $hook ) );
|
||||
}
|
||||
|
||||
// Overdue events gauge.
|
||||
$overdue_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'cron_overdue_total',
|
||||
'Number of overdue cron events',
|
||||
array()
|
||||
);
|
||||
$overdue_gauge->set( $overdue_count, array() );
|
||||
|
||||
// Next run timestamp.
|
||||
$next_run_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'cron_next_run_timestamp',
|
||||
'Unix timestamp of next scheduled cron event',
|
||||
array()
|
||||
);
|
||||
if ( $next_run !== PHP_INT_MAX ) {
|
||||
$next_run_gauge->set( $next_run, array() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect transient metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_transient_metrics(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Count all transients.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$transient_count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%'"
|
||||
);
|
||||
|
||||
// Count transients with expiration.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$expiring_count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%'"
|
||||
);
|
||||
|
||||
// Count expired transients.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$expired_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%%' AND option_value < %d",
|
||||
time()
|
||||
)
|
||||
);
|
||||
|
||||
// Transients total gauge.
|
||||
$transients_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'transients_total',
|
||||
'Total number of transients in database',
|
||||
array( 'type' )
|
||||
);
|
||||
|
||||
$transients_gauge->set( (int) $transient_count, array( 'total' ) );
|
||||
$transients_gauge->set( (int) $expiring_count, array( 'with_expiration' ) );
|
||||
$transients_gauge->set( (int) $transient_count - (int) $expiring_count, array( 'persistent' ) );
|
||||
$transients_gauge->set( (int) $expired_count, array( 'expired' ) );
|
||||
|
||||
// Site transients (for multisite).
|
||||
if ( is_multisite() ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$site_transient_count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->sitemeta} WHERE meta_key LIKE '_site_transient_%' AND meta_key NOT LIKE '_site_transient_timeout_%'"
|
||||
);
|
||||
|
||||
$transients_gauge->set( (int) $site_transient_count, array( 'site_transients' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_woocommerce_active(): bool {
|
||||
return class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce metrics.
|
||||
*
|
||||
* @param array $enabled_metrics List of enabled metrics.
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_metrics( array $enabled_metrics ): void {
|
||||
// Products total.
|
||||
if ( in_array( 'wordpress_woocommerce_products_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_products();
|
||||
}
|
||||
|
||||
// Orders total.
|
||||
if ( in_array( 'wordpress_woocommerce_orders_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_orders();
|
||||
}
|
||||
|
||||
// Revenue.
|
||||
if ( in_array( 'wordpress_woocommerce_revenue_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_revenue();
|
||||
}
|
||||
|
||||
// Customers.
|
||||
if ( in_array( 'wordpress_woocommerce_customers_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_customers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce products metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_products(): void {
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_products_total',
|
||||
'Total number of WooCommerce products by status and type',
|
||||
array( 'status', 'type' )
|
||||
);
|
||||
|
||||
// Get product counts by status.
|
||||
$product_counts = wp_count_posts( 'product' );
|
||||
$product_types = wc_get_product_types();
|
||||
|
||||
foreach ( get_object_vars( $product_counts ) as $status => $count ) {
|
||||
if ( (int) $count > 0 ) {
|
||||
$gauge->set( (int) $count, array( $status, 'all' ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Count by product type (for published products only).
|
||||
foreach ( array_keys( $product_types ) as $type ) {
|
||||
$args = array(
|
||||
'status' => 'publish',
|
||||
'type' => $type,
|
||||
'limit' => -1,
|
||||
'return' => 'ids',
|
||||
);
|
||||
$products = wc_get_products( $args );
|
||||
$gauge->set( count( $products ), array( 'publish', $type ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce orders metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_orders(): void {
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_orders_total',
|
||||
'Total number of WooCommerce orders by status',
|
||||
array( 'status' )
|
||||
);
|
||||
|
||||
// Use WooCommerce's built-in order count function.
|
||||
$order_counts = wc_orders_count();
|
||||
|
||||
foreach ( $order_counts as $status => $count ) {
|
||||
$gauge->set( (int) $count, array( $status ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce revenue metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_revenue(): void {
|
||||
global $wpdb;
|
||||
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_revenue_total',
|
||||
'Total WooCommerce revenue',
|
||||
array( 'period', 'currency' )
|
||||
);
|
||||
|
||||
$currency = get_woocommerce_currency();
|
||||
|
||||
// Check if HPOS (High-Performance Order Storage) is enabled.
|
||||
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
||||
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
|
||||
if ( $hpos_enabled ) {
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
|
||||
// Total revenue (all time) - completed and processing orders.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$total_revenue = $wpdb->get_var(
|
||||
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing')"
|
||||
);
|
||||
|
||||
// Today's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$today_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing') AND DATE(date_created_gmt) = %s",
|
||||
gmdate( 'Y-m-d' )
|
||||
)
|
||||
);
|
||||
|
||||
// This month's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$month_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing') AND YEAR(date_created_gmt) = %d AND MONTH(date_created_gmt) = %d",
|
||||
gmdate( 'Y' ),
|
||||
gmdate( 'm' )
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Legacy post-based orders.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$total_revenue = $wpdb->get_var(
|
||||
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_order_total'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND p.post_status IN ('wc-completed', 'wc-processing')"
|
||||
);
|
||||
|
||||
// Today's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$today_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_order_total'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND p.post_status IN ('wc-completed', 'wc-processing')
|
||||
AND DATE(p.post_date_gmt) = %s",
|
||||
gmdate( 'Y-m-d' )
|
||||
)
|
||||
);
|
||||
|
||||
// This month's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$month_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_order_total'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND p.post_status IN ('wc-completed', 'wc-processing')
|
||||
AND YEAR(p.post_date_gmt) = %d
|
||||
AND MONTH(p.post_date_gmt) = %d",
|
||||
gmdate( 'Y' ),
|
||||
gmdate( 'm' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$gauge->set( (float) ( $total_revenue ?? 0 ), array( 'all_time', $currency ) );
|
||||
$gauge->set( (float) ( $today_revenue ?? 0 ), array( 'today', $currency ) );
|
||||
$gauge->set( (float) ( $month_revenue ?? 0 ), array( 'month', $currency ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce customers metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_customers(): void {
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_customers_total',
|
||||
'Total number of WooCommerce customers',
|
||||
array( 'type' )
|
||||
);
|
||||
|
||||
// Count users with customer role.
|
||||
$customer_count = count_users();
|
||||
$customers = $customer_count['avail_roles']['customer'] ?? 0;
|
||||
|
||||
$gauge->set( $customers, array( 'registered' ) );
|
||||
|
||||
// Count guest orders (orders without user_id).
|
||||
global $wpdb;
|
||||
|
||||
// Check if HPOS is enabled.
|
||||
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
||||
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
|
||||
if ( $hpos_enabled ) {
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$guest_orders = $wpdb->get_var(
|
||||
"SELECT COUNT(DISTINCT billing_email) FROM {$orders_table} WHERE customer_id = 0 AND billing_email != ''"
|
||||
);
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$guest_orders = $wpdb->get_var(
|
||||
"SELECT COUNT(DISTINCT pm.meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
LEFT JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_customer_user'
|
||||
WHERE pm.meta_key = '_billing_email'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND (pm2.meta_value = '0' OR pm2.meta_value IS NULL)"
|
||||
);
|
||||
}
|
||||
|
||||
$gauge->set( (int) $guest_orders, array( 'guest' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect runtime metrics from stored data.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user