4 Commits

Author SHA1 Message Date
9bfed06466 fix: Defer DashboardProvider translations to avoid early textdomain loading (v0.4.8)
All checks were successful
Create Release Package / build-release (push) Successful in 54s
DashboardProvider constructor also had __() calls during plugins_loaded.
Applied same lazy-initialization pattern as Settings tab labels.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-07 11:44:06 +01:00
b605d0c299 fix: Defer textdomain loading to init action for WordPress 6.7+ compatibility (v0.4.8)
All checks were successful
Create Release Package / build-release (push) Successful in 1m1s
Fixes _load_textdomain_just_in_time notice and headers already sent warnings
on admin pages by deferring load_plugin_textdomain() and Settings tab label
initialization to the init action instead of plugins_loaded.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-07 11:39:25 +01:00
63660202c4 docs: Clarify translation compilation is handled by CI/CD
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 19:33:02 +01:00
3b71a0f7c9 docs: Add database query timing documentation and dashboard panel (v0.4.7)
All checks were successful
Create Release Package / build-release (push) Successful in 56s
- Add Query Duration Distribution panel to Grafana Runtime dashboard
- Add wordpress_db_query_duration_seconds to Help tab metrics reference
- Add SAVEQUERIES documentation section to README
- Update translation files with new strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 19:27:57 +01:00
10 changed files with 252 additions and 53 deletions

View File

@@ -5,6 +5,27 @@ 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.8] - 2026-02-07
### Fixed
- Fixed `_load_textdomain_just_in_time` notice on admin pages (WordPress 6.7+ compatibility)
- Deferred `load_plugin_textdomain()` to `init` action instead of `plugins_loaded`
- Deferred Settings tab label and DashboardProvider initialization to avoid early translation loading
## [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 ## [0.4.6] - 2026-02-03
### Added ### Added

View File

@@ -34,7 +34,9 @@ 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. **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 pending roadmap items.* ### Known Bugs
*No known bugs at this time.*
## Technical Stack ## Technical Stack
@@ -79,11 +81,7 @@ Text domain: `wp-prometheus`
- `en_US` - English (United States) [base language - .pot template] - `en_US` - English (United States) [base language - .pot template]
- `de_CH` - German (Switzerland, formal) - `de_CH` - German (Switzerland, formal)
To compile translations to .mo files for production: Translation compilation (.po → .mo) is handled automatically by CI/CD pipeline during release builds. No local compilation needed.
```bash
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
```
### Create releases ### Create releases
@@ -292,6 +290,35 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
## Session History ## Session History
### 2026-02-07 - Fix Early Textdomain Loading (v0.4.8)
- Fixed `_load_textdomain_just_in_time` warning on admin pages (WordPress 6.7+ compatibility)
- Root cause: `load_plugin_textdomain()` was called during `plugins_loaded` in `Plugin::__construct()`
- WordPress 6.7+ requires textdomain loading at the `init` action or later
- Three classes needed fixing:
- `Plugin.php`: Deferred `load_textdomain()` to `init` action hook, changed method visibility to public
- `Settings.php`: Deferred tab label initialization (which uses `__()`) to a lazy `get_tabs()` method
- `DashboardProvider.php`: Deferred built-in dashboard definitions (with `__()` calls) to a lazy `get_builtin_dashboards()` method
- Cleared Known Bugs section — no remaining known issues
- **Key Learning**: WordPress 6.7 textdomain loading requirements
- `load_plugin_textdomain()` must be called at `init` or later
- WordPress's JIT textdomain loader (`_load_textdomain_just_in_time`) also triggers too-early warnings
- Any `__()` / `_e()` calls before `init` for a plugin textdomain will trigger the notice
- The warning causes "headers already sent" errors because the notice output breaks header modifications
- Solution: Defer both explicit `load_plugin_textdomain()` and any `__()` calls to `init` or later hooks
### 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) ### 2026-02-03 - Dashboard Extension Hook (v0.4.6)
- Added `wp_prometheus_register_dashboards` action hook for third-party plugins - Added `wp_prometheus_register_dashboards` action hook for third-party plugins

View File

@@ -98,6 +98,21 @@ scrape_configs:
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact. **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+) ### Cron Metrics (v0.2.0+)
| Metric | Type | Labels | Description | | Metric | Type | Labels | Description |

View File

@@ -946,6 +946,95 @@
], ],
"title": "Average Query Duration (Overall)", "title": "Average Query Duration (Overall)",
"type": "stat" "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", "refresh": "30s",

View File

@@ -317,6 +317,10 @@ msgstr "HTTP-Anfragedauer-Verteilung"
msgid "Database queries by endpoint" msgid "Database queries by endpoint"
msgstr "Datenbank-Abfragen nach Endpunkt" 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 #: src/Admin/Settings.php
msgid "Scheduled cron events by hook" msgid "Scheduled cron events by hook"
msgstr "Geplante Cron-Ereignisse nach Hook" msgstr "Geplante Cron-Ereignisse nach Hook"

View File

@@ -314,6 +314,10 @@ msgstr ""
msgid "Database queries by endpoint" msgid "Database queries by endpoint"
msgstr "" msgstr ""
#: src/Admin/Settings.php
msgid "Database query duration distribution (requires SAVEQUERIES)"
msgstr ""
#: src/Admin/Settings.php #: src/Admin/Settings.php
msgid "Scheduled cron events by hook" msgid "Scheduled cron events by hook"
msgstr "" msgstr ""

View File

@@ -53,30 +53,43 @@ class DashboardProvider {
*/ */
public function __construct() { public function __construct() {
$this->dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/'; $this->dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/';
}
$this->builtin_dashboards = array( /**
'wordpress-overview' => array( * Get built-in dashboard definitions.
'title' => __( 'WordPress Overview', 'wp-prometheus' ), *
'description' => __( 'General WordPress metrics including users, posts, comments, and plugins.', 'wp-prometheus' ), * Lazily initializes dashboard labels to avoid triggering textdomain loading
'file' => 'wordpress-overview.json', * before the 'init' action (required since WordPress 6.7).
'icon' => 'dashicons-wordpress', *
'source' => 'builtin', * @return array
), */
'wordpress-runtime' => array( private function get_builtin_dashboards(): array {
'title' => __( 'Runtime Performance', 'wp-prometheus' ), if ( empty( $this->builtin_dashboards ) ) {
'description' => __( 'HTTP request metrics, database query performance, and response times.', 'wp-prometheus' ), $this->builtin_dashboards = array(
'file' => 'wordpress-runtime.json', 'wordpress-overview' => array(
'icon' => 'dashicons-performance', 'title' => __( 'WordPress Overview', 'wp-prometheus' ),
'source' => 'builtin', 'description' => __( 'General WordPress metrics including users, posts, comments, and plugins.', 'wp-prometheus' ),
), 'file' => 'wordpress-overview.json',
'wordpress-woocommerce' => array( 'icon' => 'dashicons-wordpress',
'title' => __( 'WooCommerce Store', 'wp-prometheus' ), 'source' => 'builtin',
'description' => __( 'WooCommerce metrics including products, orders, revenue, and customers.', 'wp-prometheus' ), ),
'file' => 'wordpress-woocommerce.json', 'wordpress-runtime' => array(
'icon' => 'dashicons-cart', 'title' => __( 'Runtime Performance', 'wp-prometheus' ),
'source' => 'builtin', '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',
),
);
}
return $this->builtin_dashboards;
} }
/** /**
@@ -106,7 +119,7 @@ class DashboardProvider {
} }
// Check for duplicate slugs (built-in takes precedence). // Check for duplicate slugs (built-in takes precedence).
if ( isset( $this->builtin_dashboards[ $slug ] ) ) { if ( isset( $this->get_builtin_dashboards()[ $slug ] ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( "WP Prometheus: Dashboard slug '$slug' conflicts with built-in dashboard" ); error_log( "WP Prometheus: Dashboard slug '$slug' conflicts with built-in dashboard" );
@@ -273,7 +286,7 @@ class DashboardProvider {
$available = array(); $available = array();
// Add built-in dashboards (check file exists). // Add built-in dashboards (check file exists).
foreach ( $this->builtin_dashboards as $slug => $dashboard ) { foreach ( $this->get_builtin_dashboards() as $slug => $dashboard ) {
$file_path = $this->dashboard_dir . $dashboard['file']; $file_path = $this->dashboard_dir . $dashboard['file'];
if ( file_exists( $file_path ) ) { if ( file_exists( $file_path ) ) {
$available[ $slug ] = $dashboard; $available[ $slug ] = $dashboard;
@@ -306,8 +319,9 @@ class DashboardProvider {
$slug = sanitize_file_name( $slug ); $slug = sanitize_file_name( $slug );
// Check built-in dashboards first. // Check built-in dashboards first.
if ( isset( $this->builtin_dashboards[ $slug ] ) ) { $builtin = $this->get_builtin_dashboards();
$dashboard = $this->builtin_dashboards[ $slug ]; if ( isset( $builtin[ $slug ] ) ) {
$dashboard = $builtin[ $slug ];
$file_path = $this->dashboard_dir . $dashboard['file']; $file_path = $this->dashboard_dir . $dashboard['file'];
// Security: Ensure file is within dashboard directory. // Security: Ensure file is within dashboard directory.
@@ -377,8 +391,9 @@ class DashboardProvider {
$slug = sanitize_file_name( $slug ); $slug = sanitize_file_name( $slug );
if ( isset( $this->builtin_dashboards[ $slug ] ) ) { $builtin = $this->get_builtin_dashboards();
return $this->builtin_dashboards[ $slug ]; if ( isset( $builtin[ $slug ] ) ) {
return $builtin[ $slug ];
} }
if ( isset( $this->registered_dashboards[ $slug ] ) ) { if ( isset( $this->registered_dashboards[ $slug ] ) ) {
@@ -401,8 +416,9 @@ class DashboardProvider {
$slug = sanitize_file_name( $slug ); $slug = sanitize_file_name( $slug );
// Built-in dashboards have predefined filenames. // Built-in dashboards have predefined filenames.
if ( isset( $this->builtin_dashboards[ $slug ] ) ) { $builtin = $this->get_builtin_dashboards();
return $this->builtin_dashboards[ $slug ]['file']; if ( isset( $builtin[ $slug ] ) ) {
return $builtin[ $slug ]['file'];
} }
// Registered dashboards - use file basename or generate from slug. // Registered dashboards - use file basename or generate from slug.

View File

@@ -49,15 +49,6 @@ class Settings {
* Constructor. * Constructor.
*/ */
public function __construct() { public function __construct() {
$this->tabs = array(
'license' => __( 'License', 'wp-prometheus' ),
'metrics' => __( 'Metrics', 'wp-prometheus' ),
'storage' => __( 'Storage', 'wp-prometheus' ),
'custom' => __( 'Custom Metrics', 'wp-prometheus' ),
'dashboards' => __( 'Dashboards', 'wp-prometheus' ),
'help' => __( 'Help', 'wp-prometheus' ),
);
$this->metric_builder = new CustomMetricBuilder(); $this->metric_builder = new CustomMetricBuilder();
$this->dashboard_provider = new DashboardProvider(); $this->dashboard_provider = new DashboardProvider();
@@ -76,14 +67,37 @@ class Settings {
add_action( 'wp_ajax_wp_prometheus_test_storage', array( $this, 'ajax_test_storage' ) ); add_action( 'wp_ajax_wp_prometheus_test_storage', array( $this, 'ajax_test_storage' ) );
} }
/**
* Get available tabs.
*
* Lazily initializes tab labels to avoid triggering textdomain loading
* before the 'init' action (required since WordPress 6.7).
*
* @return array
*/
private function get_tabs(): array {
if ( empty( $this->tabs ) ) {
$this->tabs = array(
'license' => __( 'License', 'wp-prometheus' ),
'metrics' => __( 'Metrics', 'wp-prometheus' ),
'storage' => __( 'Storage', 'wp-prometheus' ),
'custom' => __( 'Custom Metrics', 'wp-prometheus' ),
'dashboards' => __( 'Dashboards', 'wp-prometheus' ),
'help' => __( 'Help', 'wp-prometheus' ),
);
}
return $this->tabs;
}
/** /**
* Get current tab. * Get current tab.
* *
* @return string * @return string
*/ */
private function get_current_tab(): string { private function get_current_tab(): string {
$tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license'; $tabs = $this->get_tabs();
return array_key_exists( $tab, $this->tabs ) ? $tab : 'license'; $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license';
return array_key_exists( $tab, $tabs ) ? $tab : 'license';
} }
/** /**
@@ -268,7 +282,7 @@ class Settings {
?> ?>
<nav class="nav-tab-wrapper wp-clearfix"> <nav class="nav-tab-wrapper wp-clearfix">
<?php <?php
foreach ( $this->tabs as $tab_id => $tab_name ) { foreach ( $this->get_tabs() as $tab_id => $tab_name ) {
$tab_url = add_query_arg( $tab_url = add_query_arg(
array( array(
'page' => 'wp-prometheus', 'page' => 'wp-prometheus',
@@ -1365,6 +1379,11 @@ class Settings {
<td><?php esc_html_e( 'Counter', 'wp-prometheus' ); ?></td> <td><?php esc_html_e( 'Counter', 'wp-prometheus' ); ?></td>
<td><?php esc_html_e( 'Database queries by endpoint', 'wp-prometheus' ); ?></td> <td><?php esc_html_e( 'Database queries by endpoint', 'wp-prometheus' ); ?></td>
</tr> </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> <tr>
<td><code>wordpress_cron_events_total</code></td> <td><code>wordpress_cron_events_total</code></td>
<td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td> <td><?php esc_html_e( 'Gauge', 'wp-prometheus' ); ?></td>

View File

@@ -57,7 +57,9 @@ final class Plugin {
private function __construct() { private function __construct() {
$this->init_components(); $this->init_components();
$this->init_hooks(); $this->init_hooks();
$this->load_textdomain();
// Defer textdomain loading to 'init' action (required since WordPress 6.7).
add_action( 'init', array( $this, 'load_textdomain' ) );
} }
/** /**
@@ -144,9 +146,11 @@ final class Plugin {
/** /**
* Load plugin textdomain. * Load plugin textdomain.
* *
* Hooked to 'init' action to comply with WordPress 6.7+ requirements.
*
* @return void * @return void
*/ */
private function load_textdomain(): void { public function load_textdomain(): void {
load_plugin_textdomain( load_plugin_textdomain(
'wp-prometheus', 'wp-prometheus',
false, false,

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.6 * Version: 0.4.8
* Requires at least: 6.4 * Requires at least: 6.4
* Requires PHP: 8.3 * Requires PHP: 8.3
* Author: Marco Graetsch * Author: Marco Graetsch
@@ -169,7 +169,7 @@ wp_prometheus_early_metrics_check();
* *
* @var string * @var string
*/ */
define( 'WP_PROMETHEUS_VERSION', '0.4.6' ); define( 'WP_PROMETHEUS_VERSION', '0.4.8' );
/** /**
* Plugin file path. * Plugin file path.