You've already forked wp-prometheus
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5f2edbafa | |||
| 7f0b6ec8a6 | |||
| 192da4588a | |||
| cf1797d4bf | |||
| 19d75ab7b2 | |||
| fa63857f5f | |||
| 41f16a9fbd | |||
| f984e3eb23 | |||
| 898af5e9d2 | |||
| bad977bef0 | |||
| da6d5081f7 | |||
| 3eb66b0ebe | |||
| bc108f6bd5 |
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip, intl, gettext
|
||||
extensions: mbstring, xml, zip, intl, gettext, redis, apcu
|
||||
tools: composer:v2
|
||||
|
||||
- name: Get version from tag
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ wp-plugins
|
||||
wp-core
|
||||
vendor/
|
||||
releases/*
|
||||
|
||||
# Marketing texts (not for distribution)
|
||||
MARKETING.md
|
||||
|
||||
181
CHANGELOG.md
181
CHANGELOG.md
@@ -5,6 +5,187 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.4.5] - 2026-02-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Settings now persist correctly across Metrics sub-tabs
|
||||
- Auth token no longer gets cleared when saving from Selection sub-tab
|
||||
- Enabled metrics no longer get cleared when saving from Endpoint sub-tab
|
||||
- Isolated mode setting no longer gets cleared when saving from other sub-tabs
|
||||
|
||||
### Changed
|
||||
|
||||
- Split Metrics settings into separate WordPress option groups per sub-tab
|
||||
- Each sub-tab now uses its own settings group to prevent cross-tab overwrites
|
||||
|
||||
## [0.4.4] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Safe mode for metrics collection (default):
|
||||
- Removes problematic content filters early
|
||||
- Allows third-party plugins to register `wp_prometheus_collect_metrics` hooks
|
||||
- Wraps custom hooks in output buffering and try-catch for protection
|
||||
- Isolated mode option for maximum compatibility:
|
||||
- Outputs metrics before other plugins fully load
|
||||
- Use only if Safe mode causes issues
|
||||
- `WP_PROMETHEUS_ISOLATED_MODE` environment variable support
|
||||
- Mode comparison table in admin settings
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced "early mode" with two clear modes: Safe (default) and Isolated
|
||||
- Custom metrics hooks now fire by default with protection against recursion
|
||||
- Filter removal now also includes `the_content_feed` and `comment_text`
|
||||
- Updated admin UI with clearer explanations of each mode
|
||||
|
||||
### Fixed
|
||||
|
||||
- Third-party plugins can now add custom metrics without memory issues
|
||||
- Twig-based plugins (like wp-fedistream) no longer cause recursion
|
||||
|
||||
## [0.4.3] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Sub-tabs navigation within Metrics tab (Endpoint, Selection, Runtime, Advanced)
|
||||
- Option to disable early mode in admin settings (Metrics → Advanced)
|
||||
- Support for `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable
|
||||
- Early mode status display in settings
|
||||
|
||||
### Fixed
|
||||
|
||||
- Early mode setting now saves correctly (moved into form with proper settings group)
|
||||
|
||||
### Changed
|
||||
|
||||
- Reorganized Metrics tab into logical sub-sections for better usability
|
||||
- Early mode can now be disabled for users who need the `wp_prometheus_collect_metrics` hook
|
||||
- Updated translations with sub-tab and early mode strings (English and German)
|
||||
|
||||
## [0.4.1] - 2026-02-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed memory exhaustion when wp-fedistream (Twig-based) plugin is active
|
||||
- Added early metrics endpoint handler that intercepts `/metrics` requests before full WordPress initialization
|
||||
- Removed content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`) during metrics collection to prevent recursion
|
||||
- Skip third-party extensibility hooks during early metrics mode to avoid conflicts
|
||||
- Changed `template_redirect` hook to `parse_request` for earlier request interception
|
||||
|
||||
## [0.4.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Persistent Storage Support:
|
||||
- Redis storage adapter for shared metrics across multiple instances
|
||||
- APCu storage adapter for single-server high-performance caching
|
||||
- StorageFactory class for automatic adapter selection and fallback
|
||||
- Connection testing with detailed error messages
|
||||
- New "Storage" tab in admin settings:
|
||||
- Storage adapter selection (In-Memory, Redis, APCu)
|
||||
- Redis configuration (host, port, password, database, key prefix)
|
||||
- APCu configuration (key prefix)
|
||||
- Connection test button
|
||||
- Environment variables documentation
|
||||
- Environment variable configuration for Docker/containerized environments:
|
||||
- `WP_PROMETHEUS_STORAGE_ADAPTER` - Select storage adapter
|
||||
- `WP_PROMETHEUS_REDIS_HOST` - Redis server hostname
|
||||
- `WP_PROMETHEUS_REDIS_PORT` - Redis server port
|
||||
- `WP_PROMETHEUS_REDIS_PASSWORD` - Redis authentication
|
||||
- `WP_PROMETHEUS_REDIS_DATABASE` - Redis database index (0-15)
|
||||
- `WP_PROMETHEUS_REDIS_PREFIX` - Redis key prefix
|
||||
- `WP_PROMETHEUS_APCU_PREFIX` - APCu key prefix
|
||||
- Automatic fallback to In-Memory storage if configured adapter fails
|
||||
- Docker Compose example in admin settings
|
||||
|
||||
### Changed
|
||||
|
||||
- Settings page now has 6 tabs: License, Metrics, Storage, Custom Metrics, Dashboards, Help
|
||||
- Updated translations with all new strings (English and German)
|
||||
- Collector now uses StorageFactory for storage adapter instantiation
|
||||
|
||||
## [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
|
||||
|
||||
- Fixed `wc_orders_count()` call missing required status parameter in WooCommerce orders metrics
|
||||
|
||||
## [0.2.1] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Localhost license bypass for development environments (localhost, 127.0.0.1, ::1, \*.localhost, \*.local)
|
||||
- Automatic rewrite rules flush when license status changes
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed 404 error on `/metrics` endpoint when license becomes valid after plugin activation
|
||||
|
||||
## [0.2.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- WooCommerce integration metrics (when WooCommerce is active):
|
||||
- `wordpress_woocommerce_products_total` - Products by status and type
|
||||
- `wordpress_woocommerce_orders_total` - Orders by status
|
||||
- `wordpress_woocommerce_revenue_total` - Revenue (all time, today, month)
|
||||
- `wordpress_woocommerce_customers_total` - Customers (registered, guest)
|
||||
- Cron job metrics:
|
||||
- `wordpress_cron_events_total` - Scheduled cron events by hook
|
||||
- `wordpress_cron_overdue_total` - Number of overdue cron events
|
||||
- `wordpress_cron_next_run_timestamp` - Unix timestamp of next scheduled cron
|
||||
- Transient cache metrics:
|
||||
- `wordpress_transients_total` - Transients by type (total, with_expiration, persistent, expired)
|
||||
- WooCommerce metrics section in settings (only visible when WooCommerce is active)
|
||||
- Support for WooCommerce HPOS (High-Performance Order Storage)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Help tab with new metrics reference
|
||||
|
||||
## [0.1.1] - 2026-02-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Reorganized settings page with tabbed interface (License, Metrics, Help tabs)
|
||||
- Moved Prometheus configuration help to dedicated Help tab
|
||||
- Separated static and runtime metrics in settings with descriptions
|
||||
- Added admin CSS for improved tab styling
|
||||
|
||||
### Added
|
||||
|
||||
- New Help tab with endpoint information, curl examples, and metrics reference table
|
||||
- Custom code examples section in Help tab
|
||||
|
||||
## [0.1.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
208
CLAUDE.md
208
CLAUDE.md
@@ -15,6 +15,12 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a
|
||||
- Prometheus compatible authenticated `/metrics` endpoint
|
||||
- Optional default metrics (users, posts, comments, plugins)
|
||||
- Runtime metrics (HTTP requests, request duration, database queries)
|
||||
- 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
|
||||
@@ -27,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.2.0 (Planned)
|
||||
|
||||
- WooCommerce integration metrics
|
||||
- Cron job metrics
|
||||
- Transient cache metrics
|
||||
*No planned features at this time.*
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -211,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
|
||||
@@ -219,6 +225,7 @@ wp-prometheus/
|
||||
├── releases/ # Release packages
|
||||
├── src/
|
||||
│ ├── Admin/
|
||||
│ │ ├── DashboardProvider.php # Grafana dashboard provider
|
||||
│ │ └── Settings.php # Settings page
|
||||
│ ├── Endpoint/
|
||||
│ │ └── MetricsEndpoint.php # /metrics endpoint
|
||||
@@ -226,7 +233,9 @@ wp-prometheus/
|
||||
│ │ └── Manager.php # License management
|
||||
│ ├── Metrics/
|
||||
│ │ ├── Collector.php # Prometheus metrics collector
|
||||
│ │ └── RuntimeCollector.php # Runtime metrics collector
|
||||
│ │ ├── CustomMetricBuilder.php # Custom metric CRUD
|
||||
│ │ ├── RuntimeCollector.php # Runtime metrics collector
|
||||
│ │ └── StorageFactory.php # Storage adapter factory
|
||||
│ ├── Installer.php # Activation/Deactivation
|
||||
│ ├── Plugin.php # Main plugin class
|
||||
│ └── index.php
|
||||
@@ -282,6 +291,193 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
|
||||
|
||||
## Session History
|
||||
|
||||
### 2026-02-02 - Settings Persistence Fix (v0.4.5)
|
||||
|
||||
- Fixed critical bug where settings would get cleared when saving from different Metrics sub-tabs
|
||||
- Root cause: All settings were registered under single `wp_prometheus_metrics_settings` group
|
||||
- When saving from "Endpoint" sub-tab, only auth token was in POST data
|
||||
- WordPress Settings API would process all registered settings in the group
|
||||
- Missing fields (enabled_metrics, isolated_mode) would receive null/undefined
|
||||
- Sanitize callbacks returned empty values, overwriting existing settings
|
||||
- Solution: Split into separate settings groups per sub-tab:
|
||||
- `wp_prometheus_endpoint_settings` for auth token
|
||||
- `wp_prometheus_selection_settings` for enabled metrics
|
||||
- `wp_prometheus_advanced_settings` for isolated mode
|
||||
- **Key Learning**: WordPress Settings API and multiple forms
|
||||
- When multiple forms share the same settings group, saving one form can clear settings from another
|
||||
- Each form with `settings_fields()` should use a unique option group
|
||||
- `register_setting()` group name must match `settings_fields()` group name
|
||||
|
||||
### 2026-02-02 - Safe Mode & Custom Hooks Fix (v0.4.4)
|
||||
|
||||
- Redesigned metrics collection to support both plugin compatibility AND custom metrics:
|
||||
- **Safe Mode (default)**: Removes content filters early but lets WordPress load normally
|
||||
- **Isolated Mode**: Legacy early mode that skips custom hooks entirely
|
||||
- Implementation:
|
||||
- `WP_PROMETHEUS_METRICS_REQUEST` constant set for any /metrics request
|
||||
- Content filters removed via `plugins_loaded` hook at priority 0
|
||||
- Collector fires `wp_prometheus_collect_metrics` with protection (output buffering, try-catch)
|
||||
- `wp_prometheus_isolated_mode` option replaces `wp_prometheus_disable_early_mode`
|
||||
- `WP_PROMETHEUS_ISOLATED_MODE` environment variable for containerized deployments
|
||||
- Collector now wraps custom hooks in `fire_custom_metrics_hook()` method:
|
||||
- Removes content filters again before hook (in case re-added)
|
||||
- Uses output buffering to discard accidental output
|
||||
- Catches exceptions to prevent breaking metrics output
|
||||
- Logs errors when WP_DEBUG is enabled
|
||||
- Updated admin UI with mode comparison table
|
||||
- **Key Learning**: Hybrid approach for plugin compatibility
|
||||
- The memory issue comes from content filter recursion, not just plugin loading
|
||||
- Removing filters early (before any plugin can trigger them) prevents recursion
|
||||
- Plugins still load and can register their `wp_prometheus_collect_metrics` hooks
|
||||
- Hooks fire after filters are removed, in a protected context
|
||||
- **Key Learning**: Defense in depth for custom hooks
|
||||
- Remove filters again right before hook fires (plugins may re-add them)
|
||||
- Output buffering catches any echo/print from misbehaving plugins
|
||||
- Try-catch prevents one broken plugin from breaking metrics entirely
|
||||
|
||||
### 2026-02-02 - Sub-tabs & Early Mode Fix (v0.4.3)
|
||||
|
||||
- Split Metrics tab into sub-tabs for better organization:
|
||||
- **Endpoint**: Authentication token configuration
|
||||
- **Selection**: Enable/disable individual metrics
|
||||
- **Runtime**: Reset runtime metrics data
|
||||
- **Advanced**: Early mode toggle and status
|
||||
- Fixed early mode setting not being saved (was outside form element)
|
||||
- Added CSS styling for horizontal sub-tab navigation
|
||||
- **Key Learning**: WordPress Settings API form structure
|
||||
- Settings must be inside `<form action="options.php">` with `settings_fields()` call
|
||||
- Each sub-tab needs its own form wrapper for proper saving
|
||||
- Sub-tabs use URL query parameter (`subtab`) within the main tab
|
||||
- **Key Learning**: WordPress plugin versioning requires TWO updates
|
||||
- Plugin header comment `Version: x.x.x` (line ~6) - used by WordPress admin
|
||||
- PHP constant `WP_PROMETHEUS_VERSION` (line ~133) - used internally
|
||||
- CI/CD checks both must match the git tag, causing release failures if mismatched
|
||||
|
||||
### 2026-02-02 - Early Mode Toggle (v0.4.2)
|
||||
|
||||
- Added option to disable early mode for users who need extensibility
|
||||
- Implementation:
|
||||
- Added `wp_prometheus_disable_early_mode` WordPress option
|
||||
- Added `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable support
|
||||
- Option check in `wp_prometheus_early_metrics_check()` before early interception
|
||||
- Environment variable accepts `1`, `true`, `yes`, `on` (case-insensitive)
|
||||
- Admin UI in Metrics tab:
|
||||
- "Early Mode" section with description of functionality
|
||||
- Checkbox to disable early metrics interception
|
||||
- Environment override notice when env var is set
|
||||
- Current status indicator showing early mode state
|
||||
- **Key Learning**: Balancing compatibility vs extensibility
|
||||
- Early mode fixes memory issues but disables `wp_prometheus_collect_metrics` hook
|
||||
- Users with custom metrics need the hook, so early mode must be optional
|
||||
- Default remains enabled (safe) with explicit opt-out for advanced users
|
||||
|
||||
### 2026-02-02 - Plugin Compatibility Fix (v0.4.1)
|
||||
|
||||
- Fixed memory exhaustion (1GB limit) when wp-fedistream (Twig-based) plugin is active
|
||||
- Root cause: Infinite recursion through WordPress hook system when content filters trigger Twig rendering
|
||||
- Solution: Early metrics endpoint interception before full WordPress initialization
|
||||
- Implementation changes:
|
||||
- Added `wp_prometheus_early_metrics_check()` in bootstrap file (wp-prometheus.php)
|
||||
- Checks REQUEST_URI for `/metrics` pattern before `plugins_loaded` fires
|
||||
- Defines `WP_PROMETHEUS_EARLY_METRICS` constant to signal early mode
|
||||
- Removes content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`)
|
||||
- Collector skips `wp_prometheus_collect_metrics` action in early mode
|
||||
- Changed MetricsEndpoint from `template_redirect` to `parse_request` hook
|
||||
- **Key Learning**: WordPress plugin loading order and hook timing
|
||||
- Plugins load alphabetically, so wp-fedistream ('f') loads before wp-prometheus ('p')
|
||||
- `template_redirect` fires too late - after themes and Twig initialize
|
||||
- `parse_request` fires earlier but still after plugin files load
|
||||
- Earliest interception point: top-level code in plugin bootstrap file
|
||||
- **Key Learning**: Content filter recursion in WordPress
|
||||
- `get_the_excerpt()` internally triggers `apply_filters('the_content', ...)`
|
||||
- This creates unexpected recursion vectors when Twig templates process content
|
||||
- Solution: Remove all content-related filters before metrics collection
|
||||
- **Key Learning**: Isolating metrics collection from WordPress template system
|
||||
- Use `remove_all_filters()` to clear problematic filter chains
|
||||
- Skip extensibility hooks (`do_action`) when in isolated early mode
|
||||
- Exit immediately after output to prevent further WordPress processing
|
||||
|
||||
### 2026-02-02 - Persistent Storage (v0.4.0)
|
||||
|
||||
- Added persistent storage support for metrics:
|
||||
- `StorageFactory.php` - Factory class for storage adapter instantiation
|
||||
- Redis storage adapter for shared metrics across multiple instances
|
||||
- APCu storage adapter for single-server high-performance caching
|
||||
- Automatic fallback to In-Memory if configured adapter fails
|
||||
- Added new "Storage" tab in admin settings:
|
||||
- Storage adapter selection (In-Memory, Redis, APCu)
|
||||
- Redis configuration (host, port, password, database, key prefix)
|
||||
- APCu configuration (key prefix)
|
||||
- Connection test button with detailed error messages
|
||||
- Added environment variable support for Docker deployments:
|
||||
- `WP_PROMETHEUS_STORAGE_ADAPTER` - Adapter selection
|
||||
- `WP_PROMETHEUS_REDIS_HOST`, `_PORT`, `_PASSWORD`, `_DATABASE`, `_PREFIX`
|
||||
- `WP_PROMETHEUS_APCU_PREFIX`
|
||||
- Environment variables take precedence over admin settings
|
||||
- Updated `Collector.php` to use `StorageFactory::get_adapter()`
|
||||
- Updated Help tab with storage backends documentation
|
||||
- Updated translation files with all new strings
|
||||
- **Key Learning**: promphp/prometheus_client_php storage adapters
|
||||
- Redis adapter requires options array with host, port, password, timeout
|
||||
- APCu adapter just needs a prefix string
|
||||
- Use `Redis::setPrefix()` before instantiation for custom key prefixes
|
||||
- **Key Learning**: Docker environment variable configuration
|
||||
- Use `getenv()` with explicit false check (`false !== getenv()`)
|
||||
- Environment variables should override WordPress options for containerized deployments
|
||||
|
||||
### 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):
|
||||
- `wordpress_woocommerce_products_total` - Products by status and type
|
||||
- `wordpress_woocommerce_orders_total` - Orders by status
|
||||
- `wordpress_woocommerce_revenue_total` - Revenue (all time, today, month)
|
||||
- `wordpress_woocommerce_customers_total` - Customers (registered, guest)
|
||||
- Added cron job metrics:
|
||||
- `wordpress_cron_events_total` - Scheduled cron events by hook (top 20)
|
||||
- `wordpress_cron_overdue_total` - Number of overdue cron events
|
||||
- `wordpress_cron_next_run_timestamp` - Unix timestamp of next scheduled cron
|
||||
- Added transient cache metrics:
|
||||
- `wordpress_transients_total` - Transients by type (total, with_expiration, persistent, expired)
|
||||
- Updated Settings page with new metric categories
|
||||
- Updated Help tab with new metrics reference
|
||||
- **Key Learning**: WooCommerce HPOS (High-Performance Order Storage) requires different queries
|
||||
- Check `OrderUtil::custom_orders_table_usage_is_enabled()` to determine storage type
|
||||
- HPOS uses `wc_orders` table instead of `posts` and `postmeta`
|
||||
- **Key Learning**: Cron event labeling requires cardinality control
|
||||
- Limit to top 20 hooks to prevent label explosion
|
||||
- Use `arsort()` to get most frequent hooks first
|
||||
|
||||
### 2026-02-02 - Runtime Metrics (v0.1.0)
|
||||
|
||||
- Implemented runtime metrics collection for HTTP requests and database queries
|
||||
|
||||
67
PLAN.md
67
PLAN.md
@@ -59,6 +59,7 @@ wp-prometheus/
|
||||
│ └── release.yml # CI/CD pipeline
|
||||
├── assets/
|
||||
│ ├── css/ # Admin/Frontend styles
|
||||
│ ├── dashboards/ # Grafana dashboard templates
|
||||
│ └── js/
|
||||
│ └── admin.js # Admin JavaScript
|
||||
├── languages/ # Translation files
|
||||
@@ -67,13 +68,17 @@ wp-prometheus/
|
||||
├── releases/ # Release packages
|
||||
├── src/
|
||||
│ ├── Admin/
|
||||
│ │ ├── DashboardProvider.php
|
||||
│ │ └── Settings.php
|
||||
│ ├── Endpoint/
|
||||
│ │ └── MetricsEndpoint.php
|
||||
│ ├── License/
|
||||
│ │ └── Manager.php
|
||||
│ ├── Metrics/
|
||||
│ │ └── Collector.php
|
||||
│ │ ├── Collector.php
|
||||
│ │ ├── CustomMetricBuilder.php
|
||||
│ │ ├── RuntimeCollector.php
|
||||
│ │ └── StorageFactory.php
|
||||
│ ├── Installer.php
|
||||
│ ├── Plugin.php
|
||||
│ └── index.php
|
||||
@@ -159,19 +164,57 @@ Alternatively, the token can be passed as a query parameter (for testing):
|
||||
https://example.com/metrics/?token=your-auth-token
|
||||
```
|
||||
|
||||
## Storage Configuration
|
||||
|
||||
The plugin supports multiple storage backends for Prometheus metrics:
|
||||
|
||||
### Available Adapters
|
||||
|
||||
| Adapter | Description | Use Case |
|
||||
| --------- | ------------------------------- | ------------------------------------- |
|
||||
| In-Memory | Default, no persistence | Development, single request metrics |
|
||||
| Redis | Shared storage across instances | Production, load-balanced environments|
|
||||
| APCu | High-performance local cache | Production, single-server deployments |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
For Docker or containerized environments, configure storage via environment variables:
|
||||
|
||||
```bash
|
||||
# Storage adapter selection
|
||||
WP_PROMETHEUS_STORAGE_ADAPTER=redis
|
||||
|
||||
# Redis configuration
|
||||
WP_PROMETHEUS_REDIS_HOST=redis
|
||||
WP_PROMETHEUS_REDIS_PORT=6379
|
||||
WP_PROMETHEUS_REDIS_PASSWORD=secret
|
||||
WP_PROMETHEUS_REDIS_DATABASE=0
|
||||
WP_PROMETHEUS_REDIS_PREFIX=WORDPRESS_PROMETHEUS_
|
||||
|
||||
# APCu configuration
|
||||
WP_PROMETHEUS_APCU_PREFIX=wp_prom
|
||||
```
|
||||
|
||||
### Docker Compose Example
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wordpress:
|
||||
image: wordpress:latest
|
||||
environment:
|
||||
WP_PROMETHEUS_STORAGE_ADAPTER: redis
|
||||
WP_PROMETHEUS_REDIS_HOST: redis
|
||||
WP_PROMETHEUS_REDIS_PORT: 6379
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Version 0.2.0
|
||||
|
||||
- WooCommerce integration metrics
|
||||
- Cron job metrics
|
||||
- Transient cache metrics
|
||||
|
||||
### Version 0.3.0
|
||||
|
||||
- Custom metric builder in admin
|
||||
- Metric export/import
|
||||
- Grafana dashboard templates
|
||||
*No planned features at this time.*
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -92,6 +92,33 @@ scrape_configs:
|
||||
|
||||
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact.
|
||||
|
||||
### Cron Metrics (v0.2.0+)
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| wordpress_cron_events_total | Gauge | hook | Scheduled cron events by hook |
|
||||
| wordpress_cron_overdue_total | Gauge | - | Number of overdue cron events |
|
||||
| wordpress_cron_next_run_timestamp | Gauge | - | Unix timestamp of next scheduled cron |
|
||||
|
||||
### Transient Metrics (v0.2.0+)
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| wordpress_transients_total | Gauge | type | Transients by type (total, with_expiration, persistent, expired) |
|
||||
|
||||
### WooCommerce Metrics (v0.2.0+)
|
||||
|
||||
These metrics are only available when WooCommerce is active.
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| wordpress_woocommerce_products_total | Gauge | status, type | Products by status and type |
|
||||
| wordpress_woocommerce_orders_total | Gauge | status | Orders by status |
|
||||
| wordpress_woocommerce_revenue_total | Gauge | period, currency | Revenue (all_time, today, month) |
|
||||
| wordpress_woocommerce_customers_total | Gauge | type | Customers (registered, guest) |
|
||||
|
||||
**Note:** WooCommerce metrics support both legacy post-based orders and HPOS (High-Performance Order Storage).
|
||||
|
||||
## Extending with Custom Metrics
|
||||
|
||||
Add your own metrics using the `wp_prometheus_collect_metrics` action:
|
||||
|
||||
262
assets/css/admin.css
Normal file
262
assets/css/admin.css
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* WP Prometheus Admin Styles
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
/* Tab content styling */
|
||||
.wp-prometheus-tab-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Sub-tabs navigation */
|
||||
.wp-prometheus-subtabs {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-nav {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-item a {
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
color: #50575e;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
margin-bottom: -1px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-item a:hover {
|
||||
color: #2271b1;
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-item.active a {
|
||||
color: #1d2327;
|
||||
background: #fff;
|
||||
border-color: #c3c4c7;
|
||||
border-bottom-color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-content {
|
||||
background: #fff;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-top: none;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wp-prometheus-subtab-content h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* License status box */
|
||||
.wp-prometheus-license-status {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
/* Help tab code blocks */
|
||||
.wp-prometheus-tab-content pre {
|
||||
background: #f1f1f1;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
margin: 15px 0;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Help tab tables */
|
||||
.wp-prometheus-tab-content .widefat {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.wp-prometheus-tab-content .widefat code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Metrics fieldset */
|
||||
.wp-prometheus-tab-content fieldset p strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Form table adjustments for tabs */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
851
assets/dashboards/wordpress-overview.json
Normal file
851
assets/dashboards/wordpress-overview.json
Normal file
@@ -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": ""
|
||||
}
|
||||
987
assets/dashboards/wordpress-runtime.json
Normal file
987
assets/dashboards/wordpress-runtime.json
Normal file
@@ -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": ""
|
||||
}
|
||||
1296
assets/dashboards/wordpress-woocommerce.json
Normal file
1296
assets/dashboards/wordpress-woocommerce.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,29 @@
|
||||
(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();
|
||||
|
||||
// Storage tab handlers.
|
||||
initStorageHandlers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize license tab handlers.
|
||||
*/
|
||||
function initLicenseHandlers() {
|
||||
// Validate license button.
|
||||
$('#wp-prometheus-validate-license').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -23,78 +45,702 @@
|
||||
// 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('<p>' + response.data.message + '</p>')
|
||||
.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('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
||||
.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 = $('<div class="metric-label-row">' +
|
||||
'<input type="text" name="labels[]" class="regular-text" placeholder="label_name" pattern="[a-zA-Z_][a-zA-Z0-9_]*">' +
|
||||
'<button type="button" class="button remove-label">×</button>' +
|
||||
'</div>');
|
||||
$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 = $('<div class="metric-value-row"></div>');
|
||||
|
||||
// Add label value inputs.
|
||||
var labels = getLabels();
|
||||
for (var i = 0; i < labelCount; i++) {
|
||||
$row.append('<input type="text" name="label_values[' + rowCount + '][]" class="small-text" placeholder="' + (labels[i] || 'value') + '">');
|
||||
}
|
||||
|
||||
// Add metric value input.
|
||||
$row.append('<input type="number" name="label_values[' + rowCount + '][]" class="small-text" step="any" placeholder="Value">');
|
||||
$row.append('<button type="button" class="button remove-value-row">×</button>');
|
||||
|
||||
$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('<p>' + response.data.message + '</p>')
|
||||
.show();
|
||||
|
||||
// Reload page after successful validation/activation.
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
$message
|
||||
.removeClass('notice-success')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>Connection error. Please try again.</p>')
|
||||
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
||||
.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('<p>Connection error. Please try again.</p>')
|
||||
.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('<p>' + response.data.message + '</p>')
|
||||
.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('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
||||
.show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$spinner.removeClass('is-active');
|
||||
$message
|
||||
.removeClass('notice-success')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>Connection error. Please try again.</p>')
|
||||
.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('<p>' + response.data.message + '</p>')
|
||||
.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('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
||||
.show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$spinner.removeClass('is-active');
|
||||
$message
|
||||
.removeClass('notice-success')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>Connection error. Please try again.</p>')
|
||||
.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('<p>' + response.data.message + '</p>')
|
||||
.show();
|
||||
} else {
|
||||
$message
|
||||
.removeClass('notice-success')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
||||
.show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$spinner.removeClass('is-active');
|
||||
$message
|
||||
.removeClass('notice-success')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>Connection error. Please try again.</p>')
|
||||
.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(
|
||||
'<div class="metric-label-row">' +
|
||||
'<input type="text" name="labels[]" class="regular-text" placeholder="label_name" pattern="[a-zA-Z_][a-zA-Z0-9_]*">' +
|
||||
'<button type="button" class="button remove-label">×</button>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
// Reset values to single empty row.
|
||||
$('#metric-values-container').html(
|
||||
'<div class="metric-value-row">' +
|
||||
'<input type="number" name="label_values[0][]" class="small-text" step="any" placeholder="Value">' +
|
||||
'<button type="button" class="button remove-value-row">×</button>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('<input type="text" name="label_values[' + rowIndex + '][]" class="small-text" placeholder="' + (labels[i] || 'value') + '" value="' + val + '">');
|
||||
}
|
||||
|
||||
// Re-add value input.
|
||||
var metricVal = currentValues[currentValues.length - 1] || '';
|
||||
$row.find('.remove-value-row').before('<input type="number" name="label_values[' + rowIndex + '][]" class="small-text" step="any" placeholder="Value" value="' + metricVal + '">');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize storage tab handlers.
|
||||
*/
|
||||
function initStorageHandlers() {
|
||||
var $form = $('#wp-prometheus-storage-form');
|
||||
var $adapterSelect = $('#storage-adapter');
|
||||
|
||||
// Show/hide adapter-specific config.
|
||||
$adapterSelect.on('change', function() {
|
||||
var adapter = $(this).val();
|
||||
$('#redis-config').toggle(adapter === 'redis');
|
||||
$('#apcu-config').toggle(adapter === 'apcu');
|
||||
});
|
||||
|
||||
// Save storage settings.
|
||||
$form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveStorageSettings();
|
||||
});
|
||||
|
||||
// Test storage connection.
|
||||
$('#test-storage').on('click', function() {
|
||||
testStorageConnection();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save storage settings via AJAX.
|
||||
*/
|
||||
function saveStorageSettings() {
|
||||
var $spinner = $('#wp-prometheus-storage-spinner');
|
||||
var $message = $('#wp-prometheus-storage-message');
|
||||
var $form = $('#wp-prometheus-storage-form');
|
||||
|
||||
$spinner.addClass('is-active');
|
||||
$message.hide();
|
||||
|
||||
var formData = $form.serialize();
|
||||
formData += '&action=wp_prometheus_save_storage';
|
||||
formData += '&nonce=' + wpPrometheus.storageNonce;
|
||||
|
||||
$.ajax({
|
||||
url: wpPrometheus.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
$spinner.removeClass('is-active');
|
||||
|
||||
if (response.success) {
|
||||
var noticeClass = response.data.warning ? 'notice-warning' : 'notice-success';
|
||||
$message
|
||||
.removeClass('notice-error notice-success notice-warning')
|
||||
.addClass('notice ' + noticeClass)
|
||||
.html('<p>' + response.data.message + '</p>')
|
||||
.show();
|
||||
|
||||
if (!response.data.warning) {
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
$message
|
||||
.removeClass('notice-success notice-warning')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
||||
.show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$spinner.removeClass('is-active');
|
||||
$message
|
||||
.removeClass('notice-success notice-warning')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>Connection error. Please try again.</p>')
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test storage connection via AJAX.
|
||||
*/
|
||||
function testStorageConnection() {
|
||||
var $spinner = $('#wp-prometheus-storage-spinner');
|
||||
var $message = $('#wp-prometheus-storage-message');
|
||||
var $form = $('#wp-prometheus-storage-form');
|
||||
|
||||
$spinner.addClass('is-active');
|
||||
$message.hide();
|
||||
|
||||
var formData = $form.serialize();
|
||||
formData += '&action=wp_prometheus_test_storage';
|
||||
formData += '&nonce=' + wpPrometheus.storageNonce;
|
||||
|
||||
$.ajax({
|
||||
url: wpPrometheus.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
$spinner.removeClass('is-active');
|
||||
|
||||
if (response.success) {
|
||||
$message
|
||||
.removeClass('notice-error notice-warning')
|
||||
.addClass('notice notice-success')
|
||||
.html('<p>' + response.data.message + '</p>')
|
||||
.show();
|
||||
} else {
|
||||
$message
|
||||
.removeClass('notice-success notice-warning')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>' + (response.data.message || 'Connection test failed.') + '</p>')
|
||||
.show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$spinner.removeClass('is-active');
|
||||
$message
|
||||
.removeClass('notice-success notice-warning')
|
||||
.addClass('notice notice-error')
|
||||
.html('<p>Connection error. Please try again.</p>')
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
})(jQuery);
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
151
src/Admin/DashboardProvider.php
Normal file
151
src/Admin/DashboardProvider.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Dashboard provider class.
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
namespace Magdev\WpPrometheus\Admin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* DashboardProvider class.
|
||||
*
|
||||
* Provides Grafana dashboard templates for download.
|
||||
*/
|
||||
class DashboardProvider {
|
||||
|
||||
/**
|
||||
* Dashboard directory path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $dashboard_dir;
|
||||
|
||||
/**
|
||||
* Available dashboard definitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $dashboards = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->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'];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,9 @@ class MetricsEndpoint {
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( 'init', array( $this, 'register_endpoint' ) );
|
||||
add_action( 'template_redirect', array( $this, 'handle_request' ) );
|
||||
// Use parse_request instead of template_redirect to handle the request early,
|
||||
// before themes and other plugins (like Twig-based ones) can interfere.
|
||||
add_action( 'parse_request', array( $this, 'handle_request' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,10 +68,13 @@ class MetricsEndpoint {
|
||||
/**
|
||||
* Handle the metrics endpoint request.
|
||||
*
|
||||
* Called during parse_request to intercept before themes/plugins load.
|
||||
*
|
||||
* @param \WP $wp WordPress environment instance.
|
||||
* @return void
|
||||
*/
|
||||
public function handle_request(): void {
|
||||
if ( ! get_query_var( 'wp_prometheus_metrics' ) ) {
|
||||
public function handle_request( \WP $wp ): void {
|
||||
if ( empty( $wp->query_vars['wp_prometheus_metrics'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -302,10 +302,47 @@ final class Manager {
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_license_valid(): bool {
|
||||
// Bypass license check on localhost for development.
|
||||
if ( self::is_localhost() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||
return 'valid' === $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current site is running on localhost.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_localhost(): bool {
|
||||
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
|
||||
$localhost_patterns = array(
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
);
|
||||
|
||||
// Check exact matches.
|
||||
if ( in_array( $host, $localhost_patterns, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check .localhost TLD (e.g., mysite.localhost).
|
||||
if ( str_ends_with( $host, '.localhost' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check .local TLD (common for local development).
|
||||
if ( str_ends_with( $host, '.local' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the license key.
|
||||
*
|
||||
@@ -396,9 +433,16 @@ final class Manager {
|
||||
* @return void
|
||||
*/
|
||||
private function update_cached_status( string $status, array $data = array() ): void {
|
||||
$previous_status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||
|
||||
update_option( self::OPTION_LICENSE_STATUS, $status );
|
||||
update_option( self::OPTION_LICENSE_DATA, $data );
|
||||
update_option( self::OPTION_LAST_CHECK, time() );
|
||||
|
||||
// Flush rewrite rules when license becomes valid to register the /metrics endpoint.
|
||||
if ( 'valid' === $status && 'valid' !== $previous_status ) {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,6 +509,9 @@ final class Manager {
|
||||
update_option( self::OPTION_LICENSE_DATA, array() );
|
||||
delete_transient( self::TRANSIENT_LICENSE_CHECK );
|
||||
|
||||
// Flush rewrite rules to remove the /metrics endpoint.
|
||||
flush_rewrite_rules();
|
||||
|
||||
wp_send_json_success( array(
|
||||
'success' => true,
|
||||
'message' => __( 'License deactivated.', 'wp-prometheus' ),
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
namespace Magdev\WpPrometheus\Metrics;
|
||||
|
||||
use Prometheus\CollectorRegistry;
|
||||
use Prometheus\Storage\InMemory;
|
||||
use Prometheus\RenderTextFormat;
|
||||
use Magdev\WpPrometheus\Metrics\CustomMetricBuilder;
|
||||
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
@@ -41,7 +42,7 @@ class Collector {
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->registry = new CollectorRegistry( new InMemory() );
|
||||
$this->registry = new CollectorRegistry( StorageFactory::get_adapter() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,15 +96,87 @@ class Collector {
|
||||
$this->collect_plugins_total();
|
||||
}
|
||||
|
||||
// Collect cron metrics.
|
||||
if ( in_array( 'wordpress_cron_events_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_cron_metrics();
|
||||
}
|
||||
|
||||
// Collect transient metrics.
|
||||
if ( in_array( 'wordpress_transients_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_transient_metrics();
|
||||
}
|
||||
|
||||
// Collect WooCommerce metrics (if WooCommerce is active).
|
||||
if ( $this->is_woocommerce_active() ) {
|
||||
$this->collect_woocommerce_metrics( $enabled_metrics );
|
||||
}
|
||||
|
||||
// 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.
|
||||
*
|
||||
* In isolated mode, skip custom hooks to avoid any potential issues.
|
||||
* In safe mode (default), fire hooks with protection against recursion.
|
||||
*
|
||||
* @param Collector $collector The metrics collector instance.
|
||||
*/
|
||||
do_action( 'wp_prometheus_collect_metrics', $this );
|
||||
if ( defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE ) {
|
||||
// Isolated mode: skip all third-party hooks for maximum safety.
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe mode: fire custom hooks with protection.
|
||||
$this->fire_custom_metrics_hook();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire custom metrics hook with protection against recursion.
|
||||
*
|
||||
* Removes potentially problematic filters, uses output buffering,
|
||||
* and catches any errors from third-party plugins.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function fire_custom_metrics_hook(): void {
|
||||
// Remove content filters again (in case any plugin re-added them).
|
||||
if ( function_exists( 'wp_prometheus_remove_content_filters' ) ) {
|
||||
wp_prometheus_remove_content_filters();
|
||||
} else {
|
||||
// Fallback if function doesn't exist.
|
||||
remove_all_filters( 'the_content' );
|
||||
remove_all_filters( 'the_excerpt' );
|
||||
remove_all_filters( 'get_the_excerpt' );
|
||||
remove_all_filters( 'the_title' );
|
||||
}
|
||||
|
||||
// Use output buffering to prevent any accidental output from plugins.
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Fires after default metrics are collected.
|
||||
*
|
||||
* Third-party plugins can use this hook to add custom metrics.
|
||||
*
|
||||
* @param Collector $collector The metrics collector instance.
|
||||
*/
|
||||
do_action( 'wp_prometheus_collect_metrics', $this );
|
||||
} catch ( \Throwable $e ) {
|
||||
// Log the error but don't let it break metrics output.
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'WP Prometheus: Error in custom metrics hook: ' . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
// Discard any output from plugins.
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,6 +309,375 @@ class Collector {
|
||||
$gauge->set( count( $all_plugins ) - count( $active_plugins ), array( 'inactive' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect cron metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_cron_metrics(): void {
|
||||
$cron_array = _get_cron_array();
|
||||
|
||||
if ( ! is_array( $cron_array ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Events total gauge.
|
||||
$events_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'cron_events_total',
|
||||
'Total number of scheduled cron events',
|
||||
array( 'hook' )
|
||||
);
|
||||
|
||||
// Count events by hook.
|
||||
$hook_counts = array();
|
||||
$total_events = 0;
|
||||
$overdue_count = 0;
|
||||
$current_time = time();
|
||||
$next_run = PHP_INT_MAX;
|
||||
|
||||
foreach ( $cron_array as $timestamp => $cron ) {
|
||||
if ( $timestamp < $next_run ) {
|
||||
$next_run = $timestamp;
|
||||
}
|
||||
|
||||
foreach ( $cron as $hook => $events ) {
|
||||
$event_count = count( $events );
|
||||
$total_events += $event_count;
|
||||
|
||||
if ( ! isset( $hook_counts[ $hook ] ) ) {
|
||||
$hook_counts[ $hook ] = 0;
|
||||
}
|
||||
$hook_counts[ $hook ] += $event_count;
|
||||
|
||||
// Check if overdue.
|
||||
if ( $timestamp < $current_time ) {
|
||||
$overdue_count += $event_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set events by hook (limit to top 20 to avoid cardinality explosion).
|
||||
arsort( $hook_counts );
|
||||
$hook_counts = array_slice( $hook_counts, 0, 20, true );
|
||||
foreach ( $hook_counts as $hook => $count ) {
|
||||
$events_gauge->set( $count, array( $hook ) );
|
||||
}
|
||||
|
||||
// Overdue events gauge.
|
||||
$overdue_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'cron_overdue_total',
|
||||
'Number of overdue cron events',
|
||||
array()
|
||||
);
|
||||
$overdue_gauge->set( $overdue_count, array() );
|
||||
|
||||
// Next run timestamp.
|
||||
$next_run_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'cron_next_run_timestamp',
|
||||
'Unix timestamp of next scheduled cron event',
|
||||
array()
|
||||
);
|
||||
if ( $next_run !== PHP_INT_MAX ) {
|
||||
$next_run_gauge->set( $next_run, array() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect transient metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_transient_metrics(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Count all transients.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$transient_count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%'"
|
||||
);
|
||||
|
||||
// Count transients with expiration.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$expiring_count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%'"
|
||||
);
|
||||
|
||||
// Count expired transients.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$expired_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%%' AND option_value < %d",
|
||||
time()
|
||||
)
|
||||
);
|
||||
|
||||
// Transients total gauge.
|
||||
$transients_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'transients_total',
|
||||
'Total number of transients in database',
|
||||
array( 'type' )
|
||||
);
|
||||
|
||||
$transients_gauge->set( (int) $transient_count, array( 'total' ) );
|
||||
$transients_gauge->set( (int) $expiring_count, array( 'with_expiration' ) );
|
||||
$transients_gauge->set( (int) $transient_count - (int) $expiring_count, array( 'persistent' ) );
|
||||
$transients_gauge->set( (int) $expired_count, array( 'expired' ) );
|
||||
|
||||
// Site transients (for multisite).
|
||||
if ( is_multisite() ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$site_transient_count = $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->sitemeta} WHERE meta_key LIKE '_site_transient_%' AND meta_key NOT LIKE '_site_transient_timeout_%'"
|
||||
);
|
||||
|
||||
$transients_gauge->set( (int) $site_transient_count, array( 'site_transients' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_woocommerce_active(): bool {
|
||||
return class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce metrics.
|
||||
*
|
||||
* @param array $enabled_metrics List of enabled metrics.
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_metrics( array $enabled_metrics ): void {
|
||||
// Products total.
|
||||
if ( in_array( 'wordpress_woocommerce_products_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_products();
|
||||
}
|
||||
|
||||
// Orders total.
|
||||
if ( in_array( 'wordpress_woocommerce_orders_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_orders();
|
||||
}
|
||||
|
||||
// Revenue.
|
||||
if ( in_array( 'wordpress_woocommerce_revenue_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_revenue();
|
||||
}
|
||||
|
||||
// Customers.
|
||||
if ( in_array( 'wordpress_woocommerce_customers_total', $enabled_metrics, true ) ) {
|
||||
$this->collect_woocommerce_customers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce products metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_products(): void {
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_products_total',
|
||||
'Total number of WooCommerce products by status and type',
|
||||
array( 'status', 'type' )
|
||||
);
|
||||
|
||||
// Get product counts by status.
|
||||
$product_counts = wp_count_posts( 'product' );
|
||||
$product_types = wc_get_product_types();
|
||||
|
||||
foreach ( get_object_vars( $product_counts ) as $status => $count ) {
|
||||
if ( (int) $count > 0 ) {
|
||||
$gauge->set( (int) $count, array( $status, 'all' ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Count by product type (for published products only).
|
||||
foreach ( array_keys( $product_types ) as $type ) {
|
||||
$args = array(
|
||||
'status' => 'publish',
|
||||
'type' => $type,
|
||||
'limit' => -1,
|
||||
'return' => 'ids',
|
||||
);
|
||||
$products = wc_get_products( $args );
|
||||
$gauge->set( count( $products ), array( 'publish', $type ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce orders metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_orders(): void {
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_orders_total',
|
||||
'Total number of WooCommerce orders by status',
|
||||
array( 'status' )
|
||||
);
|
||||
|
||||
// Get all registered order statuses and count each.
|
||||
$statuses = wc_get_order_statuses();
|
||||
|
||||
foreach ( array_keys( $statuses ) as $status ) {
|
||||
// Remove 'wc-' prefix for the label.
|
||||
$status_label = str_replace( 'wc-', '', $status );
|
||||
$count = wc_orders_count( $status );
|
||||
$gauge->set( (int) $count, array( $status_label ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce revenue metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_revenue(): void {
|
||||
global $wpdb;
|
||||
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_revenue_total',
|
||||
'Total WooCommerce revenue',
|
||||
array( 'period', 'currency' )
|
||||
);
|
||||
|
||||
$currency = get_woocommerce_currency();
|
||||
|
||||
// Check if HPOS (High-Performance Order Storage) is enabled.
|
||||
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
||||
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
|
||||
if ( $hpos_enabled ) {
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
|
||||
// Total revenue (all time) - completed and processing orders.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$total_revenue = $wpdb->get_var(
|
||||
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing')"
|
||||
);
|
||||
|
||||
// Today's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$today_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing') AND DATE(date_created_gmt) = %s",
|
||||
gmdate( 'Y-m-d' )
|
||||
)
|
||||
);
|
||||
|
||||
// This month's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$month_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing') AND YEAR(date_created_gmt) = %d AND MONTH(date_created_gmt) = %d",
|
||||
gmdate( 'Y' ),
|
||||
gmdate( 'm' )
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Legacy post-based orders.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$total_revenue = $wpdb->get_var(
|
||||
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_order_total'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND p.post_status IN ('wc-completed', 'wc-processing')"
|
||||
);
|
||||
|
||||
// Today's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$today_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_order_total'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND p.post_status IN ('wc-completed', 'wc-processing')
|
||||
AND DATE(p.post_date_gmt) = %s",
|
||||
gmdate( 'Y-m-d' )
|
||||
)
|
||||
);
|
||||
|
||||
// This month's revenue.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$month_revenue = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE pm.meta_key = '_order_total'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND p.post_status IN ('wc-completed', 'wc-processing')
|
||||
AND YEAR(p.post_date_gmt) = %d
|
||||
AND MONTH(p.post_date_gmt) = %d",
|
||||
gmdate( 'Y' ),
|
||||
gmdate( 'm' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$gauge->set( (float) ( $total_revenue ?? 0 ), array( 'all_time', $currency ) );
|
||||
$gauge->set( (float) ( $today_revenue ?? 0 ), array( 'today', $currency ) );
|
||||
$gauge->set( (float) ( $month_revenue ?? 0 ), array( 'month', $currency ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect WooCommerce customers metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function collect_woocommerce_customers(): void {
|
||||
$gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
'woocommerce_customers_total',
|
||||
'Total number of WooCommerce customers',
|
||||
array( 'type' )
|
||||
);
|
||||
|
||||
// Count users with customer role.
|
||||
$customer_count = count_users();
|
||||
$customers = $customer_count['avail_roles']['customer'] ?? 0;
|
||||
|
||||
$gauge->set( $customers, array( 'registered' ) );
|
||||
|
||||
// Count guest orders (orders without user_id).
|
||||
global $wpdb;
|
||||
|
||||
// Check if HPOS is enabled.
|
||||
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
||||
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
|
||||
if ( $hpos_enabled ) {
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$guest_orders = $wpdb->get_var(
|
||||
"SELECT COUNT(DISTINCT billing_email) FROM {$orders_table} WHERE customer_id = 0 AND billing_email != ''"
|
||||
);
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$guest_orders = $wpdb->get_var(
|
||||
"SELECT COUNT(DISTINCT pm.meta_value) FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
LEFT JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_customer_user'
|
||||
WHERE pm.meta_key = '_billing_email'
|
||||
AND p.post_type = 'shop_order'
|
||||
AND (pm2.meta_value = '0' OR pm2.meta_value IS NULL)"
|
||||
);
|
||||
}
|
||||
|
||||
$gauge->set( (int) $guest_orders, array( 'guest' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect runtime metrics from stored data.
|
||||
*
|
||||
|
||||
496
src/Metrics/CustomMetricBuilder.php
Normal file
496
src/Metrics/CustomMetricBuilder.php
Normal file
@@ -0,0 +1,496 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom metric builder class.
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
namespace Magdev\WpPrometheus\Metrics;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* CustomMetricBuilder class.
|
||||
*
|
||||
* Manages custom user-defined Prometheus metrics.
|
||||
*/
|
||||
class CustomMetricBuilder {
|
||||
|
||||
/**
|
||||
* Option name for storing custom metrics.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = 'wp_prometheus_custom_metrics';
|
||||
|
||||
/**
|
||||
* Export format version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EXPORT_VERSION = '1.0.0';
|
||||
|
||||
/**
|
||||
* Maximum number of labels per metric.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_LABELS = 5;
|
||||
|
||||
/**
|
||||
* Maximum number of label value combinations per metric.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_LABEL_VALUES = 50;
|
||||
|
||||
/**
|
||||
* Get all custom metrics.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$metrics = get_option( self::OPTION_NAME, array() );
|
||||
return is_array( $metrics ) ? $metrics : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single metric by ID.
|
||||
*
|
||||
* @param string $id Metric ID.
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $id ): ?array {
|
||||
$metrics = $this->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() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
502
src/Metrics/StorageFactory.php
Normal file
502
src/Metrics/StorageFactory.php
Normal file
@@ -0,0 +1,502 @@
|
||||
<?php
|
||||
/**
|
||||
* Storage factory for Prometheus metrics.
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
namespace Magdev\WpPrometheus\Metrics;
|
||||
|
||||
use Prometheus\Storage\Adapter;
|
||||
use Prometheus\Storage\InMemory;
|
||||
use Prometheus\Storage\Redis;
|
||||
use Prometheus\Storage\APC;
|
||||
use Prometheus\Exception\StorageException;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* StorageFactory class.
|
||||
*
|
||||
* Creates and configures storage adapters for Prometheus metrics.
|
||||
* Supports configuration via WordPress options or environment variables.
|
||||
*/
|
||||
class StorageFactory {
|
||||
|
||||
/**
|
||||
* Available storage adapters.
|
||||
*/
|
||||
public const ADAPTER_INMEMORY = 'inmemory';
|
||||
public const ADAPTER_REDIS = 'redis';
|
||||
public const ADAPTER_APCU = 'apcu';
|
||||
|
||||
/**
|
||||
* Environment variable names.
|
||||
*/
|
||||
private const ENV_STORAGE_ADAPTER = 'WP_PROMETHEUS_STORAGE_ADAPTER';
|
||||
private const ENV_REDIS_HOST = 'WP_PROMETHEUS_REDIS_HOST';
|
||||
private const ENV_REDIS_PORT = 'WP_PROMETHEUS_REDIS_PORT';
|
||||
private const ENV_REDIS_PASSWORD = 'WP_PROMETHEUS_REDIS_PASSWORD';
|
||||
private const ENV_REDIS_DATABASE = 'WP_PROMETHEUS_REDIS_DATABASE';
|
||||
private const ENV_REDIS_PREFIX = 'WP_PROMETHEUS_REDIS_PREFIX';
|
||||
private const ENV_APCU_PREFIX = 'WP_PROMETHEUS_APCU_PREFIX';
|
||||
|
||||
/**
|
||||
* Default Redis prefix.
|
||||
*/
|
||||
private const DEFAULT_REDIS_PREFIX = 'WORDPRESS_PROMETHEUS_';
|
||||
|
||||
/**
|
||||
* Default APCu prefix.
|
||||
*/
|
||||
private const DEFAULT_APCU_PREFIX = 'wp_prom';
|
||||
|
||||
/**
|
||||
* Singleton instance of the storage adapter.
|
||||
*
|
||||
* @var Adapter|null
|
||||
*/
|
||||
private static ?Adapter $instance = null;
|
||||
|
||||
/**
|
||||
* Last error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $last_error = '';
|
||||
|
||||
/**
|
||||
* Get the storage adapter instance.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
public static function get_adapter(): Adapter {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = self::create_adapter();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the singleton instance (useful for testing or config changes).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function reset(): void {
|
||||
self::$instance = null;
|
||||
self::$last_error = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_last_error(): string {
|
||||
return self::$last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available storage adapters.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function get_available_adapters(): array {
|
||||
return array(
|
||||
self::ADAPTER_INMEMORY => __( 'In-Memory (default, no persistence)', 'wp-prometheus' ),
|
||||
self::ADAPTER_REDIS => __( 'Redis (requires PHP Redis extension)', 'wp-prometheus' ),
|
||||
self::ADAPTER_APCU => __( 'APCu (requires APCu extension)', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a storage adapter is available on this system.
|
||||
*
|
||||
* @param string $adapter Adapter name.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_adapter_available( string $adapter ): bool {
|
||||
switch ( $adapter ) {
|
||||
case self::ADAPTER_INMEMORY:
|
||||
return true;
|
||||
|
||||
case self::ADAPTER_REDIS:
|
||||
return extension_loaded( 'redis' );
|
||||
|
||||
case self::ADAPTER_APCU:
|
||||
return extension_loaded( 'apcu' ) && apcu_enabled();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured storage adapter name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_configured_adapter(): string {
|
||||
// Check environment variable first.
|
||||
$env_adapter = getenv( self::ENV_STORAGE_ADAPTER );
|
||||
if ( false !== $env_adapter && ! empty( $env_adapter ) ) {
|
||||
return strtolower( $env_adapter );
|
||||
}
|
||||
|
||||
// Fall back to WordPress option.
|
||||
return get_option( 'wp_prometheus_storage_adapter', self::ADAPTER_INMEMORY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active storage adapter name (may differ from configured if fallback occurred).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_active_adapter(): string {
|
||||
// Ensure adapter is created.
|
||||
self::get_adapter();
|
||||
|
||||
$configured = self::get_configured_adapter();
|
||||
if ( self::is_adapter_available( $configured ) && empty( self::$last_error ) ) {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
return self::ADAPTER_INMEMORY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the storage adapter based on configuration.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
private static function create_adapter(): Adapter {
|
||||
$adapter = self::get_configured_adapter();
|
||||
self::$last_error = '';
|
||||
|
||||
switch ( $adapter ) {
|
||||
case self::ADAPTER_REDIS:
|
||||
return self::create_redis_adapter();
|
||||
|
||||
case self::ADAPTER_APCU:
|
||||
return self::create_apcu_adapter();
|
||||
|
||||
case self::ADAPTER_INMEMORY:
|
||||
default:
|
||||
return new InMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Redis storage adapter.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
private static function create_redis_adapter(): Adapter {
|
||||
if ( ! extension_loaded( 'redis' ) ) {
|
||||
self::$last_error = __( 'PHP Redis extension is not installed.', 'wp-prometheus' );
|
||||
return new InMemory();
|
||||
}
|
||||
|
||||
$config = self::get_redis_config();
|
||||
|
||||
try {
|
||||
Redis::setPrefix( $config['prefix'] );
|
||||
|
||||
$redis = new Redis( array(
|
||||
'host' => $config['host'],
|
||||
'port' => $config['port'],
|
||||
'password' => $config['password'] ?: null,
|
||||
'timeout' => 0.5,
|
||||
'read_timeout' => 10,
|
||||
'persistent_connections' => true,
|
||||
) );
|
||||
|
||||
// Test connection by triggering initialization.
|
||||
// The Redis adapter connects lazily, so we need to check it works.
|
||||
return $redis;
|
||||
} catch ( StorageException $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Redis connection failed: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
} catch ( \RedisException $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Redis error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
} catch ( \Exception $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Storage error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create APCu storage adapter.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
private static function create_apcu_adapter(): Adapter {
|
||||
if ( ! extension_loaded( 'apcu' ) ) {
|
||||
self::$last_error = __( 'APCu extension is not installed.', 'wp-prometheus' );
|
||||
return new InMemory();
|
||||
}
|
||||
|
||||
if ( ! apcu_enabled() ) {
|
||||
self::$last_error = __( 'APCu is installed but not enabled.', 'wp-prometheus' );
|
||||
return new InMemory();
|
||||
}
|
||||
|
||||
$prefix = self::get_apcu_prefix();
|
||||
|
||||
try {
|
||||
return new APC( $prefix );
|
||||
} catch ( StorageException $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'APCu error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redis configuration.
|
||||
*
|
||||
* @return array{host: string, port: int, password: string, database: int, prefix: string}
|
||||
*/
|
||||
public static function get_redis_config(): array {
|
||||
// Check environment variables first.
|
||||
$env_host = getenv( self::ENV_REDIS_HOST );
|
||||
$env_port = getenv( self::ENV_REDIS_PORT );
|
||||
$env_password = getenv( self::ENV_REDIS_PASSWORD );
|
||||
$env_database = getenv( self::ENV_REDIS_DATABASE );
|
||||
$env_prefix = getenv( self::ENV_REDIS_PREFIX );
|
||||
|
||||
// Get WordPress options as fallback.
|
||||
$options = get_option( 'wp_prometheus_redis_config', array() );
|
||||
|
||||
return array(
|
||||
'host' => ( false !== $env_host && ! empty( $env_host ) ) ? $env_host : ( $options['host'] ?? '127.0.0.1' ),
|
||||
'port' => ( false !== $env_port && ! empty( $env_port ) ) ? (int) $env_port : ( (int) ( $options['port'] ?? 6379 ) ),
|
||||
'password' => ( false !== $env_password ) ? $env_password : ( $options['password'] ?? '' ),
|
||||
'database' => ( false !== $env_database && ! empty( $env_database ) ) ? (int) $env_database : ( (int) ( $options['database'] ?? 0 ) ),
|
||||
'prefix' => ( false !== $env_prefix && ! empty( $env_prefix ) ) ? $env_prefix : ( $options['prefix'] ?? self::DEFAULT_REDIS_PREFIX ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get APCu prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_apcu_prefix(): string {
|
||||
$env_prefix = getenv( self::ENV_APCU_PREFIX );
|
||||
if ( false !== $env_prefix && ! empty( $env_prefix ) ) {
|
||||
return $env_prefix;
|
||||
}
|
||||
|
||||
return get_option( 'wp_prometheus_apcu_prefix', self::DEFAULT_APCU_PREFIX );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save storage configuration.
|
||||
*
|
||||
* @param array $config Configuration array.
|
||||
* @return void
|
||||
*/
|
||||
public static function save_config( array $config ): void {
|
||||
if ( isset( $config['adapter'] ) ) {
|
||||
update_option( 'wp_prometheus_storage_adapter', sanitize_key( $config['adapter'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $config['redis'] ) && is_array( $config['redis'] ) ) {
|
||||
$redis_config = array(
|
||||
'host' => sanitize_text_field( $config['redis']['host'] ?? '127.0.0.1' ),
|
||||
'port' => absint( $config['redis']['port'] ?? 6379 ),
|
||||
'password' => sanitize_text_field( $config['redis']['password'] ?? '' ),
|
||||
'database' => absint( $config['redis']['database'] ?? 0 ),
|
||||
'prefix' => sanitize_key( $config['redis']['prefix'] ?? self::DEFAULT_REDIS_PREFIX ),
|
||||
);
|
||||
update_option( 'wp_prometheus_redis_config', $redis_config );
|
||||
}
|
||||
|
||||
if ( isset( $config['apcu_prefix'] ) ) {
|
||||
update_option( 'wp_prometheus_apcu_prefix', sanitize_key( $config['apcu_prefix'] ) );
|
||||
}
|
||||
|
||||
// Reset the singleton to apply new configuration.
|
||||
self::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test storage adapter connection.
|
||||
*
|
||||
* @param string $adapter Adapter name.
|
||||
* @param array $config Optional configuration to test.
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public static function test_connection( string $adapter, array $config = array() ): array {
|
||||
switch ( $adapter ) {
|
||||
case self::ADAPTER_REDIS:
|
||||
return self::test_redis_connection( $config );
|
||||
|
||||
case self::ADAPTER_APCU:
|
||||
return self::test_apcu_connection( $config );
|
||||
|
||||
case self::ADAPTER_INMEMORY:
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'In-Memory storage is always available.', 'wp-prometheus' ),
|
||||
);
|
||||
|
||||
default:
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Unknown storage adapter.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Redis connection.
|
||||
*
|
||||
* @param array $config Redis configuration.
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
private static function test_redis_connection( array $config ): array {
|
||||
if ( ! extension_loaded( 'redis' ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'PHP Redis extension is not installed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
$redis_config = ! empty( $config ) ? $config : self::get_redis_config();
|
||||
|
||||
try {
|
||||
$redis = new \Redis();
|
||||
|
||||
$connected = $redis->connect(
|
||||
$redis_config['host'],
|
||||
$redis_config['port'],
|
||||
0.5 // timeout
|
||||
);
|
||||
|
||||
if ( ! $connected ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Could not connect to Redis server.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $redis_config['password'] ) ) {
|
||||
$authenticated = $redis->auth( $redis_config['password'] );
|
||||
if ( ! $authenticated ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Redis authentication failed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( $redis_config['database'] > 0 ) {
|
||||
$redis->select( $redis_config['database'] );
|
||||
}
|
||||
|
||||
// Test with a ping.
|
||||
$pong = $redis->ping();
|
||||
$redis->close();
|
||||
|
||||
if ( $pong ) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Redis host:port */
|
||||
__( 'Successfully connected to Redis at %s.', 'wp-prometheus' ),
|
||||
$redis_config['host'] . ':' . $redis_config['port']
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Redis ping failed.', 'wp-prometheus' ),
|
||||
);
|
||||
} catch ( \RedisException $e ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Redis error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test APCu connection.
|
||||
*
|
||||
* @param array $config APCu configuration.
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
private static function test_apcu_connection( array $config ): array {
|
||||
if ( ! extension_loaded( 'apcu' ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu extension is not installed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! apcu_enabled() ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu is installed but not enabled. Check your php.ini settings.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Test with a simple store/fetch.
|
||||
$test_key = 'wp_prometheus_test_' . time();
|
||||
$test_value = 'test_' . wp_rand();
|
||||
|
||||
$stored = apcu_store( $test_key, $test_value, 5 );
|
||||
if ( ! $stored ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu store operation failed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
$fetched = apcu_fetch( $test_key );
|
||||
apcu_delete( $test_key );
|
||||
|
||||
if ( $fetched === $test_value ) {
|
||||
$info = apcu_cache_info( true );
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Memory info */
|
||||
__( 'APCu is working. Memory: %s used.', 'wp-prometheus' ),
|
||||
size_format( $info['mem_size'] ?? 0 )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu fetch operation returned unexpected value.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.1.0
|
||||
* Version: 0.4.5
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.3
|
||||
* Author: Marco Graetsch
|
||||
@@ -21,12 +21,155 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Early metrics request detection.
|
||||
*
|
||||
* Detects /metrics requests early and removes problematic content filters
|
||||
* to prevent recursion issues with Twig-based plugins. Unlike the previous
|
||||
* "early mode", this allows WordPress to continue loading so that third-party
|
||||
* plugins can register their wp_prometheus_collect_metrics hooks.
|
||||
*
|
||||
* Two modes are available:
|
||||
* - Safe mode (default): Removes filters early, lets WP load, fires custom hooks
|
||||
* - Isolated mode: Outputs metrics immediately without custom hooks (legacy early mode)
|
||||
*/
|
||||
function wp_prometheus_early_metrics_check(): void {
|
||||
// Only handle /metrics requests.
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
|
||||
if ( ! preg_match( '#/metrics/?$#', $path ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set flag to indicate we're handling a metrics request.
|
||||
define( 'WP_PROMETHEUS_METRICS_REQUEST', true );
|
||||
|
||||
// Check if isolated mode is enabled via environment variable.
|
||||
$env_isolated = getenv( 'WP_PROMETHEUS_ISOLATED_MODE' );
|
||||
$isolated_mode = false !== $env_isolated && in_array( strtolower( $env_isolated ), array( '1', 'true', 'yes', 'on' ), true );
|
||||
|
||||
// Check if isolated mode is enabled via option (legacy "early mode" setting).
|
||||
if ( ! $isolated_mode && ! get_option( 'wp_prometheus_disable_early_mode', false ) ) {
|
||||
// Check for legacy isolated mode option.
|
||||
$isolated_mode = (bool) get_option( 'wp_prometheus_isolated_mode', false );
|
||||
}
|
||||
|
||||
// Remove all content filters immediately to prevent recursion with Twig-based plugins.
|
||||
// This is done for BOTH safe mode and isolated mode.
|
||||
add_action( 'plugins_loaded', 'wp_prometheus_remove_content_filters', 0 );
|
||||
|
||||
// Also remove filters now in case they were added by mu-plugins.
|
||||
wp_prometheus_remove_content_filters();
|
||||
|
||||
// If isolated mode is enabled, handle metrics immediately without waiting for plugins.
|
||||
if ( $isolated_mode ) {
|
||||
wp_prometheus_isolated_metrics_handler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove content filters that can cause recursion.
|
||||
*
|
||||
* Called early during metrics requests to prevent infinite loops
|
||||
* with Twig-based plugins that hook into content filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_prometheus_remove_content_filters(): void {
|
||||
remove_all_filters( 'the_content' );
|
||||
remove_all_filters( 'the_excerpt' );
|
||||
remove_all_filters( 'get_the_excerpt' );
|
||||
remove_all_filters( 'the_title' );
|
||||
remove_all_filters( 'the_content_feed' );
|
||||
remove_all_filters( 'comment_text' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle metrics in isolated mode (no custom hooks).
|
||||
*
|
||||
* This is the legacy "early mode" that outputs metrics immediately
|
||||
* without allowing third-party plugins to add custom metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_prometheus_isolated_metrics_handler(): void {
|
||||
// Check if autoloader exists.
|
||||
$autoloader = __DIR__ . '/vendor/autoload.php';
|
||||
if ( ! file_exists( $autoloader ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once $autoloader;
|
||||
|
||||
// Check license validity.
|
||||
if ( ! \Magdev\WpPrometheus\License\Manager::is_license_valid() ) {
|
||||
return; // Let normal flow handle unlicensed state.
|
||||
}
|
||||
|
||||
// Authenticate.
|
||||
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
|
||||
if ( empty( $auth_token ) ) {
|
||||
status_header( 401 );
|
||||
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
echo 'Unauthorized';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check Bearer token.
|
||||
$auth_header = '';
|
||||
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
|
||||
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
|
||||
} elseif ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
|
||||
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
|
||||
}
|
||||
|
||||
$authenticated = false;
|
||||
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
|
||||
$authenticated = hash_equals( $auth_token, $matches[1] );
|
||||
}
|
||||
|
||||
// Check query parameter fallback.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
|
||||
if ( ! $authenticated && isset( $_GET['token'] ) ) {
|
||||
$authenticated = hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) );
|
||||
}
|
||||
|
||||
if ( ! $authenticated ) {
|
||||
status_header( 401 );
|
||||
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
echo 'Unauthorized';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set flag to indicate isolated mode - Collector will skip extensibility hooks.
|
||||
define( 'WP_PROMETHEUS_ISOLATED_MODE', true );
|
||||
|
||||
// Output metrics and exit immediately.
|
||||
$collector = new \Magdev\WpPrometheus\Metrics\Collector();
|
||||
|
||||
status_header( 200 );
|
||||
header( 'Content-Type: text/plain; version=0.0.4; charset=utf-8' );
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
header( 'Pragma: no-cache' );
|
||||
header( 'Expires: 0' );
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Prometheus format.
|
||||
echo $collector->render();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Try early metrics handling before full plugin initialization.
|
||||
wp_prometheus_early_metrics_check();
|
||||
|
||||
/**
|
||||
* Plugin version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_PROMETHEUS_VERSION', '0.1.0' );
|
||||
define( 'WP_PROMETHEUS_VERSION', '0.4.5' );
|
||||
|
||||
/**
|
||||
* Plugin file path.
|
||||
|
||||
Reference in New Issue
Block a user