You've already forked wp-prometheus
fix: Resolve memory exhaustion with Twig-based plugins (v0.4.1)
- Add early metrics endpoint handler to intercept /metrics before full WP init - Remove content filters during metrics collection to prevent recursion - Skip extensibility hooks in early metrics mode - Change template_redirect to parse_request for earlier interception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -5,6 +5,16 @@ 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.4.1] - 2026-02-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed memory exhaustion when wp-fedistream (Twig-based) plugin is active
|
||||||
|
- Added early metrics endpoint handler that intercepts `/metrics` requests before full WordPress initialization
|
||||||
|
- Removed content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`) during metrics collection to prevent recursion
|
||||||
|
- Skip third-party extensibility hooks during early metrics mode to avoid conflicts
|
||||||
|
- Changed `template_redirect` hook to `parse_request` for earlier request interception
|
||||||
|
|
||||||
## [0.4.0] - 2026-02-02
|
## [0.4.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ class MetricsEndpoint {
|
|||||||
*/
|
*/
|
||||||
private function init_hooks(): void {
|
private function init_hooks(): void {
|
||||||
add_action( 'init', array( $this, 'register_endpoint' ) );
|
add_action( 'init', array( $this, 'register_endpoint' ) );
|
||||||
add_action( 'template_redirect', array( $this, 'handle_request' ) );
|
// Use parse_request instead of template_redirect to handle the request early,
|
||||||
|
// before themes and other plugins (like Twig-based ones) can interfere.
|
||||||
|
add_action( 'parse_request', array( $this, 'handle_request' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,10 +68,13 @@ class MetricsEndpoint {
|
|||||||
/**
|
/**
|
||||||
* Handle the metrics endpoint request.
|
* Handle the metrics endpoint request.
|
||||||
*
|
*
|
||||||
|
* Called during parse_request to intercept before themes/plugins load.
|
||||||
|
*
|
||||||
|
* @param \WP $wp WordPress environment instance.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function handle_request(): void {
|
public function handle_request( \WP $wp ): void {
|
||||||
if ( ! get_query_var( 'wp_prometheus_metrics' ) ) {
|
if ( empty( $wp->query_vars['wp_prometheus_metrics'] ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,10 +121,15 @@ class Collector {
|
|||||||
/**
|
/**
|
||||||
* Fires after default metrics are collected.
|
* Fires after default metrics are collected.
|
||||||
*
|
*
|
||||||
|
* Skip in early metrics mode to avoid triggering third-party hooks
|
||||||
|
* that may cause recursion issues (e.g., Twig-based plugins).
|
||||||
|
*
|
||||||
* @param Collector $collector The metrics collector instance.
|
* @param Collector $collector The metrics collector instance.
|
||||||
*/
|
*/
|
||||||
|
if ( ! defined( 'WP_PROMETHEUS_EARLY_METRICS' ) || ! WP_PROMETHEUS_EARLY_METRICS ) {
|
||||||
do_action( 'wp_prometheus_collect_metrics', $this );
|
do_action( 'wp_prometheus_collect_metrics', $this );
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render metrics in Prometheus text format.
|
* Render metrics in Prometheus text format.
|
||||||
|
|||||||
@@ -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.4.0
|
* Version: 0.4.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
|
||||||
@@ -21,12 +21,104 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Early metrics endpoint handler.
|
||||||
|
*
|
||||||
|
* Intercepts /metrics requests before full WordPress initialization to avoid
|
||||||
|
* conflicts with other plugins that may cause issues during template loading.
|
||||||
|
* This runs at plugin load time, before plugins_loaded hook.
|
||||||
|
*/
|
||||||
|
function wp_prometheus_early_metrics_check(): void {
|
||||||
|
// Only handle /metrics requests.
|
||||||
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||||
|
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||||
|
|
||||||
|
if ( ! preg_match( '#/metrics/?$#', $path ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if autoloader exists.
|
||||||
|
$autoloader = __DIR__ . '/vendor/autoload.php';
|
||||||
|
if ( ! file_exists( $autoloader ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once $autoloader;
|
||||||
|
|
||||||
|
// Check license validity.
|
||||||
|
if ( ! \Magdev\WpPrometheus\License\Manager::is_license_valid() ) {
|
||||||
|
return; // Let normal flow handle unlicensed state.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate.
|
||||||
|
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
|
||||||
|
if ( empty( $auth_token ) ) {
|
||||||
|
status_header( 401 );
|
||||||
|
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
|
||||||
|
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||||
|
echo 'Unauthorized';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Bearer token.
|
||||||
|
$auth_header = '';
|
||||||
|
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
|
||||||
|
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
|
||||||
|
} elseif ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
|
||||||
|
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$authenticated = false;
|
||||||
|
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
|
||||||
|
$authenticated = hash_equals( $auth_token, $matches[1] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check query parameter fallback.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
|
||||||
|
if ( ! $authenticated && isset( $_GET['token'] ) ) {
|
||||||
|
$authenticated = hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $authenticated ) {
|
||||||
|
status_header( 401 );
|
||||||
|
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
|
||||||
|
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||||
|
echo 'Unauthorized';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set flag to indicate early metrics mode - Collector will skip extensibility hooks.
|
||||||
|
define( 'WP_PROMETHEUS_EARLY_METRICS', true );
|
||||||
|
|
||||||
|
// Remove all content filters to prevent recursion with Twig-based plugins.
|
||||||
|
remove_all_filters( 'the_content' );
|
||||||
|
remove_all_filters( 'the_excerpt' );
|
||||||
|
remove_all_filters( 'get_the_excerpt' );
|
||||||
|
remove_all_filters( 'the_title' );
|
||||||
|
|
||||||
|
// Output metrics and exit immediately.
|
||||||
|
$collector = new \Magdev\WpPrometheus\Metrics\Collector();
|
||||||
|
|
||||||
|
status_header( 200 );
|
||||||
|
header( 'Content-Type: text/plain; version=0.0.4; charset=utf-8' );
|
||||||
|
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||||
|
header( 'Pragma: no-cache' );
|
||||||
|
header( 'Expires: 0' );
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Prometheus format.
|
||||||
|
echo $collector->render();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try early metrics handling before full plugin initialization.
|
||||||
|
wp_prometheus_early_metrics_check();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin version.
|
* Plugin version.
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
define( 'WP_PROMETHEUS_VERSION', '0.4.0' );
|
define( 'WP_PROMETHEUS_VERSION', '0.4.1' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin file path.
|
* Plugin file path.
|
||||||
|
|||||||
Reference in New Issue
Block a user