fix: Fix metric name sanitization, optimize transient queries, add domain binding (v0.5.1)

- Add sanitize_metric_name() to preserve colons/uppercase in Prometheus names
- Combine 3 transient COUNT queries into single aggregated query
- Deduplicate inline HPOS check using existing is_hpos_enabled() method
- Add license domain binding for authorized deployment domains

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 17:20:09 +01:00
parent 9a94b4a7a5
commit e2a73297cd
5 changed files with 88 additions and 25 deletions

View File

@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.1] - 2026-03-07
### Fixed
- Custom metric name sanitization: `sanitize_key()` was stripping colons and lowercasing names, silently mangling valid Prometheus metric names (e.g. `my:Custom_metric` became `mycustom_metric`). Added dedicated `sanitize_metric_name()` that preserves valid Prometheus characters.
### Changed
- Consolidated 3 separate transient COUNT queries into a single query with conditional aggregation for better database performance.
- Deduplicated inline HPOS check in WooCommerce customer metrics to use existing `is_hpos_enabled()` method.
- Added license domain binding for authorized deployment domains.
## [0.5.0] - 2026-02-26 ## [0.5.0] - 2026-02-26
### Added ### Added

View File

@@ -47,6 +47,16 @@ final class Manager {
*/ */
private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check'; private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check';
/**
* HMAC-SHA256 signatures of authorized domain suffixes.
*
* @var string[]
*/
private const DOMAIN_BINDING_SIGNATURES = array(
'aeb2e64ca8f815d4a552c0a2beeefa8580d6808a60d1aa91ddca719933b12868',
'a2fbaafd39e3085cd70995eb5773d6659c90cb3160ddccd66c52a21fac43fd13',
);
/** /**
* Cache TTL in seconds (24 hours). * Cache TTL in seconds (24 hours).
*/ */
@@ -307,10 +317,43 @@ final class Manager {
return true; return true;
} }
// Bypass license check on bound domains.
if ( self::verify_domain_binding() ) {
return true;
}
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' ); $status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
return 'valid' === $status; return 'valid' === $status;
} }
/**
* Verify if the current domain matches a bound domain signature.
*
* @return bool
*/
private static function verify_domain_binding(): bool {
$host = wp_parse_url( home_url(), PHP_URL_HOST );
if ( empty( $host ) ) {
return false;
}
$key = hash( 'sha256', 'wp-prometheus:domain-binding:v1:3016a5e8' );
$parts = explode( '.', $host );
$count = count( $parts );
// Iterate through all possible domain suffixes.
for ( $i = 0; $i < $count - 1; $i++ ) {
$suffix = implode( '.', array_slice( $parts, $i ) );
$sig = hash_hmac( 'sha256', $suffix, $key );
if ( in_array( $sig, self::DOMAIN_BINDING_SIGNATURES, true ) ) {
return true;
}
}
return false;
}
/** /**
* Check if the current site is running on localhost. * Check if the current site is running on localhost.
* *

View File

@@ -393,27 +393,24 @@ class Collector {
private function collect_transient_metrics(): void { private function collect_transient_metrics(): void {
global $wpdb; global $wpdb;
// Count all transients. // Count all transient types in a single query.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$transient_count = $wpdb->get_var( $counts = $wpdb->get_row(
"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( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%%' AND option_value < %d", "SELECT
SUM(CASE WHEN option_name LIKE '_transient_%%' AND option_name NOT LIKE '_transient_timeout_%%' THEN 1 ELSE 0 END) AS total,
SUM(CASE WHEN option_name LIKE '_transient_timeout_%%' THEN 1 ELSE 0 END) AS with_expiration,
SUM(CASE WHEN option_name LIKE '_transient_timeout_%%' AND option_value < %d THEN 1 ELSE 0 END) AS expired
FROM {$wpdb->options}
WHERE option_name LIKE '_transient_%%'",
time() time()
) )
); );
$transient_count = (int) ( $counts->total ?? 0 );
$expiring_count = (int) ( $counts->with_expiration ?? 0 );
$expired_count = (int) ( $counts->expired ?? 0 );
// Transients total gauge. // Transients total gauge.
$transients_gauge = $this->registry->getOrRegisterGauge( $transients_gauge = $this->registry->getOrRegisterGauge(
$this->namespace, $this->namespace,
@@ -422,10 +419,10 @@ class Collector {
array( 'type' ) array( 'type' )
); );
$transients_gauge->set( (int) $transient_count, array( 'total' ) ); $transients_gauge->set( $transient_count, array( 'total' ) );
$transients_gauge->set( (int) $expiring_count, array( 'with_expiration' ) ); $transients_gauge->set( $expiring_count, array( 'with_expiration' ) );
$transients_gauge->set( (int) $transient_count - (int) $expiring_count, array( 'persistent' ) ); $transients_gauge->set( $transient_count - $expiring_count, array( 'persistent' ) );
$transients_gauge->set( (int) $expired_count, array( 'expired' ) ); $transients_gauge->set( $expired_count, array( 'expired' ) );
// Site transients (for multisite). // Site transients (for multisite).
if ( is_multisite() ) { if ( is_multisite() ) {
@@ -662,9 +659,7 @@ class Collector {
// Count guest orders (orders without user_id). // Count guest orders (orders without user_id).
global $wpdb; global $wpdb;
// Check if HPOS is enabled. $hpos_enabled = $this->is_hpos_enabled();
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
if ( $hpos_enabled ) { if ( $hpos_enabled ) {
$orders_table = $wpdb->prefix . 'wc_orders'; $orders_table = $wpdb->prefix . 'wc_orders';

View File

@@ -126,6 +126,19 @@ class CustomMetricBuilder {
return true; return true;
} }
/**
* Sanitize a Prometheus metric name.
*
* Unlike sanitize_key(), this preserves colons and uppercase letters
* which are valid in Prometheus metric names.
*
* @param string $name Raw metric name.
* @return string Sanitized metric name.
*/
public static function sanitize_metric_name( string $name ): string {
return preg_replace( '/[^a-zA-Z0-9_:]/', '', $name );
}
/** /**
* Validate a Prometheus metric name. * Validate a Prometheus metric name.
* *
@@ -277,7 +290,7 @@ class CustomMetricBuilder {
private function sanitize_metric( array $metric ): array { private function sanitize_metric( array $metric ): array {
$sanitized = array( $sanitized = array(
'id' => sanitize_key( $metric['id'] ?? '' ), 'id' => sanitize_key( $metric['id'] ?? '' ),
'name' => sanitize_key( $metric['name'] ?? '' ), 'name' => self::sanitize_metric_name( $metric['name'] ?? '' ),
'help' => sanitize_text_field( $metric['help'] ?? '' ), 'help' => sanitize_text_field( $metric['help'] ?? '' ),
'type' => sanitize_key( $metric['type'] ?? 'gauge' ), 'type' => sanitize_key( $metric['type'] ?? 'gauge' ),
'labels' => array(), 'labels' => array(),

View File

@@ -3,7 +3,7 @@
* Plugin Name: WP Prometheus * Plugin Name: WP Prometheus
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus
* Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics. * Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics.
* Version: 0.5.0 * Version: 0.5.1
* Requires at least: 6.4 * Requires at least: 6.4
* Requires PHP: 8.3 * Requires PHP: 8.3
* Author: Marco Graetsch * Author: Marco Graetsch
@@ -199,7 +199,7 @@ wp_prometheus_early_metrics_check();
* *
* @var string * @var string
*/ */
define( 'WP_PROMETHEUS_VERSION', '0.5.0' ); define( 'WP_PROMETHEUS_VERSION', '0.5.1' );
/** /**
* Plugin file path. * Plugin file path.