You've already forked wp-prometheus
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b71a0f7c9 | |||
| 5aaa73ec24 | |||
| e5f2edbafa | |||
| 7f0b6ec8a6 | |||
| 192da4588a | |||
| cf1797d4bf | |||
| 19d75ab7b2 | |||
| fa63857f5f | |||
| 41f16a9fbd | |||
| f984e3eb23 |
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip, intl, gettext
|
||||
extensions: mbstring, xml, zip, intl, gettext, redis, apcu
|
||||
tools: composer:v2
|
||||
|
||||
- name: Get version from tag
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ wp-plugins
|
||||
wp-core
|
||||
vendor/
|
||||
releases/*
|
||||
|
||||
# Marketing texts (not for distribution)
|
||||
MARKETING.md
|
||||
|
||||
99
CHANGELOG.md
99
CHANGELOG.md
@@ -5,6 +5,105 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.4.7] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Database query duration distribution panel in Grafana Runtime dashboard
|
||||
- `wordpress_db_query_duration_seconds` metric now listed in Help tab
|
||||
- Documentation for enabling `SAVEQUERIES` constant for query timing
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated README with instructions for enabling database query timing
|
||||
- Grafana Runtime dashboard now includes bucket distribution chart for DB queries
|
||||
|
||||
## [0.4.6] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Dashboard extension hook `wp_prometheus_register_dashboards` for third-party plugins
|
||||
- Third-party plugins can now register their own Grafana dashboard templates
|
||||
- Support for file-based and inline JSON dashboard registration
|
||||
- "Extension" badge for third-party dashboards in admin UI
|
||||
- Plugin attribution display for third-party dashboards
|
||||
- Security: Path traversal protection for registered dashboard files
|
||||
- Isolated mode support for dashboard registration hook
|
||||
|
||||
### Changed
|
||||
|
||||
- DashboardProvider now supports both built-in and third-party registered dashboards
|
||||
- Dashboard cards show source (built-in vs extension) with visual distinction
|
||||
|
||||
## [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
|
||||
|
||||
### 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
|
||||
|
||||
### Added
|
||||
|
||||
147
CLAUDE.md
147
CLAUDE.md
@@ -23,6 +23,7 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a
|
||||
- Grafana dashboard templates for easy visualization
|
||||
- Dedicated plugin settings under 'Settings/Metrics' menu
|
||||
- Extensible by other plugins using `wp_prometheus_collect_metrics` action hook
|
||||
- Dashboard extension hook `wp_prometheus_register_dashboards` for third-party Grafana dashboards
|
||||
- License management integration
|
||||
|
||||
### Key Fact: 100% AI-Generated
|
||||
@@ -33,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
||||
|
||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||
|
||||
*No planned features at this time.*
|
||||
*No pending roadmap items.*
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -291,6 +292,150 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
|
||||
|
||||
## Session History
|
||||
|
||||
### 2026-02-03 - Database Query Timing Documentation (v0.4.7)
|
||||
|
||||
- Added database query duration distribution panel to Grafana Runtime dashboard
|
||||
- Added `wordpress_db_query_duration_seconds` metric to Help tab metrics reference
|
||||
- Added documentation in README explaining how to enable `SAVEQUERIES` for query timing
|
||||
- Updated translation files (.pot and .po) with new strings
|
||||
- **Key Learning**: WordPress `SAVEQUERIES` constant
|
||||
- Enables `$wpdb->queries` array with query strings, timing, and call stacks
|
||||
- Required for `wordpress_db_query_duration_seconds` histogram metric
|
||||
- Has performance overhead - recommended for development, use cautiously in production
|
||||
- Without it, only query counts are available (not timing data)
|
||||
|
||||
### 2026-02-03 - Dashboard Extension Hook (v0.4.6)
|
||||
|
||||
- Added `wp_prometheus_register_dashboards` action hook for third-party plugins
|
||||
- Third-party plugins can now register their own Grafana dashboard templates
|
||||
- Implementation in `DashboardProvider.php`:
|
||||
- `register_dashboard(slug, args)` method for registrations
|
||||
- Supports file-based dashboards (absolute path to JSON) or inline JSON content
|
||||
- Security: Path traversal protection (files must be under `WP_CONTENT_DIR`)
|
||||
- `fire_registration_hook()` with output buffering and exception handling
|
||||
- Respects isolated mode setting (skips third-party hooks when enabled)
|
||||
- `is_third_party()` and `get_plugin_name()` helper methods
|
||||
- Updated admin UI in Settings.php:
|
||||
- "Extension" badge displayed on third-party dashboard cards
|
||||
- Plugin attribution shown below third-party dashboards
|
||||
- Visual distinction with blue border for third-party cards
|
||||
- **Key Learning**: Extension hook design pattern
|
||||
- Fire hook lazily on first `get_available()` call, not in constructor
|
||||
- Use `$hook_fired` flag to prevent double-firing
|
||||
- Wrap hook execution in try-catch to isolate failures
|
||||
- Validate registrations thoroughly before accepting them
|
||||
- **Key Learning**: Security for file-based registrations
|
||||
- Require absolute paths (`path_is_absolute()`)
|
||||
- Validate files exist and are readable
|
||||
- Use `realpath()` to resolve symlinks and prevent traversal
|
||||
- Restrict to `WP_CONTENT_DIR` (not just plugin directories)
|
||||
|
||||
### 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)
|
||||
|
||||
- Added persistent storage support for metrics:
|
||||
|
||||
66
README.md
66
README.md
@@ -6,7 +6,13 @@ A WordPress plugin that provides a Prometheus-compatible `/metrics` endpoint wit
|
||||
|
||||
- Prometheus-compatible authenticated `/metrics` endpoint
|
||||
- Default WordPress metrics (users, posts, comments, plugins)
|
||||
- Runtime metrics (HTTP requests, database queries)
|
||||
- Cron job and transient cache metrics
|
||||
- WooCommerce integration (products, orders, revenue)
|
||||
- Custom metric builder with admin UI
|
||||
- Grafana dashboard templates with download
|
||||
- Extensible by other plugins using hooks
|
||||
- Dashboard extension hook for third-party Grafana dashboards
|
||||
- Settings page under Settings > Metrics
|
||||
- Bearer token authentication
|
||||
- License management integration
|
||||
@@ -92,6 +98,21 @@ scrape_configs:
|
||||
|
||||
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact.
|
||||
|
||||
#### Enabling Database Query Timing
|
||||
|
||||
The `wordpress_db_query_duration_seconds` histogram requires WordPress's `SAVEQUERIES` constant to be enabled. Add this to your `wp-config.php`:
|
||||
|
||||
```php
|
||||
define( 'SAVEQUERIES', true );
|
||||
```
|
||||
|
||||
**Important considerations:**
|
||||
|
||||
- `SAVEQUERIES` has a performance overhead as it logs all queries with timing and call stacks
|
||||
- Recommended for development/staging environments, use with caution in production
|
||||
- Without `SAVEQUERIES`, only query counts (`wordpress_db_queries_total`) are available
|
||||
- The histogram shows total query time per request, grouped by endpoint
|
||||
|
||||
### Cron Metrics (v0.2.0+)
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
@@ -154,6 +175,51 @@ $histogram = $collector->register_histogram( $name, $help, $labels, $buckets );
|
||||
$histogram->observe( $value, $labelValues );
|
||||
```
|
||||
|
||||
## Extending with Custom Dashboards (v0.4.6+)
|
||||
|
||||
Add your own Grafana dashboard templates using the `wp_prometheus_register_dashboards` action:
|
||||
|
||||
```php
|
||||
add_action( 'wp_prometheus_register_dashboards', function( $provider ) {
|
||||
// File-based dashboard
|
||||
$provider->register_dashboard( 'my-plugin-dashboard', array(
|
||||
'title' => __( 'My Plugin Metrics', 'my-plugin' ),
|
||||
'description' => __( 'Dashboard for my custom metrics', 'my-plugin' ),
|
||||
'icon' => 'dashicons-chart-bar',
|
||||
'file' => MY_PLUGIN_PATH . 'assets/dashboards/my-dashboard.json',
|
||||
'plugin' => 'My Plugin Name',
|
||||
) );
|
||||
|
||||
// OR inline JSON dashboard
|
||||
$provider->register_dashboard( 'dynamic-dashboard', array(
|
||||
'title' => __( 'Dynamic Dashboard', 'my-plugin' ),
|
||||
'description' => __( 'Dynamically generated dashboard', 'my-plugin' ),
|
||||
'icon' => 'dashicons-admin-generic',
|
||||
'json' => json_encode( $dashboard_array ),
|
||||
'plugin' => 'My Plugin Name',
|
||||
) );
|
||||
} );
|
||||
```
|
||||
|
||||
### Registration Parameters
|
||||
|
||||
| Parameter | Required | Description |
|
||||
| --------- | -------- | ----------- |
|
||||
| `title` | Yes | Dashboard title displayed in admin |
|
||||
| `description` | No | Description shown below the title |
|
||||
| `icon` | No | Dashicon class (default: `dashicons-chart-line`) |
|
||||
| `file` | Yes* | Absolute path to JSON file |
|
||||
| `json` | Yes* | Inline JSON content |
|
||||
| `plugin` | No | Plugin name for attribution |
|
||||
|
||||
*Either `file` or `json` is required, but not both.
|
||||
|
||||
### Security Notes
|
||||
|
||||
- File paths must be absolute and within `wp-content/`
|
||||
- Inline JSON is validated during registration
|
||||
- Third-party dashboards are marked with an "Extension" badge in the admin UI
|
||||
|
||||
## Development
|
||||
|
||||
### Build for Release
|
||||
|
||||
@@ -9,6 +9,61 @@
|
||||
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 */
|
||||
.wp-prometheus-license-status {
|
||||
margin: 15px 0;
|
||||
@@ -161,6 +216,32 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Third-party dashboard card styling */
|
||||
.wp-prometheus-dashboard-card.third-party {
|
||||
position: relative;
|
||||
border-color: #2271b1;
|
||||
}
|
||||
|
||||
.wp-prometheus-dashboard-card .dashboard-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.wp-prometheus-dashboard-card .dashboard-plugin {
|
||||
color: #646970;
|
||||
margin: -5px 0 15px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Import options panel */
|
||||
#import-options {
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -946,6 +946,95 @@
|
||||
],
|
||||
"title": "Average Query Duration (Overall)",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 39
|
||||
},
|
||||
"id": 15,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.97,
|
||||
"fullHighlight": false,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(wordpress_db_query_duration_seconds_bucket) by (le)",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "{{le}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Query Duration Distribution (Buckets)",
|
||||
"type": "barchart"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
# This file is distributed under the GPL v2 or later.
|
||||
msgid ""
|
||||
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"
|
||||
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
|
||||
"PO-Revision-Date: 2026-02-02T00:00:00+00:00\n"
|
||||
@@ -317,6 +317,10 @@ msgstr "HTTP-Anfragedauer-Verteilung"
|
||||
msgid "Database queries by endpoint"
|
||||
msgstr "Datenbank-Abfragen nach Endpunkt"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Database query duration distribution (requires SAVEQUERIES)"
|
||||
msgstr "Datenbank-Abfragedauer-Verteilung (erfordert SAVEQUERIES)"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Scheduled cron events by hook"
|
||||
msgstr "Geplante Cron-Ereignisse nach Hook"
|
||||
@@ -879,3 +883,117 @@ msgstr "APCu funktioniert. Speicher: %s belegt."
|
||||
#: src/Metrics/StorageFactory.php
|
||||
msgid "APCu fetch operation returned unexpected value."
|
||||
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"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Extension"
|
||||
msgstr "Erweiterung"
|
||||
|
||||
#. translators: %s: Plugin name
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Provided by: %s"
|
||||
msgstr "Bereitgestellt von: %s"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "No dashboards available."
|
||||
msgstr "Keine Dashboards verfuegbar."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Pre-built dashboards for visualizing your WordPress metrics in Grafana."
|
||||
msgstr "Vorgefertigte Dashboards zur Visualisierung Ihrer WordPress-Metriken in Grafana."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Installation Instructions"
|
||||
msgstr "Installationsanleitung"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Download the JSON file for your desired dashboard."
|
||||
msgstr "Laden Sie die JSON-Datei fuer das gewuenschte Dashboard herunter."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "In Grafana, go to Dashboards → Import."
|
||||
msgstr "Gehen Sie in Grafana zu Dashboards → Import."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Upload the JSON file or paste its contents."
|
||||
msgstr "Laden Sie die JSON-Datei hoch oder fuegen Sie den Inhalt ein."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Select your Prometheus data source when prompted."
|
||||
msgstr "Waehlen Sie Ihre Prometheus-Datenquelle, wenn Sie dazu aufgefordert werden."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Click Import to create the dashboard."
|
||||
msgstr "Klicken Sie auf Import, um das Dashboard zu erstellen."
|
||||
|
||||
#. translators: %s: Metrics URL
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Make sure your Prometheus instance is configured to scrape %s with the correct authentication token."
|
||||
msgstr "Stellen Sie sicher, dass Ihre Prometheus-Instanz so konfiguriert ist, dass sie %s mit dem richtigen Authentifizierungs-Token abruft."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is distributed under the GPL v2 or later.
|
||||
msgid ""
|
||||
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"
|
||||
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -314,6 +314,10 @@ msgstr ""
|
||||
msgid "Database queries by endpoint"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Database query duration distribution (requires SAVEQUERIES)"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Scheduled cron events by hook"
|
||||
msgstr ""
|
||||
@@ -876,3 +880,117 @@ msgstr ""
|
||||
#: src/Metrics/StorageFactory.php
|
||||
msgid "APCu fetch operation returned unexpected value."
|
||||
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 ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Extension"
|
||||
msgstr ""
|
||||
|
||||
#. translators: %s: Plugin name
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Provided by: %s"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "No dashboards available."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Pre-built dashboards for visualizing your WordPress metrics in Grafana."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Installation Instructions"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Download the JSON file for your desired dashboard."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "In Grafana, go to Dashboards → Import."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Upload the JSON file or paste its contents."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Select your Prometheus data source when prompted."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Click Import to create the dashboard."
|
||||
msgstr ""
|
||||
|
||||
#. translators: %s: Metrics URL
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Make sure your Prometheus instance is configured to scrape %s with the correct authentication token."
|
||||
msgstr ""
|
||||
|
||||
@@ -16,22 +16,37 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
* DashboardProvider class.
|
||||
*
|
||||
* Provides Grafana dashboard templates for download.
|
||||
* Supports both built-in dashboards and third-party registrations.
|
||||
*/
|
||||
class DashboardProvider {
|
||||
|
||||
/**
|
||||
* Dashboard directory path.
|
||||
* Dashboard directory path for built-in dashboards.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $dashboard_dir;
|
||||
|
||||
/**
|
||||
* Available dashboard definitions.
|
||||
* Built-in dashboard definitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $dashboards = array();
|
||||
private array $builtin_dashboards = array();
|
||||
|
||||
/**
|
||||
* Third-party registered dashboard definitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $registered_dashboards = array();
|
||||
|
||||
/**
|
||||
* Whether the registration hook has been fired.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $hook_fired = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -39,43 +54,241 @@ class DashboardProvider {
|
||||
public function __construct() {
|
||||
$this->dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/';
|
||||
|
||||
$this->dashboards = array(
|
||||
$this->builtin_dashboards = array(
|
||||
'wordpress-overview' => array(
|
||||
'title' => __( 'WordPress Overview', 'wp-prometheus' ),
|
||||
'description' => __( 'General WordPress metrics including users, posts, comments, and plugins.', 'wp-prometheus' ),
|
||||
'file' => 'wordpress-overview.json',
|
||||
'icon' => 'dashicons-wordpress',
|
||||
'source' => 'builtin',
|
||||
),
|
||||
'wordpress-runtime' => array(
|
||||
'title' => __( 'Runtime Performance', 'wp-prometheus' ),
|
||||
'description' => __( 'HTTP request metrics, database query performance, and response times.', 'wp-prometheus' ),
|
||||
'file' => 'wordpress-runtime.json',
|
||||
'icon' => 'dashicons-performance',
|
||||
'source' => 'builtin',
|
||||
),
|
||||
'wordpress-woocommerce' => array(
|
||||
'title' => __( 'WooCommerce Store', 'wp-prometheus' ),
|
||||
'description' => __( 'WooCommerce metrics including products, orders, revenue, and customers.', 'wp-prometheus' ),
|
||||
'file' => 'wordpress-woocommerce.json',
|
||||
'icon' => 'dashicons-cart',
|
||||
'source' => 'builtin',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a third-party dashboard.
|
||||
*
|
||||
* @param string $slug Dashboard slug (unique identifier).
|
||||
* @param array $args Dashboard configuration {
|
||||
* @type string $title Dashboard title (required).
|
||||
* @type string $description Dashboard description.
|
||||
* @type string $icon Dashicon class (e.g., 'dashicons-chart-bar').
|
||||
* @type string $file Absolute path to JSON file (mutually exclusive with 'json').
|
||||
* @type string $json Inline JSON content (mutually exclusive with 'file').
|
||||
* @type string $plugin Plugin name for attribution.
|
||||
* }
|
||||
* @return bool True if registered successfully, false otherwise.
|
||||
*/
|
||||
public function register_dashboard( string $slug, array $args ): bool {
|
||||
// Sanitize slug - must be valid identifier.
|
||||
$slug = sanitize_key( $slug );
|
||||
|
||||
if ( empty( $slug ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'WP Prometheus: Dashboard registration failed - invalid slug' );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate slugs (built-in takes precedence).
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard slug '$slug' conflicts with built-in dashboard" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate slugs in already registered dashboards.
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard slug '$slug' already registered" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields.
|
||||
if ( empty( $args['title'] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' missing required 'title'" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have either 'file' or 'json', not both.
|
||||
$has_file = ! empty( $args['file'] );
|
||||
$has_json = ! empty( $args['json'] );
|
||||
|
||||
if ( ! $has_file && ! $has_json ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' must have 'file' or 'json'" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $has_file && $has_json ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' cannot have both 'file' and 'json'" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate file path if provided.
|
||||
if ( $has_file ) {
|
||||
$file_path = $args['file'];
|
||||
|
||||
// Must be absolute path.
|
||||
if ( ! path_is_absolute( $file_path ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' file path must be absolute" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// File must exist and be readable.
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' file not found: $file_path" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Security: Prevent path traversal - file must be under wp-content.
|
||||
$real_path = realpath( $file_path );
|
||||
$wp_content_dir = realpath( WP_CONTENT_DIR );
|
||||
|
||||
if ( false === $real_path || false === $wp_content_dir ||
|
||||
strpos( $real_path, $wp_content_dir ) !== 0 ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' file outside wp-content" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JSON if provided inline.
|
||||
if ( $has_json ) {
|
||||
$decoded = json_decode( $args['json'], true );
|
||||
if ( null === $decoded && json_last_error() !== JSON_ERROR_NONE ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' has invalid JSON" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Build dashboard entry.
|
||||
$this->registered_dashboards[ $slug ] = array(
|
||||
'title' => sanitize_text_field( $args['title'] ),
|
||||
'description' => sanitize_text_field( $args['description'] ?? '' ),
|
||||
'icon' => sanitize_html_class( $args['icon'] ?? 'dashicons-chart-line' ),
|
||||
'file' => $has_file ? $file_path : null,
|
||||
'json' => $has_json ? $args['json'] : null,
|
||||
'plugin' => sanitize_text_field( $args['plugin'] ?? '' ),
|
||||
'source' => 'third-party',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the dashboard registration hook with protection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function fire_registration_hook(): void {
|
||||
if ( $this->hook_fired ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->hook_fired = true;
|
||||
|
||||
// Check for isolated mode - skip third-party hooks.
|
||||
$isolated_mode = defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE;
|
||||
|
||||
// Also check option for admin-side isolated mode.
|
||||
if ( ! $isolated_mode ) {
|
||||
$isolated_mode = (bool) get_option( 'wp_prometheus_isolated_mode', false );
|
||||
}
|
||||
|
||||
if ( $isolated_mode ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use output buffering to capture any accidental output.
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Fires to allow third-party plugins to register dashboards.
|
||||
*
|
||||
* @since 0.4.6
|
||||
*
|
||||
* @param DashboardProvider $provider The dashboard provider instance.
|
||||
*/
|
||||
do_action( 'wp_prometheus_register_dashboards', $this );
|
||||
} catch ( \Throwable $e ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'WP Prometheus: Error in dashboard registration hook: ' . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
// Discard any output from plugins.
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available dashboards.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available(): array {
|
||||
// Fire registration hook first (only once).
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$available = array();
|
||||
|
||||
foreach ( $this->dashboards as $slug => $dashboard ) {
|
||||
// Add built-in dashboards (check file exists).
|
||||
foreach ( $this->builtin_dashboards as $slug => $dashboard ) {
|
||||
$file_path = $this->dashboard_dir . $dashboard['file'];
|
||||
if ( file_exists( $file_path ) ) {
|
||||
$available[ $slug ] = $dashboard;
|
||||
}
|
||||
}
|
||||
|
||||
// Add registered third-party dashboards.
|
||||
foreach ( $this->registered_dashboards as $slug => $dashboard ) {
|
||||
// Already validated during registration, but double-check.
|
||||
if ( ! empty( $dashboard['json'] ) ||
|
||||
( ! empty( $dashboard['file'] ) && file_exists( $dashboard['file'] ) ) ) {
|
||||
$available[ $slug ] = $dashboard;
|
||||
}
|
||||
}
|
||||
|
||||
return $available;
|
||||
}
|
||||
|
||||
@@ -86,35 +299,70 @@ class DashboardProvider {
|
||||
* @return string|null JSON content or null if not found.
|
||||
*/
|
||||
public function get_dashboard( string $slug ): ?string {
|
||||
// Validate slug to prevent directory traversal.
|
||||
// Fire registration hook first.
|
||||
$this->fire_registration_hook();
|
||||
|
||||
// Validate slug.
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( ! isset( $this->dashboards[ $slug ] ) ) {
|
||||
return null;
|
||||
// Check built-in dashboards first.
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
$dashboard = $this->builtin_dashboards[ $slug ];
|
||||
$file_path = $this->dashboard_dir . $dashboard['file'];
|
||||
|
||||
// Security: Ensure file is within dashboard directory.
|
||||
$real_path = realpath( $file_path );
|
||||
$real_dir = realpath( $this->dashboard_dir );
|
||||
|
||||
if ( false === $real_path || false === $real_dir ||
|
||||
strpos( $real_path, $real_dir ) !== 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
return false === $content ? null : $content;
|
||||
}
|
||||
|
||||
$file_path = $this->dashboard_dir . $this->dashboards[ $slug ]['file'];
|
||||
// Check registered dashboards.
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
$dashboard = $this->registered_dashboards[ $slug ];
|
||||
|
||||
// Security: Ensure file is within dashboard directory.
|
||||
$real_path = realpath( $file_path );
|
||||
$real_dir = realpath( $this->dashboard_dir );
|
||||
// Inline JSON.
|
||||
if ( ! empty( $dashboard['json'] ) ) {
|
||||
return $dashboard['json'];
|
||||
}
|
||||
|
||||
if ( false === $real_path || false === $real_dir || strpos( $real_path, $real_dir ) !== 0 ) {
|
||||
return null;
|
||||
// File-based.
|
||||
if ( ! empty( $dashboard['file'] ) ) {
|
||||
$file_path = $dashboard['file'];
|
||||
|
||||
// Security: Re-verify file is under wp-content.
|
||||
$real_path = realpath( $file_path );
|
||||
$wp_content_dir = realpath( WP_CONTENT_DIR );
|
||||
|
||||
if ( false === $real_path || false === $wp_content_dir ||
|
||||
strpos( $real_path, $wp_content_dir ) !== 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
return false === $content ? null : $content;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
if ( false === $content ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $content;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,13 +372,20 @@ class DashboardProvider {
|
||||
* @return array|null Dashboard metadata or null if not found.
|
||||
*/
|
||||
public function get_metadata( string $slug ): ?array {
|
||||
// Fire registration hook first.
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( ! isset( $this->dashboards[ $slug ] ) ) {
|
||||
return null;
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
return $this->builtin_dashboards[ $slug ];
|
||||
}
|
||||
|
||||
return $this->dashboards[ $slug ];
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
return $this->registered_dashboards[ $slug ];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,12 +395,61 @@ class DashboardProvider {
|
||||
* @return string|null Filename or null if not found.
|
||||
*/
|
||||
public function get_filename( string $slug ): ?string {
|
||||
// Fire registration hook first.
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( ! isset( $this->dashboards[ $slug ] ) ) {
|
||||
return null;
|
||||
// Built-in dashboards have predefined filenames.
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
return $this->builtin_dashboards[ $slug ]['file'];
|
||||
}
|
||||
|
||||
return $this->dashboards[ $slug ]['file'];
|
||||
// Registered dashboards - use file basename or generate from slug.
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
$dashboard = $this->registered_dashboards[ $slug ];
|
||||
|
||||
if ( ! empty( $dashboard['file'] ) ) {
|
||||
return basename( $dashboard['file'] );
|
||||
}
|
||||
|
||||
// Generate filename from slug for inline JSON.
|
||||
return $slug . '.json';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a dashboard is from a third-party plugin.
|
||||
*
|
||||
* @param string $slug Dashboard slug.
|
||||
* @return bool True if third-party, false if built-in or not found.
|
||||
*/
|
||||
public function is_third_party( string $slug ): bool {
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
return isset( $this->registered_dashboards[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin name for a third-party dashboard.
|
||||
*
|
||||
* @param string $slug Dashboard slug.
|
||||
* @return string|null Plugin name or null if not found/built-in.
|
||||
*/
|
||||
public function get_plugin_name( string $slug ): ?string {
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
$plugin = $this->registered_dashboards[ $slug ]['plugin'];
|
||||
return ! empty( $plugin ) ? $plugin : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,17 +107,25 @@ class Settings {
|
||||
* @return void
|
||||
*/
|
||||
public function register_settings(): void {
|
||||
// Register settings for metrics tab.
|
||||
register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_auth_token', array(
|
||||
// Register settings for endpoint sub-tab.
|
||||
register_setting( 'wp_prometheus_endpoint_settings', 'wp_prometheus_auth_token', array(
|
||||
'type' => 'string',
|
||||
'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',
|
||||
'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.
|
||||
add_settings_section(
|
||||
'wp_prometheus_auth_section',
|
||||
@@ -394,32 +402,261 @@ class Settings {
|
||||
<?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.
|
||||
*
|
||||
* @return 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">
|
||||
<?php
|
||||
settings_fields( 'wp_prometheus_metrics_settings' );
|
||||
do_settings_sections( 'wp-prometheus-metrics' );
|
||||
submit_button();
|
||||
?>
|
||||
<?php settings_fields( 'wp_prometheus_endpoint_settings' ); ?>
|
||||
|
||||
<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
|
||||
}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</p>
|
||||
<div id="wp-prometheus-reset-message" style="display: none; margin-top: 10px;"></div>
|
||||
<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>
|
||||
<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
|
||||
}
|
||||
|
||||
@@ -977,20 +1214,45 @@ class Settings {
|
||||
<h2><?php esc_html_e( 'Grafana Dashboard Templates', 'wp-prometheus' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Pre-built dashboards for visualizing your WordPress metrics in Grafana.', 'wp-prometheus' ); ?></p>
|
||||
|
||||
<div class="wp-prometheus-dashboard-grid">
|
||||
<?php foreach ( $dashboards as $slug => $dashboard ) : ?>
|
||||
<div class="wp-prometheus-dashboard-card">
|
||||
<div class="dashboard-icon">
|
||||
<span class="dashicons <?php echo esc_attr( $dashboard['icon'] ); ?>"></span>
|
||||
<?php if ( empty( $dashboards ) ) : ?>
|
||||
<p class="description"><?php esc_html_e( 'No dashboards available.', 'wp-prometheus' ); ?></p>
|
||||
<?php else : ?>
|
||||
<div class="wp-prometheus-dashboard-grid">
|
||||
<?php
|
||||
foreach ( $dashboards as $slug => $dashboard ) :
|
||||
$is_third_party = $this->dashboard_provider->is_third_party( $slug );
|
||||
$plugin_name = $this->dashboard_provider->get_plugin_name( $slug );
|
||||
$card_class = 'wp-prometheus-dashboard-card' . ( $is_third_party ? ' third-party' : '' );
|
||||
?>
|
||||
<div class="<?php echo esc_attr( $card_class ); ?>">
|
||||
<?php if ( $is_third_party ) : ?>
|
||||
<span class="dashboard-badge"><?php esc_html_e( 'Extension', 'wp-prometheus' ); ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="dashboard-icon">
|
||||
<span class="dashicons <?php echo esc_attr( $dashboard['icon'] ); ?>"></span>
|
||||
</div>
|
||||
<h3><?php echo esc_html( $dashboard['title'] ); ?></h3>
|
||||
<p><?php echo esc_html( $dashboard['description'] ); ?></p>
|
||||
<?php if ( $is_third_party && $plugin_name ) : ?>
|
||||
<p class="dashboard-plugin">
|
||||
<small>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Plugin name */
|
||||
esc_html__( 'Provided by: %s', 'wp-prometheus' ),
|
||||
esc_html( $plugin_name )
|
||||
);
|
||||
?>
|
||||
</small>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button button-primary download-dashboard" data-slug="<?php echo esc_attr( $slug ); ?>">
|
||||
<?php esc_html_e( 'Download', 'wp-prometheus' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<h3><?php echo esc_html( $dashboard['title'] ); ?></h3>
|
||||
<p><?php echo esc_html( $dashboard['description'] ); ?></p>
|
||||
<button type="button" class="button button-primary download-dashboard" data-slug="<?php echo esc_attr( $slug ); ?>">
|
||||
<?php esc_html_e( 'Download', 'wp-prometheus' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<hr style="margin: 30px 0;">
|
||||
|
||||
@@ -1103,6 +1365,11 @@ class Settings {
|
||||
<td><?php esc_html_e( 'Counter', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Database queries by endpoint', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_db_query_duration_seconds</code></td>
|
||||
<td><?php esc_html_e( 'Histogram', 'wp-prometheus' ); ?></td>
|
||||
<td><?php esc_html_e( 'Database query duration distribution (requires SAVEQUERIES)', 'wp-prometheus' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wordpress_cron_events_total</code></td>
|
||||
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>
|
||||
|
||||
@@ -45,7 +45,9 @@ class MetricsEndpoint {
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
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.
|
||||
*
|
||||
* Called during parse_request to intercept before themes/plugins load.
|
||||
*
|
||||
* @param \WP $wp WordPress environment instance.
|
||||
* @return void
|
||||
*/
|
||||
public function handle_request(): void {
|
||||
if ( ! get_query_var( 'wp_prometheus_metrics' ) ) {
|
||||
public function handle_request( \WP $wp ): void {
|
||||
if ( empty( $wp->query_vars['wp_prometheus_metrics'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,9 +121,62 @@ class Collector {
|
||||
/**
|
||||
* Fires after default metrics are collected.
|
||||
*
|
||||
* In isolated mode, skip custom hooks to avoid any potential issues.
|
||||
* In safe mode (default), fire hooks with protection against recursion.
|
||||
*
|
||||
* @param Collector $collector The metrics collector instance.
|
||||
*/
|
||||
do_action( 'wp_prometheus_collect_metrics', $this );
|
||||
if ( defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE ) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WP Prometheus
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus
|
||||
* Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics.
|
||||
* Version: 0.4.0
|
||||
* Version: 0.4.7
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.3
|
||||
* Author: Marco Graetsch
|
||||
@@ -21,12 +21,155 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Early metrics request detection.
|
||||
*
|
||||
* Detects /metrics requests early and removes problematic content filters
|
||||
* to prevent recursion issues with Twig-based plugins. Unlike the previous
|
||||
* "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 {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
$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 isolated mode - Collector will skip extensibility hooks.
|
||||
define( 'WP_PROMETHEUS_ISOLATED_MODE', true );
|
||||
|
||||
// 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.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_PROMETHEUS_VERSION', '0.4.0' );
|
||||
define( 'WP_PROMETHEUS_VERSION', '0.4.7' );
|
||||
|
||||
/**
|
||||
* Plugin file path.
|
||||
|
||||
Reference in New Issue
Block a user