6 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
19d75ab7b2 feat: Add option to disable early mode (v0.4.2)
All checks were successful
Create Release Package / build-release (push) Successful in 56s
- Add wp_prometheus_disable_early_mode option in admin settings
- Support WP_PROMETHEUS_DISABLE_EARLY_MODE environment variable
- Add Early Mode section in Metrics tab with status indicator
- Allow users to enable wp_prometheus_collect_metrics hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 21:09:14 +01:00
fa63857f5f docs: Update CLAUDE.md with v0.4.1 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 20:25:46 +01:00
9 changed files with 730 additions and 38 deletions

View File

@@ -5,6 +5,65 @@ 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.5] - 2026-02-02
### Fixed
- Settings now persist correctly across Metrics sub-tabs
- Auth token no longer gets cleared when saving from Selection sub-tab
- 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
- Split Metrics settings into separate WordPress option groups per sub-tab
- 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
### Fixed ### Fixed

106
CLAUDE.md
View File

@@ -291,6 +291,112 @@ 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)
- Added option to disable early mode for users who need extensibility
- Implementation:
- Added `wp_prometheus_disable_early_mode` WordPress option
- Added `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable support
- Option check in `wp_prometheus_early_metrics_check()` before early interception
- Environment variable accepts `1`, `true`, `yes`, `on` (case-insensitive)
- Admin UI in Metrics tab:
- "Early Mode" section with description of functionality
- Checkbox to disable early metrics interception
- Environment override notice when env var is set
- Current status indicator showing early mode state
- **Key Learning**: Balancing compatibility vs extensibility
- Early mode fixes memory issues but disables `wp_prometheus_collect_metrics` hook
- Users with custom metrics need the hook, so early mode must be optional
- Default remains enabled (safe) with explicit opt-out for advanced users
### 2026-02-02 - Plugin Compatibility Fix (v0.4.1)
- Fixed memory exhaustion (1GB limit) when wp-fedistream (Twig-based) plugin is active
- Root cause: Infinite recursion through WordPress hook system when content filters trigger Twig rendering
- Solution: Early metrics endpoint interception before full WordPress initialization
- Implementation changes:
- Added `wp_prometheus_early_metrics_check()` in bootstrap file (wp-prometheus.php)
- Checks REQUEST_URI for `/metrics` pattern before `plugins_loaded` fires
- Defines `WP_PROMETHEUS_EARLY_METRICS` constant to signal early mode
- Removes content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`)
- Collector skips `wp_prometheus_collect_metrics` action in early mode
- Changed MetricsEndpoint from `template_redirect` to `parse_request` hook
- **Key Learning**: WordPress plugin loading order and hook timing
- Plugins load alphabetically, so wp-fedistream ('f') loads before wp-prometheus ('p')
- `template_redirect` fires too late - after themes and Twig initialize
- `parse_request` fires earlier but still after plugin files load
- Earliest interception point: top-level code in plugin bootstrap file
- **Key Learning**: Content filter recursion in WordPress
- `get_the_excerpt()` internally triggers `apply_filters('the_content', ...)`
- This creates unexpected recursion vectors when Twig templates process content
- Solution: Remove all content-related filters before metrics collection
- **Key Learning**: Isolating metrics collection from WordPress template system
- Use `remove_all_filters()` to clear problematic filter chains
- Skip extensibility hooks (`do_action`) when in isolated early mode
- Exit immediately after output to prevent further WordPress processing
### 2026-02-02 - Persistent Storage (v0.4.0) ### 2026-02-02 - Persistent Storage (v0.4.0)
- Added persistent storage support for metrics: - Added persistent storage support for metrics:

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

@@ -3,7 +3,7 @@
# This file is distributed under the GPL v2 or later. # This file is distributed under the GPL v2 or later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WP Prometheus 0.4.0\n" "Project-Id-Version: WP Prometheus 0.4.2\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n" "Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n" "POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
"PO-Revision-Date: 2026-02-02T00:00:00+00:00\n" "PO-Revision-Date: 2026-02-02T00:00:00+00:00\n"
@@ -879,3 +879,71 @@ msgstr "APCu funktioniert. Speicher: %s belegt."
#: src/Metrics/StorageFactory.php #: src/Metrics/StorageFactory.php
msgid "APCu fetch operation returned unexpected value." msgid "APCu fetch operation returned unexpected value."
msgstr "APCu-Abrufoperation hat unerwarteten Wert zurueckgegeben." msgstr "APCu-Abrufoperation hat unerwarteten Wert zurueckgegeben."
#: src/Admin/Settings.php
msgid "Early Mode"
msgstr "Fruehzeitiger Modus"
#: src/Admin/Settings.php
msgid "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."
msgstr "Der fruehzeitige Modus faengt /metrics-Anfragen vor der vollstaendigen WordPress-Initialisierung ab. Dies verhindert Speichererschoepfungsprobleme, die durch einige Plugins verursacht werden (z.B. Twig-basierte Themes/Plugins), deaktiviert jedoch den wp_prometheus_collect_metrics-Hook fuer benutzerdefinierte Metriken."
#: src/Admin/Settings.php
msgid "Early mode is configured via WP_PROMETHEUS_DISABLE_EARLY_MODE environment variable. Admin settings will be ignored."
msgstr "Der fruehzeitige Modus ist ueber die Umgebungsvariable WP_PROMETHEUS_DISABLE_EARLY_MODE konfiguriert. Admin-Einstellungen werden ignoriert."
#: src/Admin/Settings.php
msgid "Disable Early Mode"
msgstr "Fruehzeitigen Modus deaktivieren"
#: src/Admin/Settings.php
msgid "Disable early metrics interception"
msgstr "Fruehzeitige Metriken-Abfangung deaktivieren"
#: src/Admin/Settings.php
msgid "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."
msgstr "Wenn deaktiviert, werden Metriken ueber das normale WordPress-Template-Laden erfasst. Dies aktiviert den wp_prometheus_collect_metrics-Hook fuer benutzerdefinierte Metriken, kann jedoch Probleme mit einigen Plugins verursachen."
#: src/Admin/Settings.php
msgid "Early mode is active (this request was served via early interception)"
msgstr "Fruehzeitiger Modus ist aktiv (diese Anfrage wurde ueber fruehzeitige Abfangung verarbeitet)"
#: src/Admin/Settings.php
msgid "Early mode is disabled"
msgstr "Fruehzeitiger Modus ist deaktiviert"
#: src/Admin/Settings.php
msgid "Early mode is enabled (active for /metrics requests)"
msgstr "Fruehzeitiger Modus ist aktiviert (aktiv fuer /metrics-Anfragen)"
#: src/Admin/Settings.php
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."
#: 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

@@ -2,7 +2,7 @@
# This file is distributed under the GPL v2 or later. # This file is distributed under the GPL v2 or later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WP Prometheus 0.4.0\n" "Project-Id-Version: WP Prometheus 0.4.2\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n" "Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n" "POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -876,3 +876,71 @@ msgstr ""
#: src/Metrics/StorageFactory.php #: src/Metrics/StorageFactory.php
msgid "APCu fetch operation returned unexpected value." msgid "APCu fetch operation returned unexpected value."
msgstr "" msgstr ""
#: src/Admin/Settings.php
msgid "Early Mode"
msgstr ""
#: src/Admin/Settings.php
msgid "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."
msgstr ""
#: src/Admin/Settings.php
msgid "Early mode is configured via WP_PROMETHEUS_DISABLE_EARLY_MODE environment variable. Admin settings will be ignored."
msgstr ""
#: src/Admin/Settings.php
msgid "Disable Early Mode"
msgstr ""
#: src/Admin/Settings.php
msgid "Disable early metrics interception"
msgstr ""
#: src/Admin/Settings.php
msgid "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."
msgstr ""
#: src/Admin/Settings.php
msgid "Early mode is active (this request was served via early interception)"
msgstr ""
#: src/Admin/Settings.php
msgid "Early mode is disabled"
msgstr ""
#: src/Admin/Settings.php
msgid "Early mode is enabled (active for /metrics requests)"
msgstr ""
#: src/Admin/Settings.php
msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh."
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,17 +107,25 @@ 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 settings for advanced sub-tab.
register_setting( 'wp_prometheus_advanced_settings', 'wp_prometheus_isolated_mode', array(
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'default' => false,
) );
// Auth token section. // Auth token section.
add_settings_section( add_settings_section(
'wp_prometheus_auth_section', 'wp_prometheus_auth_section',
@@ -394,32 +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();
?>
<div class="wp-prometheus-subtabs">
<ul class="wp-prometheus-subtab-nav">
<?php foreach ( $subtabs as $subtab_id => $subtab_name ) : ?>
<?php
$subtab_url = add_query_arg(
array(
'page' => 'wp-prometheus',
'tab' => 'metrics',
'subtab' => $subtab_id,
),
admin_url( 'options-general.php' )
);
$active_class = ( $current_subtab === $subtab_id ) ? ' active' : '';
?>
<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>
<div class="wp-prometheus-subtab-content">
<?php
switch ( $current_subtab ) {
case 'endpoint':
$this->render_metrics_endpoint_subtab();
break;
case 'selection':
$this->render_metrics_selection_subtab();
break;
case 'runtime':
$this->render_metrics_runtime_subtab();
break;
case 'advanced':
$this->render_metrics_advanced_subtab();
break;
}
?>
</div>
</div>
<?php
}
/**
* Render metrics endpoint sub-tab.
*
* @return void
*/
private function render_metrics_endpoint_subtab(): void {
?> ?>
<form method="post" action="options.php"> <form method="post" action="options.php">
<?php <?php settings_fields( 'wp_prometheus_endpoint_settings' ); ?>
settings_fields( 'wp_prometheus_metrics_settings' );
do_settings_sections( 'wp-prometheus-metrics' ); <h3><?php esc_html_e( 'Authentication', 'wp-prometheus' ); ?></h3>
submit_button(); <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> </form>
<?php
}
<hr style="margin: 30px 0;"> /**
* 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( 'Reset Runtime Metrics', 'wp-prometheus' ); ?></h3> <h3><?php esc_html_e( 'Enabled Metrics', 'wp-prometheus' ); ?></h3>
<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> <p class="description"><?php esc_html_e( 'Select which metrics to expose on the /metrics endpoint.', 'wp-prometheus' ); ?></p>
<p>
<button type="button" id="wp-prometheus-reset-runtime" class="button button-secondary"> <table class="form-table" role="presentation">
<?php esc_html_e( 'Reset Runtime Metrics', 'wp-prometheus' ); ?> <tr>
</button> <th scope="row"><?php esc_html_e( 'Select Metrics', 'wp-prometheus' ); ?></th>
<span id="wp-prometheus-reset-spinner" class="spinner" style="float: none;"></span> <td>
</p> <?php $this->render_enabled_metrics_field(); ?>
<div id="wp-prometheus-reset-message" style="display: none; margin-top: 10px;"></div> </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>
<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 ) : ?>
<div class="notice notice-warning inline" style="padding: 12px; margin: 15px 0;">
<strong><?php esc_html_e( 'Environment Override Active', 'wp-prometheus' ); ?></strong>
<p><?php esc_html_e( 'Mode is configured via WP_PROMETHEUS_ISOLATED_MODE environment variable. Admin settings will be ignored.', 'wp-prometheus' ); ?></p>
</div>
<?php endif; ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Isolated Mode', 'wp-prometheus' ); ?></th>
<td>
<label>
<input type="checkbox" name="wp_prometheus_isolated_mode" value="1"
<?php checked( $isolated_mode ); ?>
<?php disabled( $env_override ); ?>>
<?php esc_html_e( 'Enable isolated mode', 'wp-prometheus' ); ?>
</label>
<p class="description">
<?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>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Current Status', 'wp-prometheus' ); ?></th>
<td>
<?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>
<?php esc_html_e( 'Safe mode active - custom hooks enabled with filter protection', 'wp-prometheus' ); ?>
<?php elseif ( $isolated_mode ) : ?>
<span class="dashicons dashicons-lock" style="color: orange;"></span>
<?php esc_html_e( 'Isolated mode enabled (active for /metrics requests)', 'wp-prometheus' ); ?>
<?php else : ?>
<span class="dashicons dashicons-yes-alt" style="color: green;"></span>
<?php esc_html_e( 'Safe mode enabled (default) - custom hooks with filter protection', 'wp-prometheus' ); ?>
<?php endif; ?>
</td>
</tr>
</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.1 * 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,6 +42,58 @@ function wp_prometheus_early_metrics_check(): void {
return; return;
} }
// Set flag to indicate we're handling a metrics request.
define( 'WP_PROMETHEUS_METRICS_REQUEST', true );
// 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 );
}
// Remove all content filters immediately to prevent recursion with Twig-based plugins.
// This is done for BOTH safe mode and isolated mode.
add_action( 'plugins_loaded', 'wp_prometheus_remove_content_filters', 0 );
// 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 ) ) {
@@ -87,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();
@@ -118,7 +169,7 @@ wp_prometheus_early_metrics_check();
* *
* @var string * @var string
*/ */
define( 'WP_PROMETHEUS_VERSION', '0.4.1' ); define( 'WP_PROMETHEUS_VERSION', '0.4.5' );
/** /**
* Plugin file path. * Plugin file path.