4 Commits

Author SHA1 Message Date
e5f2edbafa fix: Separate settings groups to prevent cross-tab overwrites (v0.4.5)
All checks were successful
Create Release Package / build-release (push) Successful in 1m2s
Split Metrics sub-tab settings into separate WordPress option groups:
- wp_prometheus_endpoint_settings for auth token
- wp_prometheus_selection_settings for enabled metrics
- wp_prometheus_advanced_settings for isolated mode

This fixes the bug where saving from one sub-tab would clear settings
from other sub-tabs due to all settings sharing a single option group.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 23:12:41 +01:00
7f0b6ec8a6 docs: Add version mismatch learning to CLAUDE.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 21:50:56 +01:00
192da4588a fix: Update plugin header version to 0.4.3
All checks were successful
Create Release Package / build-release (push) Successful in 59s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 21:49:26 +01:00
cf1797d4bf feat: Split Metrics tab into sub-tabs and fix early mode storage (v0.4.3)
Some checks failed
Create Release Package / build-release (push) Failing after 52s
- Add sub-tabs: Endpoint, Selection, Runtime, Advanced
- Fix early mode checkbox not saving (was outside form element)
- Add CSS styling for horizontal sub-tab navigation
- Update translations with new sub-tab strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 21:45:36 +01:00
9 changed files with 576 additions and 99 deletions

View File

@@ -5,18 +5,64 @@ 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.2] - 2026-02-02 ## [0.4.5] - 2026-02-02
### Added ### Fixed
- Option to disable early mode in admin settings (Metrics tab) - Settings now persist correctly across Metrics sub-tabs
- Support for `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable - Auth token no longer gets cleared when saving from Selection sub-tab
- Early mode status display in settings - Enabled metrics no longer get cleared when saving from Endpoint sub-tab
- Isolated mode setting no longer gets cleared when saving from other sub-tabs
### Changed ### Changed
- Early mode can now be disabled for users who need the `wp_prometheus_collect_metrics` hook for custom metrics - Split Metrics settings into separate WordPress option groups per sub-tab
- Updated translations with new early mode strings (English and German) - Each sub-tab now uses its own settings group to prevent cross-tab overwrites
## [0.4.4] - 2026-02-02
### Added
- Safe mode for metrics collection (default):
- Removes problematic content filters early
- Allows third-party plugins to register `wp_prometheus_collect_metrics` hooks
- Wraps custom hooks in output buffering and try-catch for protection
- Isolated mode option for maximum compatibility:
- Outputs metrics before other plugins fully load
- Use only if Safe mode causes issues
- `WP_PROMETHEUS_ISOLATED_MODE` environment variable support
- Mode comparison table in admin settings
### Changed
- Replaced "early mode" with two clear modes: Safe (default) and Isolated
- Custom metrics hooks now fire by default with protection against recursion
- Filter removal now also includes `the_content_feed` and `comment_text`
- Updated admin UI with clearer explanations of each mode
### Fixed
- Third-party plugins can now add custom metrics without memory issues
- Twig-based plugins (like wp-fedistream) no longer cause recursion
## [0.4.3] - 2026-02-02
### Added
- Sub-tabs navigation within Metrics tab (Endpoint, Selection, Runtime, Advanced)
- Option to disable early mode in admin settings (Metrics → Advanced)
- Support for `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable
- Early mode status display in settings
### Fixed
- Early mode setting now saves correctly (moved into form with proper settings group)
### Changed
- Reorganized Metrics tab into logical sub-sections for better usability
- Early mode can now be disabled for users who need the `wp_prometheus_collect_metrics` hook
- Updated translations with sub-tab and early mode strings (English and German)
## [0.4.1] - 2026-02-02 ## [0.4.1] - 2026-02-02

View File

@@ -291,6 +291,68 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
## Session History ## Session History
### 2026-02-02 - Settings Persistence Fix (v0.4.5)
- Fixed critical bug where settings would get cleared when saving from different Metrics sub-tabs
- Root cause: All settings were registered under single `wp_prometheus_metrics_settings` group
- When saving from "Endpoint" sub-tab, only auth token was in POST data
- WordPress Settings API would process all registered settings in the group
- Missing fields (enabled_metrics, isolated_mode) would receive null/undefined
- Sanitize callbacks returned empty values, overwriting existing settings
- Solution: Split into separate settings groups per sub-tab:
- `wp_prometheus_endpoint_settings` for auth token
- `wp_prometheus_selection_settings` for enabled metrics
- `wp_prometheus_advanced_settings` for isolated mode
- **Key Learning**: WordPress Settings API and multiple forms
- When multiple forms share the same settings group, saving one form can clear settings from another
- Each form with `settings_fields()` should use a unique option group
- `register_setting()` group name must match `settings_fields()` group name
### 2026-02-02 - Safe Mode & Custom Hooks Fix (v0.4.4)
- Redesigned metrics collection to support both plugin compatibility AND custom metrics:
- **Safe Mode (default)**: Removes content filters early but lets WordPress load normally
- **Isolated Mode**: Legacy early mode that skips custom hooks entirely
- Implementation:
- `WP_PROMETHEUS_METRICS_REQUEST` constant set for any /metrics request
- Content filters removed via `plugins_loaded` hook at priority 0
- Collector fires `wp_prometheus_collect_metrics` with protection (output buffering, try-catch)
- `wp_prometheus_isolated_mode` option replaces `wp_prometheus_disable_early_mode`
- `WP_PROMETHEUS_ISOLATED_MODE` environment variable for containerized deployments
- Collector now wraps custom hooks in `fire_custom_metrics_hook()` method:
- Removes content filters again before hook (in case re-added)
- Uses output buffering to discard accidental output
- Catches exceptions to prevent breaking metrics output
- Logs errors when WP_DEBUG is enabled
- Updated admin UI with mode comparison table
- **Key Learning**: Hybrid approach for plugin compatibility
- The memory issue comes from content filter recursion, not just plugin loading
- Removing filters early (before any plugin can trigger them) prevents recursion
- Plugins still load and can register their `wp_prometheus_collect_metrics` hooks
- Hooks fire after filters are removed, in a protected context
- **Key Learning**: Defense in depth for custom hooks
- Remove filters again right before hook fires (plugins may re-add them)
- Output buffering catches any echo/print from misbehaving plugins
- Try-catch prevents one broken plugin from breaking metrics entirely
### 2026-02-02 - Sub-tabs & Early Mode Fix (v0.4.3)
- Split Metrics tab into sub-tabs for better organization:
- **Endpoint**: Authentication token configuration
- **Selection**: Enable/disable individual metrics
- **Runtime**: Reset runtime metrics data
- **Advanced**: Early mode toggle and status
- Fixed early mode setting not being saved (was outside form element)
- Added CSS styling for horizontal sub-tab navigation
- **Key Learning**: WordPress Settings API form structure
- Settings must be inside `<form action="options.php">` with `settings_fields()` call
- Each sub-tab needs its own form wrapper for proper saving
- Sub-tabs use URL query parameter (`subtab`) within the main tab
- **Key Learning**: WordPress plugin versioning requires TWO updates
- Plugin header comment `Version: x.x.x` (line ~6) - used by WordPress admin
- PHP constant `WP_PROMETHEUS_VERSION` (line ~133) - used internally
- CI/CD checks both must match the git tag, causing release failures if mismatched
### 2026-02-02 - Early Mode Toggle (v0.4.2) ### 2026-02-02 - Early Mode Toggle (v0.4.2)
- Added option to disable early mode for users who need extensibility - Added option to disable early mode for users who need extensibility

View File

@@ -9,6 +9,61 @@
margin-top: 20px; margin-top: 20px;
} }
/* Sub-tabs navigation */
.wp-prometheus-subtabs {
margin-top: 15px;
}
.wp-prometheus-subtab-nav {
display: flex;
margin: 0;
padding: 0;
list-style: none;
border-bottom: 1px solid #c3c4c7;
}
.wp-prometheus-subtab-item {
margin: 0;
padding: 0;
}
.wp-prometheus-subtab-item a {
display: block;
padding: 8px 16px;
text-decoration: none;
color: #50575e;
border: 1px solid transparent;
border-bottom: none;
margin-bottom: -1px;
background: transparent;
font-size: 13px;
font-weight: 400;
}
.wp-prometheus-subtab-item a:hover {
color: #2271b1;
background: #f6f7f7;
}
.wp-prometheus-subtab-item.active a {
color: #1d2327;
background: #fff;
border-color: #c3c4c7;
border-bottom-color: #fff;
font-weight: 600;
}
.wp-prometheus-subtab-content {
background: #fff;
border: 1px solid #c3c4c7;
border-top: none;
padding: 20px;
}
.wp-prometheus-subtab-content h3:first-child {
margin-top: 0;
}
/* License status box */ /* License status box */
.wp-prometheus-license-status { .wp-prometheus-license-status {
margin: 15px 0; margin: 15px 0;

Binary file not shown.

View File

@@ -919,3 +919,31 @@ msgstr "Fruehzeitiger Modus ist aktiviert (aktiv fuer /metrics-Anfragen)"
#: src/Admin/Settings.php #: src/Admin/Settings.php
msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh." msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh."
msgstr "Alle gesammelten Laufzeit-Metrikdaten loeschen (HTTP-Anfragen, Datenbank-Abfragen). Dies ist nuetzlich zum Testen oder fuer einen Neuanfang." msgstr "Alle gesammelten Laufzeit-Metrikdaten loeschen (HTTP-Anfragen, Datenbank-Abfragen). Dies ist nuetzlich zum Testen oder fuer einen Neuanfang."
#: src/Admin/Settings.php
msgid "Endpoint"
msgstr "Endpunkt"
#: src/Admin/Settings.php
msgid "Selection"
msgstr "Auswahl"
#: src/Admin/Settings.php
msgid "Runtime"
msgstr "Laufzeit"
#: src/Admin/Settings.php
msgid "Advanced"
msgstr "Erweitert"
#: src/Admin/Settings.php
msgid "Runtime Metrics Management"
msgstr "Laufzeit-Metriken Verwaltung"
#: src/Admin/Settings.php
msgid "Runtime metrics track HTTP requests and database queries across requests. Use this section to manage accumulated data."
msgstr "Laufzeit-Metriken erfassen HTTP-Anfragen und Datenbank-Abfragen ueber mehrere Anfragen hinweg. Verwenden Sie diesen Bereich zur Verwaltung der gesammelten Daten."
#: src/Admin/Settings.php
msgid "Reset Data"
msgstr "Daten zuruecksetzen"

View File

@@ -916,3 +916,31 @@ msgstr ""
#: src/Admin/Settings.php #: src/Admin/Settings.php
msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh." msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh."
msgstr "" msgstr ""
#: src/Admin/Settings.php
msgid "Endpoint"
msgstr ""
#: src/Admin/Settings.php
msgid "Selection"
msgstr ""
#: src/Admin/Settings.php
msgid "Runtime"
msgstr ""
#: src/Admin/Settings.php
msgid "Advanced"
msgstr ""
#: src/Admin/Settings.php
msgid "Runtime Metrics Management"
msgstr ""
#: src/Admin/Settings.php
msgid "Runtime metrics track HTTP requests and database queries across requests. Use this section to manage accumulated data."
msgstr ""
#: src/Admin/Settings.php
msgid "Reset Data"
msgstr ""

View File

@@ -107,18 +107,20 @@ class Settings {
* @return void * @return void
*/ */
public function register_settings(): void { public function register_settings(): void {
// Register settings for metrics tab. // Register settings for endpoint sub-tab.
register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_auth_token', array( register_setting( 'wp_prometheus_endpoint_settings', 'wp_prometheus_auth_token', array(
'type' => 'string', 'type' => 'string',
'sanitize_callback' => 'sanitize_text_field', 'sanitize_callback' => 'sanitize_text_field',
) ); ) );
register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_enabled_metrics', array( // Register settings for selection sub-tab.
register_setting( 'wp_prometheus_selection_settings', 'wp_prometheus_enabled_metrics', array(
'type' => 'array', 'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_metrics' ), 'sanitize_callback' => array( $this, 'sanitize_metrics' ),
) ); ) );
register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_disable_early_mode', array( // Register settings for advanced sub-tab.
register_setting( 'wp_prometheus_advanced_settings', 'wp_prometheus_isolated_mode', array(
'type' => 'boolean', 'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean', 'sanitize_callback' => 'rest_sanitize_boolean',
'default' => false, 'default' => false,
@@ -400,92 +402,261 @@ class Settings {
<?php <?php
} }
/**
* Get metrics sub-tabs.
*
* @return array
*/
private function get_metrics_subtabs(): array {
return array(
'endpoint' => __( 'Endpoint', 'wp-prometheus' ),
'selection' => __( 'Selection', 'wp-prometheus' ),
'runtime' => __( 'Runtime', 'wp-prometheus' ),
'advanced' => __( 'Advanced', 'wp-prometheus' ),
);
}
/**
* Get current metrics sub-tab.
*
* @return string
*/
private function get_current_metrics_subtab(): string {
$subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'endpoint';
$subtabs = $this->get_metrics_subtabs();
return array_key_exists( $subtab, $subtabs ) ? $subtab : 'endpoint';
}
/** /**
* Render metrics tab content. * Render metrics tab content.
* *
* @return void * @return void
*/ */
private function render_metrics_tab(): void { private function render_metrics_tab(): void {
$subtabs = $this->get_metrics_subtabs();
$current_subtab = $this->get_current_metrics_subtab();
?> ?>
<form method="post" action="options.php"> <div class="wp-prometheus-subtabs">
<ul class="wp-prometheus-subtab-nav">
<?php foreach ( $subtabs as $subtab_id => $subtab_name ) : ?>
<?php <?php
settings_fields( 'wp_prometheus_metrics_settings' ); $subtab_url = add_query_arg(
do_settings_sections( 'wp-prometheus-metrics' ); array(
submit_button(); 'page' => 'wp-prometheus',
'tab' => 'metrics',
'subtab' => $subtab_id,
),
admin_url( 'options-general.php' )
);
$active_class = ( $current_subtab === $subtab_id ) ? ' active' : '';
?> ?>
</form> <li class="wp-prometheus-subtab-item<?php echo esc_attr( $active_class ); ?>">
<a href="<?php echo esc_url( $subtab_url ); ?>"><?php echo esc_html( $subtab_name ); ?></a>
</li>
<?php endforeach; ?>
</ul>
<hr style="margin: 30px 0;"> <div class="wp-prometheus-subtab-content">
<?php
<h3><?php esc_html_e( 'Reset Runtime Metrics', 'wp-prometheus' ); ?></h3> switch ( $current_subtab ) {
<p class="description"><?php esc_html_e( 'Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh.', 'wp-prometheus' ); ?></p> case 'endpoint':
<p> $this->render_metrics_endpoint_subtab();
<button type="button" id="wp-prometheus-reset-runtime" class="button button-secondary"> break;
<?php esc_html_e( 'Reset Runtime Metrics', 'wp-prometheus' ); ?> case 'selection':
</button> $this->render_metrics_selection_subtab();
<span id="wp-prometheus-reset-spinner" class="spinner" style="float: none;"></span> break;
</p> case 'runtime':
<div id="wp-prometheus-reset-message" style="display: none; margin-top: 10px;"></div> $this->render_metrics_runtime_subtab();
break;
<hr style="margin: 30px 0;"> case 'advanced':
$this->render_metrics_advanced_subtab();
<?php $this->render_early_mode_section(); ?> break;
}
?>
</div>
</div>
<?php <?php
} }
/** /**
* Render early mode section. * Render metrics endpoint sub-tab.
* *
* @return void * @return void
*/ */
private function render_early_mode_section(): void { private function render_metrics_endpoint_subtab(): void {
$disabled = get_option( 'wp_prometheus_disable_early_mode', false );
$env_override = false !== getenv( 'WP_PROMETHEUS_DISABLE_EARLY_MODE' );
$early_active = defined( 'WP_PROMETHEUS_EARLY_METRICS' ) && WP_PROMETHEUS_EARLY_METRICS;
?> ?>
<h3><?php esc_html_e( 'Early Mode', 'wp-prometheus' ); ?></h3> <form method="post" action="options.php">
<p class="description"> <?php settings_fields( 'wp_prometheus_endpoint_settings' ); ?>
<?php esc_html_e( 'Early mode intercepts /metrics requests before full WordPress initialization. This prevents memory exhaustion issues caused by some plugins (e.g., Twig-based themes/plugins) but disables the wp_prometheus_collect_metrics hook for custom metrics.', 'wp-prometheus' ); ?>
<h3><?php esc_html_e( 'Authentication', 'wp-prometheus' ); ?></h3>
<p class="description"><?php esc_html_e( 'Configure authentication for the /metrics endpoint.', 'wp-prometheus' ); ?></p>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="wp_prometheus_auth_token"><?php esc_html_e( 'Auth Token', 'wp-prometheus' ); ?></label>
</th>
<td>
<?php $this->render_auth_token_field(); ?>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<?php
}
/**
* Render metrics selection sub-tab.
*
* @return void
*/
private function render_metrics_selection_subtab(): void {
?>
<form method="post" action="options.php">
<?php settings_fields( 'wp_prometheus_selection_settings' ); ?>
<h3><?php esc_html_e( 'Enabled Metrics', 'wp-prometheus' ); ?></h3>
<p class="description"><?php esc_html_e( 'Select which metrics to expose on the /metrics endpoint.', 'wp-prometheus' ); ?></p>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Select Metrics', 'wp-prometheus' ); ?></th>
<td>
<?php $this->render_enabled_metrics_field(); ?>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<?php
}
/**
* Render metrics runtime sub-tab.
*
* @return void
*/
private function render_metrics_runtime_subtab(): void {
?>
<h3><?php esc_html_e( 'Runtime Metrics Management', 'wp-prometheus' ); ?></h3>
<p class="description"><?php esc_html_e( 'Runtime metrics track HTTP requests and database queries across requests. Use this section to manage accumulated data.', 'wp-prometheus' ); ?></p>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Reset Data', 'wp-prometheus' ); ?></th>
<td>
<p class="description" style="margin-bottom: 10px;">
<?php esc_html_e( 'Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh.', 'wp-prometheus' ); ?>
</p> </p>
<button type="button" id="wp-prometheus-reset-runtime" class="button button-secondary">
<?php esc_html_e( 'Reset Runtime Metrics', 'wp-prometheus' ); ?>
</button>
<span id="wp-prometheus-reset-spinner" class="spinner" style="float: none;"></span>
<div id="wp-prometheus-reset-message" style="display: none; margin-top: 10px;"></div>
</td>
</tr>
</table>
<?php
}
/**
* Render metrics advanced sub-tab.
*
* @return void
*/
private function render_metrics_advanced_subtab(): void {
$isolated_mode = get_option( 'wp_prometheus_isolated_mode', false );
$env_override = false !== getenv( 'WP_PROMETHEUS_ISOLATED_MODE' );
$is_metrics_request = defined( 'WP_PROMETHEUS_METRICS_REQUEST' ) && WP_PROMETHEUS_METRICS_REQUEST;
$is_isolated = defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE;
?>
<form method="post" action="options.php">
<?php settings_fields( 'wp_prometheus_advanced_settings' ); ?>
<h3><?php esc_html_e( 'Metrics Collection Mode', 'wp-prometheus' ); ?></h3>
<div class="notice notice-info inline" style="padding: 12px; margin: 15px 0;">
<p><strong><?php esc_html_e( 'Safe Mode (Default)', 'wp-prometheus' ); ?></strong></p>
<p><?php esc_html_e( 'Content filters are removed early to prevent memory issues with Twig-based plugins, but WordPress loads normally. Third-party plugins can add custom metrics via the wp_prometheus_collect_metrics hook.', 'wp-prometheus' ); ?></p>
</div>
<?php if ( $env_override ) : ?> <?php if ( $env_override ) : ?>
<div class="notice notice-info inline" style="padding: 12px; margin: 15px 0;"> <div class="notice notice-warning inline" style="padding: 12px; margin: 15px 0;">
<strong><?php esc_html_e( 'Environment Override Active', 'wp-prometheus' ); ?></strong> <strong><?php esc_html_e( 'Environment Override Active', 'wp-prometheus' ); ?></strong>
<p><?php esc_html_e( 'Early mode is configured via WP_PROMETHEUS_DISABLE_EARLY_MODE environment variable. Admin settings will be ignored.', 'wp-prometheus' ); ?></p> <p><?php esc_html_e( 'Mode is configured via WP_PROMETHEUS_ISOLATED_MODE environment variable. Admin settings will be ignored.', 'wp-prometheus' ); ?></p>
</div> </div>
<?php endif; ?> <?php endif; ?>
<table class="form-table" role="presentation"> <table class="form-table" role="presentation">
<tr> <tr>
<th scope="row"><?php esc_html_e( 'Disable Early Mode', 'wp-prometheus' ); ?></th> <th scope="row"><?php esc_html_e( 'Isolated Mode', 'wp-prometheus' ); ?></th>
<td> <td>
<label> <label>
<input type="checkbox" name="wp_prometheus_disable_early_mode" value="1" <input type="checkbox" name="wp_prometheus_isolated_mode" value="1"
<?php checked( $disabled ); ?> <?php checked( $isolated_mode ); ?>
<?php disabled( $env_override ); ?>> <?php disabled( $env_override ); ?>>
<?php esc_html_e( 'Disable early metrics interception', 'wp-prometheus' ); ?> <?php esc_html_e( 'Enable isolated mode', 'wp-prometheus' ); ?>
</label> </label>
<p class="description"> <p class="description">
<?php esc_html_e( 'When disabled, metrics are collected through normal WordPress template loading. This enables the wp_prometheus_collect_metrics hook for custom metrics but may cause issues with some plugins.', 'wp-prometheus' ); ?> <?php esc_html_e( 'Isolated mode outputs metrics immediately before other plugins fully load. This provides maximum isolation but disables the wp_prometheus_collect_metrics hook. Use this only if you experience issues with Safe Mode.', 'wp-prometheus' ); ?>
</p> </p>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"><?php esc_html_e( 'Current Status', 'wp-prometheus' ); ?></th> <th scope="row"><?php esc_html_e( 'Current Status', 'wp-prometheus' ); ?></th>
<td> <td>
<?php if ( $early_active ) : ?> <?php if ( $is_isolated ) : ?>
<span class="dashicons dashicons-lock" style="color: orange;"></span>
<?php esc_html_e( 'Isolated mode active - custom hooks are disabled', 'wp-prometheus' ); ?>
<?php elseif ( $is_metrics_request ) : ?>
<span class="dashicons dashicons-yes-alt" style="color: green;"></span> <span class="dashicons dashicons-yes-alt" style="color: green;"></span>
<?php esc_html_e( 'Early mode is active (this request was served via early interception)', 'wp-prometheus' ); ?> <?php esc_html_e( 'Safe mode active - custom hooks enabled with filter protection', 'wp-prometheus' ); ?>
<?php elseif ( $disabled || $env_override ) : ?> <?php elseif ( $isolated_mode ) : ?>
<span class="dashicons dashicons-dismiss" style="color: gray;"></span> <span class="dashicons dashicons-lock" style="color: orange;"></span>
<?php esc_html_e( 'Early mode is disabled', 'wp-prometheus' ); ?> <?php esc_html_e( 'Isolated mode enabled (active for /metrics requests)', 'wp-prometheus' ); ?>
<?php else : ?> <?php else : ?>
<span class="dashicons dashicons-yes-alt" style="color: green;"></span> <span class="dashicons dashicons-yes-alt" style="color: green;"></span>
<?php esc_html_e( 'Early mode is enabled (active for /metrics requests)', 'wp-prometheus' ); ?> <?php esc_html_e( 'Safe mode enabled (default) - custom hooks with filter protection', 'wp-prometheus' ); ?>
<?php endif; ?> <?php endif; ?>
</td> </td>
</tr> </tr>
</table> </table>
<hr style="margin: 20px 0;">
<h4><?php esc_html_e( 'Mode Comparison', 'wp-prometheus' ); ?></h4>
<table class="widefat striped" style="max-width: 700px;">
<thead>
<tr>
<th><?php esc_html_e( 'Feature', 'wp-prometheus' ); ?></th>
<th><?php esc_html_e( 'Safe Mode', 'wp-prometheus' ); ?></th>
<th><?php esc_html_e( 'Isolated Mode', 'wp-prometheus' ); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><?php esc_html_e( 'Custom metrics hook', 'wp-prometheus' ); ?></td>
<td><span class="dashicons dashicons-yes" style="color: green;"></span></td>
<td><span class="dashicons dashicons-no" style="color: red;"></span></td>
</tr>
<tr>
<td><?php esc_html_e( 'Plugin compatibility', 'wp-prometheus' ); ?></td>
<td><?php esc_html_e( 'High', 'wp-prometheus' ); ?></td>
<td><?php esc_html_e( 'Maximum', 'wp-prometheus' ); ?></td>
</tr>
<tr>
<td><?php esc_html_e( 'Memory usage', 'wp-prometheus' ); ?></td>
<td><?php esc_html_e( 'Normal', 'wp-prometheus' ); ?></td>
<td><?php esc_html_e( 'Minimal', 'wp-prometheus' ); ?></td>
</tr>
</tbody>
</table>
<?php submit_button(); ?>
</form>
<?php <?php
} }

View File

@@ -121,14 +121,62 @@ 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 * In isolated mode, skip custom hooks to avoid any potential issues.
* that may cause recursion issues (e.g., Twig-based plugins). * In safe mode (default), fire hooks with protection against recursion.
* *
* @param Collector $collector The metrics collector instance. * @param Collector $collector The metrics collector instance.
*/ */
if ( ! defined( 'WP_PROMETHEUS_EARLY_METRICS' ) || ! WP_PROMETHEUS_EARLY_METRICS ) { if ( defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE ) {
do_action( 'wp_prometheus_collect_metrics', $this ); // Isolated mode: skip all third-party hooks for maximum safety.
return;
} }
// Safe mode: fire custom hooks with protection.
$this->fire_custom_metrics_hook();
}
/**
* Fire custom metrics hook with protection against recursion.
*
* Removes potentially problematic filters, uses output buffering,
* and catches any errors from third-party plugins.
*
* @return void
*/
private function fire_custom_metrics_hook(): void {
// Remove content filters again (in case any plugin re-added them).
if ( function_exists( 'wp_prometheus_remove_content_filters' ) ) {
wp_prometheus_remove_content_filters();
} else {
// Fallback if function doesn't exist.
remove_all_filters( 'the_content' );
remove_all_filters( 'the_excerpt' );
remove_all_filters( 'get_the_excerpt' );
remove_all_filters( 'the_title' );
}
// Use output buffering to prevent any accidental output from plugins.
ob_start();
try {
/**
* Fires after default metrics are collected.
*
* Third-party plugins can use this hook to add custom metrics.
*
* @param Collector $collector The metrics collector instance.
*/
do_action( 'wp_prometheus_collect_metrics', $this );
} catch ( \Throwable $e ) {
// Log the error but don't let it break metrics output.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'WP Prometheus: Error in custom metrics hook: ' . $e->getMessage() );
}
}
// Discard any output from plugins.
ob_end_clean();
} }
/** /**

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.4.2 * Version: 0.4.5
* Requires at least: 6.4 * Requires at least: 6.4
* Requires PHP: 8.3 * Requires PHP: 8.3
* Author: Marco Graetsch * Author: Marco Graetsch
@@ -22,11 +22,16 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
/** /**
* Early metrics endpoint handler. * Early metrics request detection.
* *
* Intercepts /metrics requests before full WordPress initialization to avoid * Detects /metrics requests early and removes problematic content filters
* conflicts with other plugins that may cause issues during template loading. * to prevent recursion issues with Twig-based plugins. Unlike the previous
* This runs at plugin load time, before plugins_loaded hook. * "early mode", this allows WordPress to continue loading so that third-party
* plugins can register their wp_prometheus_collect_metrics hooks.
*
* Two modes are available:
* - Safe mode (default): Removes filters early, lets WP load, fires custom hooks
* - Isolated mode: Outputs metrics immediately without custom hooks (legacy early mode)
*/ */
function wp_prometheus_early_metrics_check(): void { function wp_prometheus_early_metrics_check(): void {
// Only handle /metrics requests. // Only handle /metrics requests.
@@ -37,18 +42,58 @@ function wp_prometheus_early_metrics_check(): void {
return; return;
} }
// Check if early mode is disabled via environment variable. // Set flag to indicate we're handling a metrics request.
$env_disable = getenv( 'WP_PROMETHEUS_DISABLE_EARLY_MODE' ); define( 'WP_PROMETHEUS_METRICS_REQUEST', true );
if ( false !== $env_disable && in_array( strtolower( $env_disable ), array( '1', 'true', 'yes', 'on' ), true ) ) {
return; // Check if isolated mode is enabled via environment variable.
$env_isolated = getenv( 'WP_PROMETHEUS_ISOLATED_MODE' );
$isolated_mode = false !== $env_isolated && in_array( strtolower( $env_isolated ), array( '1', 'true', 'yes', 'on' ), true );
// Check if isolated mode is enabled via option (legacy "early mode" setting).
if ( ! $isolated_mode && ! get_option( 'wp_prometheus_disable_early_mode', false ) ) {
// Check for legacy isolated mode option.
$isolated_mode = (bool) get_option( 'wp_prometheus_isolated_mode', false );
} }
// Check if early mode is disabled via option. // Remove all content filters immediately to prevent recursion with Twig-based plugins.
// We can use get_option() here because WordPress core is already loaded. // This is done for BOTH safe mode and isolated mode.
if ( get_option( 'wp_prometheus_disable_early_mode', false ) ) { add_action( 'plugins_loaded', 'wp_prometheus_remove_content_filters', 0 );
return;
// Also remove filters now in case they were added by mu-plugins.
wp_prometheus_remove_content_filters();
// If isolated mode is enabled, handle metrics immediately without waiting for plugins.
if ( $isolated_mode ) {
wp_prometheus_isolated_metrics_handler();
}
} }
/**
* Remove content filters that can cause recursion.
*
* Called early during metrics requests to prevent infinite loops
* with Twig-based plugins that hook into content filters.
*
* @return void
*/
function wp_prometheus_remove_content_filters(): void {
remove_all_filters( 'the_content' );
remove_all_filters( 'the_excerpt' );
remove_all_filters( 'get_the_excerpt' );
remove_all_filters( 'the_title' );
remove_all_filters( 'the_content_feed' );
remove_all_filters( 'comment_text' );
}
/**
* Handle metrics in isolated mode (no custom hooks).
*
* This is the legacy "early mode" that outputs metrics immediately
* without allowing third-party plugins to add custom metrics.
*
* @return void
*/
function wp_prometheus_isolated_metrics_handler(): void {
// Check if autoloader exists. // Check if autoloader exists.
$autoloader = __DIR__ . '/vendor/autoload.php'; $autoloader = __DIR__ . '/vendor/autoload.php';
if ( ! file_exists( $autoloader ) ) { if ( ! file_exists( $autoloader ) ) {
@@ -99,14 +144,8 @@ function wp_prometheus_early_metrics_check(): void {
exit; exit;
} }
// Set flag to indicate early metrics mode - Collector will skip extensibility hooks. // Set flag to indicate isolated mode - Collector will skip extensibility hooks.
define( 'WP_PROMETHEUS_EARLY_METRICS', true ); define( 'WP_PROMETHEUS_ISOLATED_MODE', 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. // Output metrics and exit immediately.
$collector = new \Magdev\WpPrometheus\Metrics\Collector(); $collector = new \Magdev\WpPrometheus\Metrics\Collector();
@@ -130,7 +169,7 @@ wp_prometheus_early_metrics_check();
* *
* @var string * @var string
*/ */
define( 'WP_PROMETHEUS_VERSION', '0.4.2' ); define( 'WP_PROMETHEUS_VERSION', '0.4.5' );
/** /**
* Plugin file path. * Plugin file path.