You've already forked wp-prometheus
All checks were successful
Create Release Package / build-release (push) Successful in 59s
- Custom Metrics Builder with admin UI for gauge metrics - Support for static values and WordPress option-based values - JSON-based export/import with skip/overwrite/rename modes - Three Grafana dashboard templates (overview, runtime, WooCommerce) - New tabs: Custom Metrics and Dashboards - Reset runtime metrics button Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
828 lines
27 KiB
PHP
828 lines
27 KiB
PHP
<?php
|
|
/**
|
|
* Metrics collector class.
|
|
*
|
|
* @package WP_Prometheus
|
|
*/
|
|
|
|
namespace Magdev\WpPrometheus\Metrics;
|
|
|
|
use Prometheus\CollectorRegistry;
|
|
use Prometheus\Storage\InMemory;
|
|
use Prometheus\RenderTextFormat;
|
|
use Magdev\WpPrometheus\Metrics\CustomMetricBuilder;
|
|
|
|
// Prevent direct file access.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Collector class.
|
|
*
|
|
* Collects and manages Prometheus metrics.
|
|
*/
|
|
class Collector {
|
|
|
|
/**
|
|
* Prometheus collector registry.
|
|
*
|
|
* @var CollectorRegistry
|
|
*/
|
|
private CollectorRegistry $registry;
|
|
|
|
/**
|
|
* Metric namespace.
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $namespace = 'wordpress';
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
$this->registry = new CollectorRegistry( new InMemory() );
|
|
}
|
|
|
|
/**
|
|
* Get the collector registry.
|
|
*
|
|
* @return CollectorRegistry
|
|
*/
|
|
public function get_registry(): CollectorRegistry {
|
|
return $this->registry;
|
|
}
|
|
|
|
/**
|
|
* Get the metric namespace.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_namespace(): string {
|
|
return $this->namespace;
|
|
}
|
|
|
|
/**
|
|
* Collect all enabled metrics.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function collect(): void {
|
|
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
|
|
|
|
// Always collect WordPress info.
|
|
if ( in_array( 'wordpress_info', $enabled_metrics, true ) ) {
|
|
$this->collect_wordpress_info();
|
|
}
|
|
|
|
// Collect user metrics.
|
|
if ( in_array( 'wordpress_users_total', $enabled_metrics, true ) ) {
|
|
$this->collect_users_total();
|
|
}
|
|
|
|
// Collect posts metrics.
|
|
if ( in_array( 'wordpress_posts_total', $enabled_metrics, true ) ) {
|
|
$this->collect_posts_total();
|
|
}
|
|
|
|
// Collect comments metrics.
|
|
if ( in_array( 'wordpress_comments_total', $enabled_metrics, true ) ) {
|
|
$this->collect_comments_total();
|
|
}
|
|
|
|
// Collect plugins metrics.
|
|
if ( in_array( 'wordpress_plugins_total', $enabled_metrics, true ) ) {
|
|
$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 );
|
|
|
|
// Collect custom user-defined metrics.
|
|
$custom_builder = new CustomMetricBuilder();
|
|
$custom_builder->register_with_collector( $this );
|
|
|
|
/**
|
|
* Fires after default metrics are collected.
|
|
*
|
|
* @param Collector $collector The metrics collector instance.
|
|
*/
|
|
do_action( 'wp_prometheus_collect_metrics', $this );
|
|
}
|
|
|
|
/**
|
|
* Render metrics in Prometheus text format.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function render(): string {
|
|
$this->collect();
|
|
|
|
$renderer = new RenderTextFormat();
|
|
return $renderer->render( $this->registry->getMetricFamilySamples() );
|
|
}
|
|
|
|
/**
|
|
* Collect WordPress info metric.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function collect_wordpress_info(): void {
|
|
$gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
'info',
|
|
'WordPress installation information',
|
|
array( 'version', 'php_version', 'multisite' )
|
|
);
|
|
|
|
$gauge->set(
|
|
1,
|
|
array(
|
|
get_bloginfo( 'version' ),
|
|
PHP_VERSION,
|
|
is_multisite() ? 'yes' : 'no',
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Collect total users metric.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function collect_users_total(): void {
|
|
$gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
'users_total',
|
|
'Total number of WordPress users',
|
|
array( 'role' )
|
|
);
|
|
|
|
$user_count = count_users();
|
|
foreach ( $user_count['avail_roles'] as $role => $count ) {
|
|
$gauge->set( $count, array( $role ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect total posts metric.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function collect_posts_total(): void {
|
|
$gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
'posts_total',
|
|
'Total number of posts by type and status',
|
|
array( 'post_type', 'status' )
|
|
);
|
|
|
|
$post_types = get_post_types( array( 'public' => true ) );
|
|
foreach ( $post_types as $post_type ) {
|
|
$counts = wp_count_posts( $post_type );
|
|
foreach ( get_object_vars( $counts ) as $status => $count ) {
|
|
if ( $count > 0 ) {
|
|
$gauge->set( (int) $count, array( $post_type, $status ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect total comments metric.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function collect_comments_total(): void {
|
|
$gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
'comments_total',
|
|
'Total number of comments by status',
|
|
array( 'status' )
|
|
);
|
|
|
|
$comments = wp_count_comments();
|
|
$statuses = array(
|
|
'approved' => $comments->approved,
|
|
'moderated' => $comments->moderated,
|
|
'spam' => $comments->spam,
|
|
'trash' => $comments->trash,
|
|
'total_comments' => $comments->total_comments,
|
|
);
|
|
|
|
foreach ( $statuses as $status => $count ) {
|
|
$gauge->set( (int) $count, array( $status ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect total plugins metric.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function collect_plugins_total(): void {
|
|
if ( ! function_exists( 'get_plugins' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
}
|
|
|
|
$gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
'plugins_total',
|
|
'Total number of plugins by status',
|
|
array( 'status' )
|
|
);
|
|
|
|
$all_plugins = get_plugins();
|
|
$active_plugins = get_option( 'active_plugins', array() );
|
|
|
|
$gauge->set( count( $all_plugins ), array( 'installed' ) );
|
|
$gauge->set( count( $active_plugins ), array( 'active' ) );
|
|
$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' )
|
|
);
|
|
|
|
// Get all registered order statuses and count each.
|
|
$statuses = wc_get_order_statuses();
|
|
|
|
foreach ( array_keys( $statuses ) as $status ) {
|
|
// Remove 'wc-' prefix for the label.
|
|
$status_label = str_replace( 'wc-', '', $status );
|
|
$count = wc_orders_count( $status );
|
|
$gauge->set( (int) $count, array( $status_label ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param array $enabled_metrics List of enabled metrics.
|
|
* @return void
|
|
*/
|
|
private function collect_runtime_metrics( array $enabled_metrics ): void {
|
|
$runtime_collector = RuntimeCollector::get_instance();
|
|
$stored_metrics = $runtime_collector->get_stored_metrics();
|
|
|
|
// HTTP requests total counter.
|
|
if ( in_array( 'wordpress_http_requests_total', $enabled_metrics, true ) && ! empty( $stored_metrics['counters'] ) ) {
|
|
foreach ( $stored_metrics['counters'] as $counter_data ) {
|
|
if ( 'http_requests_total' !== $counter_data['name'] ) {
|
|
continue;
|
|
}
|
|
|
|
$counter = $this->registry->getOrRegisterCounter(
|
|
$this->namespace,
|
|
'http_requests_total',
|
|
'Total number of HTTP requests',
|
|
array( 'method', 'status', 'endpoint' )
|
|
);
|
|
|
|
$counter->incBy(
|
|
(int) $counter_data['value'],
|
|
array(
|
|
$counter_data['labels']['method'] ?? 'GET',
|
|
$counter_data['labels']['status'] ?? '200',
|
|
$counter_data['labels']['endpoint'] ?? 'unknown',
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// HTTP request duration histogram.
|
|
if ( in_array( 'wordpress_http_request_duration_seconds', $enabled_metrics, true ) && ! empty( $stored_metrics['histograms'] ) ) {
|
|
foreach ( $stored_metrics['histograms'] as $histogram_data ) {
|
|
if ( 'http_request_duration_seconds' !== $histogram_data['name'] ) {
|
|
continue;
|
|
}
|
|
|
|
// For histograms, we expose as a gauge with pre-aggregated bucket counts.
|
|
// This is a workaround since we can't directly populate histogram buckets.
|
|
$this->expose_histogram_as_gauges(
|
|
'http_request_duration_seconds',
|
|
'HTTP request duration in seconds',
|
|
$histogram_data,
|
|
array( 'method', 'endpoint' )
|
|
);
|
|
}
|
|
}
|
|
|
|
// Database queries total counter.
|
|
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) && ! empty( $stored_metrics['counters'] ) ) {
|
|
foreach ( $stored_metrics['counters'] as $counter_data ) {
|
|
if ( 'db_queries_total' !== $counter_data['name'] ) {
|
|
continue;
|
|
}
|
|
|
|
$counter = $this->registry->getOrRegisterCounter(
|
|
$this->namespace,
|
|
'db_queries_total',
|
|
'Total number of database queries',
|
|
array( 'endpoint' )
|
|
);
|
|
|
|
$counter->incBy(
|
|
(int) $counter_data['value'],
|
|
array(
|
|
$counter_data['labels']['endpoint'] ?? 'unknown',
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Database query duration histogram (if SAVEQUERIES is enabled).
|
|
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) && ! empty( $stored_metrics['histograms'] ) ) {
|
|
foreach ( $stored_metrics['histograms'] as $histogram_data ) {
|
|
if ( 'db_query_duration_seconds' !== $histogram_data['name'] ) {
|
|
continue;
|
|
}
|
|
|
|
$this->expose_histogram_as_gauges(
|
|
'db_query_duration_seconds',
|
|
'Database query duration in seconds',
|
|
$histogram_data,
|
|
array( 'endpoint' )
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expose pre-aggregated histogram data as gauge metrics.
|
|
*
|
|
* Since we store histogram data externally, we expose it using gauges
|
|
* that follow Prometheus histogram naming conventions.
|
|
*
|
|
* @param string $name Metric name.
|
|
* @param string $help Metric description.
|
|
* @param array $histogram_data Stored histogram data.
|
|
* @param array $label_names Label names.
|
|
* @return void
|
|
*/
|
|
private function expose_histogram_as_gauges( string $name, string $help, array $histogram_data, array $label_names ): void {
|
|
$label_values = array();
|
|
foreach ( $label_names as $label_name ) {
|
|
$label_values[] = $histogram_data['labels'][ $label_name ] ?? 'unknown';
|
|
}
|
|
|
|
// Expose bucket counts.
|
|
$bucket_gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
$name . '_bucket',
|
|
$help . ' (bucket)',
|
|
array_merge( $label_names, array( 'le' ) )
|
|
);
|
|
|
|
$cumulative_count = 0;
|
|
foreach ( $histogram_data['buckets'] as $le => $count ) {
|
|
$cumulative_count += $count;
|
|
$bucket_gauge->set(
|
|
$cumulative_count,
|
|
array_merge( $label_values, array( $le ) )
|
|
);
|
|
}
|
|
|
|
// Expose sum.
|
|
$sum_gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
$name . '_sum',
|
|
$help . ' (sum)',
|
|
$label_names
|
|
);
|
|
$sum_gauge->set( $histogram_data['sum'], $label_values );
|
|
|
|
// Expose count.
|
|
$count_gauge = $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
$name . '_count',
|
|
$help . ' (count)',
|
|
$label_names
|
|
);
|
|
$count_gauge->set( $histogram_data['count'], $label_values );
|
|
}
|
|
|
|
/**
|
|
* Register a custom gauge metric.
|
|
*
|
|
* @param string $name Metric name.
|
|
* @param string $help Metric description.
|
|
* @param array $labels Label names.
|
|
* @return \Prometheus\Gauge
|
|
*/
|
|
public function register_gauge( string $name, string $help, array $labels = array() ): \Prometheus\Gauge {
|
|
return $this->registry->getOrRegisterGauge(
|
|
$this->namespace,
|
|
$name,
|
|
$help,
|
|
$labels
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register a custom counter metric.
|
|
*
|
|
* @param string $name Metric name.
|
|
* @param string $help Metric description.
|
|
* @param array $labels Label names.
|
|
* @return \Prometheus\Counter
|
|
*/
|
|
public function register_counter( string $name, string $help, array $labels = array() ): \Prometheus\Counter {
|
|
return $this->registry->getOrRegisterCounter(
|
|
$this->namespace,
|
|
$name,
|
|
$help,
|
|
$labels
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register a custom histogram metric.
|
|
*
|
|
* @param string $name Metric name.
|
|
* @param string $help Metric description.
|
|
* @param array $labels Label names.
|
|
* @param array|null $buckets Histogram buckets.
|
|
* @return \Prometheus\Histogram
|
|
*/
|
|
public function register_histogram( string $name, string $help, array $labels = array(), ?array $buckets = null ): \Prometheus\Histogram {
|
|
return $this->registry->getOrRegisterHistogram(
|
|
$this->namespace,
|
|
$name,
|
|
$help,
|
|
$labels,
|
|
$buckets
|
|
);
|
|
}
|
|
}
|