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 beb6cbc..b3f8808 100644 Binary files a/languages/wp-prometheus-de_CH.mo and b/languages/wp-prometheus-de_CH.mo differ diff --git a/languages/wp-prometheus-de_CH.po b/languages/wp-prometheus-de_CH.po index 08483cf..706b046 100644 --- a/languages/wp-prometheus-de_CH.po +++ b/languages/wp-prometheus-de_CH.po @@ -3,7 +3,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" "PO-Revision-Date: 2026-02-02T00:00:00+00:00\n" @@ -31,6 +31,14 @@ msgstr "Lizenz" msgid "Help" msgstr "Hilfe" +#: src/Admin/Settings.php +msgid "Custom Metrics" +msgstr "Eigene Metriken" + +#: src/Admin/Settings.php +msgid "Dashboards" +msgstr "Dashboards" + #: src/Admin/Settings.php msgid "License settings saved." msgstr "Lizenz-Einstellungen gespeichert." @@ -213,6 +221,18 @@ msgstr "WooCommerce-Umsatz (gesamt, heute, Monat)" msgid "WooCommerce Customers (registered, guest)" msgstr "WooCommerce-Kunden (registriert, Gast)" +#: src/Admin/Settings.php +msgid "Reset Runtime Metrics" +msgstr "Laufzeit-Metriken zuruecksetzen" + +#: src/Admin/Settings.php +msgid "Clear all accumulated runtime metric data." +msgstr "Alle gesammelten Laufzeit-Metrikdaten loeschen." + +#: src/Admin/Settings.php +msgid "Reset Metrics" +msgstr "Metriken zuruecksetzen" + #: src/Admin/Settings.php msgid "Prometheus Configuration" msgstr "Prometheus-Konfiguration" @@ -329,14 +349,252 @@ msgstr "WooCommerce-Umsatz nach Zeitraum" msgid "WooCommerce customers by type" msgstr "WooCommerce-Kunden nach Typ" -#: src/Admin/Settings.php -msgid "Custom Metrics" -msgstr "Benutzerdefinierte Metriken" - #: src/Admin/Settings.php msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:" msgstr "Sie koennen benutzerdefinierte Metriken mit der wp_prometheus_collect_metrics-Aktion hinzufuegen:" +#: src/Admin/Settings.php +msgid "Add Custom Metric" +msgstr "Eigene Metrik hinzufuegen" + +#: src/Admin/Settings.php +msgid "Edit Custom Metric" +msgstr "Eigene Metrik bearbeiten" + +#: src/Admin/Settings.php +msgid "Metric Name" +msgstr "Metrik-Name" + +#: src/Admin/Settings.php +msgid "Must follow Prometheus naming conventions." +msgstr "Muss den Prometheus-Namenskonventionen entsprechen." + +#: src/Admin/Settings.php +msgid "Help Text" +msgstr "Hilfetext" + +#: src/Admin/Settings.php +msgid "Description shown in Prometheus output." +msgstr "Beschreibung in der Prometheus-Ausgabe." + +#: src/Admin/Settings.php +msgid "Value Type" +msgstr "Werttyp" + +#: src/Admin/Settings.php +msgid "Static Value" +msgstr "Statischer Wert" + +#: src/Admin/Settings.php +msgid "WordPress Option" +msgstr "WordPress-Option" + +#: src/Admin/Settings.php +msgid "Static Value:" +msgstr "Statischer Wert:" + +#: src/Admin/Settings.php +msgid "Option Name:" +msgstr "Optionsname:" + +#: src/Admin/Settings.php +msgid "The name of the WordPress option to read." +msgstr "Der Name der zu lesenden WordPress-Option." + +#: src/Admin/Settings.php +msgid "Labels" +msgstr "Labels" + +#: src/Admin/Settings.php +msgid "Label name" +msgstr "Label-Name" + +#: src/Admin/Settings.php +msgid "Add Label" +msgstr "Label hinzufuegen" + +#: src/Admin/Settings.php +msgid "Label Values" +msgstr "Label-Werte" + +#: src/Admin/Settings.php +msgid "Value" +msgstr "Wert" + +#: src/Admin/Settings.php +msgid "Add Value Row" +msgstr "Wertezeile hinzufuegen" + +#: src/Admin/Settings.php +msgid "Enabled" +msgstr "Aktiviert" + +#: src/Admin/Settings.php +msgid "Save Metric" +msgstr "Metrik speichern" + +#: src/Admin/Settings.php +msgid "Cancel" +msgstr "Abbrechen" + +#: src/Admin/Settings.php +msgid "Your Custom Metrics" +msgstr "Ihre eigenen Metriken" + +#: src/Admin/Settings.php +msgid "Name" +msgstr "Name" + +#: src/Admin/Settings.php +msgid "Status" +msgstr "Status" + +#: src/Admin/Settings.php +msgid "Actions" +msgstr "Aktionen" + +#: src/Admin/Settings.php +msgid "Active" +msgstr "Aktiv" + +#: src/Admin/Settings.php +msgid "Inactive" +msgstr "Inaktiv" + +#: src/Admin/Settings.php +msgid "Edit" +msgstr "Bearbeiten" + +#: src/Admin/Settings.php +msgid "Delete" +msgstr "Loeschen" + +#: src/Admin/Settings.php +msgid "No custom metrics defined yet." +msgstr "Noch keine eigenen Metriken definiert." + +#: src/Admin/Settings.php +msgid "Export / Import" +msgstr "Export / Import" + +#: src/Admin/Settings.php +msgid "Export your custom metrics configuration for backup or transfer to another site." +msgstr "Exportieren Sie Ihre Metriken-Konfiguration zur Sicherung oder Uebertragung auf eine andere Website." + +#: src/Admin/Settings.php +msgid "Export Metrics" +msgstr "Metriken exportieren" + +#: src/Admin/Settings.php +msgid "Import Metrics" +msgstr "Metriken importieren" + +#: src/Admin/Settings.php +msgid "Import Options" +msgstr "Import-Optionen" + +#: src/Admin/Settings.php +msgid "Skip existing metrics" +msgstr "Bestehende Metriken ueberspringen" + +#: src/Admin/Settings.php +msgid "Overwrite existing metrics" +msgstr "Bestehende Metriken ueberschreiben" + +#: src/Admin/Settings.php +msgid "Rename duplicates" +msgstr "Duplikate umbenennen" + +#: src/Admin/Settings.php +msgid "Import" +msgstr "Importieren" + +#: src/Admin/Settings.php +msgid "Grafana Dashboard Templates" +msgstr "Grafana Dashboard-Vorlagen" + +#: src/Admin/Settings.php +msgid "Download pre-built Grafana dashboards for visualizing your WordPress metrics." +msgstr "Laden Sie vorgefertigte Grafana-Dashboards zur Visualisierung Ihrer WordPress-Metriken herunter." + +#: src/Admin/Settings.php +msgid "Download" +msgstr "Herunterladen" + +#: src/Admin/Settings.php +msgid "Import instructions:" +msgstr "Import-Anleitung:" + +#: src/Admin/Settings.php +msgid "Download the desired dashboard JSON file" +msgstr "Laden Sie die gewuenschte Dashboard-JSON-Datei herunter" + +#: src/Admin/Settings.php +msgid "In Grafana, go to Dashboards > Import" +msgstr "Gehen Sie in Grafana zu Dashboards > Import" + +#: src/Admin/Settings.php +msgid "Upload the JSON file or paste its contents" +msgstr "Laden Sie die JSON-Datei hoch oder fuegen Sie 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.