From bad977bef09a619aa9d869deacd84003d63b22dc Mon Sep 17 00:00:00 2001 From: magdev Date: Mon, 2 Feb 2026 15:27:16 +0100 Subject: [PATCH] feat: Add custom metric builder, export/import, and Grafana dashboards (v0.3.0) - Custom Metrics Builder with admin UI for gauge metrics - Support for static values and WordPress option-based values - JSON-based export/import with skip/overwrite/rename modes - Three Grafana dashboard templates (overview, runtime, WooCommerce) - New tabs: Custom Metrics and Dashboards - Reset runtime metrics button Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 27 + CLAUDE.md | 46 +- assets/css/admin.css | 160 ++ assets/dashboards/wordpress-overview.json | 851 +++++++++ assets/dashboards/wordpress-runtime.json | 987 +++++++++++ assets/dashboards/wordpress-woocommerce.json | 1296 ++++++++++++++ assets/js/admin.js | 632 ++++++- languages/wp-prometheus-de_CH.mo | Bin 7822 -> 12266 bytes languages/wp-prometheus-de_CH.po | 268 ++- languages/wp-prometheus.pot | 264 ++- src/Admin/DashboardProvider.php | 151 ++ src/Admin/Settings.php | 1671 ++++++++++++------ src/Installer.php | 1 + src/Metrics/Collector.php | 5 + src/Metrics/CustomMetricBuilder.php | 496 ++++++ wp-prometheus.php | 4 +- 16 files changed, 6239 insertions(+), 620 deletions(-) create mode 100644 assets/dashboards/wordpress-overview.json create mode 100644 assets/dashboards/wordpress-runtime.json create mode 100644 assets/dashboards/wordpress-woocommerce.json create mode 100644 src/Admin/DashboardProvider.php create mode 100644 src/Metrics/CustomMetricBuilder.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 419cea5..5f46832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ 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.3.0] - 2026-02-02 + +### Added + +- Custom Metrics Builder: + - Admin UI to define custom gauge metrics + - Support for static values and WordPress option-based values + - Label support with up to 5 labels and 50 value combinations + - Metric validation following Prometheus naming conventions +- Metric Export/Import: + - JSON-based configuration export for backup + - Import with three modes: skip existing, overwrite, or rename duplicates + - Version tracking in export format +- Grafana Dashboard Templates: + - WordPress Overview dashboard (users, posts, comments, cron, transients) + - WordPress Runtime dashboard (HTTP requests, database queries) + - WordPress WooCommerce dashboard (orders, revenue, products, customers) + - Easy download and import instructions +- New "Custom Metrics" tab in admin settings +- New "Dashboards" tab in admin settings +- Reset runtime metrics button to clear accumulated data + +### Changed + +- Settings page now has 5 tabs: License, Metrics, Custom Metrics, Dashboards, Help +- Updated translations with all new strings + ## [0.2.2] - 2026-02-02 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index cf35e28..9fd0ae8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,9 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a - Cron job metrics (scheduled events, overdue, next run) - Transient cache metrics (total, expiring, expired) - WooCommerce integration (products, orders, revenue, customers) +- Custom metric builder with admin UI (gauges with static or option-based values) +- Metric export/import for backup and site migration +- Grafana dashboard templates for easy visualization - Dedicated plugin settings under 'Settings/Metrics' menu - Extensible by other plugins using `wp_prometheus_collect_metrics` action hook - License management integration @@ -30,11 +33,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. -### Version 0.3.0 (Planned) - -- Custom metric builder in admin -- Metric export/import -- Grafana dashboard templates +*No planned features at this time.* ## Technical Stack @@ -214,6 +213,10 @@ wp-prometheus/ │ └── release.yml # CI/CD pipeline ├── assets/ │ ├── css/ # Admin/Frontend styles +│ ├── dashboards/ # Grafana dashboard templates +│ │ ├── wordpress-overview.json +│ │ ├── wordpress-runtime.json +│ │ └── wordpress-woocommerce.json │ └── js/ │ └── admin.js # Admin JavaScript ├── languages/ # Translation files @@ -222,6 +225,7 @@ wp-prometheus/ ├── releases/ # Release packages ├── src/ │ ├── Admin/ +│ │ ├── DashboardProvider.php # Grafana dashboard provider │ │ └── Settings.php # Settings page │ ├── Endpoint/ │ │ └── MetricsEndpoint.php # /metrics endpoint @@ -229,6 +233,7 @@ wp-prometheus/ │ │ └── Manager.php # License management │ ├── Metrics/ │ │ ├── Collector.php # Prometheus metrics collector +│ │ ├── CustomMetricBuilder.php # Custom metric CRUD │ │ └── RuntimeCollector.php # Runtime metrics collector │ ├── Installer.php # Activation/Deactivation │ ├── Plugin.php # Main plugin class @@ -285,6 +290,37 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) { ## Session History +### 2026-02-02 - Custom Metrics & Dashboards (v0.3.0) + +- Added Custom Metric Builder with full admin UI: + - `CustomMetricBuilder.php` - CRUD operations, validation, export/import + - Support for static values and WordPress option-based values + - Label support (max 5 labels, 50 value combinations) + - Prometheus naming convention validation (`[a-zA-Z_:][a-zA-Z0-9_:]*`) +- Added Grafana Dashboard Templates: + - `DashboardProvider.php` - Dashboard file provider with path traversal protection + - `wordpress-overview.json` - General WordPress metrics + - `wordpress-runtime.json` - HTTP/DB performance metrics + - `wordpress-woocommerce.json` - WooCommerce store metrics +- Added export/import functionality: + - JSON-based configuration export + - Three import modes: skip, overwrite, rename duplicates + - Version tracking in export format +- Updated Settings page with new tabs: + - "Custom Metrics" tab with metric form and table + - "Dashboards" tab with download buttons + - "Reset Runtime Metrics" button in Metrics tab +- Updated `Collector.php` to integrate custom metrics +- Updated translation files with all new strings +- **Key Learning**: Dynamic form handling in WordPress admin + - Use `wp_create_nonce()` with unique nonce names per AJAX action + - Localize script with `wp_localize_script()` for nonces and AJAX URL + - Always verify `current_user_can('manage_options')` in AJAX handlers +- **Key Learning**: Grafana dashboard JSON format + - Use `${DS_PROMETHEUS}` for data source variable + - Schema version 39 for current Grafana compatibility + - Panels use `gridPos` for layout positioning + ### 2026-02-02 - Extended Metrics (v0.2.0) - Added WooCommerce integration metrics (only when WooCommerce is active): diff --git a/assets/css/admin.css b/assets/css/admin.css index 6f56f3d..785bc62 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -45,3 +45,163 @@ .wp-prometheus-tab-content .form-table { margin-top: 10px; } + +/* Custom metrics form */ +.wp-prometheus-metric-form { + background: #fff; + border: 1px solid #ccd0d4; + border-radius: 4px; + padding: 20px; + margin: 20px 0; +} + +.wp-prometheus-metric-form h3 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.wp-prometheus-metric-form .required { + color: #d63638; +} + +/* Label rows */ +.metric-label-row { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.metric-label-row input { + flex: 1; + max-width: 300px; +} + +.metric-label-row .remove-label { + padding: 0 8px; + min-width: auto; +} + +/* Value rows */ +.metric-value-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + flex-wrap: wrap; +} + +.metric-value-row input { + width: 120px; +} + +.metric-value-row .remove-value-row { + padding: 0 8px; + min-width: auto; +} + +/* Custom metrics table */ +.wp-prometheus-custom-metrics .wp-list-table code { + background: #f0f0f1; + padding: 2px 6px; + border-radius: 3px; +} + +.wp-prometheus-custom-metrics .wp-list-table td { + vertical-align: middle; +} + +.wp-prometheus-custom-metrics .wp-list-table .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Dashboard grid */ +.wp-prometheus-dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin: 20px 0; +} + +.wp-prometheus-dashboard-card { + background: #fff; + border: 1px solid #ccd0d4; + border-radius: 4px; + padding: 20px; + text-align: center; + transition: box-shadow 0.2s ease; +} + +.wp-prometheus-dashboard-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.wp-prometheus-dashboard-card .dashboard-icon { + margin-bottom: 15px; +} + +.wp-prometheus-dashboard-card .dashboard-icon .dashicons { + font-size: 48px; + width: 48px; + height: 48px; + color: #2271b1; +} + +.wp-prometheus-dashboard-card h3 { + margin: 0 0 10px 0; + font-size: 16px; +} + +.wp-prometheus-dashboard-card p { + color: #646970; + margin: 0 0 15px 0; + font-size: 13px; +} + +/* Import options panel */ +#import-options { + border-radius: 4px; +} + +#import-options label { + display: block; + margin-bottom: 8px; +} + +/* Spinner alignment */ +.spinner { + float: none; + vertical-align: middle; +} + +/* Button groups */ +.wp-prometheus-tab-content .button + .button { + margin-left: 5px; +} + +/* Notice messages in forms */ +.wp-prometheus-metric-form .notice, +.wp-prometheus-custom-metrics .notice { + margin: 10px 0; + padding: 10px 15px; +} + +/* Responsive adjustments */ +@media screen and (max-width: 782px) { + .wp-prometheus-dashboard-grid { + grid-template-columns: 1fr; + } + + .metric-value-row { + flex-direction: column; + align-items: flex-start; + } + + .metric-value-row input { + width: 100%; + max-width: none; + } +} diff --git a/assets/dashboards/wordpress-overview.json b/assets/dashboards/wordpress-overview.json new file mode 100644 index 0000000..c8f896c --- /dev/null +++ b/assets/dashboards/wordpress-overview.json @@ -0,0 +1,851 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "System Info", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "/^version$/", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_info", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "WordPress Version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "/^php_version$/", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_info", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "PHP Version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_users_total)", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Total Users", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_plugins_total{status=\"active\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Active Plugins", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 6, + "panels": [], + "title": "Content", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 6 + }, + "id": 7, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_users_total", + "instant": true, + "legendFormat": "{{role}}", + "refId": "A" + } + ], + "title": "Users by Role", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 6 + }, + "id": 8, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_posts_total{post_type=\"post\"}", + "instant": true, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Posts by Status", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 6 + }, + "id": 9, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_comments_total{status!=\"total_comments\"}", + "instant": true, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Comments by Status", + "type": "piechart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 10, + "panels": [], + "title": "System Health", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 10 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 15 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_cron_overdue_total", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Overdue Cron Events", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 15 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_cron_events_total)", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Total Cron Events", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 15 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_transients_total{type=\"total\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Total Transients", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 15 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_transients_total{type=\"expired\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Expired Transients", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(10, wordpress_cron_events_total)", + "instant": false, + "legendFormat": "{{hook}}", + "range": true, + "refId": "A" + } + ], + "title": "Top 10 Cron Hooks", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["wordpress", "prometheus"], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "WordPress Overview", + "uid": "wp-prometheus-overview", + "version": 1, + "weekStart": "" +} diff --git a/assets/dashboards/wordpress-runtime.json b/assets/dashboards/wordpress-runtime.json new file mode 100644 index 0000000..7f1bbc3 --- /dev/null +++ b/assets/dashboards/wordpress-runtime.json @@ -0,0 +1,987 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "HTTP Requests", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(wordpress_http_requests_total[5m])) by (endpoint)", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests per Second by Endpoint", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/2..$/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/4..$/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/5..$/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(wordpress_http_requests_total[5m])) by (status)", + "legendFormat": "{{status}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests per Second by Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_http_requests_total) by (method)", + "instant": true, + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Requests by Method", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 9 + }, + "id": 5, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_http_requests_total) by (endpoint)", + "instant": true, + "legendFormat": "{{endpoint}}", + "refId": "A" + } + ], + "title": "Requests by Endpoint", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 9 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_http_requests_total)", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Total Requests (All Time)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 7, + "panels": [], + "title": "Request Duration", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 8, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_http_request_duration_seconds_sum / wordpress_http_request_duration_seconds_count", + "legendFormat": "{{method}} {{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Average Request Duration", + "type": "timeseries" + }, + { + "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": 12, + "x": 12, + "y": 18 + }, + "id": 9, + "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_http_request_duration_seconds_bucket) by (le)", + "format": "table", + "instant": true, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "title": "Request Duration Distribution (Buckets)", + "type": "barchart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 10, + "panels": [], + "title": "Database Queries", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 11, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(wordpress_db_queries_total[5m])", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Database Queries per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 12, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_db_query_duration_seconds_sum / wordpress_db_query_duration_seconds_count", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Average Query Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 35 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_db_queries_total)", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Total Database Queries (All Time)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "red", + "value": 0.5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 35 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(wordpress_db_query_duration_seconds_sum) / sum(wordpress_db_query_duration_seconds_count)", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Average Query Duration (Overall)", + "type": "stat" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["wordpress", "prometheus", "performance"], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "WordPress Runtime Performance", + "uid": "wp-prometheus-runtime", + "version": 1, + "weekStart": "" +} diff --git a/assets/dashboards/wordpress-woocommerce.json b/assets/dashboards/wordpress-woocommerce.json new file mode 100644 index 0000000..69a7ec7 --- /dev/null +++ b/assets/dashboards/wordpress-woocommerce.json @@ -0,0 +1,1296 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "panels": [], + "title": "Revenue Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_revenue_total{period=\"all_time\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Total Revenue (All Time)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_revenue_total{period=\"month\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Revenue (This Month)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_revenue_total{period=\"today\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Revenue (Today)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 5, + "panels": [], + "title": "Orders", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 8 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total{status=\"completed\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Completed", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 8 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total{status=\"processing\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Processing", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 8 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total{status=\"pending\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Pending", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 8 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total{status=\"on-hold\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "On Hold", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 8 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total{status=\"cancelled\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Cancelled", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 8 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total{status=\"refunded\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Refunded", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "completed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "processing" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "pending" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cancelled" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 12, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total", + "instant": true, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Orders by Status", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_orders_total", + "legendFormat": "{{status}}", + "range": true, + "refId": "A" + } + ], + "title": "Orders Over Time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 14, + "panels": [], + "title": "Products", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 21 + }, + "id": 15, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_products_total{status=\"publish\", type=\"all\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Published Products", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 21 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_products_total{status=\"draft\", type=\"all\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Draft Products", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 21 + }, + "id": 17, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_products_total{status=\"publish\", type!=\"all\"}", + "instant": true, + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Products by Type", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 18, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_products_total{type=\"all\"}", + "instant": true, + "legendFormat": "{{status}}", + "refId": "A" + } + ], + "title": "Products by Status", + "type": "piechart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 19, + "panels": [], + "title": "Customers", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 30 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_customers_total{type=\"registered\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Registered Customers", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 30 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_customers_total{type=\"guest\"}", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Guest Customers", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "registered" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "guest" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 30 + }, + "id": 22, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "donut", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "wordpress_woocommerce_customers_total", + "instant": true, + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Customer Distribution", + "type": "piechart" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["wordpress", "prometheus", "woocommerce"], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "WooCommerce Store", + "uid": "wp-prometheus-woocommerce", + "version": 1, + "weekStart": "" +} diff --git a/assets/js/admin.js b/assets/js/admin.js index eb91125..c22693f 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -7,7 +7,26 @@ (function($) { 'use strict'; + var importFileContent = null; + $(document).ready(function() { + // License tab handlers. + initLicenseHandlers(); + + // Custom metrics tab handlers. + initCustomMetricsHandlers(); + + // Dashboards tab handlers. + initDashboardsHandlers(); + + // Runtime metrics reset handler. + initResetRuntimeHandler(); + }); + + /** + * Initialize license tab handlers. + */ + function initLicenseHandlers() { // Validate license button. $('#wp-prometheus-validate-license').on('click', function(e) { e.preventDefault(); @@ -23,78 +42,575 @@ // Regenerate token button. $('#wp-prometheus-regenerate-token').on('click', function(e) { e.preventDefault(); - if (confirm('Are you sure you want to regenerate the auth token? You will need to update your Prometheus configuration.')) { + if (confirm(wpPrometheus.confirmRegenerateToken)) { var newToken = generateToken(32); $('#wp_prometheus_auth_token').val(newToken); } }); + } - /** - * Perform a license action via AJAX. - * - * @param {string} action AJAX action name. - * @param {string} message Loading message. - */ - function performLicenseAction(action, message) { - var $spinner = $('#wp-prometheus-license-spinner'); - var $message = $('#wp-prometheus-license-message'); + /** + * Initialize custom metrics tab handlers. + */ + function initCustomMetricsHandlers() { + var $formContainer = $('#wp-prometheus-metric-form-container'); + var $form = $('#wp-prometheus-metric-form'); + var $showFormBtn = $('#show-metric-form'); - $spinner.addClass('is-active'); - $message.hide(); + // Show metric form. + $showFormBtn.on('click', function() { + resetMetricForm(); + $formContainer.slideDown(); + $(this).hide(); + }); - $.ajax({ - url: wpPrometheus.ajaxUrl, - type: 'POST', - data: { - action: action, - nonce: wpPrometheus.nonce - }, - success: function(response) { - $spinner.removeClass('is-active'); + // Cancel metric form. + $('#cancel-metric-form').on('click', function() { + $formContainer.slideUp(); + $showFormBtn.show(); + // Remove edit parameter from URL. + if (window.location.search.indexOf('edit=') > -1) { + window.history.pushState({}, '', window.location.pathname + '?page=wp-prometheus&tab=custom'); + } + }); - if (response.success) { - $message - .removeClass('notice-error') - .addClass('notice notice-success') - .html('

' + response.data.message + '

') - .show(); + // Value type toggle. + $('input[name="value_type"]').on('change', function() { + var valueType = $(this).val(); + if (valueType === 'option') { + $('#option-config-row').show(); + $('#static-values-row').hide(); + } else { + $('#option-config-row').hide(); + $('#static-values-row').show(); + } + }); - // Reload page after successful validation/activation. - setTimeout(function() { - location.reload(); - }, 1500); - } else { - $message - .removeClass('notice-success') - .addClass('notice notice-error') - .html('

' + (response.data.message || 'An error occurred.') + '

') - .show(); - } - }, - error: function() { - $spinner.removeClass('is-active'); + // Add label. + $('#add-label').on('click', function() { + var $container = $('#metric-labels-container'); + var labelCount = $container.find('.metric-label-row').length; + + if (labelCount >= 5) { + alert('Maximum 5 labels allowed per metric.'); + return; + } + + var $row = $('
' + + '' + + '' + + '
'); + $container.append($row); + updateValueRows(); + }); + + // Remove label. + $(document).on('click', '.remove-label', function() { + $(this).closest('.metric-label-row').remove(); + updateValueRows(); + }); + + // Add value row. + $('#add-value-row').on('click', function() { + var $container = $('#metric-values-container'); + var rowCount = $container.find('.metric-value-row').length; + var labelCount = getLabelCount(); + + var $row = $('
'); + + // Add label value inputs. + var labels = getLabels(); + for (var i = 0; i < labelCount; i++) { + $row.append(''); + } + + // Add metric value input. + $row.append(''); + $row.append(''); + + $container.append($row); + }); + + // Remove value row. + $(document).on('click', '.remove-value-row', function() { + $(this).closest('.metric-value-row').remove(); + }); + + // Submit metric form. + $form.on('submit', function(e) { + e.preventDefault(); + saveCustomMetric(); + }); + + // Delete metric. + $(document).on('click', '.delete-metric', function() { + var id = $(this).data('id'); + if (confirm(wpPrometheus.confirmDelete)) { + deleteCustomMetric(id); + } + }); + + // Export metrics. + $('#export-metrics').on('click', function() { + exportMetrics(); + }); + + // Import metrics - trigger file input. + $('#import-metrics-btn').on('click', function() { + $('#import-metrics-file').click(); + }); + + // Import file selected. + $('#import-metrics-file').on('change', function(e) { + var file = e.target.files[0]; + if (file) { + var reader = new FileReader(); + reader.onload = function(e) { + importFileContent = e.target.result; + $('#import-options').slideDown(); + }; + reader.readAsText(file); + } + }); + + // Confirm import. + $('#confirm-import').on('click', function() { + var mode = $('input[name="import_mode"]:checked').val(); + importMetrics(importFileContent, mode); + }); + + // Cancel import. + $('#cancel-import').on('click', function() { + $('#import-options').slideUp(); + $('#import-metrics-file').val(''); + importFileContent = null; + }); + } + + /** + * Initialize dashboards tab handlers. + */ + function initDashboardsHandlers() { + $(document).on('click', '.download-dashboard', function() { + var slug = $(this).data('slug'); + downloadDashboard(slug); + }); + } + + /** + * Initialize reset runtime metrics handler. + */ + function initResetRuntimeHandler() { + $('#wp-prometheus-reset-runtime').on('click', function() { + if (confirm(wpPrometheus.confirmReset)) { + resetRuntimeMetrics(); + } + }); + } + + /** + * Perform a license action via AJAX. + * + * @param {string} action AJAX action name. + * @param {string} message Loading message. + */ + function performLicenseAction(action, message) { + var $spinner = $('#wp-prometheus-license-spinner'); + var $message = $('#wp-prometheus-license-message'); + + $spinner.addClass('is-active'); + $message.hide(); + + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: { + action: action, + nonce: wpPrometheus.nonce + }, + success: function(response) { + $spinner.removeClass('is-active'); + + if (response.success) { + $message + .removeClass('notice-error') + .addClass('notice notice-success') + .html('

' + response.data.message + '

') + .show(); + + // Reload page after successful validation/activation. + setTimeout(function() { + location.reload(); + }, 1500); + } else { $message .removeClass('notice-success') .addClass('notice notice-error') - .html('

Connection error. Please try again.

') + .html('

' + (response.data.message || 'An error occurred.') + '

') .show(); } - }); - } - - /** - * Generate a random token. - * - * @param {number} length Token length. - * @return {string} Generated token. - */ - function generateToken(length) { - var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - var token = ''; - for (var i = 0; i < length; i++) { - token += charset.charAt(Math.floor(Math.random() * charset.length)); + }, + error: function() { + $spinner.removeClass('is-active'); + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

Connection error. Please try again.

') + .show(); } - return token; + }); + } + + /** + * Save custom metric via AJAX. + */ + function saveCustomMetric() { + var $spinner = $('#wp-prometheus-metric-spinner'); + var $message = $('#wp-prometheus-metric-message'); + var $form = $('#wp-prometheus-metric-form'); + + $spinner.addClass('is-active'); + $message.hide(); + + var formData = $form.serialize(); + formData += '&action=wp_prometheus_save_custom_metric'; + formData += '&nonce=' + wpPrometheus.customMetricNonce; + + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: formData, + success: function(response) { + $spinner.removeClass('is-active'); + + if (response.success) { + $message + .removeClass('notice-error') + .addClass('notice notice-success') + .html('

' + response.data.message + '

') + .show(); + + setTimeout(function() { + window.location.href = window.location.pathname + '?page=wp-prometheus&tab=custom'; + }, 1000); + } else { + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

' + (response.data.message || 'An error occurred.') + '

') + .show(); + } + }, + error: function() { + $spinner.removeClass('is-active'); + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

Connection error. Please try again.

') + .show(); + } + }); + } + + /** + * Delete custom metric via AJAX. + * + * @param {string} id Metric ID. + */ + function deleteCustomMetric(id) { + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: { + action: 'wp_prometheus_delete_custom_metric', + nonce: wpPrometheus.customMetricNonce, + id: id + }, + success: function(response) { + if (response.success) { + $('tr[data-metric-id="' + id + '"]').fadeOut(function() { + $(this).remove(); + // Check if table is empty. + if ($('.wp-prometheus-custom-metrics tbody tr').length === 0) { + location.reload(); + } + }); + } else { + alert(response.data.message || 'An error occurred.'); + } + }, + error: function() { + alert('Connection error. Please try again.'); + } + }); + } + + /** + * Export metrics via AJAX. + */ + function exportMetrics() { + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: { + action: 'wp_prometheus_export_metrics', + nonce: wpPrometheus.exportNonce + }, + success: function(response) { + if (response.success) { + downloadFile(response.data.json, response.data.filename, 'application/json'); + } else { + alert(response.data.message || 'An error occurred.'); + } + }, + error: function() { + alert('Connection error. Please try again.'); + } + }); + } + + /** + * Import metrics via AJAX. + * + * @param {string} json JSON content. + * @param {string} mode Import mode. + */ + function importMetrics(json, mode) { + var $spinner = $('#wp-prometheus-import-spinner'); + var $message = $('#wp-prometheus-import-message'); + + $spinner.addClass('is-active'); + $message.hide(); + + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: { + action: 'wp_prometheus_import_metrics', + nonce: wpPrometheus.importNonce, + json: json, + mode: mode + }, + success: function(response) { + $spinner.removeClass('is-active'); + + if (response.success) { + $message + .removeClass('notice-error') + .addClass('notice notice-success') + .html('

' + response.data.message + '

') + .show(); + + $('#import-options').slideUp(); + $('#import-metrics-file').val(''); + importFileContent = null; + + setTimeout(function() { + location.reload(); + }, 1500); + } else { + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

' + (response.data.message || 'An error occurred.') + '

') + .show(); + } + }, + error: function() { + $spinner.removeClass('is-active'); + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

Connection error. Please try again.

') + .show(); + } + }); + } + + /** + * Download dashboard via AJAX. + * + * @param {string} slug Dashboard slug. + */ + function downloadDashboard(slug) { + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: { + action: 'wp_prometheus_download_dashboard', + nonce: wpPrometheus.dashboardNonce, + slug: slug + }, + success: function(response) { + if (response.success) { + downloadFile(response.data.json, response.data.filename, 'application/json'); + } else { + alert(response.data.message || 'An error occurred.'); + } + }, + error: function() { + alert('Connection error. Please try again.'); + } + }); + } + + /** + * Reset runtime metrics via AJAX. + */ + function resetRuntimeMetrics() { + var $spinner = $('#wp-prometheus-reset-spinner'); + var $message = $('#wp-prometheus-reset-message'); + + $spinner.addClass('is-active'); + $message.hide(); + + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: { + action: 'wp_prometheus_reset_runtime_metrics', + nonce: wpPrometheus.resetRuntimeNonce + }, + success: function(response) { + $spinner.removeClass('is-active'); + + if (response.success) { + $message + .removeClass('notice-error') + .addClass('notice notice-success') + .html('

' + response.data.message + '

') + .show(); + } else { + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

' + (response.data.message || 'An error occurred.') + '

') + .show(); + } + }, + error: function() { + $spinner.removeClass('is-active'); + $message + .removeClass('notice-success') + .addClass('notice notice-error') + .html('

Connection error. Please try again.

') + .show(); + } + }); + } + + /** + * Reset the metric form to default state. + */ + function resetMetricForm() { + var $form = $('#wp-prometheus-metric-form'); + $form[0].reset(); + $('#metric-id').val(''); + $('#wp-prometheus-form-title').text('Add New Metric'); + $('#option-config-row').hide(); + $('#static-values-row').show(); + $('#wp-prometheus-metric-message').hide(); + + // Reset labels to single empty row. + $('#metric-labels-container').html( + '
' + + '' + + '' + + '
' + ); + + // Reset values to single empty row. + $('#metric-values-container').html( + '
' + + '' + + '' + + '
' + ); + } + + /** + * Update value rows when labels change. + */ + function updateValueRows() { + var labels = getLabels(); + var labelCount = labels.length; + + $('#metric-values-container .metric-value-row').each(function(rowIndex) { + var $row = $(this); + var inputs = $row.find('input').toArray(); + var currentValues = inputs.map(function(input) { return input.value; }); + + // Remove all inputs except the value and button. + $row.find('input').remove(); + + // Re-add label inputs. + for (var i = 0; i < labelCount; i++) { + var val = currentValues[i] || ''; + $row.prepend(''); + } + + // Re-add value input. + var metricVal = currentValues[currentValues.length - 1] || ''; + $row.find('.remove-value-row').before(''); + }); + } + + /** + * Get current label names. + * + * @return {string[]} Array of label names. + */ + function getLabels() { + var labels = []; + $('#metric-labels-container input[name="labels[]"]').each(function() { + var val = $(this).val().trim(); + if (val) { + labels.push(val); + } + }); + return labels; + } + + /** + * Get current label count. + * + * @return {number} Number of labels. + */ + function getLabelCount() { + return getLabels().length; + } + + /** + * Generate a random token. + * + * @param {number} length Token length. + * @return {string} Generated token. + */ + function generateToken(length) { + var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var token = ''; + for (var i = 0; i < length; i++) { + token += charset.charAt(Math.floor(Math.random() * charset.length)); } - }); + return token; + } + + /** + * Download a file. + * + * @param {string} content File content. + * @param {string} filename Filename. + * @param {string} type MIME type. + */ + function downloadFile(content, filename, type) { + var blob = new Blob([content], { type: type }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } })(jQuery); diff --git a/languages/wp-prometheus-de_CH.mo b/languages/wp-prometheus-de_CH.mo index beb6cbcf828cd806f7378a4234bd503dab46521e..b3f880875135b9624f6e5b6c604d699bed8e51ad 100644 GIT binary patch literal 12266 zcma)>dyr*URmM*skYq>*M1#D9ofq^ZeS0Po9-%Xd^gJ>%WFF0Q&rA~0aPGak?>+Rr z=iHn}_skFo3Iye0f(VExQYuvuEyOB(lvXLCR2d$EWr`9gRMb-bNR?1!w4_SQ-?#TZ z=bqb{Mo;y=-`V@@$J%SHz1G^R|L&@bKN#_Ri1K2}moAQ?zX2b(j31uAygZ6t4n786 z0)7vC4)|Z-Gr=8KMA37>=Ykq?9mrqQ;b$j!8@L0s;IqLAsQwRv8vlOqM(`Iwri}g^ zyaoI!sP!&p&~ET@P~&gsNAn#8uLsY7qWb|*^L+$-7Wi>cbpJMZDfl>e4Ez>22i|lg zbAzkkRp33K)*FHRMI}E!488-@d>;wzp8%O6`aN(N{0s0I;MFkYMc|9TYrt27H-q

#g9*cTK8{3@#|aQ^T2-%_5T62k4s^a*10m^OF+@n0dD~Jf!fzFwBHYEo`=8} zg1-Wao<9VC2z(qAf4>8Ye@}v1N1NBYmw-PCz8DlA7D4TMAIM*HoSzqfF{u7;2CoAj z0mc8%g!aD%wcfvg;=}jB9|2!X<2?8ZQ1r#1)_(xhI&TM`4?YZP{YODq5q&y*|1D69eUkLIS&GCas zqdPzg9uMuG0Y&E*K&|sNQ1bbA@K*5YFkf=s3!=KweW2ts2I&%Qfa?EVQ1pEiRKG_- z&G-AD`0+RhY0)=9>C1OP(fvZ0_)_o|Q2Sj0VMUaK`u-5eU-YZ|==&dp_OF5J_iv!| z=IL}6-*$qkFM!(bDk#48!A0<4Q2hS__zduyp!oJ}umqm~kAg8uu^appcsKa>py<98 zrfc0UD0((PwSN$l9{ee&eLn$8e|Dg(I3weDL%^?L*q zUw#vW)abLI^y{BM@$Dj%NayovkiY0Aeqd4bdQkf>K+*YeQ0xCOD7roeYM#FWCC4W~ zt@9)(d0zgqC^`kc0F+*3;KkstgZxF0@#63BC$^8cHBKUjvH%H-NGm6)3*E z1JpVn0mZjZgWB&`!H2CaeJ`|Mfzn8C=JcY$|+9|ezq-vGsj>k*px{3`Gg@HX(R;C}EJ z_&HGWcn->Q4|o8C_0h*b@%Qtg{tXaPqKjCRtwr;o=sOLH-W-I5(T70;eiqyfej5~@ zuG;1MT?fY04}+TLlc4nNaquGW*_5|Xw5K(S_Mqp39vqE&m2T*ftKM~rF27DH{gCf0pH)StHI`m%3ofOfjXG{@|_p8D)ponMhqr91-M?7DoWEAO} ze6yZ6P~Jv4N0EFJ%DX7_BVDMUJ>lgn_&Q2KIZl!M?xvKK{S-YXD3XtKO3%HNAEUg2 zvX4?f*Myf%@B`uffq*990q_h36BFG>c@IU;k9&x22baUUKyrUS<@J;|Q4UhJJ~lL5 z3I04qXCn`7sFKq?>9K6W;m|J7*}Q_%rRY4Krbr*(LFrL8D5!Sy3lu#QN}qz7IXnFt zu%awcQp&BAcT%24(epEupQHRVMSgEYk=|ZUS)mLmIwwC%(W5hUxqIWRf;!83oppw7mtL;Z~b?*d;N-X96L8=R-Chk6@+lFGxO z;e7pJ$&788Cq1*7ltWVvlftC&*qV5h+qi$u*t1Dd77Ni*uS~MEaJUhd)*MZGHZ81g z0Q!BizbeXXY>v4JUCYt9Ye(+wbUdo8Ihk#`S~;|4EgOxp%_JR|GBfA0DmRlngP5VM ziiLCI(PFezLDFirZd3Pa(=zGBA|qPbh?7y=9oev05$%uD9!u>X**J&zk%@b~YFv#V zwQur@p=>H_wI7%90$r2dx;ZkQWO*6w&(gJIP~~i#MW-u4We!)Y+2zL;#-{yAmZT*U zWK3)GXn&ri=HLc{3Nu&qhPGdgShS3b^}GM^ z*J~MfX>guvNX%Spengdnt=bz)$mDwPV$2(_w zw8al)!>m7ViZU)M)M}~@Zq7W+*{|p9Gi9E1E2jbNE`>o|wwo5ksG+HlcuLaX)EQB3-bGG%97xaa{u@I)JJU2n4&VU(byoykfdGxPp zL5`gG-n#8eI)n95@|Q0lYWTfm@|OqEa$uKgD5>yQQy9Con}5Jf#+Kqw{$E# zt@0PFS09)*frTwgwp^G3en9xK_-rz+#^#2;@seRi7~RXpU6yq2MV33;yijLoZa#Vc za?Ja!;}S=Bcb;oTQ~ie`bSU7kr>SWWuR|wOOl&V%!xtj8Gg-Dj8;@L~aAeGqc46_3c`cVkw>Ij#uL@ zijl3=?!@%ubgXkakw0ff6D`)2t*mfHEOQ4^B5xG@1PZsAW4;;{tu1%dauKzA^fvaS z9oW=jyliyRx`q1HWF+5=i92bLyQ%r^YbS&MZCbafkDW*hnOI!2+%%e+XLyrf-w=ZL z0w$9zHivO3zk_d;!_5rbO~%Q2YbG{#N1mX-B;yJCz7VbGSk{N2Q3TiO35!-5=iOqj z?By`a)}s|%Yj3)>P~99Ry6-vb3-j#@e<8A}oyAbaS2}R3kz4nvsSA zibSPOUQ(uyGwWe&&n;}X=U+bU z>oKw0>|kcH_Eg&qm}>q1mxk$xKuOE3_-lalc`mmDLMlseMKGk`aA(wZK=sjXx1GrQ zZfK+2vqLZ}ePS)o7FrU5R6!NuiV#dyqe&;kSM7!zk^Zfpo#~!vufkcbdI@d*A-UuN@@| zSw5H!S;V@~zT?=DV+W_4xfxCnlF0UTWMCJK?kKysWF_fs#N%;J408La)jK=4PWy#x zTjyZf%lgQ5(cIci${jeQJRWuKc-N!3#h zlk|MGRw=p)ca3(-8K$=9EjMK>-n8SLuD@AHthqBAF%;>W^OZR=%(2Z=aVw?b2l~Vc ziqlT4tZ|9ASk6I&{)vXJOsO+Gu3NW@X(r%}mQX1(utkj19U%d8G_KYl3#HU)SeGwJ z%_t)QLHC8|E&@aOBoRo@`eLWt4&o!+4(l2gl`lz~R11>oPpWjiH0$9y?pQaQcqBF$W>REvGS?koR1w3f3EH!#iiCz!s+$QRP}} z+Ktoo&QfT`EMq?y{;Fu6M z>K5VV*DQdJv*pc-WM6N{63x^d1trd6B+Wp!6X#u&0UBp@yF2~hR3B1Ge+cx}(zIIW zomN)I?budprHQ*2%Vj{0;f~lrmlbgDj#WAljYfCwno049ODhEOov-r^(Z%~Y-5RSkV>^zOi_8%bla&&<&4&>|CJ4$zLD`Mjk`s$Vb1WdmG&8j zl#b_f!+b7UI49reQ$QW>e_=zYFQp^211cAzLss`^=`dZMlRrY|=NLEHBL>JmR^1D9CRNsaa&9VWiMsuM0kyC7J9M+@ES1ydCYL0C2jzG+LTcyN zT7ht9WtdG!WE{uj5WEIoJ8w2^I-5@NT!&#Uom4pANSrwKL1<7?xENAN+CFzda`xWw zI(~ zmC$N<6+=jDha(tu#-qrUN{-N_sh}K<1ibY>!=@3M4|zGSx^k2Z#13%=bWeiz)}$`@ zXh%4Xnfyb9o+0hkk!MK8#B>U{r{+Mh(l)v?QPu_OZn~S`v5<6wu!t~+O_L(s=zpGl!G!8y0(K<*+LiE$>xUhsc0@WM^d?5MR>aYV2SJS z+THL{lz!^HWrI{y%VgE}g!;rbE>(vM$LfBti(7I`b)?0A+I;RzhS@erHm)DT z*rtPIfH7BWVD=>?-`NRKtv59nO0SK{ze&LqA96|S4LMZ=vroy1O_wJrR$sJ3h7A^x z0Ow4SW0MubX$1~#mtosxHd1V@xwd7%^kM?htxR8Q>=rtj5p}+Anq}+UrF;n42A^V+ z>p%k!#amKr%^j$nit0FnGg45k8Cu6_CMniI`u%DhX|%hz=CjXiT^gI7-L`Th4!^bwAu1B}M-W2H&w^ delta 2245 zcmYk+e@vBC9LMo<5fl=ouw7kL-JQ5e=J~XxHj%3qm zsd;8?{h_N3b><(<+g90DI&*7{vi@kfC|6l+>H4Sjrhv-^Es=Q+=FpL4$F zoaedm#G$6>wRy?M45ghYBqo!LiQ=;<94NbH8Iy^-u@Vp99rzZ`!4GgQev0+@E%Gzj zsm9b|A?m&jSc+Y!e#UUNx;jq9Ps16^#`Dt`%oWrarf?yChjln3&6pBwKnb6&zb@g!RC_08;Sg#dfkbJranP+bPYG*4b(uIXw1h>q{(>b*HlNTICvOy@ORW7W~Wb2T_vvIdsVk9gV=^|qt-r|n>(-pEAbHO`R}0mo%A#Rx^R<*YRutj$~=f#f^ArVqlxwj ztmXU?F2LV#9cHovHQ-L1@R0!WGZpk!k1J7AK7eZ9g_^MgS#IaJqV5}wQqjovqSodR zF2z%*&GiM6r1=^(@?TK*{fp{2pM&0p8q~~fLJe#Pxy6j5X69AY=DmP=?)MnqLqAjD zXMW|N%+lByk6|wAk58dGJc@dc-#`uIJRZhLRHj|rq=7tx8o+Vfg6B{P%qp0kp?uWq zSB|WD)U2eU0kk5|GTTs@>_R>0<;3|JoX`13s1M0?)DorAsPk_h|0ahYNC#~kI;hNUe+c~ z)+f}CT1V&=(wyHa_a`ciNV4&rsiwP$HbT>`4^4Y~Lr}>eSfBVi0`bqEcBj_k9zr{) zf(R0PE#qYw$2G)5gw{h-`v9S$9j!Ic`aDc*CiIEaj#bfmskEBT=@;}tPT0Beg=eFg z8X}2UMciJrW19)R!>nt3$H8=2+wHkehdd|H=5{;bh+`WwI_&2eqju|@U|OVm*d6lR z!LW_^m!-6XMtU8)Jo9v_8}SBvheHFgne%FVDULVncH2`~yX|0hk^LdN%+}=O+4h_f z`(;kA4dv#Rcp=a24s3|u7%^L%i06iT174pKXb$%b4Z2}3R*=``vq$pR+fYGXY(~LE zl6`l2FE(ki|5pRz(+~YjyhPgm0 z@s9ePu$>#owtp3`NDn%Z?!IBi-98fTwap6~6RA{|oK8(-G7u{&ZJ$o1-`*@6w^z!` zZCAx&`&q?B`}*P+Y 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 deren Inhalt ein" + +#: src/Admin/Settings.php +msgid "Select your Prometheus data source" +msgstr "Waehlen Sie Ihre Prometheus-Datenquelle" + +#: src/Admin/Settings.php +msgid "Click Import" +msgstr "Klicken Sie auf Import" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Metric name is required." +msgstr "Metrik-Name ist erforderlich." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid metric name format." +msgstr "Ungueltiges Metrik-Namensformat." + +#: src/Metrics/CustomMetricBuilder.php +msgid "A metric with this name already exists." +msgstr "Eine Metrik mit diesem Namen existiert bereits." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Help text is required." +msgstr "Hilfetext ist erforderlich." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid value type." +msgstr "Ungueltiger Werttyp." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Static value must be numeric." +msgstr "Statischer Wert muss numerisch sein." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Option name is required for option value type." +msgstr "Optionsname ist fuer den Options-Werttyp erforderlich." + +#. translators: %d: Maximum number of labels +#: src/Metrics/CustomMetricBuilder.php +msgid "Maximum %d labels allowed." +msgstr "Maximal %d Labels erlaubt." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid label name format." +msgstr "Ungueltiges Label-Namensformat." + +#. translators: %d: Maximum number of label value combinations +#: src/Metrics/CustomMetricBuilder.php +msgid "Maximum %d label value combinations allowed." +msgstr "Maximal %d Label-Wert-Kombinationen erlaubt." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid JSON format." +msgstr "Ungueltiges JSON-Format." + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid export format." +msgstr "Ungueltiges Export-Format." + #: src/Plugin.php msgid "Settings" msgstr "Einstellungen" diff --git a/languages/wp-prometheus.pot b/languages/wp-prometheus.pot index f145c15..8712d47 100644 --- a/languages/wp-prometheus.pot +++ b/languages/wp-prometheus.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL v2 or later. msgid "" msgstr "" -"Project-Id-Version: WP Prometheus 0.2.0\n" +"Project-Id-Version: WP Prometheus 0.3.0\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" @@ -28,6 +28,14 @@ msgstr "" msgid "Help" msgstr "" +#: src/Admin/Settings.php +msgid "Custom Metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Dashboards" +msgstr "" + #: src/Admin/Settings.php msgid "License settings saved." msgstr "" @@ -210,6 +218,18 @@ msgstr "" msgid "WooCommerce Customers (registered, guest)" msgstr "" +#: src/Admin/Settings.php +msgid "Reset Runtime Metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Clear all accumulated runtime metric data." +msgstr "" + +#: src/Admin/Settings.php +msgid "Reset Metrics" +msgstr "" + #: src/Admin/Settings.php msgid "Prometheus Configuration" msgstr "" @@ -327,11 +347,249 @@ msgid "WooCommerce customers by type" msgstr "" #: src/Admin/Settings.php -msgid "Custom Metrics" +msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:" msgstr "" #: src/Admin/Settings.php -msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:" +msgid "Add Custom Metric" +msgstr "" + +#: src/Admin/Settings.php +msgid "Edit Custom Metric" +msgstr "" + +#: src/Admin/Settings.php +msgid "Metric Name" +msgstr "" + +#: src/Admin/Settings.php +msgid "Must follow Prometheus naming conventions." +msgstr "" + +#: src/Admin/Settings.php +msgid "Help Text" +msgstr "" + +#: src/Admin/Settings.php +msgid "Description shown in Prometheus output." +msgstr "" + +#: src/Admin/Settings.php +msgid "Value Type" +msgstr "" + +#: src/Admin/Settings.php +msgid "Static Value" +msgstr "" + +#: src/Admin/Settings.php +msgid "WordPress Option" +msgstr "" + +#: src/Admin/Settings.php +msgid "Static Value:" +msgstr "" + +#: src/Admin/Settings.php +msgid "Option Name:" +msgstr "" + +#: src/Admin/Settings.php +msgid "The name of the WordPress option to read." +msgstr "" + +#: src/Admin/Settings.php +msgid "Labels" +msgstr "" + +#: src/Admin/Settings.php +msgid "Label name" +msgstr "" + +#: src/Admin/Settings.php +msgid "Add Label" +msgstr "" + +#: src/Admin/Settings.php +msgid "Label Values" +msgstr "" + +#: src/Admin/Settings.php +msgid "Value" +msgstr "" + +#: src/Admin/Settings.php +msgid "Add Value Row" +msgstr "" + +#: src/Admin/Settings.php +msgid "Enabled" +msgstr "" + +#: src/Admin/Settings.php +msgid "Save Metric" +msgstr "" + +#: src/Admin/Settings.php +msgid "Cancel" +msgstr "" + +#: src/Admin/Settings.php +msgid "Your Custom Metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Name" +msgstr "" + +#: src/Admin/Settings.php +msgid "Status" +msgstr "" + +#: src/Admin/Settings.php +msgid "Actions" +msgstr "" + +#: src/Admin/Settings.php +msgid "Active" +msgstr "" + +#: src/Admin/Settings.php +msgid "Inactive" +msgstr "" + +#: src/Admin/Settings.php +msgid "Edit" +msgstr "" + +#: src/Admin/Settings.php +msgid "Delete" +msgstr "" + +#: src/Admin/Settings.php +msgid "No custom metrics defined yet." +msgstr "" + +#: src/Admin/Settings.php +msgid "Export / Import" +msgstr "" + +#: src/Admin/Settings.php +msgid "Export your custom metrics configuration for backup or transfer to another site." +msgstr "" + +#: src/Admin/Settings.php +msgid "Export Metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Import Metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Import Options" +msgstr "" + +#: src/Admin/Settings.php +msgid "Skip existing metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Overwrite existing metrics" +msgstr "" + +#: src/Admin/Settings.php +msgid "Rename duplicates" +msgstr "" + +#: src/Admin/Settings.php +msgid "Import" +msgstr "" + +#: src/Admin/Settings.php +msgid "Grafana Dashboard Templates" +msgstr "" + +#: src/Admin/Settings.php +msgid "Download pre-built Grafana dashboards for visualizing your WordPress metrics." +msgstr "" + +#: src/Admin/Settings.php +msgid "Download" +msgstr "" + +#: src/Admin/Settings.php +msgid "Import instructions:" +msgstr "" + +#: src/Admin/Settings.php +msgid "Download the desired dashboard JSON file" +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" +msgstr "" + +#: src/Admin/Settings.php +msgid "Click Import" +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Metric name is required." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid metric name format." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "A metric with this name already exists." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Help text is required." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid value type." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Static value must be numeric." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Option name is required for option value type." +msgstr "" + +#. translators: %d: Maximum number of labels +#: src/Metrics/CustomMetricBuilder.php +msgid "Maximum %d labels allowed." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid label name format." +msgstr "" + +#. translators: %d: Maximum number of label value combinations +#: src/Metrics/CustomMetricBuilder.php +msgid "Maximum %d label value combinations allowed." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid JSON format." +msgstr "" + +#: src/Metrics/CustomMetricBuilder.php +msgid "Invalid export format." msgstr "" #: src/Plugin.php diff --git a/src/Admin/DashboardProvider.php b/src/Admin/DashboardProvider.php new file mode 100644 index 0000000..59f458f --- /dev/null +++ b/src/Admin/DashboardProvider.php @@ -0,0 +1,151 @@ +dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/'; + + $this->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', + ), + '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', + ), + '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', + ), + ); + } + + /** + * Get list of available dashboards. + * + * @return array + */ + public function get_available(): array { + $available = array(); + + foreach ( $this->dashboards as $slug => $dashboard ) { + $file_path = $this->dashboard_dir . $dashboard['file']; + if ( file_exists( $file_path ) ) { + $available[ $slug ] = $dashboard; + } + } + + return $available; + } + + /** + * Get dashboard content by slug. + * + * @param string $slug Dashboard slug. + * @return string|null JSON content or null if not found. + */ + public function get_dashboard( string $slug ): ?string { + // Validate slug to prevent directory traversal. + $slug = sanitize_file_name( $slug ); + + if ( ! isset( $this->dashboards[ $slug ] ) ) { + return null; + } + + $file_path = $this->dashboard_dir . $this->dashboards[ $slug ]['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 ); + + if ( false === $content ) { + return null; + } + + return $content; + } + + /** + * Get dashboard metadata by slug. + * + * @param string $slug Dashboard slug. + * @return array|null Dashboard metadata or null if not found. + */ + public function get_metadata( string $slug ): ?array { + $slug = sanitize_file_name( $slug ); + + if ( ! isset( $this->dashboards[ $slug ] ) ) { + return null; + } + + return $this->dashboards[ $slug ]; + } + + /** + * Get filename for download. + * + * @param string $slug Dashboard slug. + * @return string|null Filename or null if not found. + */ + public function get_filename( string $slug ): ?string { + $slug = sanitize_file_name( $slug ); + + if ( ! isset( $this->dashboards[ $slug ] ) ) { + return null; + } + + return $this->dashboards[ $slug ]['file']; + } +} diff --git a/src/Admin/Settings.php b/src/Admin/Settings.php index 0169ce1..58520bf 100644 --- a/src/Admin/Settings.php +++ b/src/Admin/Settings.php @@ -8,10 +8,12 @@ namespace Magdev\WpPrometheus\Admin; use Magdev\WpPrometheus\License\Manager as LicenseManager; +use Magdev\WpPrometheus\Metrics\CustomMetricBuilder; +use Magdev\WpPrometheus\Metrics\RuntimeCollector; // Prevent direct file access. if ( ! defined( 'ABSPATH' ) ) { - exit; + exit; } /** @@ -21,357 +23,727 @@ if ( ! defined( 'ABSPATH' ) ) { */ class Settings { - /** - * Available tabs. - * - * @var array - */ - private array $tabs = array(); + /** + * Available tabs. + * + * @var array + */ + private array $tabs = array(); - /** - * Constructor. - */ - public function __construct() { - $this->tabs = array( - 'license' => __( 'License', 'wp-prometheus' ), - 'metrics' => __( 'Metrics', 'wp-prometheus' ), - 'help' => __( 'Help', 'wp-prometheus' ), - ); + /** + * Custom metric builder instance. + * + * @var CustomMetricBuilder + */ + private CustomMetricBuilder $metric_builder; - add_action( 'admin_menu', array( $this, 'add_settings_page' ) ); - add_action( 'admin_init', array( $this, 'register_settings' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); - } + /** + * Dashboard provider instance. + * + * @var DashboardProvider + */ + private DashboardProvider $dashboard_provider; - /** - * Get current tab. - * - * @return string - */ - private function get_current_tab(): string { - $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license'; - return array_key_exists( $tab, $this->tabs ) ? $tab : 'license'; - } + /** + * Constructor. + */ + public function __construct() { + $this->tabs = array( + 'license' => __( 'License', 'wp-prometheus' ), + 'metrics' => __( 'Metrics', 'wp-prometheus' ), + 'custom' => __( 'Custom Metrics', 'wp-prometheus' ), + 'dashboards' => __( 'Dashboards', 'wp-prometheus' ), + 'help' => __( 'Help', 'wp-prometheus' ), + ); - /** - * Add settings page to admin menu. - * - * @return void - */ - public function add_settings_page(): void { - add_options_page( - __( 'Metrics Settings', 'wp-prometheus' ), - __( 'Metrics', 'wp-prometheus' ), - 'manage_options', - 'wp-prometheus', - array( $this, 'render_settings_page' ) - ); - } + $this->metric_builder = new CustomMetricBuilder(); + $this->dashboard_provider = new DashboardProvider(); - /** - * Register plugin settings. - * - * @return void - */ - public function register_settings(): void { - // Register settings for metrics tab. - register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_auth_token', array( - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - ) ); + add_action( 'admin_menu', array( $this, 'add_settings_page' ) ); + add_action( 'admin_init', array( $this, 'register_settings' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); - register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_enabled_metrics', array( - 'type' => 'array', - 'sanitize_callback' => array( $this, 'sanitize_metrics' ), - ) ); + // AJAX handlers. + add_action( 'wp_ajax_wp_prometheus_save_custom_metric', array( $this, 'ajax_save_custom_metric' ) ); + add_action( 'wp_ajax_wp_prometheus_delete_custom_metric', array( $this, 'ajax_delete_custom_metric' ) ); + add_action( 'wp_ajax_wp_prometheus_export_metrics', array( $this, 'ajax_export_metrics' ) ); + add_action( 'wp_ajax_wp_prometheus_import_metrics', array( $this, 'ajax_import_metrics' ) ); + add_action( 'wp_ajax_wp_prometheus_download_dashboard', array( $this, 'ajax_download_dashboard' ) ); + add_action( 'wp_ajax_wp_prometheus_reset_runtime_metrics', array( $this, 'ajax_reset_runtime_metrics' ) ); + } - // Auth token section. - add_settings_section( - 'wp_prometheus_auth_section', - __( 'Authentication', 'wp-prometheus' ), - array( $this, 'render_auth_section' ), - 'wp-prometheus-metrics' - ); + /** + * Get current tab. + * + * @return string + */ + private function get_current_tab(): string { + $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license'; + return array_key_exists( $tab, $this->tabs ) ? $tab : 'license'; + } - // Metrics section. - add_settings_section( - 'wp_prometheus_metrics_section', - __( 'Enabled Metrics', 'wp-prometheus' ), - array( $this, 'render_metrics_section' ), - 'wp-prometheus-metrics' - ); + /** + * Add settings page to admin menu. + * + * @return void + */ + public function add_settings_page(): void { + add_options_page( + __( 'Metrics Settings', 'wp-prometheus' ), + __( 'Metrics', 'wp-prometheus' ), + 'manage_options', + 'wp-prometheus', + array( $this, 'render_settings_page' ) + ); + } - // Auth token field. - add_settings_field( - 'wp_prometheus_auth_token', - __( 'Auth Token', 'wp-prometheus' ), - array( $this, 'render_auth_token_field' ), - 'wp-prometheus-metrics', - 'wp_prometheus_auth_section' - ); + /** + * Register plugin settings. + * + * @return void + */ + public function register_settings(): void { + // Register settings for metrics tab. + register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_auth_token', array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ) ); - // Enabled metrics field. - add_settings_field( - 'wp_prometheus_enabled_metrics', - __( 'Select Metrics', 'wp-prometheus' ), - array( $this, 'render_enabled_metrics_field' ), - 'wp-prometheus-metrics', - 'wp_prometheus_metrics_section' - ); - } + register_setting( 'wp_prometheus_metrics_settings', 'wp_prometheus_enabled_metrics', array( + 'type' => 'array', + 'sanitize_callback' => array( $this, 'sanitize_metrics' ), + ) ); - /** - * Enqueue admin scripts. - * - * @param string $hook_suffix Current admin page. - * @return void - */ - public function enqueue_scripts( string $hook_suffix ): void { - if ( 'settings_page_wp-prometheus' !== $hook_suffix ) { - return; - } + // Auth token section. + add_settings_section( + 'wp_prometheus_auth_section', + __( 'Authentication', 'wp-prometheus' ), + array( $this, 'render_auth_section' ), + 'wp-prometheus-metrics' + ); - wp_enqueue_style( - 'wp-prometheus-admin', - WP_PROMETHEUS_URL . 'assets/css/admin.css', - array(), - WP_PROMETHEUS_VERSION - ); + // Metrics section. + add_settings_section( + 'wp_prometheus_metrics_section', + __( 'Enabled Metrics', 'wp-prometheus' ), + array( $this, 'render_metrics_section' ), + 'wp-prometheus-metrics' + ); - wp_enqueue_script( - 'wp-prometheus-admin', - WP_PROMETHEUS_URL . 'assets/js/admin.js', - array( 'jquery' ), - WP_PROMETHEUS_VERSION, - true - ); + // Auth token field. + add_settings_field( + 'wp_prometheus_auth_token', + __( 'Auth Token', 'wp-prometheus' ), + array( $this, 'render_auth_token_field' ), + 'wp-prometheus-metrics', + 'wp_prometheus_auth_section' + ); - wp_localize_script( 'wp-prometheus-admin', 'wpPrometheus', array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'wp_prometheus_license_action' ), - ) ); - } + // Enabled metrics field. + add_settings_field( + 'wp_prometheus_enabled_metrics', + __( 'Select Metrics', 'wp-prometheus' ), + array( $this, 'render_enabled_metrics_field' ), + 'wp-prometheus-metrics', + 'wp_prometheus_metrics_section' + ); + } - /** - * Render the settings page. - * - * @return void - */ - public function render_settings_page(): void { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } + /** + * Enqueue admin scripts. + * + * @param string $hook_suffix Current admin page. + * @return void + */ + public function enqueue_scripts( string $hook_suffix ): void { + if ( 'settings_page_wp-prometheus' !== $hook_suffix ) { + return; + } - $current_tab = $this->get_current_tab(); + wp_enqueue_style( + 'wp-prometheus-admin', + WP_PROMETHEUS_URL . 'assets/css/admin.css', + array(), + WP_PROMETHEUS_VERSION + ); - // Handle license settings save. - if ( 'license' === $current_tab && isset( $_POST['wp_prometheus_license_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['wp_prometheus_license_nonce'] ), 'wp_prometheus_save_license' ) ) { - LicenseManager::save_settings( array( - 'license_key' => isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '', - 'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '', - 'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '', - ) ); - echo '

' . esc_html__( 'License settings saved.', 'wp-prometheus' ) . '

'; - } + wp_enqueue_script( + 'wp-prometheus-admin', + WP_PROMETHEUS_URL . 'assets/js/admin.js', + array( 'jquery' ), + WP_PROMETHEUS_VERSION, + true + ); - ?> -
-

+ wp_localize_script( 'wp-prometheus-admin', 'wpPrometheus', array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'wp_prometheus_license_action' ), + 'customMetricNonce' => wp_create_nonce( 'wp_prometheus_custom_metric' ), + 'exportNonce' => wp_create_nonce( 'wp_prometheus_export' ), + 'importNonce' => wp_create_nonce( 'wp_prometheus_import' ), + 'dashboardNonce' => wp_create_nonce( 'wp_prometheus_dashboard' ), + 'resetRuntimeNonce' => wp_create_nonce( 'wp_prometheus_reset_runtime' ), + 'confirmDelete' => __( 'Are you sure you want to delete this metric?', 'wp-prometheus' ), + 'confirmReset' => __( 'Are you sure you want to reset all runtime metrics? This cannot be undone.', 'wp-prometheus' ), + 'confirmRegenerateToken' => __( 'Are you sure you want to regenerate the auth token? You will need to update your Prometheus configuration.', 'wp-prometheus' ), + ) ); + } - render_tabs( $current_tab ); ?> + /** + * Render the settings page. + * + * @return void + */ + public function render_settings_page(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } -
- render_license_tab(); - break; - case 'metrics': - $this->render_metrics_tab(); - break; - case 'help': - $this->render_help_tab(); - break; - } - ?> -
-
- get_current_tab(); - /** - * Render tabs navigation. - * - * @param string $current_tab Current active tab. - * @return void - */ - private function render_tabs( string $current_tab ): void { - ?> - - isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '', + 'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '', + 'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '', + ) ); + echo '

' . esc_html__( 'License settings saved.', 'wp-prometheus' ) . '

'; + } - /** - * Render license tab content. - * - * @return void - */ - private function render_license_tab(): void { - $license_key = LicenseManager::get_license_key(); - $server_url = LicenseManager::get_server_url(); - $license_status = LicenseManager::get_cached_status(); - $license_data = LicenseManager::get_cached_data(); - $last_check = LicenseManager::get_last_check(); + ?> +
+

- $status_classes = array( - 'valid' => 'notice-success', - 'invalid' => 'notice-error', - 'expired' => 'notice-warning', - 'revoked' => 'notice-error', - 'inactive' => 'notice-warning', - 'unchecked' => 'notice-info', - 'unconfigured' => 'notice-info', - ); + render_tabs( $current_tab ); ?> - $status_messages = array( - 'valid' => __( 'License is active and valid.', 'wp-prometheus' ), - 'invalid' => __( 'License is invalid.', 'wp-prometheus' ), - 'expired' => __( 'License has expired.', 'wp-prometheus' ), - 'revoked' => __( 'License has been revoked.', 'wp-prometheus' ), - 'inactive' => __( 'License is inactive.', 'wp-prometheus' ), - 'unchecked' => __( 'License has not been validated yet.', 'wp-prometheus' ), - 'unconfigured' => __( 'License server is not configured.', 'wp-prometheus' ), - ); +
+ render_license_tab(); + break; + case 'metrics': + $this->render_metrics_tab(); + break; + case 'custom': + $this->render_custom_metrics_tab(); + break; + case 'dashboards': + $this->render_dashboards_tab(); + break; + case 'help': + $this->render_help_tab(); + break; + } + ?> +
+
+ -
- - -
- - - - 0 ) : ?> -
- - - -
+ /** + * Render tabs navigation. + * + * @param string $current_tab Current active tab. + * @return void + */ + private function render_tabs( string $current_tab ): void { + ?> + + - + /** + * Render license tab content. + * + * @return void + */ + private function render_license_tab(): void { + $license_key = LicenseManager::get_license_key(); + $server_url = LicenseManager::get_server_url(); + $license_status = LicenseManager::get_cached_status(); + $license_data = LicenseManager::get_cached_data(); + $last_check = LicenseManager::get_last_check(); - - - - - - - - - - - - - - + $status_classes = array( + 'valid' => 'notice-success', + 'invalid' => 'notice-error', + 'expired' => 'notice-warning', + 'revoked' => 'notice-error', + 'inactive' => 'notice-warning', + 'unchecked' => 'notice-info', + 'unconfigured' => 'notice-info', + ); -

- - - - -

- + $status_messages = array( + 'valid' => __( 'License is active and valid.', 'wp-prometheus' ), + 'invalid' => __( 'License is invalid.', 'wp-prometheus' ), + 'expired' => __( 'License has expired.', 'wp-prometheus' ), + 'revoked' => __( 'License has been revoked.', 'wp-prometheus' ), + 'inactive' => __( 'License is inactive.', 'wp-prometheus' ), + 'unchecked' => __( 'License has not been validated yet.', 'wp-prometheus' ), + 'unconfigured' => __( 'License server is not configured.', 'wp-prometheus' ), + ); - - +
+ + +
+ + + + 0 ) : ?> +
+ + + +
- /** - * Render metrics tab content. - * - * @return void - */ - private function render_metrics_tab(): void { - ?> -
- -
- + - /** - * Render help tab content. - * - * @return void - */ - private function render_help_tab(): void { - $token = get_option( 'wp_prometheus_auth_token', '' ); - $endpoint_url = home_url( '/metrics/' ); - ?> -

-

-
scrape_configs:
+			
+				
+					
+					
+				
+				
+					
+					
+				
+				
+					
+					
+				
+			
+
+			

+ + + + +

+ + + + +
+ +
+ +
+ +

+

+

+ + +

+ + metric_builder->get_all(); + $editing = isset( $_GET['edit'] ) ? sanitize_key( $_GET['edit'] ) : ''; + $edit_metric = $editing ? $this->metric_builder->get( $editing ) : null; + ?> + +
+

+

+ +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + +

+
+ + +
+ +

+ +

+ +

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + +
+ +

+

+ +

+ + + + +

+ + + + +
+ dashboard_provider->get_available(); + ?> +
+

+

+ +
+ $dashboard ) : ?> +
+
+ +
+

+

+ +
+ +
+ +
+ +

+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
+ +

+ ' . esc_html( home_url( '/metrics/' ) ) . '' + ); + ?> +

+
+ +

+

+
scrape_configs:
   - job_name: 'wordpress'
     static_configs:
       - targets: ['']
@@ -381,118 +753,118 @@ class Settings {
       type: Bearer
       credentials: ''
-

- - - - - - - - - - +

+ + + + + + + + + + -

-

-
curl -H "Authorization: Bearer " 
+

+

+
curl -H "Authorization: Bearer " 
-

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
wordpress_info
wordpress_users_total
wordpress_posts_total
wordpress_comments_total
wordpress_plugins_total
wordpress_http_requests_total
wordpress_http_request_duration_seconds
wordpress_db_queries_total
wordpress_cron_events_total
wordpress_cron_overdue_total
wordpress_cron_next_run_timestamp
wordpress_transients_total
wordpress_woocommerce_products_total
wordpress_woocommerce_orders_total
wordpress_woocommerce_revenue_total
wordpress_woocommerce_customers_total
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
wordpress_info
wordpress_users_total
wordpress_posts_total
wordpress_comments_total
wordpress_plugins_total
wordpress_http_requests_total
wordpress_http_request_duration_seconds
wordpress_db_queries_total
wordpress_cron_events_total
wordpress_cron_overdue_total
wordpress_cron_next_run_timestamp
wordpress_transients_total
wordpress_woocommerce_products_total
wordpress_woocommerce_orders_total
wordpress_woocommerce_revenue_total
wordpress_woocommerce_customers_total
-

-

-
add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
+		

+

+
add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
     $gauge = $collector->register_gauge(
         'my_custom_metric',
         'Description of my metric',
@@ -500,137 +872,342 @@ class Settings {
     );
     $gauge->set( 42, array( 'value1', 'value2' ) );
 } );
- ' . esc_html__( 'Configure authentication for the /metrics endpoint.', 'wp-prometheus' ) . '

'; - } + /** + * Render auth section description. + * + * @return void + */ + public function render_auth_section(): void { + echo '

' . esc_html__( 'Configure authentication for the /metrics endpoint.', 'wp-prometheus' ) . '

'; + } - /** - * Render metrics section description. - * - * @return void - */ - public function render_metrics_section(): void { - echo '

' . esc_html__( 'Select which metrics to expose on the /metrics endpoint.', 'wp-prometheus' ) . '

'; - } + /** + * Render metrics section description. + * + * @return void + */ + public function render_metrics_section(): void { + echo '

' . esc_html__( 'Select which metrics to expose on the /metrics endpoint.', 'wp-prometheus' ) . '

'; + } - /** - * Render auth token field. - * - * @return void - */ - public function render_auth_token_field(): void { - $token = get_option( 'wp_prometheus_auth_token', '' ); - ?> - - -

- -

- + + +

+ +

+ __( 'WordPress Info (version, PHP version, multisite)', 'wp-prometheus' ), - 'wordpress_users_total' => __( 'Total Users by Role', 'wp-prometheus' ), - 'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ), - 'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ), - 'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ), - 'wordpress_cron_events_total' => __( 'Cron Events (scheduled tasks, overdue, next run)', 'wp-prometheus' ), - 'wordpress_transients_total' => __( 'Transients (total, expiring, expired)', 'wp-prometheus' ), - ); + $static_metrics = array( + 'wordpress_info' => __( 'WordPress Info (version, PHP version, multisite)', 'wp-prometheus' ), + 'wordpress_users_total' => __( 'Total Users by Role', 'wp-prometheus' ), + 'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ), + 'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ), + 'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ), + 'wordpress_cron_events_total' => __( 'Cron Events (scheduled tasks, overdue, next run)', 'wp-prometheus' ), + 'wordpress_transients_total' => __( 'Transients (total, expiring, expired)', 'wp-prometheus' ), + ); - $runtime_metrics = array( - 'wordpress_http_requests_total' => __( 'HTTP Requests Total (by method, status, endpoint)', 'wp-prometheus' ), - 'wordpress_http_request_duration_seconds' => __( 'HTTP Request Duration (histogram)', 'wp-prometheus' ), - 'wordpress_db_queries_total' => __( 'Database Queries Total (by endpoint)', 'wp-prometheus' ), - ); + $runtime_metrics = array( + 'wordpress_http_requests_total' => __( 'HTTP Requests Total (by method, status, endpoint)', 'wp-prometheus' ), + 'wordpress_http_request_duration_seconds' => __( 'HTTP Request Duration (histogram)', 'wp-prometheus' ), + 'wordpress_db_queries_total' => __( 'Database Queries Total (by endpoint)', 'wp-prometheus' ), + ); - $woocommerce_metrics = array( - 'wordpress_woocommerce_products_total' => __( 'WooCommerce Products (by status and type)', 'wp-prometheus' ), - 'wordpress_woocommerce_orders_total' => __( 'WooCommerce Orders (by status)', 'wp-prometheus' ), - 'wordpress_woocommerce_revenue_total' => __( 'WooCommerce Revenue (all time, today, month)', 'wp-prometheus' ), - 'wordpress_woocommerce_customers_total' => __( 'WooCommerce Customers (registered, guest)', 'wp-prometheus' ), - ); + $woocommerce_metrics = array( + 'wordpress_woocommerce_products_total' => __( 'WooCommerce Products (by status and type)', 'wp-prometheus' ), + 'wordpress_woocommerce_orders_total' => __( 'WooCommerce Orders (by status)', 'wp-prometheus' ), + 'wordpress_woocommerce_revenue_total' => __( 'WooCommerce Revenue (all time, today, month)', 'wp-prometheus' ), + 'wordpress_woocommerce_customers_total' => __( 'WooCommerce Customers (registered, guest)', 'wp-prometheus' ), + ); - echo '
'; - echo '' . esc_html__( 'Static Metrics', 'wp-prometheus' ) . ''; - echo '

' . esc_html__( 'Static Metrics', 'wp-prometheus' ) . '

'; + echo '
'; + echo '' . esc_html__( 'Static Metrics', 'wp-prometheus' ) . ''; + echo '

' . esc_html__( 'Static Metrics', 'wp-prometheus' ) . '

'; - foreach ( $static_metrics as $key => $label ) { - ?> - - $label ) { + ?> + +

' . esc_html__( 'Runtime Metrics', 'wp-prometheus' ) . '

'; - echo '

' . esc_html__( 'Runtime metrics track data across requests. Enable only what you need to minimize performance impact.', 'wp-prometheus' ) . '

'; + echo '

' . esc_html__( 'Runtime Metrics', 'wp-prometheus' ) . '

'; + echo '

' . esc_html__( 'Runtime metrics track data across requests. Enable only what you need to minimize performance impact.', 'wp-prometheus' ) . '

'; - foreach ( $runtime_metrics as $key => $label ) { - ?> - - $label ) { + ?> + +

' . esc_html__( 'WooCommerce Metrics', 'wp-prometheus' ) . '

'; - echo '

' . esc_html__( 'Metrics specific to WooCommerce stores. Only available when WooCommerce is active.', 'wp-prometheus' ) . '

'; + // WooCommerce metrics (only show if WooCommerce is active). + if ( class_exists( 'WooCommerce' ) ) { + echo '

' . esc_html__( 'WooCommerce Metrics', 'wp-prometheus' ) . '

'; + echo '

' . esc_html__( 'Metrics specific to WooCommerce stores. Only available when WooCommerce is active.', 'wp-prometheus' ) . '

'; - foreach ( $woocommerce_metrics as $key => $label ) { - ?> - - $label ) { + ?> + + '; - } + echo '
'; + } - /** - * Sanitize enabled metrics. - * - * @param mixed $input Input value. - * @return array - */ - public function sanitize_metrics( $input ): array { - if ( ! is_array( $input ) ) { - return array(); - } + /** + * Sanitize enabled metrics. + * + * @param mixed $input Input value. + * @return array + */ + public function sanitize_metrics( $input ): array { + if ( ! is_array( $input ) ) { + return array(); + } - return array_map( 'sanitize_text_field', $input ); - } + return array_map( 'sanitize_text_field', $input ); + } + + /** + * AJAX handler for saving custom metrics. + * + * @return void + */ + public function ajax_save_custom_metric(): void { + check_ajax_referer( 'wp_prometheus_custom_metric', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + $metric = array( + 'id' => isset( $_POST['id'] ) ? sanitize_key( $_POST['id'] ) : '', + 'name' => isset( $_POST['name'] ) ? sanitize_key( $_POST['name'] ) : '', + 'help' => isset( $_POST['help'] ) ? sanitize_text_field( wp_unslash( $_POST['help'] ) ) : '', + 'type' => 'gauge', + 'labels' => array(), + 'value_type' => isset( $_POST['value_type'] ) ? sanitize_key( $_POST['value_type'] ) : 'static', + 'value_config' => array(), + 'label_values' => array(), + 'enabled' => ! empty( $_POST['enabled'] ), + ); + + // Process labels. + if ( ! empty( $_POST['labels'] ) && is_array( $_POST['labels'] ) ) { + foreach ( $_POST['labels'] as $label ) { + $label = sanitize_key( $label ); + if ( ! empty( $label ) ) { + $metric['labels'][] = $label; + } + } + } + + // Process value config. + if ( 'option' === $metric['value_type'] ) { + $metric['value_config'] = array( + 'option_name' => isset( $_POST['option_name'] ) ? sanitize_key( $_POST['option_name'] ) : '', + 'default' => isset( $_POST['option_default'] ) ? floatval( $_POST['option_default'] ) : 0, + ); + } + + // Process label values. + if ( 'static' === $metric['value_type'] && ! empty( $_POST['label_values'] ) && is_array( $_POST['label_values'] ) ) { + foreach ( $_POST['label_values'] as $row ) { + if ( is_array( $row ) ) { + $sanitized_row = array(); + foreach ( $row as $index => $value ) { + // Last value is numeric. + if ( $index === count( $row ) - 1 ) { + $sanitized_row[] = floatval( $value ); + } else { + $sanitized_row[] = sanitize_text_field( wp_unslash( $value ) ); + } + } + if ( ! empty( array_filter( $sanitized_row, function( $v ) { return $v !== '' && $v !== 0.0; } ) ) || end( $sanitized_row ) !== '' ) { + $metric['label_values'][] = $sanitized_row; + } + } + } + } + + try { + $id = $this->metric_builder->save( $metric ); + wp_send_json_success( array( + 'message' => __( 'Metric saved successfully.', 'wp-prometheus' ), + 'id' => $id, + ) ); + } catch ( \InvalidArgumentException $e ) { + wp_send_json_error( array( 'message' => $e->getMessage() ) ); + } + } + + /** + * AJAX handler for deleting custom metrics. + * + * @return void + */ + public function ajax_delete_custom_metric(): void { + check_ajax_referer( 'wp_prometheus_custom_metric', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + $id = isset( $_POST['id'] ) ? sanitize_key( $_POST['id'] ) : ''; + + if ( empty( $id ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid metric ID.', 'wp-prometheus' ) ) ); + } + + if ( $this->metric_builder->delete( $id ) ) { + wp_send_json_success( array( 'message' => __( 'Metric deleted.', 'wp-prometheus' ) ) ); + } else { + wp_send_json_error( array( 'message' => __( 'Metric not found.', 'wp-prometheus' ) ) ); + } + } + + /** + * AJAX handler for exporting metrics. + * + * @return void + */ + public function ajax_export_metrics(): void { + check_ajax_referer( 'wp_prometheus_export', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + $json = $this->metric_builder->export(); + + wp_send_json_success( array( + 'json' => $json, + 'filename' => 'wp-prometheus-metrics-' . gmdate( 'Y-m-d' ) . '.json', + ) ); + } + + /** + * AJAX handler for importing metrics. + * + * @return void + */ + public function ajax_import_metrics(): void { + check_ajax_referer( 'wp_prometheus_import', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + $json = isset( $_POST['json'] ) ? wp_unslash( $_POST['json'] ) : ''; + $mode = isset( $_POST['mode'] ) ? sanitize_key( $_POST['mode'] ) : 'skip'; + + if ( empty( $json ) ) { + wp_send_json_error( array( 'message' => __( 'No data provided.', 'wp-prometheus' ) ) ); + } + + try { + $result = $this->metric_builder->import( $json, $mode ); + + $message = sprintf( + /* translators: 1: Imported count, 2: Skipped count, 3: Error count */ + __( 'Import complete: %1$d imported, %2$d skipped, %3$d errors.', 'wp-prometheus' ), + $result['imported'], + $result['skipped'], + $result['errors'] + ); + + wp_send_json_success( array( + 'message' => $message, + 'result' => $result, + ) ); + } catch ( \InvalidArgumentException $e ) { + wp_send_json_error( array( 'message' => $e->getMessage() ) ); + } + } + + /** + * AJAX handler for downloading dashboards. + * + * @return void + */ + public function ajax_download_dashboard(): void { + check_ajax_referer( 'wp_prometheus_dashboard', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + $slug = isset( $_POST['slug'] ) ? sanitize_file_name( $_POST['slug'] ) : ''; + + if ( empty( $slug ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid dashboard.', 'wp-prometheus' ) ) ); + } + + $content = $this->dashboard_provider->get_dashboard( $slug ); + $filename = $this->dashboard_provider->get_filename( $slug ); + + if ( null === $content || null === $filename ) { + wp_send_json_error( array( 'message' => __( 'Dashboard not found.', 'wp-prometheus' ) ) ); + } + + wp_send_json_success( array( + 'json' => $content, + 'filename' => $filename, + ) ); + } + + /** + * AJAX handler for resetting runtime metrics. + * + * @return void + */ + public function ajax_reset_runtime_metrics(): void { + check_ajax_referer( 'wp_prometheus_reset_runtime', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + RuntimeCollector::reset_metrics(); + + wp_send_json_success( array( 'message' => __( 'Runtime metrics have been reset.', 'wp-prometheus' ) ) ); + } } diff --git a/src/Installer.php b/src/Installer.php index e0cb442..eb1eff0 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -64,6 +64,7 @@ final class Installer { 'wp_prometheus_enable_default_metrics', 'wp_prometheus_enabled_metrics', 'wp_prometheus_runtime_metrics', + 'wp_prometheus_custom_metrics', ); foreach ( $options as $option ) { diff --git a/src/Metrics/Collector.php b/src/Metrics/Collector.php index c5ef065..512c697 100644 --- a/src/Metrics/Collector.php +++ b/src/Metrics/Collector.php @@ -10,6 +10,7 @@ namespace Magdev\WpPrometheus\Metrics; use Prometheus\CollectorRegistry; use Prometheus\Storage\InMemory; use Prometheus\RenderTextFormat; +use Magdev\WpPrometheus\Metrics\CustomMetricBuilder; // Prevent direct file access. if ( ! defined( 'ABSPATH' ) ) { @@ -113,6 +114,10 @@ class Collector { // Collect runtime metrics (HTTP requests, DB queries). $this->collect_runtime_metrics( $enabled_metrics ); + // Collect custom user-defined metrics. + $custom_builder = new CustomMetricBuilder(); + $custom_builder->register_with_collector( $this ); + /** * Fires after default metrics are collected. * diff --git a/src/Metrics/CustomMetricBuilder.php b/src/Metrics/CustomMetricBuilder.php new file mode 100644 index 0000000..0632d45 --- /dev/null +++ b/src/Metrics/CustomMetricBuilder.php @@ -0,0 +1,496 @@ +get_all(); + return $metrics[ $id ] ?? null; + } + + /** + * Save a metric (create or update). + * + * @param array $metric Metric data. + * @return string Metric ID. + * @throws \InvalidArgumentException If validation fails. + */ + public function save( array $metric ): string { + $errors = $this->validate( $metric ); + if ( ! empty( $errors ) ) { + throw new \InvalidArgumentException( implode( ', ', $errors ) ); + } + + $metrics = $this->get_all(); + + // Generate ID if not provided. + if ( empty( $metric['id'] ) ) { + $metric['id'] = wp_generate_uuid4(); + $metric['created_at'] = time(); + } + + $metric['updated_at'] = time(); + + // Sanitize and normalize the metric data. + $metric = $this->sanitize_metric( $metric ); + + $metrics[ $metric['id'] ] = $metric; + update_option( self::OPTION_NAME, $metrics ); + + return $metric['id']; + } + + /** + * Delete a metric. + * + * @param string $id Metric ID. + * @return bool True if deleted, false if not found. + */ + public function delete( string $id ): bool { + $metrics = $this->get_all(); + + if ( ! isset( $metrics[ $id ] ) ) { + return false; + } + + unset( $metrics[ $id ] ); + update_option( self::OPTION_NAME, $metrics ); + + return true; + } + + /** + * Validate a Prometheus metric name. + * + * @param string $name Metric name. + * @return bool True if valid. + */ + public function validate_name( string $name ): bool { + // Prometheus metric names must match: [a-zA-Z_:][a-zA-Z0-9_:]* + return (bool) preg_match( '/^[a-zA-Z_:][a-zA-Z0-9_:]*$/', $name ); + } + + /** + * Validate a Prometheus label name. + * + * @param string $name Label name. + * @return bool True if valid. + */ + public function validate_label_name( string $name ): bool { + // Prometheus label names must match: [a-zA-Z_][a-zA-Z0-9_]* + // Labels starting with __ are reserved. + if ( strpos( $name, '__' ) === 0 ) { + return false; + } + return (bool) preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name ); + } + + /** + * Validate a complete metric definition. + * + * @param array $metric Metric data. + * @return array Array of error messages (empty if valid). + */ + public function validate( array $metric ): array { + $errors = array(); + + // Name is required. + if ( empty( $metric['name'] ) ) { + $errors[] = __( 'Metric name is required.', 'wp-prometheus' ); + } elseif ( ! $this->validate_name( $metric['name'] ) ) { + $errors[] = __( 'Metric name must start with a letter, underscore, or colon, and contain only letters, numbers, underscores, and colons.', 'wp-prometheus' ); + } + + // Check for reserved prefixes. + if ( ! empty( $metric['name'] ) ) { + $reserved_prefixes = array( 'wordpress_', 'go_', 'process_', 'promhttp_' ); + foreach ( $reserved_prefixes as $prefix ) { + if ( strpos( $metric['name'], $prefix ) === 0 ) { + $errors[] = sprintf( + /* translators: %s: Reserved prefix */ + __( 'Metric name cannot start with reserved prefix "%s".', 'wp-prometheus' ), + $prefix + ); + break; + } + } + } + + // Check for duplicate names (excluding current metric if editing). + if ( ! empty( $metric['name'] ) ) { + $existing = $this->get_all(); + foreach ( $existing as $id => $existing_metric ) { + if ( $existing_metric['name'] === $metric['name'] && ( empty( $metric['id'] ) || $metric['id'] !== $id ) ) { + $errors[] = __( 'A metric with this name already exists.', 'wp-prometheus' ); + break; + } + } + } + + // Help text is required. + if ( empty( $metric['help'] ) ) { + $errors[] = __( 'Help text is required.', 'wp-prometheus' ); + } + + // Validate type. + $valid_types = array( 'gauge' ); + if ( empty( $metric['type'] ) || ! in_array( $metric['type'], $valid_types, true ) ) { + $errors[] = __( 'Invalid metric type. Only gauge is supported.', 'wp-prometheus' ); + } + + // Validate labels. + if ( ! empty( $metric['labels'] ) ) { + if ( ! is_array( $metric['labels'] ) ) { + $errors[] = __( 'Labels must be an array.', 'wp-prometheus' ); + } elseif ( count( $metric['labels'] ) > self::MAX_LABELS ) { + $errors[] = sprintf( + /* translators: %d: Maximum labels */ + __( 'Maximum %d labels allowed per metric.', 'wp-prometheus' ), + self::MAX_LABELS + ); + } else { + foreach ( $metric['labels'] as $label ) { + if ( ! $this->validate_label_name( $label ) ) { + $errors[] = sprintf( + /* translators: %s: Label name */ + __( 'Invalid label name: %s', 'wp-prometheus' ), + $label + ); + } + } + } + } + + // Validate value type. + $valid_value_types = array( 'static', 'option' ); + if ( empty( $metric['value_type'] ) || ! in_array( $metric['value_type'], $valid_value_types, true ) ) { + $errors[] = __( 'Invalid value type. Must be "static" or "option".', 'wp-prometheus' ); + } + + // Validate value config based on type. + if ( ! empty( $metric['value_type'] ) ) { + if ( 'static' === $metric['value_type'] ) { + // Static values validated in label_values. + } elseif ( 'option' === $metric['value_type'] ) { + if ( empty( $metric['value_config']['option_name'] ) ) { + $errors[] = __( 'Option name is required for option-based metrics.', 'wp-prometheus' ); + } + } + } + + // Validate label values count. + if ( ! empty( $metric['label_values'] ) && is_array( $metric['label_values'] ) ) { + if ( count( $metric['label_values'] ) > self::MAX_LABEL_VALUES ) { + $errors[] = sprintf( + /* translators: %d: Maximum label combinations */ + __( 'Maximum %d label value combinations allowed.', 'wp-prometheus' ), + self::MAX_LABEL_VALUES + ); + } + + // Validate each row has correct number of values. + $label_count = count( $metric['labels'] ?? array() ); + foreach ( $metric['label_values'] as $row ) { + if ( is_array( $row ) && count( $row ) !== $label_count + 1 ) { // +1 for value. + $errors[] = __( 'Each label value row must have values for all labels plus a metric value.', 'wp-prometheus' ); + break; + } + } + } + + return $errors; + } + + /** + * Sanitize metric data. + * + * @param array $metric Raw metric data. + * @return array Sanitized metric data. + */ + private function sanitize_metric( array $metric ): array { + $sanitized = array( + 'id' => sanitize_key( $metric['id'] ?? '' ), + 'name' => sanitize_key( $metric['name'] ?? '' ), + 'help' => sanitize_text_field( $metric['help'] ?? '' ), + 'type' => sanitize_key( $metric['type'] ?? 'gauge' ), + 'labels' => array(), + 'value_type' => sanitize_key( $metric['value_type'] ?? 'static' ), + 'value_config' => array(), + 'label_values' => array(), + 'enabled' => ! empty( $metric['enabled'] ), + 'created_at' => absint( $metric['created_at'] ?? time() ), + 'updated_at' => absint( $metric['updated_at'] ?? time() ), + ); + + // Sanitize labels. + if ( ! empty( $metric['labels'] ) && is_array( $metric['labels'] ) ) { + foreach ( $metric['labels'] as $label ) { + $sanitized['labels'][] = sanitize_key( $label ); + } + } + + // Sanitize value config. + if ( 'static' === $sanitized['value_type'] ) { + $sanitized['value_config'] = array(); + } elseif ( 'option' === $sanitized['value_type'] ) { + $sanitized['value_config'] = array( + 'option_name' => sanitize_key( $metric['value_config']['option_name'] ?? '' ), + 'default' => floatval( $metric['value_config']['default'] ?? 0 ), + ); + } + + // Sanitize label values. + if ( ! empty( $metric['label_values'] ) && is_array( $metric['label_values'] ) ) { + foreach ( $metric['label_values'] as $row ) { + if ( is_array( $row ) ) { + $sanitized_row = array(); + foreach ( $row as $index => $value ) { + // Last value is the metric value (numeric). + if ( $index === count( $row ) - 1 ) { + $sanitized_row[] = floatval( $value ); + } else { + $sanitized_row[] = sanitize_text_field( $value ); + } + } + $sanitized['label_values'][] = $sanitized_row; + } + } + } + + return $sanitized; + } + + /** + * Export all metrics to JSON. + * + * @return string JSON string. + */ + public function export(): string { + $metrics = $this->get_all(); + + $export_data = array( + 'version' => self::EXPORT_VERSION, + 'plugin_version' => WP_PROMETHEUS_VERSION, + 'exported_at' => gmdate( 'c' ), + 'site_url' => home_url(), + 'metrics' => array_values( $metrics ), + ); + + return wp_json_encode( $export_data, JSON_PRETTY_PRINT ); + } + + /** + * Import metrics from JSON. + * + * @param string $json JSON string. + * @param string $mode Import mode: 'skip', 'overwrite', or 'rename'. + * @return array Result with 'imported', 'skipped', 'errors' counts. + * @throws \InvalidArgumentException If JSON is invalid. + */ + public function import( string $json, string $mode = 'skip' ): array { + $data = json_decode( $json, true ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + throw new \InvalidArgumentException( __( 'Invalid JSON format.', 'wp-prometheus' ) ); + } + + if ( empty( $data['metrics'] ) || ! is_array( $data['metrics'] ) ) { + throw new \InvalidArgumentException( __( 'No metrics found in import file.', 'wp-prometheus' ) ); + } + + $result = array( + 'imported' => 0, + 'skipped' => 0, + 'errors' => 0, + 'messages' => array(), + ); + + $existing_metrics = $this->get_all(); + $existing_names = array_column( $existing_metrics, 'name', 'id' ); + + foreach ( $data['metrics'] as $metric ) { + if ( empty( $metric['name'] ) ) { + $result['errors']++; + continue; + } + + // Check for name collision. + $name_exists = in_array( $metric['name'], $existing_names, true ); + + if ( $name_exists ) { + if ( 'skip' === $mode ) { + $result['skipped']++; + $result['messages'][] = sprintf( + /* translators: %s: Metric name */ + __( 'Skipped "%s" (already exists).', 'wp-prometheus' ), + $metric['name'] + ); + continue; + } elseif ( 'rename' === $mode ) { + // Generate unique name. + $base_name = $metric['name']; + $counter = 1; + while ( in_array( $metric['name'], $existing_names, true ) ) { + $metric['name'] = $base_name . '_imported_' . $counter; + $counter++; + } + } + // 'overwrite' mode: continue with same name, will overwrite below. + } + + // Clear ID to create new metric (unless overwriting). + if ( 'overwrite' === $mode && $name_exists ) { + // Find existing ID by name. + $metric['id'] = array_search( $metric['name'], $existing_names, true ); + } else { + unset( $metric['id'] ); + } + + try { + $this->save( $metric ); + $result['imported']++; + + // Update existing names for subsequent collision checks. + $existing_names = array_column( $this->get_all(), 'name', 'id' ); + } catch ( \InvalidArgumentException $e ) { + $result['errors']++; + $result['messages'][] = sprintf( + /* translators: 1: Metric name, 2: Error message */ + __( 'Error importing "%1$s": %2$s', 'wp-prometheus' ), + $metric['name'], + $e->getMessage() + ); + } + } + + return $result; + } + + /** + * Register custom metrics with the Collector. + * + * @param Collector $collector The metrics collector instance. + * @return void + */ + public function register_with_collector( Collector $collector ): void { + $metrics = $this->get_all(); + + foreach ( $metrics as $metric ) { + if ( empty( $metric['enabled'] ) ) { + continue; + } + + try { + $gauge = $collector->register_gauge( + $metric['name'], + $metric['help'], + $metric['labels'] ?? array() + ); + + // Set values based on value type. + if ( 'option' === $metric['value_type'] ) { + // Option-based metric: read from WordPress option. + $option_name = $metric['value_config']['option_name'] ?? ''; + $default = $metric['value_config']['default'] ?? 0; + + if ( ! empty( $option_name ) ) { + $value = get_option( $option_name, $default ); + $value = is_numeric( $value ) ? floatval( $value ) : $default; + + // For option-based, use empty labels if no labels defined. + $label_values = array(); + if ( ! empty( $metric['labels'] ) && ! empty( $metric['label_values'][0] ) ) { + // Use first row of labels (without the value). + $label_values = array_slice( $metric['label_values'][0], 0, count( $metric['labels'] ) ); + } + + $gauge->set( $value, $label_values ); + } + } elseif ( 'static' === $metric['value_type'] ) { + // Static metric: use predefined label values. + if ( ! empty( $metric['label_values'] ) ) { + foreach ( $metric['label_values'] as $row ) { + if ( ! is_array( $row ) || count( $row ) < 1 ) { + continue; + } + + // Last element is the value. + $value = array_pop( $row ); + + // Remaining elements are label values. + $gauge->set( floatval( $value ), $row ); + } + } else { + // No labels, single value. + $gauge->set( 0, array() ); + } + } + } catch ( \Exception $e ) { + // Log error but don't break metric collection. + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( sprintf( 'WP Prometheus: Failed to register custom metric "%s": %s', $metric['name'], $e->getMessage() ) ); + } + } + } + } +} diff --git a/wp-prometheus.php b/wp-prometheus.php index 7793983..2de83fe 100644 --- a/wp-prometheus.php +++ b/wp-prometheus.php @@ -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.2.2 + * Version: 0.3.0 * Requires at least: 6.4 * Requires PHP: 8.3 * Author: Marco Graetsch @@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @var string */ -define( 'WP_PROMETHEUS_VERSION', '0.2.2' ); +define( 'WP_PROMETHEUS_VERSION', '0.3.0' ); /** * Plugin file path.