You've already forked wp-prometheus
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52fd6da1d1 | |||
| 9a94b4a7a5 | |||
| 1b1e818ff4 | |||
| 88ce597f1e | |||
| 9bfed06466 | |||
| b605d0c299 | |||
| 63660202c4 | |||
| 3b71a0f7c9 | |||
| 5aaa73ec24 | |||
| e5f2edbafa | |||
| 7f0b6ec8a6 | |||
| 192da4588a | |||
| cf1797d4bf | |||
| 19d75ab7b2 | |||
| fa63857f5f | |||
| 41f16a9fbd | |||
| f984e3eb23 | |||
| 898af5e9d2 | |||
| bad977bef0 | |||
| da6d5081f7 |
@@ -6,7 +6,7 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-release:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -18,7 +18,32 @@ jobs:
|
|||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: '8.3'
|
php-version: '8.3'
|
||||||
extensions: mbstring, xml, zip, intl, gettext
|
extensions: mbstring, xml, zip, intl, gettext, redis, apcu
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Validate composer.json
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: composer install --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: composer test
|
||||||
|
|
||||||
|
build-release:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, xml, zip, intl, gettext, redis, apcu
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
|
|
||||||
- name: Get version from tag
|
- name: Get version from tag
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,3 +4,9 @@ wp-plugins
|
|||||||
wp-core
|
wp-core
|
||||||
vendor/
|
vendor/
|
||||||
releases/*
|
releases/*
|
||||||
|
|
||||||
|
# Marketing texts (not for distribution)
|
||||||
|
MARKETING.md
|
||||||
|
|
||||||
|
# PHPUnit cache
|
||||||
|
.phpunit.cache
|
||||||
|
|||||||
235
CHANGELOG.md
235
CHANGELOG.md
@@ -5,6 +5,241 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.5.1] - 2026-03-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Custom metric name sanitization: `sanitize_key()` was stripping colons and lowercasing names, silently mangling valid Prometheus metric names (e.g. `my:Custom_metric` became `mycustom_metric`). Added dedicated `sanitize_metric_name()` that preserves valid Prometheus characters.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Consolidated 3 separate transient COUNT queries into a single query with conditional aggregation for better database performance.
|
||||||
|
- Deduplicated inline HPOS check in WooCommerce customer metrics to use existing `is_hpos_enabled()` method.
|
||||||
|
- Added license domain binding for authorized deployment domains.
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Comprehensive PHPUnit test suite with 189 tests and 329 assertions:
|
||||||
|
- CustomMetricBuilderTest (35 tests) - validation, CRUD, import/export
|
||||||
|
- AuthenticationTest (13 tests) - Bearer token, query param, header extraction
|
||||||
|
- StorageFactoryTest (25 tests) - adapter config, env vars, connection testing
|
||||||
|
- RuntimeCollectorTest (22 tests) - endpoint normalization, histograms, singleton
|
||||||
|
- DashboardProviderTest (27 tests) - registration validation, path traversal security
|
||||||
|
- InstallerTest (11 tests) - activation, deactivation, uninstall cleanup
|
||||||
|
- CollectorTest (10 tests) - registry, metric registration, render output
|
||||||
|
- MetricsEndpointTest (5 tests) - rewrite rules, request routing
|
||||||
|
- Test bootstrap with WordPress function stubs and GlobalFunctionState helper
|
||||||
|
- CI/CD test job in Gitea release workflow that gates release builds
|
||||||
|
- php-mock/php-mock-phpunit dependency for mocking WordPress functions in namespaced code
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Release pipeline now requires passing tests before building release packages
|
||||||
|
|
||||||
|
## [0.4.9] - 2026-02-26
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fixed XSS vulnerability: replaced all jQuery `.html()` injections with safe `.text()` DOM construction in admin.js
|
||||||
|
- Fixed insecure token generation: replaced `Math.random()` with `crypto.getRandomValues()` (Web Crypto API)
|
||||||
|
- Fixed XSS via string interpolation in `updateValueRows()`: replaced HTML string building with jQuery DOM construction
|
||||||
|
- Added 1 MB import size limit to prevent DoS via large JSON payloads in CustomMetricBuilder
|
||||||
|
- Removed `site_url` from metric export data to prevent information disclosure
|
||||||
|
- Added import mode validation (allowlist check) in CustomMetricBuilder
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extracted shared authentication logic (`wp_prometheus_authenticate_request()`) to eliminate code duplication between MetricsEndpoint and isolated mode handler
|
||||||
|
- Extracted `showNotice()` helper in admin.js to DRY up 10+ duplicated AJAX response handling patterns
|
||||||
|
- Extracted `is_hpos_enabled()` helper method in Collector to DRY up WooCommerce HPOS checks
|
||||||
|
- Optimized WooCommerce product type counting: uses `paginate: true` COUNT query instead of loading all product IDs into memory
|
||||||
|
- Added missing options to `Installer::uninstall()` cleanup (isolated_mode, storage adapter, Redis/APCu config)
|
||||||
|
|
||||||
|
## [0.4.8] - 2026-02-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `_load_textdomain_just_in_time` notice on admin pages (WordPress 6.7+ compatibility)
|
||||||
|
- Deferred `load_plugin_textdomain()` to `init` action instead of `plugins_loaded`
|
||||||
|
- Deferred Settings tab label and DashboardProvider initialization to avoid early translation loading
|
||||||
|
|
||||||
|
## [0.4.7] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Database query duration distribution panel in Grafana Runtime dashboard
|
||||||
|
- `wordpress_db_query_duration_seconds` metric now listed in Help tab
|
||||||
|
- Documentation for enabling `SAVEQUERIES` constant for query timing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated README with instructions for enabling database query timing
|
||||||
|
- Grafana Runtime dashboard now includes bucket distribution chart for DB queries
|
||||||
|
|
||||||
|
## [0.4.6] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Dashboard extension hook `wp_prometheus_register_dashboards` for third-party plugins
|
||||||
|
- Third-party plugins can now register their own Grafana dashboard templates
|
||||||
|
- Support for file-based and inline JSON dashboard registration
|
||||||
|
- "Extension" badge for third-party dashboards in admin UI
|
||||||
|
- Plugin attribution display for third-party dashboards
|
||||||
|
- Security: Path traversal protection for registered dashboard files
|
||||||
|
- Isolated mode support for dashboard registration hook
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- DashboardProvider now supports both built-in and third-party registered dashboards
|
||||||
|
- Dashboard cards show source (built-in vs extension) with visual distinction
|
||||||
|
|
||||||
|
## [0.4.5] - 2026-02-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Settings now persist correctly across Metrics sub-tabs
|
||||||
|
- Auth token no longer gets cleared when saving from Selection sub-tab
|
||||||
|
- Enabled metrics no longer get cleared when saving from Endpoint sub-tab
|
||||||
|
- Isolated mode setting no longer gets cleared when saving from other sub-tabs
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Split Metrics settings into separate WordPress option groups per sub-tab
|
||||||
|
- Each sub-tab now uses its own settings group to prevent cross-tab overwrites
|
||||||
|
|
||||||
|
## [0.4.4] - 2026-02-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Safe mode for metrics collection (default):
|
||||||
|
- Removes problematic content filters early
|
||||||
|
- Allows third-party plugins to register `wp_prometheus_collect_metrics` hooks
|
||||||
|
- Wraps custom hooks in output buffering and try-catch for protection
|
||||||
|
- Isolated mode option for maximum compatibility:
|
||||||
|
- Outputs metrics before other plugins fully load
|
||||||
|
- Use only if Safe mode causes issues
|
||||||
|
- `WP_PROMETHEUS_ISOLATED_MODE` environment variable support
|
||||||
|
- Mode comparison table in admin settings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Replaced "early mode" with two clear modes: Safe (default) and Isolated
|
||||||
|
- Custom metrics hooks now fire by default with protection against recursion
|
||||||
|
- Filter removal now also includes `the_content_feed` and `comment_text`
|
||||||
|
- Updated admin UI with clearer explanations of each mode
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Third-party plugins can now add custom metrics without memory issues
|
||||||
|
- Twig-based plugins (like wp-fedistream) no longer cause recursion
|
||||||
|
|
||||||
|
## [0.4.3] - 2026-02-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Sub-tabs navigation within Metrics tab (Endpoint, Selection, Runtime, Advanced)
|
||||||
|
- Option to disable early mode in admin settings (Metrics → Advanced)
|
||||||
|
- Support for `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable
|
||||||
|
- Early mode status display in settings
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Early mode setting now saves correctly (moved into form with proper settings group)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reorganized Metrics tab into logical sub-sections for better usability
|
||||||
|
- Early mode can now be disabled for users who need the `wp_prometheus_collect_metrics` hook
|
||||||
|
- Updated translations with sub-tab and early mode strings (English and German)
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-02-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed memory exhaustion when wp-fedistream (Twig-based) plugin is active
|
||||||
|
- Added early metrics endpoint handler that intercepts `/metrics` requests before full WordPress initialization
|
||||||
|
- Removed content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`) during metrics collection to prevent recursion
|
||||||
|
- Skip third-party extensibility hooks during early metrics mode to avoid conflicts
|
||||||
|
- Changed `template_redirect` hook to `parse_request` for earlier request interception
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-02-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 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
|
## [0.2.0] - 2026-02-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
305
CLAUDE.md
305
CLAUDE.md
@@ -18,8 +18,12 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a
|
|||||||
- Cron job metrics (scheduled events, overdue, next run)
|
- Cron job metrics (scheduled events, overdue, next run)
|
||||||
- Transient cache metrics (total, expiring, expired)
|
- Transient cache metrics (total, expiring, expired)
|
||||||
- WooCommerce integration (products, orders, revenue, customers)
|
- 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
|
- Dedicated plugin settings under 'Settings/Metrics' menu
|
||||||
- Extensible by other plugins using `wp_prometheus_collect_metrics` action hook
|
- Extensible by other plugins using `wp_prometheus_collect_metrics` action hook
|
||||||
|
- Dashboard extension hook `wp_prometheus_register_dashboards` for third-party Grafana dashboards
|
||||||
- License management integration
|
- License management integration
|
||||||
|
|
||||||
### Key Fact: 100% AI-Generated
|
### Key Fact: 100% AI-Generated
|
||||||
@@ -30,11 +34,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||||
|
|
||||||
### Version 0.3.0 (Planned)
|
### Known Bugs
|
||||||
|
|
||||||
- Custom metric builder in admin
|
*No known bugs at this time.*
|
||||||
- Metric export/import
|
|
||||||
- Grafana dashboard templates
|
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -79,11 +81,7 @@ Text domain: `wp-prometheus`
|
|||||||
- `en_US` - English (United States) [base language - .pot template]
|
- `en_US` - English (United States) [base language - .pot template]
|
||||||
- `de_CH` - German (Switzerland, formal)
|
- `de_CH` - German (Switzerland, formal)
|
||||||
|
|
||||||
To compile translations to .mo files for production:
|
Translation compilation (.po → .mo) is handled automatically by CI/CD pipeline during release builds. No local compilation needed.
|
||||||
|
|
||||||
```bash
|
|
||||||
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create releases
|
### Create releases
|
||||||
|
|
||||||
@@ -211,9 +209,13 @@ When editing CLAUDE.md or other markdown files, follow these rules to avoid lint
|
|||||||
```txt
|
```txt
|
||||||
wp-prometheus/
|
wp-prometheus/
|
||||||
├── .gitea/workflows/
|
├── .gitea/workflows/
|
||||||
│ └── release.yml # CI/CD pipeline
|
│ └── release.yml # CI/CD pipeline (test + build)
|
||||||
├── assets/
|
├── assets/
|
||||||
│ ├── css/ # Admin/Frontend styles
|
│ ├── css/ # Admin/Frontend styles
|
||||||
|
│ ├── dashboards/ # Grafana dashboard templates
|
||||||
|
│ │ ├── wordpress-overview.json
|
||||||
|
│ │ ├── wordpress-runtime.json
|
||||||
|
│ │ └── wordpress-woocommerce.json
|
||||||
│ └── js/
|
│ └── js/
|
||||||
│ └── admin.js # Admin JavaScript
|
│ └── admin.js # Admin JavaScript
|
||||||
├── languages/ # Translation files
|
├── languages/ # Translation files
|
||||||
@@ -222,6 +224,7 @@ wp-prometheus/
|
|||||||
├── releases/ # Release packages
|
├── releases/ # Release packages
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── Admin/
|
│ ├── Admin/
|
||||||
|
│ │ ├── DashboardProvider.php # Grafana dashboard provider
|
||||||
│ │ └── Settings.php # Settings page
|
│ │ └── Settings.php # Settings page
|
||||||
│ ├── Endpoint/
|
│ ├── Endpoint/
|
||||||
│ │ └── MetricsEndpoint.php # /metrics endpoint
|
│ │ └── MetricsEndpoint.php # /metrics endpoint
|
||||||
@@ -229,14 +232,34 @@ wp-prometheus/
|
|||||||
│ │ └── Manager.php # License management
|
│ │ └── Manager.php # License management
|
||||||
│ ├── Metrics/
|
│ ├── Metrics/
|
||||||
│ │ ├── Collector.php # Prometheus metrics collector
|
│ │ ├── 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
|
│ ├── Installer.php # Activation/Deactivation
|
||||||
│ ├── Plugin.php # Main plugin class
|
│ ├── Plugin.php # Main plugin class
|
||||||
│ └── index.php
|
│ └── index.php
|
||||||
|
├── tests/
|
||||||
|
│ ├── bootstrap.php # WP constants + function stubs
|
||||||
|
│ ├── Helpers/
|
||||||
|
│ │ └── GlobalFunctionState.php # Controllable stub state
|
||||||
|
│ └── Unit/
|
||||||
|
│ ├── TestCase.php # Base class with PHPMock
|
||||||
|
│ ├── AuthenticationTest.php
|
||||||
|
│ ├── InstallerTest.php
|
||||||
|
│ ├── Admin/
|
||||||
|
│ │ └── DashboardProviderTest.php
|
||||||
|
│ ├── Endpoint/
|
||||||
|
│ │ └── MetricsEndpointTest.php
|
||||||
|
│ └── Metrics/
|
||||||
|
│ ├── CollectorTest.php
|
||||||
|
│ ├── CustomMetricBuilderTest.php
|
||||||
|
│ ├── RuntimeCollectorTest.php
|
||||||
|
│ └── StorageFactoryTest.php
|
||||||
├── CHANGELOG.md
|
├── CHANGELOG.md
|
||||||
├── CLAUDE.md
|
├── CLAUDE.md
|
||||||
├── composer.json
|
├── composer.json
|
||||||
├── index.php
|
├── index.php
|
||||||
|
├── phpunit.xml # PHPUnit 10 configuration
|
||||||
├── PLAN.md
|
├── PLAN.md
|
||||||
├── README.md
|
├── README.md
|
||||||
├── uninstall.php
|
├── uninstall.php
|
||||||
@@ -285,6 +308,266 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
|
|||||||
|
|
||||||
## Session History
|
## Session History
|
||||||
|
|
||||||
|
### 2026-02-26 - PHPUnit Test Suite & CI/CD Integration (v0.5.0)
|
||||||
|
|
||||||
|
- Created comprehensive PHPUnit test suite with 189 tests and 329 assertions
|
||||||
|
- Added `php-mock/php-mock-phpunit:^2.10` to composer.json require-dev
|
||||||
|
- Created test infrastructure:
|
||||||
|
- `tests/bootstrap.php`: ~45 WordPress function stubs with `if (!function_exists())` guards
|
||||||
|
- `tests/Helpers/GlobalFunctionState.php`: Static class for controlling stub behavior
|
||||||
|
- `tests/Unit/TestCase.php`: Abstract base class with PHPMock trait
|
||||||
|
- 8 test classes covering all core plugin classes:
|
||||||
|
- `CustomMetricBuilderTest` (35 tests) - validation, CRUD, import/export
|
||||||
|
- `StorageFactoryTest` (25 tests) - adapter config, env vars, connection tests
|
||||||
|
- `AuthenticationTest` (13 tests) - Bearer/query auth, header extraction
|
||||||
|
- `DashboardProviderTest` (27 tests) - registration, path traversal security
|
||||||
|
- `RuntimeCollectorTest` (22 tests) - endpoint normalization, histograms
|
||||||
|
- `InstallerTest` (11 tests) - activate, deactivate, uninstall cleanup
|
||||||
|
- `CollectorTest` (10 tests) - registry, register_gauge/counter/histogram, render
|
||||||
|
- `MetricsEndpointTest` (5 tests) - rewrite rules, request routing
|
||||||
|
- Added `test` job to `.gitea/workflows/release.yml` that gates `build-release`
|
||||||
|
- **Key Learning**: php-mock/php-mock-phpunit for WordPress testing without WP environment
|
||||||
|
- Intercepts unqualified function calls in namespaced code via PHP namespace fallback
|
||||||
|
- `$this->getFunctionMock('Namespace', 'function_name')` creates expectations
|
||||||
|
- Does NOT work for functions called from global namespace (bootstrap stubs) or in PHP 8.4 for some edge cases
|
||||||
|
- Solution for global-scope stubs: Make them controllable via `GlobalFunctionState::$options`
|
||||||
|
- **Key Learning**: Testing singletons and static state
|
||||||
|
- Use `ReflectionClass::newInstanceWithoutConstructor()` to bypass private constructors
|
||||||
|
- Reset static `$instance` properties via `ReflectionProperty::setValue(null, null)` in tearDown
|
||||||
|
- Always reset StorageFactory and RuntimeCollector singletons between tests
|
||||||
|
- **Key Learning**: CI/CD pipeline structure for test gating
|
||||||
|
- `test` job uses `composer install` (WITH dev deps) to run tests
|
||||||
|
- `build-release` job uses `--no-dev` (unchanged) for production builds
|
||||||
|
- `needs: test` dependency ensures failing tests block releases
|
||||||
|
|
||||||
|
### 2026-02-26 - Security Audit & Refactoring (v0.4.9)
|
||||||
|
|
||||||
|
- Fixed XSS vulnerabilities in admin.js (jQuery `.html()` → safe DOM construction)
|
||||||
|
- Fixed insecure token generation (`Math.random()` → Web Crypto API)
|
||||||
|
- Added 1 MB import size limit, import mode validation, removed `site_url` from exports
|
||||||
|
- Extracted shared authentication logic and helper methods
|
||||||
|
- Optimized WooCommerce product counting with COUNT query
|
||||||
|
|
||||||
|
### 2026-02-07 - Fix Early Textdomain Loading (v0.4.8)
|
||||||
|
|
||||||
|
- Fixed `_load_textdomain_just_in_time` warning on admin pages (WordPress 6.7+ compatibility)
|
||||||
|
- Root cause: `load_plugin_textdomain()` was called during `plugins_loaded` in `Plugin::__construct()`
|
||||||
|
- WordPress 6.7+ requires textdomain loading at the `init` action or later
|
||||||
|
- Three classes needed fixing:
|
||||||
|
- `Plugin.php`: Deferred `load_textdomain()` to `init` action hook, changed method visibility to public
|
||||||
|
- `Settings.php`: Deferred tab label initialization (which uses `__()`) to a lazy `get_tabs()` method
|
||||||
|
- `DashboardProvider.php`: Deferred built-in dashboard definitions (with `__()` calls) to a lazy `get_builtin_dashboards()` method
|
||||||
|
- Cleared Known Bugs section — no remaining known issues
|
||||||
|
- **Key Learning**: WordPress 6.7 textdomain loading requirements
|
||||||
|
- `load_plugin_textdomain()` must be called at `init` or later
|
||||||
|
- WordPress's JIT textdomain loader (`_load_textdomain_just_in_time`) also triggers too-early warnings
|
||||||
|
- Any `__()` / `_e()` calls before `init` for a plugin textdomain will trigger the notice
|
||||||
|
- The warning causes "headers already sent" errors because the notice output breaks header modifications
|
||||||
|
- Solution: Defer both explicit `load_plugin_textdomain()` and any `__()` calls to `init` or later hooks
|
||||||
|
|
||||||
|
### 2026-02-03 - Database Query Timing Documentation (v0.4.7)
|
||||||
|
|
||||||
|
- Added database query duration distribution panel to Grafana Runtime dashboard
|
||||||
|
- Added `wordpress_db_query_duration_seconds` metric to Help tab metrics reference
|
||||||
|
- Added documentation in README explaining how to enable `SAVEQUERIES` for query timing
|
||||||
|
- Updated translation files (.pot and .po) with new strings
|
||||||
|
- **Key Learning**: WordPress `SAVEQUERIES` constant
|
||||||
|
- Enables `$wpdb->queries` array with query strings, timing, and call stacks
|
||||||
|
- Required for `wordpress_db_query_duration_seconds` histogram metric
|
||||||
|
- Has performance overhead - recommended for development, use cautiously in production
|
||||||
|
- Without it, only query counts are available (not timing data)
|
||||||
|
|
||||||
|
### 2026-02-03 - Dashboard Extension Hook (v0.4.6)
|
||||||
|
|
||||||
|
- Added `wp_prometheus_register_dashboards` action hook for third-party plugins
|
||||||
|
- Third-party plugins can now register their own Grafana dashboard templates
|
||||||
|
- Implementation in `DashboardProvider.php`:
|
||||||
|
- `register_dashboard(slug, args)` method for registrations
|
||||||
|
- Supports file-based dashboards (absolute path to JSON) or inline JSON content
|
||||||
|
- Security: Path traversal protection (files must be under `WP_CONTENT_DIR`)
|
||||||
|
- `fire_registration_hook()` with output buffering and exception handling
|
||||||
|
- Respects isolated mode setting (skips third-party hooks when enabled)
|
||||||
|
- `is_third_party()` and `get_plugin_name()` helper methods
|
||||||
|
- Updated admin UI in Settings.php:
|
||||||
|
- "Extension" badge displayed on third-party dashboard cards
|
||||||
|
- Plugin attribution shown below third-party dashboards
|
||||||
|
- Visual distinction with blue border for third-party cards
|
||||||
|
- **Key Learning**: Extension hook design pattern
|
||||||
|
- Fire hook lazily on first `get_available()` call, not in constructor
|
||||||
|
- Use `$hook_fired` flag to prevent double-firing
|
||||||
|
- Wrap hook execution in try-catch to isolate failures
|
||||||
|
- Validate registrations thoroughly before accepting them
|
||||||
|
- **Key Learning**: Security for file-based registrations
|
||||||
|
- Require absolute paths (`path_is_absolute()`)
|
||||||
|
- Validate files exist and are readable
|
||||||
|
- Use `realpath()` to resolve symlinks and prevent traversal
|
||||||
|
- Restrict to `WP_CONTENT_DIR` (not just plugin directories)
|
||||||
|
|
||||||
|
### 2026-02-02 - Settings Persistence Fix (v0.4.5)
|
||||||
|
|
||||||
|
- Fixed critical bug where settings would get cleared when saving from different Metrics sub-tabs
|
||||||
|
- Root cause: All settings were registered under single `wp_prometheus_metrics_settings` group
|
||||||
|
- When saving from "Endpoint" sub-tab, only auth token was in POST data
|
||||||
|
- WordPress Settings API would process all registered settings in the group
|
||||||
|
- Missing fields (enabled_metrics, isolated_mode) would receive null/undefined
|
||||||
|
- Sanitize callbacks returned empty values, overwriting existing settings
|
||||||
|
- Solution: Split into separate settings groups per sub-tab:
|
||||||
|
- `wp_prometheus_endpoint_settings` for auth token
|
||||||
|
- `wp_prometheus_selection_settings` for enabled metrics
|
||||||
|
- `wp_prometheus_advanced_settings` for isolated mode
|
||||||
|
- **Key Learning**: WordPress Settings API and multiple forms
|
||||||
|
- When multiple forms share the same settings group, saving one form can clear settings from another
|
||||||
|
- Each form with `settings_fields()` should use a unique option group
|
||||||
|
- `register_setting()` group name must match `settings_fields()` group name
|
||||||
|
|
||||||
|
### 2026-02-02 - Safe Mode & Custom Hooks Fix (v0.4.4)
|
||||||
|
|
||||||
|
- Redesigned metrics collection to support both plugin compatibility AND custom metrics:
|
||||||
|
- **Safe Mode (default)**: Removes content filters early but lets WordPress load normally
|
||||||
|
- **Isolated Mode**: Legacy early mode that skips custom hooks entirely
|
||||||
|
- Implementation:
|
||||||
|
- `WP_PROMETHEUS_METRICS_REQUEST` constant set for any /metrics request
|
||||||
|
- Content filters removed via `plugins_loaded` hook at priority 0
|
||||||
|
- Collector fires `wp_prometheus_collect_metrics` with protection (output buffering, try-catch)
|
||||||
|
- `wp_prometheus_isolated_mode` option replaces `wp_prometheus_disable_early_mode`
|
||||||
|
- `WP_PROMETHEUS_ISOLATED_MODE` environment variable for containerized deployments
|
||||||
|
- Collector now wraps custom hooks in `fire_custom_metrics_hook()` method:
|
||||||
|
- Removes content filters again before hook (in case re-added)
|
||||||
|
- Uses output buffering to discard accidental output
|
||||||
|
- Catches exceptions to prevent breaking metrics output
|
||||||
|
- Logs errors when WP_DEBUG is enabled
|
||||||
|
- Updated admin UI with mode comparison table
|
||||||
|
- **Key Learning**: Hybrid approach for plugin compatibility
|
||||||
|
- The memory issue comes from content filter recursion, not just plugin loading
|
||||||
|
- Removing filters early (before any plugin can trigger them) prevents recursion
|
||||||
|
- Plugins still load and can register their `wp_prometheus_collect_metrics` hooks
|
||||||
|
- Hooks fire after filters are removed, in a protected context
|
||||||
|
- **Key Learning**: Defense in depth for custom hooks
|
||||||
|
- Remove filters again right before hook fires (plugins may re-add them)
|
||||||
|
- Output buffering catches any echo/print from misbehaving plugins
|
||||||
|
- Try-catch prevents one broken plugin from breaking metrics entirely
|
||||||
|
|
||||||
|
### 2026-02-02 - Sub-tabs & Early Mode Fix (v0.4.3)
|
||||||
|
|
||||||
|
- Split Metrics tab into sub-tabs for better organization:
|
||||||
|
- **Endpoint**: Authentication token configuration
|
||||||
|
- **Selection**: Enable/disable individual metrics
|
||||||
|
- **Runtime**: Reset runtime metrics data
|
||||||
|
- **Advanced**: Early mode toggle and status
|
||||||
|
- Fixed early mode setting not being saved (was outside form element)
|
||||||
|
- Added CSS styling for horizontal sub-tab navigation
|
||||||
|
- **Key Learning**: WordPress Settings API form structure
|
||||||
|
- Settings must be inside `<form action="options.php">` with `settings_fields()` call
|
||||||
|
- Each sub-tab needs its own form wrapper for proper saving
|
||||||
|
- Sub-tabs use URL query parameter (`subtab`) within the main tab
|
||||||
|
- **Key Learning**: WordPress plugin versioning requires TWO updates
|
||||||
|
- Plugin header comment `Version: x.x.x` (line ~6) - used by WordPress admin
|
||||||
|
- PHP constant `WP_PROMETHEUS_VERSION` (line ~133) - used internally
|
||||||
|
- CI/CD checks both must match the git tag, causing release failures if mismatched
|
||||||
|
|
||||||
|
### 2026-02-02 - Early Mode Toggle (v0.4.2)
|
||||||
|
|
||||||
|
- Added option to disable early mode for users who need extensibility
|
||||||
|
- Implementation:
|
||||||
|
- Added `wp_prometheus_disable_early_mode` WordPress option
|
||||||
|
- Added `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable support
|
||||||
|
- Option check in `wp_prometheus_early_metrics_check()` before early interception
|
||||||
|
- Environment variable accepts `1`, `true`, `yes`, `on` (case-insensitive)
|
||||||
|
- Admin UI in Metrics tab:
|
||||||
|
- "Early Mode" section with description of functionality
|
||||||
|
- Checkbox to disable early metrics interception
|
||||||
|
- Environment override notice when env var is set
|
||||||
|
- Current status indicator showing early mode state
|
||||||
|
- **Key Learning**: Balancing compatibility vs extensibility
|
||||||
|
- Early mode fixes memory issues but disables `wp_prometheus_collect_metrics` hook
|
||||||
|
- Users with custom metrics need the hook, so early mode must be optional
|
||||||
|
- Default remains enabled (safe) with explicit opt-out for advanced users
|
||||||
|
|
||||||
|
### 2026-02-02 - Plugin Compatibility Fix (v0.4.1)
|
||||||
|
|
||||||
|
- Fixed memory exhaustion (1GB limit) when wp-fedistream (Twig-based) plugin is active
|
||||||
|
- Root cause: Infinite recursion through WordPress hook system when content filters trigger Twig rendering
|
||||||
|
- Solution: Early metrics endpoint interception before full WordPress initialization
|
||||||
|
- Implementation changes:
|
||||||
|
- Added `wp_prometheus_early_metrics_check()` in bootstrap file (wp-prometheus.php)
|
||||||
|
- Checks REQUEST_URI for `/metrics` pattern before `plugins_loaded` fires
|
||||||
|
- Defines `WP_PROMETHEUS_EARLY_METRICS` constant to signal early mode
|
||||||
|
- Removes content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`)
|
||||||
|
- Collector skips `wp_prometheus_collect_metrics` action in early mode
|
||||||
|
- Changed MetricsEndpoint from `template_redirect` to `parse_request` hook
|
||||||
|
- **Key Learning**: WordPress plugin loading order and hook timing
|
||||||
|
- Plugins load alphabetically, so wp-fedistream ('f') loads before wp-prometheus ('p')
|
||||||
|
- `template_redirect` fires too late - after themes and Twig initialize
|
||||||
|
- `parse_request` fires earlier but still after plugin files load
|
||||||
|
- Earliest interception point: top-level code in plugin bootstrap file
|
||||||
|
- **Key Learning**: Content filter recursion in WordPress
|
||||||
|
- `get_the_excerpt()` internally triggers `apply_filters('the_content', ...)`
|
||||||
|
- This creates unexpected recursion vectors when Twig templates process content
|
||||||
|
- Solution: Remove all content-related filters before metrics collection
|
||||||
|
- **Key Learning**: Isolating metrics collection from WordPress template system
|
||||||
|
- Use `remove_all_filters()` to clear problematic filter chains
|
||||||
|
- Skip extensibility hooks (`do_action`) when in isolated early mode
|
||||||
|
- Exit immediately after output to prevent further WordPress processing
|
||||||
|
|
||||||
|
### 2026-02-02 - Persistent Storage (v0.4.0)
|
||||||
|
|
||||||
|
- Added persistent storage support for metrics:
|
||||||
|
- `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)
|
### 2026-02-02 - Extended Metrics (v0.2.0)
|
||||||
|
|
||||||
- Added WooCommerce integration metrics (only when WooCommerce is active):
|
- Added WooCommerce integration metrics (only when WooCommerce is active):
|
||||||
|
|||||||
61
PLAN.md
61
PLAN.md
@@ -59,6 +59,7 @@ wp-prometheus/
|
|||||||
│ └── release.yml # CI/CD pipeline
|
│ └── release.yml # CI/CD pipeline
|
||||||
├── assets/
|
├── assets/
|
||||||
│ ├── css/ # Admin/Frontend styles
|
│ ├── css/ # Admin/Frontend styles
|
||||||
|
│ ├── dashboards/ # Grafana dashboard templates
|
||||||
│ └── js/
|
│ └── js/
|
||||||
│ └── admin.js # Admin JavaScript
|
│ └── admin.js # Admin JavaScript
|
||||||
├── languages/ # Translation files
|
├── languages/ # Translation files
|
||||||
@@ -67,13 +68,17 @@ wp-prometheus/
|
|||||||
├── releases/ # Release packages
|
├── releases/ # Release packages
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── Admin/
|
│ ├── Admin/
|
||||||
|
│ │ ├── DashboardProvider.php
|
||||||
│ │ └── Settings.php
|
│ │ └── Settings.php
|
||||||
│ ├── Endpoint/
|
│ ├── Endpoint/
|
||||||
│ │ └── MetricsEndpoint.php
|
│ │ └── MetricsEndpoint.php
|
||||||
│ ├── License/
|
│ ├── License/
|
||||||
│ │ └── Manager.php
|
│ │ └── Manager.php
|
||||||
│ ├── Metrics/
|
│ ├── Metrics/
|
||||||
│ │ └── Collector.php
|
│ │ ├── Collector.php
|
||||||
|
│ │ ├── CustomMetricBuilder.php
|
||||||
|
│ │ ├── RuntimeCollector.php
|
||||||
|
│ │ └── StorageFactory.php
|
||||||
│ ├── Installer.php
|
│ ├── Installer.php
|
||||||
│ ├── Plugin.php
|
│ ├── Plugin.php
|
||||||
│ └── index.php
|
│ └── index.php
|
||||||
@@ -159,13 +164,57 @@ Alternatively, the token can be passed as a query parameter (for testing):
|
|||||||
https://example.com/metrics/?token=your-auth-token
|
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
|
## Future Enhancements
|
||||||
|
|
||||||
### Version 0.3.0
|
*No planned features at this time.*
|
||||||
|
|
||||||
- Custom metric builder in admin
|
|
||||||
- Metric export/import
|
|
||||||
- Grafana dashboard templates
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
106
README.md
106
README.md
@@ -6,7 +6,13 @@ A WordPress plugin that provides a Prometheus-compatible `/metrics` endpoint wit
|
|||||||
|
|
||||||
- Prometheus-compatible authenticated `/metrics` endpoint
|
- Prometheus-compatible authenticated `/metrics` endpoint
|
||||||
- Default WordPress metrics (users, posts, comments, plugins)
|
- Default WordPress metrics (users, posts, comments, plugins)
|
||||||
|
- Runtime metrics (HTTP requests, database queries)
|
||||||
|
- Cron job and transient cache metrics
|
||||||
|
- WooCommerce integration (products, orders, revenue)
|
||||||
|
- Custom metric builder with admin UI
|
||||||
|
- Grafana dashboard templates with download
|
||||||
- Extensible by other plugins using hooks
|
- Extensible by other plugins using hooks
|
||||||
|
- Dashboard extension hook for third-party Grafana dashboards
|
||||||
- Settings page under Settings > Metrics
|
- Settings page under Settings > Metrics
|
||||||
- Bearer token authentication
|
- Bearer token authentication
|
||||||
- License management integration
|
- License management integration
|
||||||
@@ -92,6 +98,21 @@ scrape_configs:
|
|||||||
|
|
||||||
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact.
|
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact.
|
||||||
|
|
||||||
|
#### Enabling Database Query Timing
|
||||||
|
|
||||||
|
The `wordpress_db_query_duration_seconds` histogram requires WordPress's `SAVEQUERIES` constant to be enabled. Add this to your `wp-config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define( 'SAVEQUERIES', true );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important considerations:**
|
||||||
|
|
||||||
|
- `SAVEQUERIES` has a performance overhead as it logs all queries with timing and call stacks
|
||||||
|
- Recommended for development/staging environments, use with caution in production
|
||||||
|
- Without `SAVEQUERIES`, only query counts (`wordpress_db_queries_total`) are available
|
||||||
|
- The histogram shows total query time per request, grouped by endpoint
|
||||||
|
|
||||||
### Cron Metrics (v0.2.0+)
|
### Cron Metrics (v0.2.0+)
|
||||||
|
|
||||||
| Metric | Type | Labels | Description |
|
| Metric | Type | Labels | Description |
|
||||||
@@ -154,8 +175,93 @@ $histogram = $collector->register_histogram( $name, $help, $labels, $buckets );
|
|||||||
$histogram->observe( $value, $labelValues );
|
$histogram->observe( $value, $labelValues );
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Extending with Custom Dashboards (v0.4.6+)
|
||||||
|
|
||||||
|
Add your own Grafana dashboard templates using the `wp_prometheus_register_dashboards` action:
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_action( 'wp_prometheus_register_dashboards', function( $provider ) {
|
||||||
|
// File-based dashboard
|
||||||
|
$provider->register_dashboard( 'my-plugin-dashboard', array(
|
||||||
|
'title' => __( 'My Plugin Metrics', 'my-plugin' ),
|
||||||
|
'description' => __( 'Dashboard for my custom metrics', 'my-plugin' ),
|
||||||
|
'icon' => 'dashicons-chart-bar',
|
||||||
|
'file' => MY_PLUGIN_PATH . 'assets/dashboards/my-dashboard.json',
|
||||||
|
'plugin' => 'My Plugin Name',
|
||||||
|
) );
|
||||||
|
|
||||||
|
// OR inline JSON dashboard
|
||||||
|
$provider->register_dashboard( 'dynamic-dashboard', array(
|
||||||
|
'title' => __( 'Dynamic Dashboard', 'my-plugin' ),
|
||||||
|
'description' => __( 'Dynamically generated dashboard', 'my-plugin' ),
|
||||||
|
'icon' => 'dashicons-admin-generic',
|
||||||
|
'json' => json_encode( $dashboard_array ),
|
||||||
|
'plugin' => 'My Plugin Name',
|
||||||
|
) );
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registration Parameters
|
||||||
|
|
||||||
|
| Parameter | Required | Description |
|
||||||
|
| --------- | -------- | ----------- |
|
||||||
|
| `title` | Yes | Dashboard title displayed in admin |
|
||||||
|
| `description` | No | Description shown below the title |
|
||||||
|
| `icon` | No | Dashicon class (default: `dashicons-chart-line`) |
|
||||||
|
| `file` | Yes* | Absolute path to JSON file |
|
||||||
|
| `json` | Yes* | Inline JSON content |
|
||||||
|
| `plugin` | No | Plugin name for attribution |
|
||||||
|
|
||||||
|
*Either `file` or `json` is required, but not both.
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
|
||||||
|
- File paths must be absolute and within `wp-content/`
|
||||||
|
- Inline JSON is validated during registration
|
||||||
|
- Third-party dashboards are marked with an "Extension" badge in the admin UI
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The plugin includes a comprehensive PHPUnit test suite with 189 tests covering all core classes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies (including dev)
|
||||||
|
composer install
|
||||||
|
|
||||||
|
# Run the full test suite
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test Architecture
|
||||||
|
|
||||||
|
Tests use [php-mock/php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit) to mock WordPress functions called from namespaced plugin code, and a `GlobalFunctionState` helper for controlling global-scope function stubs (authentication, options, conditionals).
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── bootstrap.php # WP constants + function stubs
|
||||||
|
├── Helpers/
|
||||||
|
│ └── GlobalFunctionState.php # Controllable state for stubs
|
||||||
|
└── Unit/
|
||||||
|
├── TestCase.php # Base class with PHPMock trait
|
||||||
|
├── AuthenticationTest.php # Bearer/query auth, header extraction
|
||||||
|
├── InstallerTest.php # Activate, deactivate, uninstall
|
||||||
|
├── Metrics/
|
||||||
|
│ ├── CustomMetricBuilderTest.php # Validation, CRUD, import/export
|
||||||
|
│ ├── RuntimeCollectorTest.php # Endpoint normalization, histograms
|
||||||
|
│ ├── StorageFactoryTest.php # Adapter config, env vars, connections
|
||||||
|
│ └── CollectorTest.php # Registry, register_*, render
|
||||||
|
├── Admin/
|
||||||
|
│ └── DashboardProviderTest.php # Registration, path security
|
||||||
|
└── Endpoint/
|
||||||
|
└── MetricsEndpointTest.php # Rewrite rules, request routing
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CI/CD Integration
|
||||||
|
|
||||||
|
Tests run automatically in the Gitea CI/CD pipeline before release builds. A failing test suite blocks the release.
|
||||||
|
|
||||||
### Build for Release
|
### Build for Release
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -9,6 +9,61 @@
|
|||||||
margin-top: 20px;
|
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 */
|
/* License status box */
|
||||||
.wp-prometheus-license-status {
|
.wp-prometheus-license-status {
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
@@ -45,3 +100,189 @@
|
|||||||
.wp-prometheus-tab-content .form-table {
|
.wp-prometheus-tab-content .form-table {
|
||||||
margin-top: 10px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Third-party dashboard card styling */
|
||||||
|
.wp-prometheus-dashboard-card.third-party {
|
||||||
|
position: relative;
|
||||||
|
border-color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-prometheus-dashboard-card .dashboard-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
background: #2271b1;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-prometheus-dashboard-card .dashboard-plugin {
|
||||||
|
color: #646970;
|
||||||
|
margin: -5px 0 15px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import options panel */
|
||||||
|
#import-options {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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": ""
|
||||||
|
}
|
||||||
1076
assets/dashboards/wordpress-runtime.json
Normal file
1076
assets/dashboards/wordpress-runtime.json
Normal file
File diff suppressed because it is too large
Load Diff
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($) {
|
(function($) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var importFileContent = null;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(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.
|
// Validate license button.
|
||||||
$('#wp-prometheus-validate-license').on('click', function(e) {
|
$('#wp-prometheus-validate-license').on('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -23,78 +45,664 @@
|
|||||||
// Regenerate token button.
|
// Regenerate token button.
|
||||||
$('#wp-prometheus-regenerate-token').on('click', function(e) {
|
$('#wp-prometheus-regenerate-token').on('click', function(e) {
|
||||||
e.preventDefault();
|
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);
|
var newToken = generateToken(32);
|
||||||
$('#wp_prometheus_auth_token').val(newToken);
|
$('#wp_prometheus_auth_token').val(newToken);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a license action via AJAX.
|
* Initialize custom metrics tab handlers.
|
||||||
*
|
*/
|
||||||
* @param {string} action AJAX action name.
|
function initCustomMetricsHandlers() {
|
||||||
* @param {string} message Loading message.
|
var $formContainer = $('#wp-prometheus-metric-form-container');
|
||||||
*/
|
var $form = $('#wp-prometheus-metric-form');
|
||||||
function performLicenseAction(action, message) {
|
var $showFormBtn = $('#show-metric-form');
|
||||||
var $spinner = $('#wp-prometheus-license-spinner');
|
|
||||||
var $message = $('#wp-prometheus-license-message');
|
|
||||||
|
|
||||||
$spinner.addClass('is-active');
|
// Show metric form.
|
||||||
$message.hide();
|
$showFormBtn.on('click', function() {
|
||||||
|
resetMetricForm();
|
||||||
|
$formContainer.slideDown();
|
||||||
|
$(this).hide();
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
// Cancel metric form.
|
||||||
url: wpPrometheus.ajaxUrl,
|
$('#cancel-metric-form').on('click', function() {
|
||||||
type: 'POST',
|
$formContainer.slideUp();
|
||||||
data: {
|
$showFormBtn.show();
|
||||||
action: action,
|
// Remove edit parameter from URL.
|
||||||
nonce: wpPrometheus.nonce
|
if (window.location.search.indexOf('edit=') > -1) {
|
||||||
},
|
window.history.pushState({}, '', window.location.pathname + '?page=wp-prometheus&tab=custom');
|
||||||
success: function(response) {
|
}
|
||||||
$spinner.removeClass('is-active');
|
});
|
||||||
|
|
||||||
if (response.success) {
|
// Value type toggle.
|
||||||
$message
|
$('input[name="value_type"]').on('change', function() {
|
||||||
.removeClass('notice-error')
|
var valueType = $(this).val();
|
||||||
.addClass('notice notice-success')
|
if (valueType === 'option') {
|
||||||
.html('<p>' + response.data.message + '</p>')
|
$('#option-config-row').show();
|
||||||
.show();
|
$('#static-values-row').hide();
|
||||||
|
} else {
|
||||||
|
$('#option-config-row').hide();
|
||||||
|
$('#static-values-row').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Reload page after successful validation/activation.
|
// 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) {
|
||||||
|
showNotice($message, response.data.message, 'success');
|
||||||
|
|
||||||
|
// Reload page after successful validation/activation.
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
showNotice($message, response.data.message || 'An error occurred.', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
showNotice($message, 'Connection error. Please try again.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
showNotice($message, response.data.message, 'success');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = window.location.pathname + '?page=wp-prometheus&tab=custom';
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showNotice($message, response.data.message || 'An error occurred.', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
showNotice($message, 'Connection error. Please try again.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
showNotice($message, response.data.message, 'success');
|
||||||
|
|
||||||
|
$('#import-options').slideUp();
|
||||||
|
$('#import-metrics-file').val('');
|
||||||
|
importFileContent = null;
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
showNotice($message, response.data.message || 'An error occurred.', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
showNotice($message, 'Connection error. Please try again.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
showNotice($message, response.data.message, 'success');
|
||||||
|
} else {
|
||||||
|
showNotice($message, response.data.message || 'An error occurred.', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
showNotice($message, 'Connection error. Please try again.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 using safe DOM construction.
|
||||||
|
for (var i = 0; i < labelCount; i++) {
|
||||||
|
var val = currentValues[i] || '';
|
||||||
|
var $input = $('<input>', {
|
||||||
|
type: 'text',
|
||||||
|
name: 'label_values[' + rowIndex + '][]',
|
||||||
|
'class': 'small-text',
|
||||||
|
placeholder: labels[i] || 'value',
|
||||||
|
value: val
|
||||||
|
});
|
||||||
|
$row.prepend($input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add value input using safe DOM construction.
|
||||||
|
var metricVal = currentValues[currentValues.length - 1] || '';
|
||||||
|
var $valueInput = $('<input>', {
|
||||||
|
type: 'number',
|
||||||
|
name: 'label_values[' + rowIndex + '][]',
|
||||||
|
'class': 'small-text',
|
||||||
|
step: 'any',
|
||||||
|
placeholder: 'Value',
|
||||||
|
value: metricVal
|
||||||
|
});
|
||||||
|
$row.find('.remove-value-row').before($valueInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 cryptographically secure random token.
|
||||||
|
*
|
||||||
|
* @param {number} length Token length.
|
||||||
|
* @return {string} Generated token.
|
||||||
|
*/
|
||||||
|
function generateToken(length) {
|
||||||
|
var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
var token = '';
|
||||||
|
var randomValues = new Uint32Array(length);
|
||||||
|
window.crypto.getRandomValues(randomValues);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
token += charset.charAt(randomValues[i] % charset.length);
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a notice message safely (XSS-safe).
|
||||||
|
*
|
||||||
|
* @param {jQuery} $element The message container element.
|
||||||
|
* @param {string} message The message text.
|
||||||
|
* @param {string} type Notice type: 'success', 'error', or 'warning'.
|
||||||
|
*/
|
||||||
|
function showNotice($element, message, type) {
|
||||||
|
var removeClasses = 'notice-error notice-success notice-warning';
|
||||||
|
$element
|
||||||
|
.removeClass(removeClasses)
|
||||||
|
.addClass('notice notice-' + type)
|
||||||
|
.empty()
|
||||||
|
.append($('<p>').text(message))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 type = response.data.warning ? 'warning' : 'success';
|
||||||
|
showNotice($message, response.data.message, type);
|
||||||
|
|
||||||
|
if (!response.data.warning) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
location.reload();
|
location.reload();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
|
||||||
$message
|
|
||||||
.removeClass('notice-success')
|
|
||||||
.addClass('notice notice-error')
|
|
||||||
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
error: function() {
|
showNotice($message, response.data.message || 'An error occurred.', 'error');
|
||||||
$spinner.removeClass('is-active');
|
|
||||||
$message
|
|
||||||
.removeClass('notice-success')
|
|
||||||
.addClass('notice notice-error')
|
|
||||||
.html('<p>Connection error. Please try again.</p>')
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
/**
|
showNotice($message, 'Connection error. Please try again.', 'error');
|
||||||
* 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;
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
showNotice($message, response.data.message, 'success');
|
||||||
|
} else {
|
||||||
|
showNotice($message, response.data.message || 'Connection test failed.', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
showNotice($message, 'Connection error. Please try again.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"promphp/prometheus_client_php": "^2.10"
|
"promphp/prometheus_client_php": "^2.10"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"php-mock/php-mock-phpunit": "^2.10",
|
||||||
"phpunit/phpunit": "^10.0",
|
"phpunit/phpunit": "^10.0",
|
||||||
"squizlabs/php_codesniffer": "^3.7",
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
"wp-coding-standards/wpcs": "^3.0",
|
"wp-coding-standards/wpcs": "^3.0",
|
||||||
|
|||||||
209
composer.lock
generated
209
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "3cacd9c609c3c49fb4600d09a38c04be",
|
"content-hash": "cfd3853b3cf76d82f972f3326e4f94d3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "magdev/wc-licensed-product-client",
|
"name": "magdev/wc-licensed-product-client",
|
||||||
@@ -1128,6 +1128,213 @@
|
|||||||
},
|
},
|
||||||
"time": "2022-02-21T01:04:05+00:00"
|
"time": "2022-02-21T01:04:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "php-mock/php-mock",
|
||||||
|
"version": "2.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-mock/php-mock.git",
|
||||||
|
"reference": "b59734f19765296bb0311942850d02288a224890"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-mock/php-mock/zipball/b59734f19765296bb0311942850d02288a224890",
|
||||||
|
"reference": "b59734f19765296bb0311942850d02288a224890",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^5.6 || ^7.0 || ^8.0",
|
||||||
|
"phpunit/php-text-template": "^1 || ^2 || ^3 || ^4 || ^5 || ^6"
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"malkusch/php-mock": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.8"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"autoload.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"phpmock\\": [
|
||||||
|
"classes/",
|
||||||
|
"tests/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"WTFPL"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Markus Malkusch",
|
||||||
|
"email": "markus@malkusch.de",
|
||||||
|
"homepage": "http://markus.malkusch.de",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.",
|
||||||
|
"homepage": "https://github.com/php-mock/php-mock",
|
||||||
|
"keywords": [
|
||||||
|
"BDD",
|
||||||
|
"TDD",
|
||||||
|
"function",
|
||||||
|
"mock",
|
||||||
|
"stub",
|
||||||
|
"test",
|
||||||
|
"test double",
|
||||||
|
"testing"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-mock/php-mock/issues",
|
||||||
|
"source": "https://github.com/php-mock/php-mock/tree/2.7.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/michalbundyra",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-02-06T07:39:37+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "php-mock/php-mock-integration",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-mock/php-mock-integration.git",
|
||||||
|
"reference": "cbbf39705ec13dece5b04133cef4e2fd3137a345"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/cbbf39705ec13dece5b04133cef4e2fd3137a345",
|
||||||
|
"reference": "cbbf39705ec13dece5b04133cef4e2fd3137a345",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.6",
|
||||||
|
"php-mock/php-mock": "^2.5",
|
||||||
|
"phpunit/php-text-template": "^1 || ^2 || ^3 || ^4 || ^5 || ^6"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || ^13"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"phpmock\\integration\\": "classes/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"WTFPL"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Markus Malkusch",
|
||||||
|
"email": "markus@malkusch.de",
|
||||||
|
"homepage": "http://markus.malkusch.de",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Integration package for PHP-Mock",
|
||||||
|
"homepage": "https://github.com/php-mock/php-mock-integration",
|
||||||
|
"keywords": [
|
||||||
|
"BDD",
|
||||||
|
"TDD",
|
||||||
|
"function",
|
||||||
|
"mock",
|
||||||
|
"stub",
|
||||||
|
"test",
|
||||||
|
"test double"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-mock/php-mock-integration/issues",
|
||||||
|
"source": "https://github.com/php-mock/php-mock-integration/tree/3.1.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/michalbundyra",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-02-06T07:44:43+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "php-mock/php-mock-phpunit",
|
||||||
|
"version": "2.15.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-mock/php-mock-phpunit.git",
|
||||||
|
"reference": "701df15b183f25af663af134eb71353cd838b955"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/701df15b183f25af663af134eb71353cd838b955",
|
||||||
|
"reference": "701df15b183f25af663af134eb71353cd838b955",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7",
|
||||||
|
"php-mock/php-mock-integration": "^3.0",
|
||||||
|
"phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10.0.17 || ^11 || ^12.0.9 || ^13"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.3.6"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"autoload.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"phpmock\\phpunit\\": "classes/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"WTFPL"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Markus Malkusch",
|
||||||
|
"email": "markus@malkusch.de",
|
||||||
|
"homepage": "http://markus.malkusch.de",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Mock built-in PHP functions (e.g. time()) with PHPUnit. This package relies on PHP's namespace fallback policy. No further extension is needed.",
|
||||||
|
"homepage": "https://github.com/php-mock/php-mock-phpunit",
|
||||||
|
"keywords": [
|
||||||
|
"BDD",
|
||||||
|
"TDD",
|
||||||
|
"function",
|
||||||
|
"mock",
|
||||||
|
"phpunit",
|
||||||
|
"stub",
|
||||||
|
"test",
|
||||||
|
"test double",
|
||||||
|
"testing"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-mock/php-mock-phpunit/issues",
|
||||||
|
"source": "https://github.com/php-mock/php-mock-phpunit/tree/2.15.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/michalbundyra",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-02-06T09:12:10+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpcompatibility/php-compatibility",
|
"name": "phpcompatibility/php-compatibility",
|
||||||
"version": "9.3.5",
|
"version": "9.3.5",
|
||||||
|
|||||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
|||||||
# This file is distributed under the GPL v2 or later.
|
# This file is distributed under the GPL v2 or later.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WP Prometheus 0.2.0\n"
|
"Project-Id-Version: WP Prometheus 0.4.2\n"
|
||||||
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
|
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
|
||||||
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
|
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
|
||||||
"PO-Revision-Date: 2026-02-02T00:00:00+00:00\n"
|
"PO-Revision-Date: 2026-02-02T00:00:00+00:00\n"
|
||||||
@@ -31,17 +31,25 @@ msgstr "Lizenz"
|
|||||||
msgid "Help"
|
msgid "Help"
|
||||||
msgstr "Hilfe"
|
msgstr "Hilfe"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Custom Metrics"
|
||||||
|
msgstr "Eigene Metriken"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Dashboards"
|
||||||
|
msgstr "Dashboards"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License settings saved."
|
msgid "License settings saved."
|
||||||
msgstr "Lizenz-Einstellungen gespeichert."
|
msgstr "Lizenz-Einstellungen gespeichert."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License is active and valid."
|
msgid "License is active and valid."
|
||||||
msgstr "Lizenz ist aktiv und gueltig."
|
msgstr "Lizenz ist aktiv und gültig."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License is invalid."
|
msgid "License is invalid."
|
||||||
msgstr "Lizenz ist ungueltig."
|
msgstr "Lizenz ist ungültig."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License has expired."
|
msgid "License has expired."
|
||||||
@@ -70,12 +78,12 @@ msgstr "Unbekannter Status."
|
|||||||
#. translators: %s: Expiration date
|
#. translators: %s: Expiration date
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Expires: %s"
|
msgid "Expires: %s"
|
||||||
msgstr "Laeuft ab: %s"
|
msgstr "Läuft ab: %s"
|
||||||
|
|
||||||
#. translators: %s: Time ago
|
#. translators: %s: Time ago
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Last checked: %s ago"
|
msgid "Last checked: %s ago"
|
||||||
msgstr "Zuletzt geprueft: vor %s"
|
msgstr "Zuletzt geprüft: vor %s"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License Server URL"
|
msgid "License Server URL"
|
||||||
@@ -83,7 +91,7 @@ msgstr "Lizenz-Server URL"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License Key"
|
msgid "License Key"
|
||||||
msgstr "Lizenzschluessel"
|
msgstr "Lizenzschlüssel"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Server Secret"
|
msgid "Server Secret"
|
||||||
@@ -93,6 +101,18 @@ msgstr "Server-Geheimnis"
|
|||||||
msgid "Leave empty to keep existing."
|
msgid "Leave empty to keep existing."
|
||||||
msgstr "Leer lassen, um bestehenden Wert zu behalten."
|
msgstr "Leer lassen, um bestehenden Wert zu behalten."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overridden by WP_PROMETHEUS_LICENSE_SERVER_URL environment variable."
|
||||||
|
msgstr "Überschrieben durch die Umgebungsvariable WP_PROMETHEUS_LICENSE_SERVER_URL."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overridden by WP_PROMETHEUS_LICENSE_KEY environment variable."
|
||||||
|
msgstr "Überschrieben durch die Umgebungsvariable WP_PROMETHEUS_LICENSE_KEY."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overridden by WP_PROMETHEUS_LICENSE_SERVER_SECRET environment variable."
|
||||||
|
msgstr "Überschrieben durch die Umgebungsvariable WP_PROMETHEUS_LICENSE_SERVER_SECRET."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Save License Settings"
|
msgid "Save License Settings"
|
||||||
msgstr "Lizenz-Einstellungen speichern"
|
msgstr "Lizenz-Einstellungen speichern"
|
||||||
@@ -115,11 +135,11 @@ msgstr "Aktivierte Metriken"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Configure authentication for the /metrics endpoint."
|
msgid "Configure authentication for the /metrics endpoint."
|
||||||
msgstr "Authentifizierung fuer den /metrics-Endpunkt konfigurieren."
|
msgstr "Authentifizierung für den /metrics-Endpunkt konfigurieren."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Select which metrics to expose on the /metrics endpoint."
|
msgid "Select which metrics to expose on the /metrics endpoint."
|
||||||
msgstr "Waehlen Sie, welche Metriken auf dem /metrics-Endpunkt bereitgestellt werden sollen."
|
msgstr "Wählen Sie, welche Metriken auf dem /metrics-Endpunkt bereitgestellt werden sollen."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Auth Token"
|
msgid "Auth Token"
|
||||||
@@ -127,7 +147,7 @@ msgstr "Auth-Token"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Select Metrics"
|
msgid "Select Metrics"
|
||||||
msgstr "Metriken auswaehlen"
|
msgstr "Metriken auswählen"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Regenerate"
|
msgid "Regenerate"
|
||||||
@@ -151,7 +171,7 @@ msgstr "Benutzer nach Rolle"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Total Posts by Type and Status"
|
msgid "Total Posts by Type and Status"
|
||||||
msgstr "Beitraege nach Typ und Status"
|
msgstr "Beiträge nach Typ und Status"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Total Comments by Status"
|
msgid "Total Comments by Status"
|
||||||
@@ -163,7 +183,7 @@ msgstr "Plugins (aktiv/inaktiv)"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Cron Events (scheduled tasks, overdue, next run)"
|
msgid "Cron Events (scheduled tasks, overdue, next run)"
|
||||||
msgstr "Cron-Ereignisse (geplante Aufgaben, ueberfaellig, naechste Ausfuehrung)"
|
msgstr "Cron-Ereignisse (geplante Aufgaben, überfällig, nächste Ausführung)"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Transients (total, expiring, expired)"
|
msgid "Transients (total, expiring, expired)"
|
||||||
@@ -175,7 +195,7 @@ msgstr "Laufzeit-Metriken"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Runtime metrics track data across requests. Enable only what you need to minimize performance impact."
|
msgid "Runtime metrics track data across requests. Enable only what you need to minimize performance impact."
|
||||||
msgstr "Laufzeit-Metriken erfassen Daten ueber Anfragen hinweg. Aktivieren Sie nur, was Sie benoetigen, um Auswirkungen auf die Leistung zu minimieren."
|
msgstr "Laufzeit-Metriken erfassen Daten über Anfragen hinweg. Aktivieren Sie nur, was Sie benötigen, um Auswirkungen auf die Leistung zu minimieren."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "HTTP Requests Total (by method, status, endpoint)"
|
msgid "HTTP Requests Total (by method, status, endpoint)"
|
||||||
@@ -195,7 +215,7 @@ msgstr "WooCommerce-Metriken"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Metrics specific to WooCommerce stores. Only available when WooCommerce is active."
|
msgid "Metrics specific to WooCommerce stores. Only available when WooCommerce is active."
|
||||||
msgstr "Metriken speziell fuer WooCommerce-Shops. Nur verfuegbar, wenn WooCommerce aktiv ist."
|
msgstr "Metriken speziell für WooCommerce-Shops. Nur verfügbar, wenn WooCommerce aktiv ist."
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "WooCommerce Products (by status and type)"
|
msgid "WooCommerce Products (by status and type)"
|
||||||
@@ -213,13 +233,25 @@ msgstr "WooCommerce-Umsatz (gesamt, heute, Monat)"
|
|||||||
msgid "WooCommerce Customers (registered, guest)"
|
msgid "WooCommerce Customers (registered, guest)"
|
||||||
msgstr "WooCommerce-Kunden (registriert, Gast)"
|
msgstr "WooCommerce-Kunden (registriert, Gast)"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Reset Runtime Metrics"
|
||||||
|
msgstr "Laufzeit-Metriken zurücksetzen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Clear all accumulated runtime metric data."
|
||||||
|
msgstr "Alle gesammelten Laufzeit-Metrikdaten löschen."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Reset Metrics"
|
||||||
|
msgstr "Metriken zurücksetzen"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Prometheus Configuration"
|
msgid "Prometheus Configuration"
|
||||||
msgstr "Prometheus-Konfiguration"
|
msgstr "Prometheus-Konfiguration"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Add the following to your prometheus.yml:"
|
msgid "Add the following to your prometheus.yml:"
|
||||||
msgstr "Fuegen Sie Folgendes zu Ihrer prometheus.yml hinzu:"
|
msgstr "Fügen Sie Folgendes zu Ihrer prometheus.yml hinzu:"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Endpoint Information"
|
msgid "Endpoint Information"
|
||||||
@@ -235,11 +267,11 @@ msgstr "Endpunkt testen"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "You can test the endpoint using curl:"
|
msgid "You can test the endpoint using curl:"
|
||||||
msgstr "Sie koennen den Endpunkt mit curl testen:"
|
msgstr "Sie können den Endpunkt mit curl testen:"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Available Metrics"
|
msgid "Available Metrics"
|
||||||
msgstr "Verfuegbare Metriken"
|
msgstr "Verfügbare Metriken"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Metric"
|
msgid "Metric"
|
||||||
@@ -275,7 +307,7 @@ msgstr "Benutzer gesamt nach Rolle"
|
|||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Total posts by type and status"
|
msgid "Total posts by type and status"
|
||||||
msgstr "Beitraege gesamt nach Typ und Status"
|
msgstr "Beiträge gesamt nach Typ und Status"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Total comments by status"
|
msgid "Total comments by status"
|
||||||
@@ -297,17 +329,21 @@ msgstr "HTTP-Anfragedauer-Verteilung"
|
|||||||
msgid "Database queries by endpoint"
|
msgid "Database queries by endpoint"
|
||||||
msgstr "Datenbank-Abfragen nach Endpunkt"
|
msgstr "Datenbank-Abfragen nach Endpunkt"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Database query duration distribution (requires SAVEQUERIES)"
|
||||||
|
msgstr "Datenbank-Abfragedauer-Verteilung (erfordert SAVEQUERIES)"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Scheduled cron events by hook"
|
msgid "Scheduled cron events by hook"
|
||||||
msgstr "Geplante Cron-Ereignisse nach Hook"
|
msgstr "Geplante Cron-Ereignisse nach Hook"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Number of overdue cron events"
|
msgid "Number of overdue cron events"
|
||||||
msgstr "Anzahl ueberfaelliger Cron-Ereignisse"
|
msgstr "Anzahl überfälliger Cron-Ereignisse"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Unix timestamp of next scheduled cron"
|
msgid "Unix timestamp of next scheduled cron"
|
||||||
msgstr "Unix-Zeitstempel des naechsten geplanten Crons"
|
msgstr "Unix-Zeitstempel des nächsten geplanten Crons"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Total transients by type"
|
msgid "Total transients by type"
|
||||||
@@ -330,12 +366,250 @@ msgid "WooCommerce customers by type"
|
|||||||
msgstr "WooCommerce-Kunden nach Typ"
|
msgstr "WooCommerce-Kunden nach Typ"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Custom Metrics"
|
msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:"
|
||||||
msgstr "Benutzerdefinierte Metriken"
|
msgstr "Sie können benutzerdefinierte Metriken mit der wp_prometheus_collect_metrics-Aktion hinzufügen:"
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:"
|
msgid "Add Custom Metric"
|
||||||
msgstr "Sie koennen benutzerdefinierte Metriken mit der wp_prometheus_collect_metrics-Aktion hinzufuegen:"
|
msgstr "Eigene Metrik hinzufügen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Edit Custom Metric"
|
||||||
|
msgstr "Eigene Metrik bearbeiten"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Metric Name"
|
||||||
|
msgstr "Metrik-Name"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Must follow Prometheus naming conventions."
|
||||||
|
msgstr "Muss den Prometheus-Namenskonventionen entsprechen."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Help Text"
|
||||||
|
msgstr "Hilfetext"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Description shown in Prometheus output."
|
||||||
|
msgstr "Beschreibung in der Prometheus-Ausgabe."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Value Type"
|
||||||
|
msgstr "Werttyp"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Static Value"
|
||||||
|
msgstr "Statischer Wert"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "WordPress Option"
|
||||||
|
msgstr "WordPress-Option"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Static Value:"
|
||||||
|
msgstr "Statischer Wert:"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Option Name:"
|
||||||
|
msgstr "Optionsname:"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "The name of the WordPress option to read."
|
||||||
|
msgstr "Der Name der zu lesenden WordPress-Option."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Labels"
|
||||||
|
msgstr "Labels"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Label name"
|
||||||
|
msgstr "Label-Name"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Add Label"
|
||||||
|
msgstr "Label hinzufügen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Label Values"
|
||||||
|
msgstr "Label-Werte"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Value"
|
||||||
|
msgstr "Wert"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Add Value Row"
|
||||||
|
msgstr "Wertezeile hinzufügen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Aktiviert"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Save Metric"
|
||||||
|
msgstr "Metrik speichern"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Your Custom Metrics"
|
||||||
|
msgstr "Ihre eigenen Metriken"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr "Aktionen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Active"
|
||||||
|
msgstr "Aktiv"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Inactive"
|
||||||
|
msgstr "Inaktiv"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr "Bearbeiten"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Löschen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "No custom metrics defined yet."
|
||||||
|
msgstr "Noch keine eigenen Metriken definiert."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Export / Import"
|
||||||
|
msgstr "Export / Import"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Export your custom metrics configuration for backup or transfer to another site."
|
||||||
|
msgstr "Exportieren Sie Ihre Metriken-Konfiguration zur Sicherung oder Übertragung auf eine andere Website."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Export Metrics"
|
||||||
|
msgstr "Metriken exportieren"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import Metrics"
|
||||||
|
msgstr "Metriken importieren"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import Options"
|
||||||
|
msgstr "Import-Optionen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Skip existing metrics"
|
||||||
|
msgstr "Bestehende Metriken überspringen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overwrite existing metrics"
|
||||||
|
msgstr "Bestehende Metriken überschreiben"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Rename duplicates"
|
||||||
|
msgstr "Duplikate umbenennen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import"
|
||||||
|
msgstr "Importieren"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Grafana Dashboard Templates"
|
||||||
|
msgstr "Grafana Dashboard-Vorlagen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download pre-built Grafana dashboards for visualizing your WordPress metrics."
|
||||||
|
msgstr "Laden Sie vorgefertigte Grafana-Dashboards zur Visualisierung Ihrer WordPress-Metriken herunter."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Herunterladen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import instructions:"
|
||||||
|
msgstr "Import-Anleitung:"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download the desired dashboard JSON file"
|
||||||
|
msgstr "Laden Sie die gewünschte Dashboard-JSON-Datei herunter"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "In Grafana, go to Dashboards > Import"
|
||||||
|
msgstr "Gehen Sie in Grafana zu Dashboards > Import"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Upload the JSON file or paste its contents"
|
||||||
|
msgstr "Laden Sie die JSON-Datei hoch oder fügen Sie deren Inhalt ein"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Select your Prometheus data source"
|
||||||
|
msgstr "Wählen Sie Ihre Prometheus-Datenquelle"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Click Import"
|
||||||
|
msgstr "Klicken Sie auf Import"
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Metric name is required."
|
||||||
|
msgstr "Metrik-Name ist erforderlich."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid metric name format."
|
||||||
|
msgstr "Ungültiges Metrik-Namensformat."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "A metric with this name already exists."
|
||||||
|
msgstr "Eine Metrik mit diesem Namen existiert bereits."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Help text is required."
|
||||||
|
msgstr "Hilfetext ist erforderlich."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid value type."
|
||||||
|
msgstr "Ungültiger Werttyp."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Static value must be numeric."
|
||||||
|
msgstr "Statischer Wert muss numerisch sein."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Option name is required for option value type."
|
||||||
|
msgstr "Optionsname ist für den Options-Werttyp erforderlich."
|
||||||
|
|
||||||
|
#. translators: %d: Maximum number of labels
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Maximum %d labels allowed."
|
||||||
|
msgstr "Maximal %d Labels erlaubt."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid label name format."
|
||||||
|
msgstr "Ungültiges Label-Namensformat."
|
||||||
|
|
||||||
|
#. translators: %d: Maximum number of label value combinations
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Maximum %d label value combinations allowed."
|
||||||
|
msgstr "Maximal %d Label-Wert-Kombinationen erlaubt."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid JSON format."
|
||||||
|
msgstr "Ungültiges JSON-Format."
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid export format."
|
||||||
|
msgstr "Ungültiges Export-Format."
|
||||||
|
|
||||||
#: src/Plugin.php
|
#: src/Plugin.php
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
@@ -344,18 +618,406 @@ msgstr "Einstellungen"
|
|||||||
#. translators: 1: Required PHP version, 2: Current PHP version
|
#. translators: 1: Required PHP version, 2: Current PHP version
|
||||||
#: wp-prometheus.php
|
#: wp-prometheus.php
|
||||||
msgid "WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s."
|
msgid "WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s."
|
||||||
msgstr "WP Prometheus erfordert PHP-Version %1$s oder hoeher. Sie verwenden PHP %2$s."
|
msgstr "WP Prometheus erfordert PHP-Version %1$s oder höher. Sie verwenden PHP %2$s."
|
||||||
|
|
||||||
#. translators: 1: Required WordPress version, 2: Current WordPress version
|
#. translators: 1: Required WordPress version, 2: Current WordPress version
|
||||||
#: wp-prometheus.php
|
#: wp-prometheus.php
|
||||||
msgid "WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s."
|
msgid "WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s."
|
||||||
msgstr "WP Prometheus erfordert WordPress-Version %1$s oder hoeher. Sie verwenden WordPress %2$s."
|
msgstr "WP Prometheus erfordert WordPress-Version %1$s oder höher. Sie verwenden WordPress %2$s."
|
||||||
|
|
||||||
#: wp-prometheus.php
|
#: wp-prometheus.php
|
||||||
msgid "WP Prometheus requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
|
msgid "WP Prometheus requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
|
||||||
msgstr "WP Prometheus erfordert installierte Composer-Abhaengigkeiten. Bitte fuehren Sie \"composer install\" im Plugin-Verzeichnis aus."
|
msgstr "WP Prometheus erfordert installierte Composer-Abhängigkeiten. Bitte führen Sie \"composer install\" im Plugin-Verzeichnis aus."
|
||||||
|
|
||||||
#. translators: %s: Required PHP version
|
#. translators: %s: Required PHP version
|
||||||
#: wp-prometheus.php
|
#: wp-prometheus.php
|
||||||
msgid "WP Prometheus requires PHP version %s or higher."
|
msgid "WP Prometheus requires PHP version %s or higher."
|
||||||
msgstr "WP Prometheus erfordert PHP-Version %s oder hoeher."
|
msgstr "WP Prometheus erfordert PHP-Version %s oder höher."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage"
|
||||||
|
msgstr "Speicher"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Metrics Storage Configuration"
|
||||||
|
msgstr "Metriken-Speicherkonfiguration"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Configure how Prometheus metrics are stored. Persistent storage (Redis, APCu) allows metrics to survive between requests and aggregate data over time."
|
||||||
|
msgstr "Konfigurieren Sie, wie Prometheus-Metriken gespeichert werden. Persistenter Speicher (Redis, APCu) ermöglicht es, Metriken zwischen Anfragen zu erhalten und Daten über Zeit zu aggregieren."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Environment Override Active"
|
||||||
|
msgstr "Umgebungsvariablen-Überschreibung aktiv"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage adapter is configured via environment variable. Admin settings will be ignored."
|
||||||
|
msgstr "Speicher-Adapter ist über Umgebungsvariable konfiguriert. Admin-Einstellungen werden ignoriert."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage Fallback Active"
|
||||||
|
msgstr "Speicher-Fallback aktiv"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Falling back to In-Memory storage."
|
||||||
|
msgstr "Fällt zurück auf In-Memory-Speicher."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Current Status:"
|
||||||
|
msgstr "Aktueller Status:"
|
||||||
|
|
||||||
|
#. translators: %s: Active adapter name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Using %s storage."
|
||||||
|
msgstr "Verwende %s-Speicher."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage Adapter"
|
||||||
|
msgstr "Speicher-Adapter"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "unavailable"
|
||||||
|
msgstr "nicht verfügbar"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Select the storage backend for metrics. Redis and APCu require their respective PHP extensions."
|
||||||
|
msgstr "Wählen Sie das Speicher-Backend für Metriken. Redis und APCu erfordern ihre jeweiligen PHP-Erweiterungen."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis Configuration"
|
||||||
|
msgstr "Redis-Konfiguration"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Host"
|
||||||
|
msgstr "Host"
|
||||||
|
|
||||||
|
#. translators: %s: Environment variable name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Can be overridden with %s environment variable."
|
||||||
|
msgstr "Kann mit Umgebungsvariable %s überschrieben werden."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Port"
|
||||||
|
msgstr "Port"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Passwort"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Leave empty if not required"
|
||||||
|
msgstr "Leer lassen, falls nicht erforderlich"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Database"
|
||||||
|
msgstr "Datenbank"
|
||||||
|
|
||||||
|
#. translators: %s: Environment variable name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis database index (0-15). Can be overridden with %s."
|
||||||
|
msgstr "Redis-Datenbankindex (0-15). Kann mit %s überschrieben werden."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Key Prefix"
|
||||||
|
msgstr "Schlüssel-Präfix"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Prefix for Redis keys. Useful when sharing Redis with other applications."
|
||||||
|
msgstr "Präfix für Redis-Schlüssel. Nützlich bei gemeinsamer Redis-Nutzung mit anderen Anwendungen."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "APCu Configuration"
|
||||||
|
msgstr "APCu-Konfiguration"
|
||||||
|
|
||||||
|
#. translators: %s: Environment variable name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Prefix for APCu keys. Can be overridden with %s."
|
||||||
|
msgstr "Präfix für APCu-Schlüssel. Kann mit %s überschrieben werden."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Save Storage Settings"
|
||||||
|
msgstr "Speicher-Einstellungen speichern"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Test Connection"
|
||||||
|
msgstr "Verbindung testen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Environment Variables"
|
||||||
|
msgstr "Umgebungsvariablen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "For Docker or containerized environments, you can configure storage using environment variables. These take precedence over admin settings."
|
||||||
|
msgstr "Für Docker- oder Container-Umgebungen können Sie den Speicher über Umgebungsvariablen konfigurieren. Diese haben Vorrang vor Admin-Einstellungen."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Variable"
|
||||||
|
msgstr "Variable"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Example"
|
||||||
|
msgstr "Beispiel"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage adapter to use"
|
||||||
|
msgstr "Zu verwendender Speicher-Adapter"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis server hostname"
|
||||||
|
msgstr "Redis-Server-Hostname"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis server port"
|
||||||
|
msgstr "Redis-Server-Port"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis authentication password"
|
||||||
|
msgstr "Redis-Authentifizierungspasswort"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis database index"
|
||||||
|
msgstr "Redis-Datenbankindex"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis key prefix"
|
||||||
|
msgstr "Redis-Schlüssel-Präfix"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "APCu key prefix"
|
||||||
|
msgstr "APCu-Schlüssel-Präfix"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "License server URL"
|
||||||
|
msgstr "Lizenz-Server-URL"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "License key"
|
||||||
|
msgstr "Lizenzschlüssel"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "License server shared secret"
|
||||||
|
msgstr "Gemeinsames Geheimnis des Lizenz-Servers"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Docker Compose Example"
|
||||||
|
msgstr "Docker Compose-Beispiel"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Permission denied."
|
||||||
|
msgstr "Zugriff verweigert."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage adapter is configured via environment variable and cannot be changed."
|
||||||
|
msgstr "Speicher-Adapter ist über Umgebungsvariable konfiguriert und kann nicht geändert werden."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Invalid storage adapter."
|
||||||
|
msgstr "Ungültiger Speicher-Adapter."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage settings saved successfully."
|
||||||
|
msgstr "Speicher-Einstellungen erfolgreich gespeichert."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage settings saved, but connection test failed:"
|
||||||
|
msgstr "Speicher-Einstellungen gespeichert, aber Verbindungstest fehlgeschlagen:"
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "In-Memory (default, no persistence)"
|
||||||
|
msgstr "In-Memory (Standard, keine Persistenz)"
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis (requires PHP Redis extension)"
|
||||||
|
msgstr "Redis (erfordert PHP-Redis-Erweiterung)"
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu (requires APCu extension)"
|
||||||
|
msgstr "APCu (erfordert APCu-Erweiterung)"
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "PHP Redis extension is not installed."
|
||||||
|
msgstr "PHP-Redis-Erweiterung ist nicht installiert."
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis connection failed: %s"
|
||||||
|
msgstr "Redis-Verbindung fehlgeschlagen: %s"
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis error: %s"
|
||||||
|
msgstr "Redis-Fehler: %s"
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Storage error: %s"
|
||||||
|
msgstr "Speicherfehler: %s"
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu extension is not installed."
|
||||||
|
msgstr "APCu-Erweiterung ist nicht installiert."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu is installed but not enabled."
|
||||||
|
msgstr "APCu ist installiert, aber nicht aktiviert."
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu error: %s"
|
||||||
|
msgstr "APCu-Fehler: %s"
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "In-Memory storage is always available."
|
||||||
|
msgstr "In-Memory-Speicher ist immer verfügbar."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Unknown storage adapter."
|
||||||
|
msgstr "Unbekannter Speicher-Adapter."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Could not connect to Redis server."
|
||||||
|
msgstr "Verbindung zum Redis-Server konnte nicht hergestellt werden."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis authentication failed."
|
||||||
|
msgstr "Redis-Authentifizierung fehlgeschlagen."
|
||||||
|
|
||||||
|
#. translators: %s: Redis host:port
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Successfully connected to Redis at %s."
|
||||||
|
msgstr "Erfolgreich mit Redis verbunden unter %s."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis ping failed."
|
||||||
|
msgstr "Redis-Ping fehlgeschlagen."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu is installed but not enabled. Check your php.ini settings."
|
||||||
|
msgstr "APCu ist installiert, aber nicht aktiviert. Prüfen Sie Ihre php.ini-Einstellungen."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu store operation failed."
|
||||||
|
msgstr "APCu-Speicheroperation fehlgeschlagen."
|
||||||
|
|
||||||
|
#. translators: %s: Memory info
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu is working. Memory: %s used."
|
||||||
|
msgstr "APCu funktioniert. Speicher: %s belegt."
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu fetch operation returned unexpected value."
|
||||||
|
msgstr "APCu-Abrufoperation hat unerwarteten Wert zurückgegeben."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early Mode"
|
||||||
|
msgstr "Frühzeitiger Modus"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode intercepts /metrics requests before full WordPress initialization. This prevents memory exhaustion issues caused by some plugins (e.g., Twig-based themes/plugins) but disables the wp_prometheus_collect_metrics hook for custom metrics."
|
||||||
|
msgstr "Der frühzeitige Modus fängt /metrics-Anfragen vor der vollständigen WordPress-Initialisierung ab. Dies verhindert Speichererschöpfungsprobleme, die durch einige Plugins verursacht werden (z.B. Twig-basierte Themes/Plugins), deaktiviert jedoch den wp_prometheus_collect_metrics-Hook für benutzerdefinierte Metriken."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is configured via WP_PROMETHEUS_DISABLE_EARLY_MODE environment variable. Admin settings will be ignored."
|
||||||
|
msgstr "Der frühzeitige Modus ist über die Umgebungsvariable WP_PROMETHEUS_DISABLE_EARLY_MODE konfiguriert. Admin-Einstellungen werden ignoriert."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Disable Early Mode"
|
||||||
|
msgstr "Frühzeitigen Modus deaktivieren"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Disable early metrics interception"
|
||||||
|
msgstr "Frühzeitige Metriken-Abfangung deaktivieren"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "When disabled, metrics are collected through normal WordPress template loading. This enables the wp_prometheus_collect_metrics hook for custom metrics but may cause issues with some plugins."
|
||||||
|
msgstr "Wenn deaktiviert, werden Metriken über das normale WordPress-Template-Laden erfasst. Dies aktiviert den wp_prometheus_collect_metrics-Hook für benutzerdefinierte Metriken, kann jedoch Probleme mit einigen Plugins verursachen."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is active (this request was served via early interception)"
|
||||||
|
msgstr "Frühzeitiger Modus ist aktiv (diese Anfrage wurde über frühzeitige Abfangung verarbeitet)"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is disabled"
|
||||||
|
msgstr "Frühzeitiger Modus ist deaktiviert"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is enabled (active for /metrics requests)"
|
||||||
|
msgstr "Frühzeitiger Modus ist aktiviert (aktiv für /metrics-Anfragen)"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh."
|
||||||
|
msgstr "Alle gesammelten Laufzeit-Metrikdaten löschen (HTTP-Anfragen, Datenbank-Abfragen). Dies ist nützlich zum Testen oder für einen Neuanfang."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Endpoint"
|
||||||
|
msgstr "Endpunkt"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Selection"
|
||||||
|
msgstr "Auswahl"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Runtime"
|
||||||
|
msgstr "Laufzeit"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Advanced"
|
||||||
|
msgstr "Erweitert"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Runtime Metrics Management"
|
||||||
|
msgstr "Laufzeit-Metriken Verwaltung"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Runtime metrics track HTTP requests and database queries across requests. Use this section to manage accumulated data."
|
||||||
|
msgstr "Laufzeit-Metriken erfassen HTTP-Anfragen und Datenbank-Abfragen über mehrere Anfragen hinweg. Verwenden Sie diesen Bereich zur Verwaltung der gesammelten Daten."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Reset Data"
|
||||||
|
msgstr "Daten zurücksetzen"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Extension"
|
||||||
|
msgstr "Erweiterung"
|
||||||
|
|
||||||
|
#. translators: %s: Plugin name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Provided by: %s"
|
||||||
|
msgstr "Bereitgestellt von: %s"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "No dashboards available."
|
||||||
|
msgstr "Keine Dashboards verfügbar."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Pre-built dashboards for visualizing your WordPress metrics in Grafana."
|
||||||
|
msgstr "Vorgefertigte Dashboards zur Visualisierung Ihrer WordPress-Metriken in Grafana."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Installation Instructions"
|
||||||
|
msgstr "Installationsanleitung"
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download the JSON file for your desired dashboard."
|
||||||
|
msgstr "Laden Sie die JSON-Datei für das gewünschte Dashboard herunter."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "In Grafana, go to Dashboards → Import."
|
||||||
|
msgstr "Gehen Sie in Grafana zu Dashboards → Import."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Upload the JSON file or paste its contents."
|
||||||
|
msgstr "Laden Sie die JSON-Datei hoch oder fügen Sie den Inhalt ein."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Select your Prometheus data source when prompted."
|
||||||
|
msgstr "Wählen Sie Ihre Prometheus-Datenquelle, wenn Sie dazu aufgefordert werden."
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Click Import to create the dashboard."
|
||||||
|
msgstr "Klicken Sie auf Import, um das Dashboard zu erstellen."
|
||||||
|
|
||||||
|
#. translators: %s: Metrics URL
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Make sure your Prometheus instance is configured to scrape %s with the correct authentication token."
|
||||||
|
msgstr "Stellen Sie sicher, dass Ihre Prometheus-Instanz so konfiguriert ist, dass sie %s mit dem richtigen Authentifizierungs-Token abruft."
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# This file is distributed under the GPL v2 or later.
|
# This file is distributed under the GPL v2 or later.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WP Prometheus 0.2.0\n"
|
"Project-Id-Version: WP Prometheus 0.4.2\n"
|
||||||
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
|
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
|
||||||
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
|
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
@@ -28,6 +28,14 @@ msgstr ""
|
|||||||
msgid "Help"
|
msgid "Help"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Custom Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Dashboards"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "License settings saved."
|
msgid "License settings saved."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -90,6 +98,18 @@ msgstr ""
|
|||||||
msgid "Leave empty to keep existing."
|
msgid "Leave empty to keep existing."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overridden by WP_PROMETHEUS_LICENSE_SERVER_URL environment variable."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overridden by WP_PROMETHEUS_LICENSE_KEY environment variable."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overridden by WP_PROMETHEUS_LICENSE_SERVER_SECRET environment variable."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Save License Settings"
|
msgid "Save License Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -210,6 +230,18 @@ msgstr ""
|
|||||||
msgid "WooCommerce Customers (registered, guest)"
|
msgid "WooCommerce Customers (registered, guest)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Reset Runtime Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Clear all accumulated runtime metric data."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Reset Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Prometheus Configuration"
|
msgid "Prometheus Configuration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -294,6 +326,10 @@ msgstr ""
|
|||||||
msgid "Database queries by endpoint"
|
msgid "Database queries by endpoint"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Database query duration distribution (requires SAVEQUERIES)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Scheduled cron events by hook"
|
msgid "Scheduled cron events by hook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -327,11 +363,249 @@ msgid "WooCommerce customers by type"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "Custom Metrics"
|
msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/Settings.php
|
#: src/Admin/Settings.php
|
||||||
msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:"
|
msgid "Add Custom Metric"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Edit Custom Metric"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Metric Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Must follow Prometheus naming conventions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Help Text"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Description shown in Prometheus output."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Value Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Static Value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "WordPress Option"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Static Value:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Option Name:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "The name of the WordPress option to read."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Labels"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Label name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Add Label"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Label Values"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Add Value Row"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Save Metric"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Your Custom Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Inactive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "No custom metrics defined yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Export / Import"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Export your custom metrics configuration for backup or transfer to another site."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Export Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import Metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Skip existing metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Overwrite existing metrics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Rename duplicates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Grafana Dashboard Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download pre-built Grafana dashboards for visualizing your WordPress metrics."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Import instructions:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download the desired dashboard JSON file"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "In Grafana, go to Dashboards > Import"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Upload the JSON file or paste its contents"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Select your Prometheus data source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Click Import"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Metric name is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid metric name format."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "A metric with this name already exists."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Help text is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid value type."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Static value must be numeric."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Option name is required for option value type."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %d: Maximum number of labels
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Maximum %d labels allowed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid label name format."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %d: Maximum number of label value combinations
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Maximum %d label value combinations allowed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid JSON format."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/CustomMetricBuilder.php
|
||||||
|
msgid "Invalid export format."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Plugin.php
|
#: src/Plugin.php
|
||||||
@@ -356,3 +630,391 @@ msgstr ""
|
|||||||
#: wp-prometheus.php
|
#: wp-prometheus.php
|
||||||
msgid "WP Prometheus requires PHP version %s or higher."
|
msgid "WP Prometheus requires PHP version %s or higher."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Metrics Storage Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Configure how Prometheus metrics are stored. Persistent storage (Redis, APCu) allows metrics to survive between requests and aggregate data over time."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Environment Override Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage adapter is configured via environment variable. Admin settings will be ignored."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage Fallback Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Falling back to In-Memory storage."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Current Status:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Active adapter name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Using %s storage."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage Adapter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "unavailable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Select the storage backend for metrics. Redis and APCu require their respective PHP extensions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Host"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Environment variable name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Can be overridden with %s environment variable."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Port"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Leave empty if not required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Database"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Environment variable name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis database index (0-15). Can be overridden with %s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Key Prefix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Prefix for Redis keys. Useful when sharing Redis with other applications."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "APCu Configuration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Environment variable name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Prefix for APCu keys. Can be overridden with %s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Save Storage Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Test Connection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Environment Variables"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "For Docker or containerized environments, you can configure storage using environment variables. These take precedence over admin settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Variable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Example"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage adapter to use"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis server hostname"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis server port"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis authentication password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis database index"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Redis key prefix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "APCu key prefix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "License server URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "License key"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "License server shared secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Docker Compose Example"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Permission denied."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage adapter is configured via environment variable and cannot be changed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Invalid storage adapter."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage settings saved successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Storage settings saved, but connection test failed:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "In-Memory (default, no persistence)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis (requires PHP Redis extension)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu (requires APCu extension)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "PHP Redis extension is not installed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis connection failed: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis error: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Storage error: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu extension is not installed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu is installed but not enabled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Error message
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu error: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "In-Memory storage is always available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Unknown storage adapter."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Could not connect to Redis server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis authentication failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Redis host:port
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Successfully connected to Redis at %s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "Redis ping failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu is installed but not enabled. Check your php.ini settings."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu store operation failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Memory info
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu is working. Memory: %s used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Metrics/StorageFactory.php
|
||||||
|
msgid "APCu fetch operation returned unexpected value."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode intercepts /metrics requests before full WordPress initialization. This prevents memory exhaustion issues caused by some plugins (e.g., Twig-based themes/plugins) but disables the wp_prometheus_collect_metrics hook for custom metrics."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is configured via WP_PROMETHEUS_DISABLE_EARLY_MODE environment variable. Admin settings will be ignored."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Disable Early Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Disable early metrics interception"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "When disabled, metrics are collected through normal WordPress template loading. This enables the wp_prometheus_collect_metrics hook for custom metrics but may cause issues with some plugins."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is active (this request was served via early interception)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Early mode is enabled (active for /metrics requests)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Clear all accumulated runtime metric data (HTTP requests, database queries). This is useful for testing or starting fresh."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Endpoint"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Selection"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Runtime"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Advanced"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Runtime Metrics Management"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Runtime metrics track HTTP requests and database queries across requests. Use this section to manage accumulated data."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Reset Data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Extension"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Plugin name
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Provided by: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "No dashboards available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Pre-built dashboards for visualizing your WordPress metrics in Grafana."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Installation Instructions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Download the JSON file for your desired dashboard."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "In Grafana, go to Dashboards → Import."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Upload the JSON file or paste its contents."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Select your Prometheus data source when prompted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Click Import to create the dashboard."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Metrics URL
|
||||||
|
#: src/Admin/Settings.php
|
||||||
|
msgid "Make sure your Prometheus instance is configured to scrape %s with the correct authentication token."
|
||||||
|
msgstr ""
|
||||||
|
|||||||
25
phpunit.xml
Normal file
25
phpunit.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
executionOrder="depends,defects"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
<exclude>
|
||||||
|
<file>src/index.php</file>
|
||||||
|
</exclude>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
471
src/Admin/DashboardProvider.php
Normal file
471
src/Admin/DashboardProvider.php
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
<?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.
|
||||||
|
* Supports both built-in dashboards and third-party registrations.
|
||||||
|
*/
|
||||||
|
class DashboardProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard directory path for built-in dashboards.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $dashboard_dir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in dashboard definitions.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $builtin_dashboards = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Third-party registered dashboard definitions.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $registered_dashboards = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the registration hook has been fired.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private bool $hook_fired = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get built-in dashboard definitions.
|
||||||
|
*
|
||||||
|
* Lazily initializes dashboard labels to avoid triggering textdomain loading
|
||||||
|
* before the 'init' action (required since WordPress 6.7).
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function get_builtin_dashboards(): array {
|
||||||
|
if ( empty( $this->builtin_dashboards ) ) {
|
||||||
|
$this->builtin_dashboards = array(
|
||||||
|
'wordpress-overview' => array(
|
||||||
|
'title' => __( 'WordPress Overview', 'wp-prometheus' ),
|
||||||
|
'description' => __( 'General WordPress metrics including users, posts, comments, and plugins.', 'wp-prometheus' ),
|
||||||
|
'file' => 'wordpress-overview.json',
|
||||||
|
'icon' => 'dashicons-wordpress',
|
||||||
|
'source' => 'builtin',
|
||||||
|
),
|
||||||
|
'wordpress-runtime' => array(
|
||||||
|
'title' => __( 'Runtime Performance', 'wp-prometheus' ),
|
||||||
|
'description' => __( 'HTTP request metrics, database query performance, and response times.', 'wp-prometheus' ),
|
||||||
|
'file' => 'wordpress-runtime.json',
|
||||||
|
'icon' => 'dashicons-performance',
|
||||||
|
'source' => 'builtin',
|
||||||
|
),
|
||||||
|
'wordpress-woocommerce' => array(
|
||||||
|
'title' => __( 'WooCommerce Store', 'wp-prometheus' ),
|
||||||
|
'description' => __( 'WooCommerce metrics including products, orders, revenue, and customers.', 'wp-prometheus' ),
|
||||||
|
'file' => 'wordpress-woocommerce.json',
|
||||||
|
'icon' => 'dashicons-cart',
|
||||||
|
'source' => 'builtin',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this->builtin_dashboards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a third-party dashboard.
|
||||||
|
*
|
||||||
|
* @param string $slug Dashboard slug (unique identifier).
|
||||||
|
* @param array $args Dashboard configuration {
|
||||||
|
* @type string $title Dashboard title (required).
|
||||||
|
* @type string $description Dashboard description.
|
||||||
|
* @type string $icon Dashicon class (e.g., 'dashicons-chart-bar').
|
||||||
|
* @type string $file Absolute path to JSON file (mutually exclusive with 'json').
|
||||||
|
* @type string $json Inline JSON content (mutually exclusive with 'file').
|
||||||
|
* @type string $plugin Plugin name for attribution.
|
||||||
|
* }
|
||||||
|
* @return bool True if registered successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
public function register_dashboard( string $slug, array $args ): bool {
|
||||||
|
// Sanitize slug - must be valid identifier.
|
||||||
|
$slug = sanitize_key( $slug );
|
||||||
|
|
||||||
|
if ( empty( $slug ) ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( 'WP Prometheus: Dashboard registration failed - invalid slug' );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate slugs (built-in takes precedence).
|
||||||
|
if ( isset( $this->get_builtin_dashboards()[ $slug ] ) ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard slug '$slug' conflicts with built-in dashboard" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate slugs in already registered dashboards.
|
||||||
|
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard slug '$slug' already registered" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields.
|
||||||
|
if ( empty( $args['title'] ) ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' missing required 'title'" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have either 'file' or 'json', not both.
|
||||||
|
$has_file = ! empty( $args['file'] );
|
||||||
|
$has_json = ! empty( $args['json'] );
|
||||||
|
|
||||||
|
if ( ! $has_file && ! $has_json ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' must have 'file' or 'json'" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $has_file && $has_json ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' cannot have both 'file' and 'json'" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file path if provided.
|
||||||
|
if ( $has_file ) {
|
||||||
|
$file_path = $args['file'];
|
||||||
|
|
||||||
|
// Must be absolute path.
|
||||||
|
if ( ! path_is_absolute( $file_path ) ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' file path must be absolute" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File must exist and be readable.
|
||||||
|
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' file not found: $file_path" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: Prevent path traversal - file must be under wp-content.
|
||||||
|
$real_path = realpath( $file_path );
|
||||||
|
$wp_content_dir = realpath( WP_CONTENT_DIR );
|
||||||
|
|
||||||
|
if ( false === $real_path || false === $wp_content_dir ||
|
||||||
|
strpos( $real_path, $wp_content_dir ) !== 0 ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' file outside wp-content" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate JSON if provided inline.
|
||||||
|
if ( $has_json ) {
|
||||||
|
$decoded = json_decode( $args['json'], true );
|
||||||
|
if ( null === $decoded && json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( "WP Prometheus: Dashboard '$slug' has invalid JSON" );
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build dashboard entry.
|
||||||
|
$this->registered_dashboards[ $slug ] = array(
|
||||||
|
'title' => sanitize_text_field( $args['title'] ),
|
||||||
|
'description' => sanitize_text_field( $args['description'] ?? '' ),
|
||||||
|
'icon' => sanitize_html_class( $args['icon'] ?? 'dashicons-chart-line' ),
|
||||||
|
'file' => $has_file ? $file_path : null,
|
||||||
|
'json' => $has_json ? $args['json'] : null,
|
||||||
|
'plugin' => sanitize_text_field( $args['plugin'] ?? '' ),
|
||||||
|
'source' => 'third-party',
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire the dashboard registration hook with protection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function fire_registration_hook(): void {
|
||||||
|
if ( $this->hook_fired ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->hook_fired = true;
|
||||||
|
|
||||||
|
// Check for isolated mode - skip third-party hooks.
|
||||||
|
$isolated_mode = defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE;
|
||||||
|
|
||||||
|
// Also check option for admin-side isolated mode.
|
||||||
|
if ( ! $isolated_mode ) {
|
||||||
|
$isolated_mode = (bool) get_option( 'wp_prometheus_isolated_mode', false );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $isolated_mode ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use output buffering to capture any accidental output.
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* Fires to allow third-party plugins to register dashboards.
|
||||||
|
*
|
||||||
|
* @since 0.4.6
|
||||||
|
*
|
||||||
|
* @param DashboardProvider $provider The dashboard provider instance.
|
||||||
|
*/
|
||||||
|
do_action( 'wp_prometheus_register_dashboards', $this );
|
||||||
|
} catch ( \Throwable $e ) {
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||||
|
error_log( 'WP Prometheus: Error in dashboard registration hook: ' . $e->getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard any output from plugins.
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available dashboards.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_available(): array {
|
||||||
|
// Fire registration hook first (only once).
|
||||||
|
$this->fire_registration_hook();
|
||||||
|
|
||||||
|
$available = array();
|
||||||
|
|
||||||
|
// Add built-in dashboards (check file exists).
|
||||||
|
foreach ( $this->get_builtin_dashboards() as $slug => $dashboard ) {
|
||||||
|
$file_path = $this->dashboard_dir . $dashboard['file'];
|
||||||
|
if ( file_exists( $file_path ) ) {
|
||||||
|
$available[ $slug ] = $dashboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add registered third-party dashboards.
|
||||||
|
foreach ( $this->registered_dashboards as $slug => $dashboard ) {
|
||||||
|
// Already validated during registration, but double-check.
|
||||||
|
if ( ! empty( $dashboard['json'] ) ||
|
||||||
|
( ! empty( $dashboard['file'] ) && file_exists( $dashboard['file'] ) ) ) {
|
||||||
|
$available[ $slug ] = $dashboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $available;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// Fire registration hook first.
|
||||||
|
$this->fire_registration_hook();
|
||||||
|
|
||||||
|
// Validate slug.
|
||||||
|
$slug = sanitize_file_name( $slug );
|
||||||
|
|
||||||
|
// Check built-in dashboards first.
|
||||||
|
$builtin = $this->get_builtin_dashboards();
|
||||||
|
if ( isset( $builtin[ $slug ] ) ) {
|
||||||
|
$dashboard = $builtin[ $slug ];
|
||||||
|
$file_path = $this->dashboard_dir . $dashboard['file'];
|
||||||
|
|
||||||
|
// Security: Ensure file is within dashboard directory.
|
||||||
|
$real_path = realpath( $file_path );
|
||||||
|
$real_dir = realpath( $this->dashboard_dir );
|
||||||
|
|
||||||
|
if ( false === $real_path || false === $real_dir ||
|
||||||
|
strpos( $real_path, $real_dir ) !== 0 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||||
|
$content = file_get_contents( $file_path );
|
||||||
|
|
||||||
|
return false === $content ? null : $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check registered dashboards.
|
||||||
|
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||||
|
$dashboard = $this->registered_dashboards[ $slug ];
|
||||||
|
|
||||||
|
// Inline JSON.
|
||||||
|
if ( ! empty( $dashboard['json'] ) ) {
|
||||||
|
return $dashboard['json'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// File-based.
|
||||||
|
if ( ! empty( $dashboard['file'] ) ) {
|
||||||
|
$file_path = $dashboard['file'];
|
||||||
|
|
||||||
|
// Security: Re-verify file is under wp-content.
|
||||||
|
$real_path = realpath( $file_path );
|
||||||
|
$wp_content_dir = realpath( WP_CONTENT_DIR );
|
||||||
|
|
||||||
|
if ( false === $real_path || false === $wp_content_dir ||
|
||||||
|
strpos( $real_path, $wp_content_dir ) !== 0 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||||
|
$content = file_get_contents( $file_path );
|
||||||
|
|
||||||
|
return false === $content ? null : $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// Fire registration hook first.
|
||||||
|
$this->fire_registration_hook();
|
||||||
|
|
||||||
|
$slug = sanitize_file_name( $slug );
|
||||||
|
|
||||||
|
$builtin = $this->get_builtin_dashboards();
|
||||||
|
if ( isset( $builtin[ $slug ] ) ) {
|
||||||
|
return $builtin[ $slug ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||||
|
return $this->registered_dashboards[ $slug ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// Fire registration hook first.
|
||||||
|
$this->fire_registration_hook();
|
||||||
|
|
||||||
|
$slug = sanitize_file_name( $slug );
|
||||||
|
|
||||||
|
// Built-in dashboards have predefined filenames.
|
||||||
|
$builtin = $this->get_builtin_dashboards();
|
||||||
|
if ( isset( $builtin[ $slug ] ) ) {
|
||||||
|
return $builtin[ $slug ]['file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registered dashboards - use file basename or generate from slug.
|
||||||
|
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||||
|
$dashboard = $this->registered_dashboards[ $slug ];
|
||||||
|
|
||||||
|
if ( ! empty( $dashboard['file'] ) ) {
|
||||||
|
return basename( $dashboard['file'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate filename from slug for inline JSON.
|
||||||
|
return $slug . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a dashboard is from a third-party plugin.
|
||||||
|
*
|
||||||
|
* @param string $slug Dashboard slug.
|
||||||
|
* @return bool True if third-party, false if built-in or not found.
|
||||||
|
*/
|
||||||
|
public function is_third_party( string $slug ): bool {
|
||||||
|
$this->fire_registration_hook();
|
||||||
|
|
||||||
|
$slug = sanitize_file_name( $slug );
|
||||||
|
|
||||||
|
return isset( $this->registered_dashboards[ $slug ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin name for a third-party dashboard.
|
||||||
|
*
|
||||||
|
* @param string $slug Dashboard slug.
|
||||||
|
* @return string|null Plugin name or null if not found/built-in.
|
||||||
|
*/
|
||||||
|
public function get_plugin_name( string $slug ): ?string {
|
||||||
|
$this->fire_registration_hook();
|
||||||
|
|
||||||
|
$slug = sanitize_file_name( $slug );
|
||||||
|
|
||||||
|
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||||
|
$plugin = $this->registered_dashboards[ $slug ]['plugin'];
|
||||||
|
return ! empty( $plugin ) ? $plugin : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,9 @@ class MetricsEndpoint {
|
|||||||
*/
|
*/
|
||||||
private function init_hooks(): void {
|
private function init_hooks(): void {
|
||||||
add_action( 'init', array( $this, 'register_endpoint' ) );
|
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.
|
* Handle the metrics endpoint request.
|
||||||
*
|
*
|
||||||
|
* Called during parse_request to intercept before themes/plugins load.
|
||||||
|
*
|
||||||
|
* @param \WP $wp WordPress environment instance.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function handle_request(): void {
|
public function handle_request( \WP $wp ): void {
|
||||||
if ( ! get_query_var( 'wp_prometheus_metrics' ) ) {
|
if ( empty( $wp->query_vars['wp_prometheus_metrics'] ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,52 +102,12 @@ class MetricsEndpoint {
|
|||||||
/**
|
/**
|
||||||
* Authenticate the metrics request.
|
* Authenticate the metrics request.
|
||||||
*
|
*
|
||||||
|
* Uses the shared authentication helper to avoid code duplication
|
||||||
|
* with the isolated mode handler in wp-prometheus.php.
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function authenticate(): bool {
|
private function authenticate(): bool {
|
||||||
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
|
return wp_prometheus_authenticate_request();
|
||||||
|
|
||||||
// If no token is set, deny access.
|
|
||||||
if ( empty( $auth_token ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for Bearer token in Authorization header.
|
|
||||||
$auth_header = $this->get_authorization_header();
|
|
||||||
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
|
|
||||||
return hash_equals( $auth_token, $matches[1] );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for token in query parameter (less secure but useful for testing).
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
|
|
||||||
if ( isset( $_GET['token'] ) && hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) ) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Authorization header from the request.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function get_authorization_header(): string {
|
|
||||||
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
|
|
||||||
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
|
|
||||||
return sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( function_exists( 'apache_request_headers' ) ) {
|
|
||||||
$headers = apache_request_headers();
|
|
||||||
if ( isset( $headers['Authorization'] ) ) {
|
|
||||||
return sanitize_text_field( $headers['Authorization'] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ final class Installer {
|
|||||||
'wp_prometheus_enable_default_metrics',
|
'wp_prometheus_enable_default_metrics',
|
||||||
'wp_prometheus_enabled_metrics',
|
'wp_prometheus_enabled_metrics',
|
||||||
'wp_prometheus_runtime_metrics',
|
'wp_prometheus_runtime_metrics',
|
||||||
|
'wp_prometheus_custom_metrics',
|
||||||
|
'wp_prometheus_isolated_mode',
|
||||||
|
'wp_prometheus_storage_adapter',
|
||||||
|
'wp_prometheus_redis_host',
|
||||||
|
'wp_prometheus_redis_port',
|
||||||
|
'wp_prometheus_redis_password',
|
||||||
|
'wp_prometheus_redis_database',
|
||||||
|
'wp_prometheus_redis_prefix',
|
||||||
|
'wp_prometheus_apcu_prefix',
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $options as $option ) {
|
foreach ( $options as $option ) {
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ final class Manager {
|
|||||||
*/
|
*/
|
||||||
private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check';
|
private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMAC-SHA256 signatures of authorized domain suffixes.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private const DOMAIN_BINDING_SIGNATURES = array(
|
||||||
|
'aeb2e64ca8f815d4a552c0a2beeefa8580d6808a60d1aa91ddca719933b12868',
|
||||||
|
'a2fbaafd39e3085cd70995eb5773d6659c90cb3160ddccd66c52a21fac43fd13',
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache TTL in seconds (24 hours).
|
* Cache TTL in seconds (24 hours).
|
||||||
*/
|
*/
|
||||||
@@ -302,37 +312,146 @@ final class Manager {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function is_license_valid(): bool {
|
public static function is_license_valid(): bool {
|
||||||
|
// Bypass license check on localhost for development.
|
||||||
|
if ( self::is_localhost() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass license check on bound domains.
|
||||||
|
if ( self::verify_domain_binding() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||||
return 'valid' === $status;
|
return 'valid' === $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if the current domain matches a bound domain signature.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function verify_domain_binding(): bool {
|
||||||
|
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||||
|
if ( empty( $host ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = hash( 'sha256', 'wp-prometheus:domain-binding:v1:3016a5e8' );
|
||||||
|
$parts = explode( '.', $host );
|
||||||
|
$count = count( $parts );
|
||||||
|
|
||||||
|
// Iterate through all possible domain suffixes.
|
||||||
|
for ( $i = 0; $i < $count - 1; $i++ ) {
|
||||||
|
$suffix = implode( '.', array_slice( $parts, $i ) );
|
||||||
|
$sig = hash_hmac( 'sha256', $suffix, $key );
|
||||||
|
|
||||||
|
if ( in_array( $sig, self::DOMAIN_BINDING_SIGNATURES, true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Get the license key.
|
||||||
*
|
*
|
||||||
|
* Environment variable WP_PROMETHEUS_LICENSE_KEY takes precedence.
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function get_license_key(): string {
|
public static function get_license_key(): string {
|
||||||
|
$env = getenv( 'WP_PROMETHEUS_LICENSE_KEY' );
|
||||||
|
if ( false !== $env && '' !== $env ) {
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
return get_option( self::OPTION_LICENSE_KEY, '' );
|
return get_option( self::OPTION_LICENSE_KEY, '' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the license server URL.
|
* Get the license server URL.
|
||||||
*
|
*
|
||||||
|
* Environment variable WP_PROMETHEUS_LICENSE_SERVER_URL takes precedence.
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function get_server_url(): string {
|
public static function get_server_url(): string {
|
||||||
|
$env = getenv( 'WP_PROMETHEUS_LICENSE_SERVER_URL' );
|
||||||
|
if ( false !== $env && '' !== $env ) {
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
return get_option( self::OPTION_SERVER_URL, '' );
|
return get_option( self::OPTION_SERVER_URL, '' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the server secret.
|
* Get the server secret.
|
||||||
*
|
*
|
||||||
|
* Environment variable WP_PROMETHEUS_LICENSE_SERVER_SECRET takes precedence.
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function get_server_secret(): string {
|
public static function get_server_secret(): string {
|
||||||
|
$env = getenv( 'WP_PROMETHEUS_LICENSE_SERVER_SECRET' );
|
||||||
|
if ( false !== $env && '' !== $env ) {
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
return get_option( self::OPTION_SERVER_SECRET, '' );
|
return get_option( self::OPTION_SERVER_SECRET, '' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a license setting is overridden by an environment variable.
|
||||||
|
*
|
||||||
|
* @param string $setting One of 'server_url', 'license_key', 'server_secret'.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_env_override( string $setting ): bool {
|
||||||
|
$map = array(
|
||||||
|
'server_url' => 'WP_PROMETHEUS_LICENSE_SERVER_URL',
|
||||||
|
'license_key' => 'WP_PROMETHEUS_LICENSE_KEY',
|
||||||
|
'server_secret' => 'WP_PROMETHEUS_LICENSE_SERVER_SECRET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! isset( $map[ $setting ] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = getenv( $map[ $setting ] );
|
||||||
|
return false !== $env && '' !== $env;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cached license status.
|
* Get cached license status.
|
||||||
*
|
*
|
||||||
@@ -396,9 +515,16 @@ final class Manager {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function update_cached_status( string $status, array $data = array() ): 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_STATUS, $status );
|
||||||
update_option( self::OPTION_LICENSE_DATA, $data );
|
update_option( self::OPTION_LICENSE_DATA, $data );
|
||||||
update_option( self::OPTION_LAST_CHECK, time() );
|
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 +591,9 @@ final class Manager {
|
|||||||
update_option( self::OPTION_LICENSE_DATA, array() );
|
update_option( self::OPTION_LICENSE_DATA, array() );
|
||||||
delete_transient( self::TRANSIENT_LICENSE_CHECK );
|
delete_transient( self::TRANSIENT_LICENSE_CHECK );
|
||||||
|
|
||||||
|
// Flush rewrite rules to remove the /metrics endpoint.
|
||||||
|
flush_rewrite_rules();
|
||||||
|
|
||||||
wp_send_json_success( array(
|
wp_send_json_success( array(
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => __( 'License deactivated.', 'wp-prometheus' ),
|
'message' => __( 'License deactivated.', 'wp-prometheus' ),
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
namespace Magdev\WpPrometheus\Metrics;
|
namespace Magdev\WpPrometheus\Metrics;
|
||||||
|
|
||||||
use Prometheus\CollectorRegistry;
|
use Prometheus\CollectorRegistry;
|
||||||
use Prometheus\Storage\InMemory;
|
|
||||||
use Prometheus\RenderTextFormat;
|
use Prometheus\RenderTextFormat;
|
||||||
|
use Magdev\WpPrometheus\Metrics\CustomMetricBuilder;
|
||||||
|
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||||
|
|
||||||
// Prevent direct file access.
|
// Prevent direct file access.
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
@@ -41,7 +42,7 @@ class Collector {
|
|||||||
* Constructor.
|
* Constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->registry = new CollectorRegistry( new InMemory() );
|
$this->registry = new CollectorRegistry( StorageFactory::get_adapter() );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,12 +114,69 @@ class Collector {
|
|||||||
// Collect runtime metrics (HTTP requests, DB queries).
|
// Collect runtime metrics (HTTP requests, DB queries).
|
||||||
$this->collect_runtime_metrics( $enabled_metrics );
|
$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.
|
* 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.
|
* @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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -335,27 +393,24 @@ class Collector {
|
|||||||
private function collect_transient_metrics(): void {
|
private function collect_transient_metrics(): void {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// Count all transients.
|
// Count all transient types in a single query.
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||||
$transient_count = $wpdb->get_var(
|
$counts = $wpdb->get_row(
|
||||||
"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(
|
$wpdb->prepare(
|
||||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%%' AND option_value < %d",
|
"SELECT
|
||||||
|
SUM(CASE WHEN option_name LIKE '_transient_%%' AND option_name NOT LIKE '_transient_timeout_%%' THEN 1 ELSE 0 END) AS total,
|
||||||
|
SUM(CASE WHEN option_name LIKE '_transient_timeout_%%' THEN 1 ELSE 0 END) AS with_expiration,
|
||||||
|
SUM(CASE WHEN option_name LIKE '_transient_timeout_%%' AND option_value < %d THEN 1 ELSE 0 END) AS expired
|
||||||
|
FROM {$wpdb->options}
|
||||||
|
WHERE option_name LIKE '_transient_%%'",
|
||||||
time()
|
time()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$transient_count = (int) ( $counts->total ?? 0 );
|
||||||
|
$expiring_count = (int) ( $counts->with_expiration ?? 0 );
|
||||||
|
$expired_count = (int) ( $counts->expired ?? 0 );
|
||||||
|
|
||||||
// Transients total gauge.
|
// Transients total gauge.
|
||||||
$transients_gauge = $this->registry->getOrRegisterGauge(
|
$transients_gauge = $this->registry->getOrRegisterGauge(
|
||||||
$this->namespace,
|
$this->namespace,
|
||||||
@@ -364,10 +419,10 @@ class Collector {
|
|||||||
array( 'type' )
|
array( 'type' )
|
||||||
);
|
);
|
||||||
|
|
||||||
$transients_gauge->set( (int) $transient_count, array( 'total' ) );
|
$transients_gauge->set( $transient_count, array( 'total' ) );
|
||||||
$transients_gauge->set( (int) $expiring_count, array( 'with_expiration' ) );
|
$transients_gauge->set( $expiring_count, array( 'with_expiration' ) );
|
||||||
$transients_gauge->set( (int) $transient_count - (int) $expiring_count, array( 'persistent' ) );
|
$transients_gauge->set( $transient_count - $expiring_count, array( 'persistent' ) );
|
||||||
$transients_gauge->set( (int) $expired_count, array( 'expired' ) );
|
$transients_gauge->set( $expired_count, array( 'expired' ) );
|
||||||
|
|
||||||
// Site transients (for multisite).
|
// Site transients (for multisite).
|
||||||
if ( is_multisite() ) {
|
if ( is_multisite() ) {
|
||||||
@@ -389,6 +444,16 @@ class Collector {
|
|||||||
return class_exists( 'WooCommerce' );
|
return class_exists( 'WooCommerce' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if WooCommerce HPOS (High-Performance Order Storage) is enabled.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function is_hpos_enabled(): bool {
|
||||||
|
return class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
||||||
|
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect WooCommerce metrics.
|
* Collect WooCommerce metrics.
|
||||||
*
|
*
|
||||||
@@ -440,16 +505,17 @@ class Collector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count by product type (for published products only).
|
// Count by product type (for published products only) using count query.
|
||||||
foreach ( array_keys( $product_types ) as $type ) {
|
foreach ( array_keys( $product_types ) as $type ) {
|
||||||
$args = array(
|
$args = array(
|
||||||
'status' => 'publish',
|
'status' => 'publish',
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'limit' => -1,
|
'limit' => 1,
|
||||||
'return' => 'ids',
|
'return' => 'ids',
|
||||||
|
'paginate' => true,
|
||||||
);
|
);
|
||||||
$products = wc_get_products( $args );
|
$result = wc_get_products( $args );
|
||||||
$gauge->set( count( $products ), array( 'publish', $type ) );
|
$gauge->set( $result->total, array( 'publish', $type ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,11 +532,14 @@ class Collector {
|
|||||||
array( 'status' )
|
array( 'status' )
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use WooCommerce's built-in order count function.
|
// Get all registered order statuses and count each.
|
||||||
$order_counts = wc_orders_count();
|
$statuses = wc_get_order_statuses();
|
||||||
|
|
||||||
foreach ( $order_counts as $status => $count ) {
|
foreach ( array_keys( $statuses ) as $status ) {
|
||||||
$gauge->set( (int) $count, array( $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 ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,8 +561,7 @@ class Collector {
|
|||||||
$currency = get_woocommerce_currency();
|
$currency = get_woocommerce_currency();
|
||||||
|
|
||||||
// Check if HPOS (High-Performance Order Storage) is enabled.
|
// Check if HPOS (High-Performance Order Storage) is enabled.
|
||||||
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
$hpos_enabled = $this->is_hpos_enabled();
|
||||||
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
|
||||||
|
|
||||||
if ( $hpos_enabled ) {
|
if ( $hpos_enabled ) {
|
||||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||||
@@ -591,9 +659,7 @@ class Collector {
|
|||||||
// Count guest orders (orders without user_id).
|
// Count guest orders (orders without user_id).
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// Check if HPOS is enabled.
|
$hpos_enabled = $this->is_hpos_enabled();
|
||||||
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|
|
||||||
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
|
||||||
|
|
||||||
if ( $hpos_enabled ) {
|
if ( $hpos_enabled ) {
|
||||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||||
|
|||||||
532
src/Metrics/CustomMetricBuilder.php
Normal file
532
src/Metrics/CustomMetricBuilder.php
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum import JSON size in bytes (1 MB).
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const MAX_IMPORT_SIZE = 1048576;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize a Prometheus metric name.
|
||||||
|
*
|
||||||
|
* Unlike sanitize_key(), this preserves colons and uppercase letters
|
||||||
|
* which are valid in Prometheus metric names.
|
||||||
|
*
|
||||||
|
* @param string $name Raw metric name.
|
||||||
|
* @return string Sanitized metric name.
|
||||||
|
*/
|
||||||
|
public static function sanitize_metric_name( string $name ): string {
|
||||||
|
return preg_replace( '/[^a-zA-Z0-9_:]/', '', $name );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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' => self::sanitize_metric_name( $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' ),
|
||||||
|
'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 {
|
||||||
|
// Prevent DoS via excessively large imports.
|
||||||
|
if ( strlen( $json ) > self::MAX_IMPORT_SIZE ) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: Maximum size */
|
||||||
|
__( 'Import data exceeds maximum size of %s.', 'wp-prometheus' ),
|
||||||
|
size_format( self::MAX_IMPORT_SIZE )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate import mode.
|
||||||
|
$valid_modes = array( 'skip', 'overwrite', 'rename' );
|
||||||
|
if ( ! in_array( $mode, $valid_modes, true ) ) {
|
||||||
|
$mode = 'skip';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,9 @@ final class Plugin {
|
|||||||
private function __construct() {
|
private function __construct() {
|
||||||
$this->init_components();
|
$this->init_components();
|
||||||
$this->init_hooks();
|
$this->init_hooks();
|
||||||
$this->load_textdomain();
|
|
||||||
|
// Defer textdomain loading to 'init' action (required since WordPress 6.7).
|
||||||
|
add_action( 'init', array( $this, 'load_textdomain' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,9 +146,11 @@ final class Plugin {
|
|||||||
/**
|
/**
|
||||||
* Load plugin textdomain.
|
* Load plugin textdomain.
|
||||||
*
|
*
|
||||||
|
* Hooked to 'init' action to comply with WordPress 6.7+ requirements.
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function load_textdomain(): void {
|
public function load_textdomain(): void {
|
||||||
load_plugin_textdomain(
|
load_plugin_textdomain(
|
||||||
'wp-prometheus',
|
'wp-prometheus',
|
||||||
false,
|
false,
|
||||||
|
|||||||
51
tests/Helpers/GlobalFunctionState.php
Normal file
51
tests/Helpers/GlobalFunctionState.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Helpers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controllable state for global WordPress function stubs.
|
||||||
|
*
|
||||||
|
* Used by the bootstrap's global get_option() stub so that tests
|
||||||
|
* for global-scope functions (e.g., wp_prometheus_authenticate_request)
|
||||||
|
* can control return values without php-mock (which requires namespaces).
|
||||||
|
*/
|
||||||
|
class GlobalFunctionState
|
||||||
|
{
|
||||||
|
/** @var array<string, mixed> Simulated WordPress options. */
|
||||||
|
public static array $options = [];
|
||||||
|
|
||||||
|
/** @var array<string, int> Track function call counts. */
|
||||||
|
public static array $callCounts = [];
|
||||||
|
|
||||||
|
/** @var array<string, list<mixed>> Track arguments passed to functions. */
|
||||||
|
public static array $callArgs = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all state. Call in setUp()/tearDown().
|
||||||
|
*/
|
||||||
|
public static function reset(): void
|
||||||
|
{
|
||||||
|
self::$options = [];
|
||||||
|
self::$callCounts = [];
|
||||||
|
self::$callArgs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a function call for later assertions.
|
||||||
|
*/
|
||||||
|
public static function recordCall(string $function, mixed ...$args): void
|
||||||
|
{
|
||||||
|
self::$callCounts[$function] = (self::$callCounts[$function] ?? 0) + 1;
|
||||||
|
self::$callArgs[$function][] = $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get call count for a function.
|
||||||
|
*/
|
||||||
|
public static function getCallCount(string $function): int
|
||||||
|
{
|
||||||
|
return self::$callCounts[$function] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
376
tests/Unit/Admin/DashboardProviderTest.php
Normal file
376
tests/Unit/Admin/DashboardProviderTest.php
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit\Admin;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Admin\DashboardProvider;
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
#[CoversClass(DashboardProvider::class)]
|
||||||
|
class DashboardProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
private DashboardProvider $provider;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->provider = new DashboardProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── register_dashboard() - Validation ────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_with_inline_json_succeeds(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('my-dashboard', [
|
||||||
|
'title' => 'My Dashboard',
|
||||||
|
'json' => '{"panels":[]}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_rejects_empty_slug(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('', [
|
||||||
|
'title' => 'Test',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_rejects_invalid_slug_characters(): void
|
||||||
|
{
|
||||||
|
// sanitize_key removes all non [a-z0-9_-] characters
|
||||||
|
// A slug like '!@#' becomes '' after sanitize_key
|
||||||
|
$result = $this->provider->register_dashboard('!@#', [
|
||||||
|
'title' => 'Test',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_rejects_duplicate_builtin_slug(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('wordpress-overview', [
|
||||||
|
'title' => 'Override Built-in',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_rejects_duplicate_registered_slug(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('my-dashboard', [
|
||||||
|
'title' => 'First',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->provider->register_dashboard('my-dashboard', [
|
||||||
|
'title' => 'Second',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_requires_title(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('no-title', [
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_requires_file_or_json(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('no-content', [
|
||||||
|
'title' => 'No Content',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_rejects_both_file_and_json(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$isReadable = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'is_readable');
|
||||||
|
$isReadable->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$realpath = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'realpath');
|
||||||
|
$realpath->expects($this->any())->willReturnArgument(0);
|
||||||
|
|
||||||
|
$result = $this->provider->register_dashboard('both', [
|
||||||
|
'title' => 'Both',
|
||||||
|
'file' => '/tmp/wordpress/wp-content/plugins/test/dashboard.json',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_file_requires_absolute_path(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('relative', [
|
||||||
|
'title' => 'Relative Path',
|
||||||
|
'file' => 'relative/path/dashboard.json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_file_must_exist(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$result = $this->provider->register_dashboard('missing-file', [
|
||||||
|
'title' => 'Missing File',
|
||||||
|
'file' => '/tmp/wordpress/wp-content/plugins/test/nonexistent.json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_file_rejects_path_traversal(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$isReadable = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'is_readable');
|
||||||
|
$isReadable->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$realpath = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'realpath');
|
||||||
|
$realpath->expects($this->any())->willReturnArgument(0);
|
||||||
|
|
||||||
|
$result = $this->provider->register_dashboard('evil', [
|
||||||
|
'title' => 'Evil Dashboard',
|
||||||
|
'file' => '/etc/passwd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_file_accepts_path_under_wp_content(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$isReadable = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'is_readable');
|
||||||
|
$isReadable->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$realpath = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'realpath');
|
||||||
|
$realpath->expects($this->any())->willReturnArgument(0);
|
||||||
|
|
||||||
|
$result = $this->provider->register_dashboard('valid-file', [
|
||||||
|
'title' => 'Valid File Dashboard',
|
||||||
|
'file' => '/tmp/wordpress/wp-content/plugins/my-plugin/dashboard.json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_rejects_invalid_inline_json(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->register_dashboard('bad-json', [
|
||||||
|
'title' => 'Bad JSON',
|
||||||
|
'json' => '{invalid json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_sets_source_to_third_party(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('ext-dashboard', [
|
||||||
|
'title' => 'Extension',
|
||||||
|
'json' => '{}',
|
||||||
|
'plugin' => 'My Plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($this->provider->is_third_party('ext-dashboard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_available() ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_available_includes_builtin_dashboards(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$available = $this->provider->get_available();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('wordpress-overview', $available);
|
||||||
|
$this->assertArrayHasKey('wordpress-runtime', $available);
|
||||||
|
$this->assertArrayHasKey('wordpress-woocommerce', $available);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_available_includes_registered_dashboards(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$this->provider->register_dashboard('custom-dash', [
|
||||||
|
'title' => 'Custom',
|
||||||
|
'json' => '{"panels":[]}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$available = $this->provider->get_available();
|
||||||
|
$this->assertArrayHasKey('custom-dash', $available);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_available_excludes_builtin_with_missing_file(): void
|
||||||
|
{
|
||||||
|
$fileExists = $this->getFunctionMock('Magdev\\WpPrometheus\\Admin', 'file_exists');
|
||||||
|
$fileExists->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$available = $this->provider->get_available();
|
||||||
|
|
||||||
|
// Built-in dashboards should be excluded (files don't exist).
|
||||||
|
$this->assertArrayNotHasKey('wordpress-overview', $available);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── is_third_party() ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function builtin_dashboard_is_not_third_party(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->provider->is_third_party('wordpress-overview'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function registered_dashboard_is_third_party(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('ext-dash', [
|
||||||
|
'title' => 'Extension Dashboard',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($this->provider->is_third_party('ext-dash'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function unknown_slug_is_not_third_party(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->provider->is_third_party('nonexistent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_plugin_name() ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_plugin_name_returns_name_for_registered(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('ext-dash', [
|
||||||
|
'title' => 'Extension',
|
||||||
|
'json' => '{}',
|
||||||
|
'plugin' => 'My Awesome Plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('My Awesome Plugin', $this->provider->get_plugin_name('ext-dash'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_plugin_name_returns_null_for_builtin(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->provider->get_plugin_name('wordpress-overview'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_plugin_name_returns_null_when_empty(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('no-plugin', [
|
||||||
|
'title' => 'No Plugin',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertNull($this->provider->get_plugin_name('no-plugin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_filename() ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_filename_returns_file_for_builtin(): void
|
||||||
|
{
|
||||||
|
$filename = $this->provider->get_filename('wordpress-overview');
|
||||||
|
$this->assertSame('wordpress-overview.json', $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_filename_returns_slug_json_for_inline(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('my-dash', [
|
||||||
|
'title' => 'My Dashboard',
|
||||||
|
'json' => '{}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('my-dash.json', $this->provider->get_filename('my-dash'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_filename_returns_null_for_unknown(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->provider->get_filename('nonexistent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_metadata() ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_metadata_returns_builtin_data(): void
|
||||||
|
{
|
||||||
|
$metadata = $this->provider->get_metadata('wordpress-overview');
|
||||||
|
|
||||||
|
$this->assertIsArray($metadata);
|
||||||
|
$this->assertSame('WordPress Overview', $metadata['title']);
|
||||||
|
$this->assertSame('builtin', $metadata['source']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_metadata_returns_registered_data(): void
|
||||||
|
{
|
||||||
|
$this->provider->register_dashboard('ext-dash', [
|
||||||
|
'title' => 'Extension',
|
||||||
|
'description' => 'A test dashboard',
|
||||||
|
'json' => '{}',
|
||||||
|
'plugin' => 'TestPlugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$metadata = $this->provider->get_metadata('ext-dash');
|
||||||
|
|
||||||
|
$this->assertIsArray($metadata);
|
||||||
|
$this->assertSame('Extension', $metadata['title']);
|
||||||
|
$this->assertSame('third-party', $metadata['source']);
|
||||||
|
$this->assertSame('TestPlugin', $metadata['plugin']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_metadata_returns_null_for_unknown(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->provider->get_metadata('nonexistent'));
|
||||||
|
}
|
||||||
|
}
|
||||||
148
tests/Unit/AuthenticationTest.php
Normal file
148
tests/Unit/AuthenticationTest.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversFunction;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
#[CoversFunction('wp_prometheus_authenticate_request')]
|
||||||
|
#[CoversFunction('wp_prometheus_get_authorization_header')]
|
||||||
|
class AuthenticationTest extends TestCase
|
||||||
|
{
|
||||||
|
private array $originalServer = [];
|
||||||
|
private array $originalGet = [];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->originalServer = $_SERVER;
|
||||||
|
$this->originalGet = $_GET;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$_SERVER = $this->originalServer;
|
||||||
|
$_GET = $this->originalGet;
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── wp_prometheus_authenticate_request() ─────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function returns_false_when_no_token_configured(): void
|
||||||
|
{
|
||||||
|
// No auth token in options → deny all.
|
||||||
|
$this->assertFalse(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function returns_false_when_token_is_empty_string(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = '';
|
||||||
|
$this->assertFalse(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function bearer_token_authenticates_successfully(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'secret-token-123';
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer secret-token-123';
|
||||||
|
|
||||||
|
$this->assertTrue(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function bearer_token_fails_with_wrong_token(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'secret-token-123';
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer wrong-token';
|
||||||
|
|
||||||
|
$this->assertFalse(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function bearer_prefix_is_case_insensitive(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'secret-token-123';
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = 'BEARER secret-token-123';
|
||||||
|
|
||||||
|
$this->assertTrue(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function query_parameter_authenticates_successfully(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'secret-token-123';
|
||||||
|
$_GET['token'] = 'secret-token-123';
|
||||||
|
|
||||||
|
$this->assertTrue(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function query_parameter_fails_with_wrong_token(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'secret-token-123';
|
||||||
|
$_GET['token'] = 'wrong-token';
|
||||||
|
|
||||||
|
$this->assertFalse(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function returns_false_when_no_auth_provided(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'secret-token-123';
|
||||||
|
unset($_SERVER['HTTP_AUTHORIZATION'], $_SERVER['REDIRECT_HTTP_AUTHORIZATION']);
|
||||||
|
unset($_GET['token']);
|
||||||
|
|
||||||
|
$this->assertFalse(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function bearer_takes_precedence_over_query_parameter(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'correct-token';
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer correct-token';
|
||||||
|
$_GET['token'] = 'wrong-token';
|
||||||
|
|
||||||
|
$this->assertTrue(wp_prometheus_authenticate_request());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── wp_prometheus_get_authorization_header() ─────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_authorization_header_from_http_authorization(): void
|
||||||
|
{
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer my-token';
|
||||||
|
|
||||||
|
$this->assertSame('Bearer my-token', wp_prometheus_get_authorization_header());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_authorization_header_from_redirect(): void
|
||||||
|
{
|
||||||
|
unset($_SERVER['HTTP_AUTHORIZATION']);
|
||||||
|
$_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = 'Bearer redirect-token';
|
||||||
|
|
||||||
|
$this->assertSame('Bearer redirect-token', wp_prometheus_get_authorization_header());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_authorization_header_returns_empty_when_absent(): void
|
||||||
|
{
|
||||||
|
unset($_SERVER['HTTP_AUTHORIZATION'], $_SERVER['REDIRECT_HTTP_AUTHORIZATION']);
|
||||||
|
|
||||||
|
$this->assertSame('', wp_prometheus_get_authorization_header());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function http_authorization_takes_precedence_over_redirect(): void
|
||||||
|
{
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer primary';
|
||||||
|
$_SERVER['REDIRECT_HTTP_AUTHORIZATION'] = 'Bearer redirect';
|
||||||
|
|
||||||
|
$this->assertSame('Bearer primary', wp_prometheus_get_authorization_header());
|
||||||
|
}
|
||||||
|
}
|
||||||
102
tests/Unit/Endpoint/MetricsEndpointTest.php
Normal file
102
tests/Unit/Endpoint/MetricsEndpointTest.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit\Endpoint;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Endpoint\MetricsEndpoint;
|
||||||
|
use Magdev\WpPrometheus\Metrics\Collector;
|
||||||
|
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
#[CoversClass(MetricsEndpoint::class)]
|
||||||
|
class MetricsEndpointTest extends TestCase
|
||||||
|
{
|
||||||
|
private Collector $collector;
|
||||||
|
private MetricsEndpoint $endpoint;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->resetStorageFactory();
|
||||||
|
$this->collector = new Collector();
|
||||||
|
$this->endpoint = new MetricsEndpoint($this->collector);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->resetStorageFactory();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Constructor ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function constructor_accepts_collector(): void
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(MetricsEndpoint::class, $this->endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── register_endpoint() ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_endpoint_adds_rewrite_rule(): void
|
||||||
|
{
|
||||||
|
$this->endpoint->register_endpoint();
|
||||||
|
|
||||||
|
$this->assertSame(1, GlobalFunctionState::getCallCount('add_rewrite_rule'));
|
||||||
|
$args = GlobalFunctionState::$callArgs['add_rewrite_rule'][0];
|
||||||
|
$this->assertSame('^metrics/?$', $args[0]);
|
||||||
|
$this->assertSame('index.php?wp_prometheus_metrics=1', $args[1]);
|
||||||
|
$this->assertSame('top', $args[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_endpoint_adds_rewrite_tag(): void
|
||||||
|
{
|
||||||
|
$this->endpoint->register_endpoint();
|
||||||
|
|
||||||
|
$this->assertSame(1, GlobalFunctionState::getCallCount('add_rewrite_tag'));
|
||||||
|
$args = GlobalFunctionState::$callArgs['add_rewrite_tag'][0];
|
||||||
|
$this->assertSame('%wp_prometheus_metrics%', $args[0]);
|
||||||
|
$this->assertSame('([^&]+)', $args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── handle_request() ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handle_request_returns_early_when_no_query_var(): void
|
||||||
|
{
|
||||||
|
$wp = new \WP();
|
||||||
|
$wp->query_vars = [];
|
||||||
|
|
||||||
|
// Should return without calling exit or outputting anything.
|
||||||
|
$this->endpoint->handle_request($wp);
|
||||||
|
|
||||||
|
// If we reach this assertion, handle_request returned early (no exit).
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function handle_request_returns_early_when_query_var_empty(): void
|
||||||
|
{
|
||||||
|
$wp = new \WP();
|
||||||
|
$wp->query_vars = ['wp_prometheus_metrics' => ''];
|
||||||
|
|
||||||
|
$this->endpoint->handle_request($wp);
|
||||||
|
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function resetStorageFactory(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(StorageFactory::class);
|
||||||
|
$property = $reflection->getProperty('instance');
|
||||||
|
$property->setValue(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
189
tests/Unit/InstallerTest.php
Normal file
189
tests/Unit/InstallerTest.php
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Installer;
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
#[CoversClass(Installer::class)]
|
||||||
|
class InstallerTest extends TestCase
|
||||||
|
{
|
||||||
|
// ── activate() ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_stores_activation_time(): void
|
||||||
|
{
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey(
|
||||||
|
'wp_prometheus_activated',
|
||||||
|
GlobalFunctionState::$options
|
||||||
|
);
|
||||||
|
$this->assertIsInt(GlobalFunctionState::$options['wp_prometheus_activated']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_flushes_rewrite_rules(): void
|
||||||
|
{
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$this->assertGreaterThanOrEqual(
|
||||||
|
1,
|
||||||
|
GlobalFunctionState::getCallCount('flush_rewrite_rules')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_generates_auth_token_when_not_set(): void
|
||||||
|
{
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey(
|
||||||
|
'wp_prometheus_auth_token',
|
||||||
|
GlobalFunctionState::$options
|
||||||
|
);
|
||||||
|
$this->assertNotEmpty(GlobalFunctionState::$options['wp_prometheus_auth_token']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_preserves_existing_auth_token(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token'] = 'existing-token';
|
||||||
|
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'existing-token',
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_auth_token']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_enables_default_metrics(): void
|
||||||
|
{
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
1,
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_enable_default_metrics']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_sets_default_enabled_metrics_list(): void
|
||||||
|
{
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$enabled = GlobalFunctionState::$options['wp_prometheus_enabled_metrics'];
|
||||||
|
$this->assertIsArray($enabled);
|
||||||
|
$this->assertContains('wordpress_info', $enabled);
|
||||||
|
$this->assertContains('wordpress_users_total', $enabled);
|
||||||
|
$this->assertContains('wordpress_posts_total', $enabled);
|
||||||
|
$this->assertContains('wordpress_comments_total', $enabled);
|
||||||
|
$this->assertContains('wordpress_plugins_total', $enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function activate_preserves_existing_enabled_metrics(): void
|
||||||
|
{
|
||||||
|
$custom = ['wordpress_info', 'wordpress_users_total'];
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_enabled_metrics'] = $custom;
|
||||||
|
|
||||||
|
Installer::activate();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
$custom,
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_enabled_metrics']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── deactivate() ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function deactivate_flushes_rewrite_rules(): void
|
||||||
|
{
|
||||||
|
Installer::deactivate();
|
||||||
|
|
||||||
|
$this->assertGreaterThanOrEqual(
|
||||||
|
1,
|
||||||
|
GlobalFunctionState::getCallCount('flush_rewrite_rules')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── uninstall() ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function uninstall_removes_all_plugin_options(): void
|
||||||
|
{
|
||||||
|
// Set up options that should be cleaned.
|
||||||
|
$expected_options = [
|
||||||
|
'wp_prometheus_activated',
|
||||||
|
'wp_prometheus_license_key',
|
||||||
|
'wp_prometheus_license_server_url',
|
||||||
|
'wp_prometheus_license_server_secret',
|
||||||
|
'wp_prometheus_license_status',
|
||||||
|
'wp_prometheus_license_data',
|
||||||
|
'wp_prometheus_license_last_check',
|
||||||
|
'wp_prometheus_auth_token',
|
||||||
|
'wp_prometheus_enable_default_metrics',
|
||||||
|
'wp_prometheus_enabled_metrics',
|
||||||
|
'wp_prometheus_runtime_metrics',
|
||||||
|
'wp_prometheus_custom_metrics',
|
||||||
|
'wp_prometheus_isolated_mode',
|
||||||
|
'wp_prometheus_storage_adapter',
|
||||||
|
'wp_prometheus_redis_host',
|
||||||
|
'wp_prometheus_redis_port',
|
||||||
|
'wp_prometheus_redis_password',
|
||||||
|
'wp_prometheus_redis_database',
|
||||||
|
'wp_prometheus_redis_prefix',
|
||||||
|
'wp_prometheus_apcu_prefix',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($expected_options as $option) {
|
||||||
|
GlobalFunctionState::$options[$option] = 'test_value';
|
||||||
|
}
|
||||||
|
|
||||||
|
Installer::uninstall();
|
||||||
|
|
||||||
|
// Verify all options were deleted.
|
||||||
|
foreach ($expected_options as $option) {
|
||||||
|
$this->assertArrayNotHasKey(
|
||||||
|
$option,
|
||||||
|
GlobalFunctionState::$options,
|
||||||
|
"Option '$option' was not deleted during uninstall"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function uninstall_removes_license_transient(): void
|
||||||
|
{
|
||||||
|
Installer::uninstall();
|
||||||
|
|
||||||
|
$this->assertGreaterThanOrEqual(
|
||||||
|
1,
|
||||||
|
GlobalFunctionState::getCallCount('delete_transient')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the specific transient was targeted.
|
||||||
|
$args = GlobalFunctionState::$callArgs['delete_transient'] ?? [];
|
||||||
|
$transientNames = array_column($args, 0);
|
||||||
|
$this->assertContains('wp_prometheus_license_check', $transientNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function uninstall_delete_option_call_count_matches(): void
|
||||||
|
{
|
||||||
|
Installer::uninstall();
|
||||||
|
|
||||||
|
// Should call delete_option for each option in the list (20 options).
|
||||||
|
$this->assertSame(
|
||||||
|
20,
|
||||||
|
GlobalFunctionState::getCallCount('delete_option')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
tests/Unit/Metrics/CollectorTest.php
Normal file
155
tests/Unit/Metrics/CollectorTest.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Metrics\Collector;
|
||||||
|
use Magdev\WpPrometheus\Metrics\RuntimeCollector;
|
||||||
|
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||||
|
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Prometheus\CollectorRegistry;
|
||||||
|
use Prometheus\Counter;
|
||||||
|
use Prometheus\Gauge;
|
||||||
|
use Prometheus\Histogram;
|
||||||
|
|
||||||
|
#[CoversClass(Collector::class)]
|
||||||
|
class CollectorTest extends TestCase
|
||||||
|
{
|
||||||
|
private Collector $collector;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->resetStorageFactory();
|
||||||
|
$this->collector = new Collector();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->resetRuntimeCollectorSingleton();
|
||||||
|
$this->resetStorageFactory();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Constructor & Basic Properties ─────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function constructor_creates_registry(): void
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(CollectorRegistry::class, $this->collector->get_registry());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_namespace_returns_wordpress(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('wordpress', $this->collector->get_namespace());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── register_gauge() ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_gauge_returns_gauge_instance(): void
|
||||||
|
{
|
||||||
|
$gauge = $this->collector->register_gauge('test_metric', 'A test gauge');
|
||||||
|
$this->assertInstanceOf(Gauge::class, $gauge);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_gauge_with_labels(): void
|
||||||
|
{
|
||||||
|
$gauge = $this->collector->register_gauge(
|
||||||
|
'labeled_metric',
|
||||||
|
'A labeled gauge',
|
||||||
|
['label1', 'label2']
|
||||||
|
);
|
||||||
|
$this->assertInstanceOf(Gauge::class, $gauge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── register_counter() ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_counter_returns_counter_instance(): void
|
||||||
|
{
|
||||||
|
$counter = $this->collector->register_counter('test_counter', 'A test counter');
|
||||||
|
$this->assertInstanceOf(Counter::class, $counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_counter_with_labels(): void
|
||||||
|
{
|
||||||
|
$counter = $this->collector->register_counter(
|
||||||
|
'labeled_counter',
|
||||||
|
'A labeled counter',
|
||||||
|
['method', 'status']
|
||||||
|
);
|
||||||
|
$this->assertInstanceOf(Counter::class, $counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── register_histogram() ──────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_histogram_returns_histogram_instance(): void
|
||||||
|
{
|
||||||
|
$histogram = $this->collector->register_histogram('test_histogram', 'A test histogram');
|
||||||
|
$this->assertInstanceOf(Histogram::class, $histogram);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function register_histogram_with_custom_buckets(): void
|
||||||
|
{
|
||||||
|
$buckets = [0.1, 0.5, 1.0, 5.0];
|
||||||
|
$histogram = $this->collector->register_histogram(
|
||||||
|
'custom_buckets_hist',
|
||||||
|
'A histogram with custom buckets',
|
||||||
|
['label1'],
|
||||||
|
$buckets
|
||||||
|
);
|
||||||
|
$this->assertInstanceOf(Histogram::class, $histogram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── render() ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function render_returns_string(): void
|
||||||
|
{
|
||||||
|
$getOption = $this->getFunctionMock('Magdev\\WpPrometheus\\Metrics', 'get_option');
|
||||||
|
$getOption->expects($this->any())->willReturn([]);
|
||||||
|
|
||||||
|
$output = $this->collector->render();
|
||||||
|
$this->assertIsString($output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function render_includes_registered_gauge_value(): void
|
||||||
|
{
|
||||||
|
$getOption = $this->getFunctionMock('Magdev\\WpPrometheus\\Metrics', 'get_option');
|
||||||
|
$getOption->expects($this->any())->willReturn([]);
|
||||||
|
|
||||||
|
$gauge = $this->collector->register_gauge('test_render_metric', 'Test metric for render');
|
||||||
|
$gauge->set(42, []);
|
||||||
|
|
||||||
|
$output = $this->collector->render();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('wordpress_test_render_metric', $output);
|
||||||
|
$this->assertStringContainsString('42', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function resetRuntimeCollectorSingleton(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(RuntimeCollector::class);
|
||||||
|
$property = $reflection->getProperty('instance');
|
||||||
|
$property->setValue(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetStorageFactory(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(StorageFactory::class);
|
||||||
|
$property = $reflection->getProperty('instance');
|
||||||
|
$property->setValue(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
655
tests/Unit/Metrics/CustomMetricBuilderTest.php
Normal file
655
tests/Unit/Metrics/CustomMetricBuilderTest.php
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Metrics\CustomMetricBuilder;
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
#[CoversClass(CustomMetricBuilder::class)]
|
||||||
|
class CustomMetricBuilderTest extends TestCase
|
||||||
|
{
|
||||||
|
private CustomMetricBuilder $builder;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->builder = new CustomMetricBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── validate_name() ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('validMetricNamesProvider')]
|
||||||
|
public function validate_name_accepts_valid_names(string $name): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->builder->validate_name($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validMetricNamesProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'simple' => ['my_metric'],
|
||||||
|
'with_colon' => ['my:metric'],
|
||||||
|
'starts_with_underscore' => ['_private_metric'],
|
||||||
|
'starts_with_colon' => [':special_metric'],
|
||||||
|
'uppercase' => ['MY_METRIC'],
|
||||||
|
'mixed_case' => ['myMetric123'],
|
||||||
|
'single_letter' => ['m'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('invalidMetricNamesProvider')]
|
||||||
|
public function validate_name_rejects_invalid_names(string $name): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->builder->validate_name($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidMetricNamesProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'starts_with_digit' => ['0metric'],
|
||||||
|
'contains_dash' => ['my-metric'],
|
||||||
|
'contains_space' => ['my metric'],
|
||||||
|
'contains_dot' => ['my.metric'],
|
||||||
|
'empty_string' => [''],
|
||||||
|
'special_chars' => ['metric@name'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── validate_label_name() ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('validLabelNamesProvider')]
|
||||||
|
public function validate_label_name_accepts_valid_names(string $name): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->builder->validate_label_name($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validLabelNamesProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'simple' => ['status'],
|
||||||
|
'with_underscore' => ['http_method'],
|
||||||
|
'starts_with_underscore' => ['_internal'],
|
||||||
|
'uppercase' => ['METHOD'],
|
||||||
|
'alphanumeric' => ['label1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('invalidLabelNamesProvider')]
|
||||||
|
public function validate_label_name_rejects_invalid_names(string $name): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->builder->validate_label_name($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidLabelNamesProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'double_underscore_prefix' => ['__reserved'],
|
||||||
|
'starts_with_digit' => ['1label'],
|
||||||
|
'contains_colon' => ['label:name'],
|
||||||
|
'contains_dash' => ['label-name'],
|
||||||
|
'empty_string' => [''],
|
||||||
|
'contains_space' => ['label name'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── validate() ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_returns_empty_for_valid_metric(): void
|
||||||
|
{
|
||||||
|
$errors = $this->builder->validate($this->validMetric());
|
||||||
|
$this->assertEmpty($errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_requires_name(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['name'] = '';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('name is required', $errors[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_rejects_invalid_metric_name(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['name'] = '0invalid';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('must start with', $errors[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('reservedPrefixProvider')]
|
||||||
|
public function validate_rejects_reserved_prefix(string $prefix): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['name'] = $prefix . 'test';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('reserved prefix', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function reservedPrefixProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'wordpress_' => ['wordpress_'],
|
||||||
|
'go_' => ['go_'],
|
||||||
|
'process_' => ['process_'],
|
||||||
|
'promhttp_' => ['promhttp_'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_detects_duplicate_name(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'existing-id' => [
|
||||||
|
'id' => 'existing-id',
|
||||||
|
'name' => 'custom_existing',
|
||||||
|
'help' => 'test',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['name'] = 'custom_existing';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('already exists', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_allows_same_name_when_editing(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'my-id' => [
|
||||||
|
'id' => 'my-id',
|
||||||
|
'name' => 'custom_existing',
|
||||||
|
'help' => 'test',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['id'] = 'my-id';
|
||||||
|
$metric['name'] = 'custom_existing';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertEmpty($errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_requires_help_text(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['help'] = '';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Help text is required', $errors[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_requires_valid_type(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['type'] = 'counter';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Invalid metric type', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_rejects_too_many_labels(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['labels'] = ['a', 'b', 'c', 'd', 'e', 'f'];
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Maximum', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_rejects_invalid_label_names_in_array(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['labels'] = ['valid', '__reserved'];
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Invalid label name', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_rejects_non_array_labels(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['labels'] = 'not_an_array';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Labels must be an array', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_requires_valid_value_type(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['value_type'] = 'invalid';
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Invalid value type', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_requires_option_name_for_option_type(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['value_type'] = 'option';
|
||||||
|
$metric['value_config'] = [];
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Option name is required', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_accepts_option_type_with_option_name(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['value_type'] = 'option';
|
||||||
|
$metric['value_config'] = ['option_name' => 'my_wp_option'];
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertEmpty($errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_rejects_too_many_label_values(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['labels'] = ['status'];
|
||||||
|
$metric['label_values'] = array_fill(0, 51, ['active', 1.0]);
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('Maximum', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function validate_checks_label_value_row_count(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['labels'] = ['status', 'type'];
|
||||||
|
// Row has 2 items but needs 3 (2 labels + 1 value).
|
||||||
|
$metric['label_values'] = [['active', 1.0]];
|
||||||
|
|
||||||
|
$errors = $this->builder->validate($metric);
|
||||||
|
$this->assertNotEmpty($errors);
|
||||||
|
$this->assertStringContainsString('values for all labels', implode(' ', $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_all() / get() ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_all_returns_empty_array_by_default(): void
|
||||||
|
{
|
||||||
|
$this->assertSame([], $this->builder->get_all());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_all_returns_stored_metrics(): void
|
||||||
|
{
|
||||||
|
$metrics = ['id1' => ['name' => 'test_metric']];
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = $metrics;
|
||||||
|
|
||||||
|
$this->assertSame($metrics, $this->builder->get_all());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_all_returns_empty_when_option_is_not_array(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = 'not_an_array';
|
||||||
|
$this->assertSame([], $this->builder->get_all());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_returns_metric_by_id(): void
|
||||||
|
{
|
||||||
|
$metric = ['id' => 'my-id', 'name' => 'test_metric', 'help' => 'Test'];
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = ['my-id' => $metric];
|
||||||
|
|
||||||
|
$this->assertSame($metric, $this->builder->get('my-id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_returns_null_for_nonexistent_id(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->builder->get('nonexistent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── save() ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_creates_new_metric_and_returns_id(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$id = $this->builder->save($metric);
|
||||||
|
|
||||||
|
$this->assertNotEmpty($id);
|
||||||
|
$this->assertGreaterThanOrEqual(1, GlobalFunctionState::getCallCount('update_option'));
|
||||||
|
|
||||||
|
$saved = GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME];
|
||||||
|
$this->assertArrayHasKey($id, $saved);
|
||||||
|
$this->assertSame('custom_test_metric', $saved[$id]['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_throws_on_validation_failure(): void
|
||||||
|
{
|
||||||
|
$metric = [
|
||||||
|
'name' => '',
|
||||||
|
'help' => '',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->builder->save($metric);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_updates_existing_metric(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'existing-id' => [
|
||||||
|
'id' => 'existing-id',
|
||||||
|
'name' => 'custom_test_metric',
|
||||||
|
'help' => 'Original help',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
'labels' => [],
|
||||||
|
'created_at' => 1000000,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['id'] = 'existing-id';
|
||||||
|
$metric['help'] = 'Updated help';
|
||||||
|
|
||||||
|
$id = $this->builder->save($metric);
|
||||||
|
|
||||||
|
$this->assertSame('existing-id', $id);
|
||||||
|
$saved = GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME];
|
||||||
|
$this->assertSame('Updated help', $saved['existing-id']['help']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_sanitizes_metric_data(): void
|
||||||
|
{
|
||||||
|
$metric = $this->validMetric();
|
||||||
|
$metric['labels'] = ['valid_label'];
|
||||||
|
$metric['label_values'] = [['active', 42.5]];
|
||||||
|
|
||||||
|
$id = $this->builder->save($metric);
|
||||||
|
$saved = GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME][$id];
|
||||||
|
|
||||||
|
$this->assertSame(['valid_label'], $saved['labels']);
|
||||||
|
$this->assertSame([['active', 42.5]], $saved['label_values']);
|
||||||
|
$this->assertIsBool($saved['enabled']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── delete() ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function delete_removes_metric(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'my-id' => ['id' => 'my-id', 'name' => 'custom_metric'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertTrue($this->builder->delete('my-id'));
|
||||||
|
$this->assertArrayNotHasKey(
|
||||||
|
'my-id',
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function delete_returns_false_for_nonexistent(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->builder->delete('nonexistent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── export() ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function export_returns_valid_json_with_metadata(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'id1' => ['name' => 'metric1', 'help' => 'Test 1'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$json = $this->builder->export();
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
|
||||||
|
$this->assertIsArray($data);
|
||||||
|
$this->assertSame(CustomMetricBuilder::EXPORT_VERSION, $data['version']);
|
||||||
|
$this->assertSame(WP_PROMETHEUS_VERSION, $data['plugin_version']);
|
||||||
|
$this->assertArrayHasKey('exported_at', $data);
|
||||||
|
$this->assertCount(1, $data['metrics']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function export_does_not_include_site_url(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [];
|
||||||
|
|
||||||
|
$json = $this->builder->export();
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('site_url', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── import() ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_rejects_oversized_json(): void
|
||||||
|
{
|
||||||
|
$json = str_repeat('x', CustomMetricBuilder::MAX_IMPORT_SIZE + 1);
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('exceeds maximum size');
|
||||||
|
$this->builder->import($json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_rejects_invalid_json(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Invalid JSON');
|
||||||
|
$this->builder->import('{invalid json');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_rejects_missing_metrics_key(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('No metrics found');
|
||||||
|
$this->builder->import('{"version":"1.0.0"}');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_skip_mode_skips_duplicates(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'existing' => [
|
||||||
|
'id' => 'existing',
|
||||||
|
'name' => 'custom_existing_metric',
|
||||||
|
'help' => 'Test',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$json = json_encode([
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'metrics' => [
|
||||||
|
[
|
||||||
|
'name' => 'custom_existing_metric',
|
||||||
|
'help' => 'Test',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->builder->import($json, 'skip');
|
||||||
|
$this->assertSame(1, $result['skipped']);
|
||||||
|
$this->assertSame(0, $result['imported']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_rename_mode_renames_duplicates(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'existing' => [
|
||||||
|
'id' => 'existing',
|
||||||
|
'name' => 'custom_existing_metric',
|
||||||
|
'help' => 'Existing',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
'labels' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$json = json_encode([
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'metrics' => [
|
||||||
|
[
|
||||||
|
'name' => 'custom_existing_metric',
|
||||||
|
'help' => 'Imported',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
'labels' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->builder->import($json, 'rename');
|
||||||
|
$this->assertSame(1, $result['imported']);
|
||||||
|
|
||||||
|
$all = $this->builder->get_all();
|
||||||
|
$names = array_column($all, 'name');
|
||||||
|
$this->assertContains('custom_existing_metric', $names);
|
||||||
|
$this->assertContains('custom_existing_metric_imported_1', $names);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_invalid_mode_defaults_to_skip(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||||
|
'existing' => [
|
||||||
|
'id' => 'existing',
|
||||||
|
'name' => 'custom_existing_metric',
|
||||||
|
'help' => 'Test',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$json = json_encode([
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'metrics' => [
|
||||||
|
[
|
||||||
|
'name' => 'custom_existing_metric',
|
||||||
|
'help' => 'Test',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->builder->import($json, 'invalid_mode');
|
||||||
|
$this->assertSame(1, $result['skipped']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_counts_metrics_without_name_as_errors(): void
|
||||||
|
{
|
||||||
|
$json = json_encode([
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'metrics' => [['help' => 'No name']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->builder->import($json);
|
||||||
|
$this->assertSame(1, $result['errors']);
|
||||||
|
$this->assertSame(0, $result['imported']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function import_successfully_imports_new_metric(): void
|
||||||
|
{
|
||||||
|
$json = json_encode([
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'metrics' => [
|
||||||
|
[
|
||||||
|
'name' => 'custom_new_metric',
|
||||||
|
'help' => 'A new metric',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
'labels' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->builder->import($json);
|
||||||
|
$this->assertSame(1, $result['imported']);
|
||||||
|
$this->assertSame(0, $result['skipped']);
|
||||||
|
$this->assertSame(0, $result['errors']);
|
||||||
|
|
||||||
|
$all = $this->builder->get_all();
|
||||||
|
$names = array_column($all, 'name');
|
||||||
|
$this->assertContains('custom_new_metric', $names);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Constants ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function constants_are_defined(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('wp_prometheus_custom_metrics', CustomMetricBuilder::OPTION_NAME);
|
||||||
|
$this->assertSame(5, CustomMetricBuilder::MAX_LABELS);
|
||||||
|
$this->assertSame(50, CustomMetricBuilder::MAX_LABEL_VALUES);
|
||||||
|
$this->assertSame(1048576, CustomMetricBuilder::MAX_IMPORT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function validMetric(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'custom_test_metric',
|
||||||
|
'help' => 'A test metric',
|
||||||
|
'type' => 'gauge',
|
||||||
|
'value_type' => 'static',
|
||||||
|
'labels' => [],
|
||||||
|
'label_values' => [],
|
||||||
|
'enabled' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
234
tests/Unit/Metrics/RuntimeCollectorTest.php
Normal file
234
tests/Unit/Metrics/RuntimeCollectorTest.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Metrics\RuntimeCollector;
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
|
||||||
|
#[CoversClass(RuntimeCollector::class)]
|
||||||
|
class RuntimeCollectorTest extends TestCase
|
||||||
|
{
|
||||||
|
private RuntimeCollector $collector;
|
||||||
|
private array $originalServer = [];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->originalServer = $_SERVER;
|
||||||
|
$this->collector = $this->createInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$_SERVER = $this->originalServer;
|
||||||
|
$this->resetSingleton();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Singleton ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_instance_returns_singleton(): void
|
||||||
|
{
|
||||||
|
$instance1 = RuntimeCollector::get_instance();
|
||||||
|
$instance2 = RuntimeCollector::get_instance();
|
||||||
|
|
||||||
|
$this->assertSame($instance1, $instance2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_instance_returns_runtime_collector(): void
|
||||||
|
{
|
||||||
|
$instance = RuntimeCollector::get_instance();
|
||||||
|
$this->assertInstanceOf(RuntimeCollector::class, $instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_stored_metrics() ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_stored_metrics_returns_default_when_empty(): void
|
||||||
|
{
|
||||||
|
$metrics = $this->collector->get_stored_metrics();
|
||||||
|
$this->assertSame([], $metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_stored_metrics_returns_stored_data(): void
|
||||||
|
{
|
||||||
|
$stored = [
|
||||||
|
'counters' => ['key' => ['name' => 'test', 'value' => 5]],
|
||||||
|
'histograms' => [],
|
||||||
|
'last_reset' => 1000000,
|
||||||
|
];
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = $stored;
|
||||||
|
|
||||||
|
$metrics = $this->collector->get_stored_metrics();
|
||||||
|
$this->assertSame($stored, $metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_stored_metrics_returns_default_for_non_array(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = 'invalid';
|
||||||
|
|
||||||
|
$metrics = $this->collector->get_stored_metrics();
|
||||||
|
$this->assertArrayHasKey('counters', $metrics);
|
||||||
|
$this->assertArrayHasKey('histograms', $metrics);
|
||||||
|
$this->assertArrayHasKey('last_reset', $metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── reset_metrics() ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function reset_metrics_deletes_option(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = ['data'];
|
||||||
|
|
||||||
|
RuntimeCollector::reset_metrics();
|
||||||
|
|
||||||
|
$this->assertSame(1, GlobalFunctionState::getCallCount('delete_option'));
|
||||||
|
$this->assertArrayNotHasKey(
|
||||||
|
'wp_prometheus_runtime_metrics',
|
||||||
|
GlobalFunctionState::$options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_duration_buckets() ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_duration_buckets_returns_expected_values(): void
|
||||||
|
{
|
||||||
|
$buckets = RuntimeCollector::get_duration_buckets();
|
||||||
|
|
||||||
|
$this->assertIsArray($buckets);
|
||||||
|
$this->assertCount(11, $buckets);
|
||||||
|
$this->assertSame(0.005, $buckets[0]);
|
||||||
|
$this->assertEquals(10, end($buckets));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function duration_buckets_are_in_ascending_order(): void
|
||||||
|
{
|
||||||
|
$buckets = RuntimeCollector::get_duration_buckets();
|
||||||
|
$sorted = $buckets;
|
||||||
|
sort($sorted);
|
||||||
|
|
||||||
|
$this->assertSame($sorted, $buckets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_normalized_endpoint() (private, via reflection) ──────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function normalized_endpoint_returns_admin_when_is_admin(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_URI'] = '/wp-admin/options-general.php';
|
||||||
|
GlobalFunctionState::$options['__is_admin'] = true;
|
||||||
|
|
||||||
|
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||||
|
$this->assertSame('admin', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function normalized_endpoint_returns_ajax_when_doing_ajax(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_URI'] = '/wp-admin/admin-ajax.php';
|
||||||
|
GlobalFunctionState::$options['__wp_doing_ajax'] = true;
|
||||||
|
|
||||||
|
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||||
|
$this->assertSame('ajax', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function normalized_endpoint_returns_cron_when_doing_cron(): void
|
||||||
|
{
|
||||||
|
// Use a generic URI (not /wp-cron.php) to ensure function is checked, not URL pattern.
|
||||||
|
$_SERVER['REQUEST_URI'] = '/some-page';
|
||||||
|
GlobalFunctionState::$options['__wp_doing_cron'] = true;
|
||||||
|
|
||||||
|
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||||
|
$this->assertSame('cron', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('urlEndpointProvider')]
|
||||||
|
public function normalized_endpoint_from_url_pattern(string $uri, string $expected): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_URI'] = $uri;
|
||||||
|
|
||||||
|
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||||
|
$this->assertSame($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function urlEndpointProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'rest api' => ['/wp-json/wp/v2/posts', 'rest-api'],
|
||||||
|
'login page' => ['/wp-login.php', 'login'],
|
||||||
|
'login with query' => ['/wp-login.php?action=login', 'login'],
|
||||||
|
'wp-cron' => ['/wp-cron.php', 'cron'],
|
||||||
|
'feed root' => ['/feed/', 'feed'],
|
||||||
|
'feed trailing' => ['/category/news/feed', 'feed'],
|
||||||
|
'feed with slash' => ['/feed', 'feed'],
|
||||||
|
'homepage' => ['/', 'frontend'],
|
||||||
|
'page' => ['/about-us', 'frontend'],
|
||||||
|
'post' => ['/2024/01/hello-world', 'frontend'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── is_metrics_request() (private, via reflection) ───────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function is_metrics_request_true_for_metrics_uri(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_URI'] = '/metrics';
|
||||||
|
$this->assertTrue($this->callPrivateMethod('is_metrics_request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function is_metrics_request_true_with_trailing_slash(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_URI'] = '/metrics/';
|
||||||
|
$this->assertTrue($this->callPrivateMethod('is_metrics_request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function is_metrics_request_false_for_other_uri(): void
|
||||||
|
{
|
||||||
|
$_SERVER['REQUEST_URI'] = '/some-page';
|
||||||
|
$this->assertFalse($this->callPrivateMethod('is_metrics_request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function is_metrics_request_false_when_no_uri(): void
|
||||||
|
{
|
||||||
|
unset($_SERVER['REQUEST_URI']);
|
||||||
|
$this->assertFalse($this->callPrivateMethod('is_metrics_request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function createInstance(): RuntimeCollector
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(RuntimeCollector::class);
|
||||||
|
return $reflection->newInstanceWithoutConstructor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetSingleton(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(RuntimeCollector::class);
|
||||||
|
$property = $reflection->getProperty('instance');
|
||||||
|
$property->setValue(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function callPrivateMethod(string $method, array $args = []): mixed
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionMethod($this->collector, $method);
|
||||||
|
return $reflection->invoke($this->collector, ...$args);
|
||||||
|
}
|
||||||
|
}
|
||||||
411
tests/Unit/Metrics/StorageFactoryTest.php
Normal file
411
tests/Unit/Metrics/StorageFactoryTest.php
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Prometheus\Storage\InMemory;
|
||||||
|
|
||||||
|
#[CoversClass(StorageFactory::class)]
|
||||||
|
class StorageFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var list<string> Environment variables to clean up after each test. */
|
||||||
|
private array $envVarsToClean = [];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
StorageFactory::reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
StorageFactory::reset();
|
||||||
|
foreach ($this->envVarsToClean as $var) {
|
||||||
|
putenv($var);
|
||||||
|
}
|
||||||
|
$this->envVarsToClean = [];
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Adapter Constants ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function adapter_constants_are_defined(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('inmemory', StorageFactory::ADAPTER_INMEMORY);
|
||||||
|
$this->assertSame('redis', StorageFactory::ADAPTER_REDIS);
|
||||||
|
$this->assertSame('apcu', StorageFactory::ADAPTER_APCU);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_available_adapters() ─────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_available_adapters_returns_all_three(): void
|
||||||
|
{
|
||||||
|
$adapters = StorageFactory::get_available_adapters();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey(StorageFactory::ADAPTER_INMEMORY, $adapters);
|
||||||
|
$this->assertArrayHasKey(StorageFactory::ADAPTER_REDIS, $adapters);
|
||||||
|
$this->assertArrayHasKey(StorageFactory::ADAPTER_APCU, $adapters);
|
||||||
|
$this->assertCount(3, $adapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── is_adapter_available() ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function inmemory_adapter_is_always_available(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(StorageFactory::is_adapter_available(StorageFactory::ADAPTER_INMEMORY));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function unknown_adapter_is_not_available(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse(StorageFactory::is_adapter_available('unknown'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function redis_availability_depends_on_extension(): void
|
||||||
|
{
|
||||||
|
$extensionLoaded = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'extension_loaded'
|
||||||
|
);
|
||||||
|
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$this->assertFalse(StorageFactory::is_adapter_available(StorageFactory::ADAPTER_REDIS));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function apcu_availability_requires_extension_and_enabled(): void
|
||||||
|
{
|
||||||
|
$extensionLoaded = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'extension_loaded'
|
||||||
|
);
|
||||||
|
$extensionLoaded->expects($this->any())->willReturn(true);
|
||||||
|
|
||||||
|
$apcuEnabled = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'apcu_enabled'
|
||||||
|
);
|
||||||
|
$apcuEnabled->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$this->assertFalse(StorageFactory::is_adapter_available(StorageFactory::ADAPTER_APCU));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_configured_adapter() ─────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function default_configured_adapter_is_inmemory(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('inmemory', StorageFactory::get_configured_adapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function configured_adapter_reads_from_env_var(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'redis');
|
||||||
|
|
||||||
|
$this->assertSame('redis', StorageFactory::get_configured_adapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function configured_adapter_reads_from_option(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_storage_adapter'] = 'apcu';
|
||||||
|
|
||||||
|
$this->assertSame('apcu', StorageFactory::get_configured_adapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function env_var_takes_precedence_over_option(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'redis');
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_storage_adapter'] = 'apcu';
|
||||||
|
|
||||||
|
$this->assertSame('redis', StorageFactory::get_configured_adapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function configured_adapter_lowercases_env_value(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'REDIS');
|
||||||
|
|
||||||
|
$this->assertSame('redis', StorageFactory::get_configured_adapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_redis_config() ───────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_redis_config_returns_defaults(): void
|
||||||
|
{
|
||||||
|
$config = StorageFactory::get_redis_config();
|
||||||
|
|
||||||
|
$this->assertSame('127.0.0.1', $config['host']);
|
||||||
|
$this->assertSame(6379, $config['port']);
|
||||||
|
$this->assertSame('', $config['password']);
|
||||||
|
$this->assertSame(0, $config['database']);
|
||||||
|
$this->assertSame('WORDPRESS_PROMETHEUS_', $config['prefix']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_redis_config_reads_from_env_vars(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_REDIS_HOST', '10.0.0.1');
|
||||||
|
$this->setEnv('WP_PROMETHEUS_REDIS_PORT', '6380');
|
||||||
|
$this->setEnv('WP_PROMETHEUS_REDIS_PASSWORD', 's3cret');
|
||||||
|
$this->setEnv('WP_PROMETHEUS_REDIS_DATABASE', '2');
|
||||||
|
$this->setEnv('WP_PROMETHEUS_REDIS_PREFIX', 'MY_PREFIX_');
|
||||||
|
|
||||||
|
$config = StorageFactory::get_redis_config();
|
||||||
|
|
||||||
|
$this->assertSame('10.0.0.1', $config['host']);
|
||||||
|
$this->assertSame(6380, $config['port']);
|
||||||
|
$this->assertSame('s3cret', $config['password']);
|
||||||
|
$this->assertSame(2, $config['database']);
|
||||||
|
$this->assertSame('MY_PREFIX_', $config['prefix']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_redis_config_reads_from_option(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_redis_config'] = [
|
||||||
|
'host' => '192.168.1.1',
|
||||||
|
'port' => 6381,
|
||||||
|
'password' => 'optpass',
|
||||||
|
'database' => 3,
|
||||||
|
'prefix' => 'OPT_',
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = StorageFactory::get_redis_config();
|
||||||
|
|
||||||
|
$this->assertSame('192.168.1.1', $config['host']);
|
||||||
|
$this->assertSame(6381, $config['port']);
|
||||||
|
$this->assertSame('optpass', $config['password']);
|
||||||
|
$this->assertSame(3, $config['database']);
|
||||||
|
$this->assertSame('OPT_', $config['prefix']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_redis_config_env_takes_precedence(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_REDIS_HOST', 'env-host');
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_redis_config'] = [
|
||||||
|
'host' => 'option-host',
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = StorageFactory::get_redis_config();
|
||||||
|
$this->assertSame('env-host', $config['host']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_apcu_prefix() ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_apcu_prefix_returns_default(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('wp_prom', StorageFactory::get_apcu_prefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_apcu_prefix_reads_from_env(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_APCU_PREFIX', 'custom_prefix');
|
||||||
|
|
||||||
|
$this->assertSame('custom_prefix', StorageFactory::get_apcu_prefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_apcu_prefix_reads_from_option(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_apcu_prefix'] = 'opt_prefix';
|
||||||
|
|
||||||
|
$this->assertSame('opt_prefix', StorageFactory::get_apcu_prefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── save_config() ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_config_stores_adapter(): void
|
||||||
|
{
|
||||||
|
StorageFactory::save_config(['adapter' => 'redis']);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'redis',
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_storage_adapter']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_config_stores_redis_config(): void
|
||||||
|
{
|
||||||
|
StorageFactory::save_config([
|
||||||
|
'redis' => [
|
||||||
|
'host' => '10.0.0.5',
|
||||||
|
'port' => 6390,
|
||||||
|
'password' => 'pass123',
|
||||||
|
'database' => 1,
|
||||||
|
'prefix' => 'TEST_',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$saved = GlobalFunctionState::$options['wp_prometheus_redis_config'];
|
||||||
|
$this->assertSame('10.0.0.5', $saved['host']);
|
||||||
|
$this->assertSame(6390, $saved['port']);
|
||||||
|
$this->assertSame('pass123', $saved['password']);
|
||||||
|
$this->assertSame(1, $saved['database']);
|
||||||
|
// sanitize_key lowercases the prefix
|
||||||
|
$this->assertSame('test_', $saved['prefix']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_config_stores_apcu_prefix(): void
|
||||||
|
{
|
||||||
|
StorageFactory::save_config(['apcu_prefix' => 'my_apcu']);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'my_apcu',
|
||||||
|
GlobalFunctionState::$options['wp_prometheus_apcu_prefix']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function save_config_resets_singleton(): void
|
||||||
|
{
|
||||||
|
// Get an adapter (creates singleton).
|
||||||
|
$adapter1 = StorageFactory::get_adapter();
|
||||||
|
|
||||||
|
// Save new config (should reset singleton).
|
||||||
|
StorageFactory::save_config(['adapter' => 'inmemory']);
|
||||||
|
|
||||||
|
// Get adapter again — should be a new instance.
|
||||||
|
$adapter2 = StorageFactory::get_adapter();
|
||||||
|
$this->assertNotSame($adapter1, $adapter2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── test_connection() ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_connection_inmemory_always_succeeds(): void
|
||||||
|
{
|
||||||
|
$result = StorageFactory::test_connection(StorageFactory::ADAPTER_INMEMORY);
|
||||||
|
|
||||||
|
$this->assertTrue($result['success']);
|
||||||
|
$this->assertNotEmpty($result['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_connection_unknown_adapter_fails(): void
|
||||||
|
{
|
||||||
|
$result = StorageFactory::test_connection('unknown');
|
||||||
|
|
||||||
|
$this->assertFalse($result['success']);
|
||||||
|
$this->assertStringContainsString('Unknown', $result['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_connection_redis_fails_without_extension(): void
|
||||||
|
{
|
||||||
|
$extensionLoaded = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'extension_loaded'
|
||||||
|
);
|
||||||
|
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$result = StorageFactory::test_connection(StorageFactory::ADAPTER_REDIS);
|
||||||
|
$this->assertFalse($result['success']);
|
||||||
|
$this->assertStringContainsString('not installed', $result['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_connection_apcu_fails_without_extension(): void
|
||||||
|
{
|
||||||
|
$extensionLoaded = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'extension_loaded'
|
||||||
|
);
|
||||||
|
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$result = StorageFactory::test_connection(StorageFactory::ADAPTER_APCU);
|
||||||
|
$this->assertFalse($result['success']);
|
||||||
|
$this->assertStringContainsString('not installed', $result['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── get_adapter() / reset() / singleton ──────────────────────────
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_adapter_returns_inmemory_by_default(): void
|
||||||
|
{
|
||||||
|
$adapter = StorageFactory::get_adapter();
|
||||||
|
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_adapter_returns_singleton(): void
|
||||||
|
{
|
||||||
|
$adapter1 = StorageFactory::get_adapter();
|
||||||
|
$adapter2 = StorageFactory::get_adapter();
|
||||||
|
|
||||||
|
$this->assertSame($adapter1, $adapter2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function reset_clears_singleton_and_error(): void
|
||||||
|
{
|
||||||
|
StorageFactory::get_adapter();
|
||||||
|
StorageFactory::reset();
|
||||||
|
|
||||||
|
// After reset, get_last_error should be empty.
|
||||||
|
$this->assertEmpty(StorageFactory::get_last_error());
|
||||||
|
|
||||||
|
// Getting adapter again creates a new instance.
|
||||||
|
$adapter = StorageFactory::get_adapter();
|
||||||
|
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_adapter_falls_back_to_inmemory_when_redis_unavailable(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'redis');
|
||||||
|
|
||||||
|
$extensionLoaded = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'extension_loaded'
|
||||||
|
);
|
||||||
|
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$adapter = StorageFactory::get_adapter();
|
||||||
|
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||||
|
$this->assertNotEmpty(StorageFactory::get_last_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function get_adapter_falls_back_to_inmemory_when_apcu_unavailable(): void
|
||||||
|
{
|
||||||
|
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'apcu');
|
||||||
|
|
||||||
|
$extensionLoaded = $this->getFunctionMock(
|
||||||
|
'Magdev\\WpPrometheus\\Metrics',
|
||||||
|
'extension_loaded'
|
||||||
|
);
|
||||||
|
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||||
|
|
||||||
|
$adapter = StorageFactory::get_adapter();
|
||||||
|
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||||
|
$this->assertNotEmpty(StorageFactory::get_last_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function setEnv(string $name, string $value): void
|
||||||
|
{
|
||||||
|
putenv("$name=$value");
|
||||||
|
$this->envVarsToClean[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
tests/Unit/TestCase.php
Normal file
32
tests/Unit/TestCase.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WpPrometheus\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
use phpmock\phpunit\PHPMock;
|
||||||
|
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base test case for WP Prometheus unit tests.
|
||||||
|
*
|
||||||
|
* Provides the PHPMock trait for mocking WordPress functions
|
||||||
|
* called from namespaced code, and resets global state between tests.
|
||||||
|
*/
|
||||||
|
abstract class TestCase extends PHPUnitTestCase
|
||||||
|
{
|
||||||
|
use PHPMock;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
GlobalFunctionState::reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::reset();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
450
tests/bootstrap.php
Normal file
450
tests/bootstrap.php
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PHPUnit bootstrap file for WP Prometheus tests.
|
||||||
|
*
|
||||||
|
* Defines WordPress constants and global function stubs required
|
||||||
|
* for loading plugin source files without a WordPress environment.
|
||||||
|
*
|
||||||
|
* @package WP_Prometheus\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||||
|
|
||||||
|
// 1. Load Composer autoloader first (for GlobalFunctionState class).
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// 2. Define WordPress constants required by source files.
|
||||||
|
define('ABSPATH', '/tmp/wordpress/');
|
||||||
|
define('WP_CONTENT_DIR', '/tmp/wordpress/wp-content');
|
||||||
|
define('WP_PROMETHEUS_VERSION', '0.5.0');
|
||||||
|
define('WP_PROMETHEUS_FILE', dirname(__DIR__) . '/wp-prometheus.php');
|
||||||
|
define('WP_PROMETHEUS_PATH', dirname(__DIR__) . '/');
|
||||||
|
define('WP_PROMETHEUS_URL', 'https://example.com/wp-content/plugins/wp-prometheus/');
|
||||||
|
define('WP_PROMETHEUS_BASENAME', 'wp-prometheus/wp-prometheus.php');
|
||||||
|
|
||||||
|
// 3. Define global WordPress function stubs.
|
||||||
|
// These exist so plugin files can be require'd without fatal errors.
|
||||||
|
// Per-test behavior in namespaced code is controlled via php-mock.
|
||||||
|
// Global-scope tests use GlobalFunctionState for controllable behavior.
|
||||||
|
|
||||||
|
// -- Translation functions --
|
||||||
|
if (!function_exists('__')) {
|
||||||
|
function __(string $text, string $domain = 'default'): string
|
||||||
|
{
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('_e')) {
|
||||||
|
function _e(string $text, string $domain = 'default'): void
|
||||||
|
{
|
||||||
|
echo $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('esc_html__')) {
|
||||||
|
function esc_html__(string $text, string $domain = 'default'): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('esc_html_e')) {
|
||||||
|
function esc_html_e(string $text, string $domain = 'default'): void
|
||||||
|
{
|
||||||
|
echo htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Escaping functions --
|
||||||
|
if (!function_exists('esc_html')) {
|
||||||
|
function esc_html(string $text): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('esc_attr')) {
|
||||||
|
function esc_attr(string $text): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('esc_url')) {
|
||||||
|
function esc_url(string $url): string
|
||||||
|
{
|
||||||
|
return filter_var($url, FILTER_SANITIZE_URL) ?: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('esc_url_raw')) {
|
||||||
|
function esc_url_raw(string $url): string
|
||||||
|
{
|
||||||
|
return filter_var($url, FILTER_SANITIZE_URL) ?: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Sanitization functions --
|
||||||
|
if (!function_exists('sanitize_text_field')) {
|
||||||
|
function sanitize_text_field(string $str): string
|
||||||
|
{
|
||||||
|
return trim(strip_tags($str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('sanitize_key')) {
|
||||||
|
function sanitize_key(string $key): string
|
||||||
|
{
|
||||||
|
return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('sanitize_file_name')) {
|
||||||
|
function sanitize_file_name(string $name): string
|
||||||
|
{
|
||||||
|
return preg_replace('/[^a-zA-Z0-9_.\-]/', '', $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('sanitize_html_class')) {
|
||||||
|
function sanitize_html_class(string $class): string
|
||||||
|
{
|
||||||
|
return preg_replace('/[^a-zA-Z0-9_\-]/', '', $class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('absint')) {
|
||||||
|
function absint($value): int
|
||||||
|
{
|
||||||
|
return abs((int) $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_unslash')) {
|
||||||
|
function wp_unslash($value)
|
||||||
|
{
|
||||||
|
return is_string($value) ? stripslashes($value) : $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- WordPress utility functions --
|
||||||
|
if (!function_exists('wp_parse_url')) {
|
||||||
|
function wp_parse_url(string $url, int $component = -1)
|
||||||
|
{
|
||||||
|
return parse_url($url, $component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_json_encode')) {
|
||||||
|
function wp_json_encode($data, int $options = 0, int $depth = 512)
|
||||||
|
{
|
||||||
|
return json_encode($data, $options, $depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_generate_uuid4')) {
|
||||||
|
function wp_generate_uuid4(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0x0fff) | 0x4000,
|
||||||
|
mt_rand(0, 0x3fff) | 0x8000,
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_generate_password')) {
|
||||||
|
function wp_generate_password(int $length = 12, bool $special = true): string
|
||||||
|
{
|
||||||
|
return substr(bin2hex(random_bytes($length)), 0, $length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('size_format')) {
|
||||||
|
function size_format(int $bytes, int $decimals = 0): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
$power = $bytes > 0 ? floor(log($bytes, 1024)) : 0;
|
||||||
|
return number_format($bytes / pow(1024, $power), $decimals) . ' ' . $units[$power];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('path_is_absolute')) {
|
||||||
|
function path_is_absolute(string $path): bool
|
||||||
|
{
|
||||||
|
return str_starts_with($path, '/') || (bool) preg_match('#^[a-zA-Z]:\\\\#', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Hook functions (no-ops) --
|
||||||
|
if (!function_exists('add_action')) {
|
||||||
|
function add_action(string $hook, $callback, int $priority = 10, int $accepted_args = 1): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('add_filter')) {
|
||||||
|
function add_filter(string $hook, $callback, int $priority = 10, int $accepted_args = 1): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('remove_all_filters')) {
|
||||||
|
function remove_all_filters(string $hook, $priority = false): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('do_action')) {
|
||||||
|
function do_action(string $hook, ...$args): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('apply_filters')) {
|
||||||
|
function apply_filters(string $hook, $value, ...$args)
|
||||||
|
{
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Option functions (controllable via GlobalFunctionState) --
|
||||||
|
if (!function_exists('get_option')) {
|
||||||
|
function get_option(string $option, $default = false)
|
||||||
|
{
|
||||||
|
if (array_key_exists($option, GlobalFunctionState::$options)) {
|
||||||
|
return GlobalFunctionState::$options[$option];
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('update_option')) {
|
||||||
|
function update_option(string $option, $value, $autoload = null): bool
|
||||||
|
{
|
||||||
|
GlobalFunctionState::recordCall('update_option', $option, $value);
|
||||||
|
GlobalFunctionState::$options[$option] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('delete_option')) {
|
||||||
|
function delete_option(string $option): bool
|
||||||
|
{
|
||||||
|
GlobalFunctionState::recordCall('delete_option', $option);
|
||||||
|
unset(GlobalFunctionState::$options[$option]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('delete_transient')) {
|
||||||
|
function delete_transient(string $transient): bool
|
||||||
|
{
|
||||||
|
GlobalFunctionState::recordCall('delete_transient', $transient);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('flush_rewrite_rules')) {
|
||||||
|
function flush_rewrite_rules(bool $hard = true): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::recordCall('flush_rewrite_rules');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- URL functions --
|
||||||
|
if (!function_exists('home_url')) {
|
||||||
|
function home_url(string $path = ''): string
|
||||||
|
{
|
||||||
|
return 'https://example.com' . $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('admin_url')) {
|
||||||
|
function admin_url(string $path = ''): string
|
||||||
|
{
|
||||||
|
return 'https://example.com/wp-admin/' . $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Conditional functions (controllable via GlobalFunctionState) --
|
||||||
|
if (!function_exists('is_admin')) {
|
||||||
|
function is_admin(): bool
|
||||||
|
{
|
||||||
|
return GlobalFunctionState::$options['__is_admin'] ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_doing_ajax')) {
|
||||||
|
function wp_doing_ajax(): bool
|
||||||
|
{
|
||||||
|
return GlobalFunctionState::$options['__wp_doing_ajax'] ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_doing_cron')) {
|
||||||
|
function wp_doing_cron(): bool
|
||||||
|
{
|
||||||
|
return GlobalFunctionState::$options['__wp_doing_cron'] ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('is_multisite')) {
|
||||||
|
function is_multisite(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Plugin functions --
|
||||||
|
if (!function_exists('load_plugin_textdomain')) {
|
||||||
|
function load_plugin_textdomain(string $domain, $deprecated = false, string $path = ''): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('register_activation_hook')) {
|
||||||
|
function register_activation_hook(string $file, $callback): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('register_deactivation_hook')) {
|
||||||
|
function register_deactivation_hook(string $file, $callback): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Rewrite functions --
|
||||||
|
if (!function_exists('add_rewrite_rule')) {
|
||||||
|
function add_rewrite_rule(string $regex, string $redirect, string $after = 'bottom'): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::recordCall('add_rewrite_rule', $regex, $redirect, $after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('add_rewrite_tag')) {
|
||||||
|
function add_rewrite_tag(string $tag, string $regex, string $query = ''): void
|
||||||
|
{
|
||||||
|
GlobalFunctionState::recordCall('add_rewrite_tag', $tag, $regex, $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- HTTP functions --
|
||||||
|
if (!function_exists('status_header')) {
|
||||||
|
function status_header(int $code, string $description = ''): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('hash_equals')) {
|
||||||
|
// hash_equals is a PHP built-in, but define stub just in case.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_rand')) {
|
||||||
|
function wp_rand(int $min = 0, int $max = 0): int
|
||||||
|
{
|
||||||
|
return random_int($min, max($min, $max ?: PHP_INT_MAX >> 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('get_bloginfo')) {
|
||||||
|
function get_bloginfo(string $show = '', bool $filter = true): string
|
||||||
|
{
|
||||||
|
return match ($show) {
|
||||||
|
'version' => '6.7',
|
||||||
|
'language' => 'en-US',
|
||||||
|
'name' => 'Test Site',
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('current_user_can')) {
|
||||||
|
function current_user_can(string $capability, ...$args): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('deactivate_plugins')) {
|
||||||
|
function deactivate_plugins($plugins, bool $silent = false, $network_wide = null): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_die')) {
|
||||||
|
function wp_die($message = '', $title = '', $args = []): void
|
||||||
|
{
|
||||||
|
throw new \RuntimeException((string) $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Plugin global authentication functions (from wp-prometheus.php) --
|
||||||
|
// Cannot include wp-prometheus.php directly due to constant definitions
|
||||||
|
// and side effects. These mirror the production code for testing.
|
||||||
|
|
||||||
|
if (!function_exists('wp_prometheus_get_authorization_header')) {
|
||||||
|
function wp_prometheus_get_authorization_header(): string
|
||||||
|
{
|
||||||
|
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||||
|
return sanitize_text_field(wp_unslash($_SERVER['HTTP_AUTHORIZATION']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
|
||||||
|
return sanitize_text_field(wp_unslash($_SERVER['REDIRECT_HTTP_AUTHORIZATION']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('apache_request_headers')) {
|
||||||
|
$headers = apache_request_headers();
|
||||||
|
if (isset($headers['Authorization'])) {
|
||||||
|
return sanitize_text_field($headers['Authorization']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- WordPress core class stubs --
|
||||||
|
if (!class_exists('WP')) {
|
||||||
|
class WP
|
||||||
|
{
|
||||||
|
public array $query_vars = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_prometheus_authenticate_request')) {
|
||||||
|
function wp_prometheus_authenticate_request(): bool
|
||||||
|
{
|
||||||
|
$auth_token = get_option('wp_prometheus_auth_token', '');
|
||||||
|
|
||||||
|
// If no token is set, deny access.
|
||||||
|
if (empty($auth_token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Bearer token in Authorization header.
|
||||||
|
$auth_header = wp_prometheus_get_authorization_header();
|
||||||
|
if (!empty($auth_header) && preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) {
|
||||||
|
return hash_equals($auth_token, $matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for token in query parameter.
|
||||||
|
if (isset($_GET['token']) && hash_equals($auth_token, sanitize_text_field(wp_unslash($_GET['token'])))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WP Prometheus
|
* Plugin Name: WP Prometheus
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus
|
||||||
* Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics.
|
* Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics.
|
||||||
* Version: 0.2.0
|
* Version: 0.5.1
|
||||||
* Requires at least: 6.4
|
* Requires at least: 6.4
|
||||||
* Requires PHP: 8.3
|
* Requires PHP: 8.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
@@ -21,12 +21,185 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
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 using shared helper.
|
||||||
|
if ( ! wp_prometheus_authenticate_request() ) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate a metrics request using Bearer token or query parameter.
|
||||||
|
*
|
||||||
|
* Shared authentication logic used by both the MetricsEndpoint class
|
||||||
|
* and the isolated mode handler to avoid code duplication.
|
||||||
|
*
|
||||||
|
* @return bool True if authenticated, false otherwise.
|
||||||
|
*/
|
||||||
|
function wp_prometheus_authenticate_request(): bool {
|
||||||
|
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
|
||||||
|
|
||||||
|
// If no token is set, deny access.
|
||||||
|
if ( empty( $auth_token ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Bearer token in Authorization header.
|
||||||
|
$auth_header = wp_prometheus_get_authorization_header();
|
||||||
|
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
|
||||||
|
return hash_equals( $auth_token, $matches[1] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for token in query parameter (less secure but useful for testing).
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
|
||||||
|
if ( isset( $_GET['token'] ) && hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Authorization header from the request.
|
||||||
|
*
|
||||||
|
* Checks multiple sources for the Authorization header to support
|
||||||
|
* different server configurations (Apache, nginx, CGI, etc.).
|
||||||
|
*
|
||||||
|
* @return string The Authorization header value, or empty string if not found.
|
||||||
|
*/
|
||||||
|
function wp_prometheus_get_authorization_header(): string {
|
||||||
|
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
|
||||||
|
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
|
||||||
|
return sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( function_exists( 'apache_request_headers' ) ) {
|
||||||
|
$headers = apache_request_headers();
|
||||||
|
if ( isset( $headers['Authorization'] ) ) {
|
||||||
|
return sanitize_text_field( $headers['Authorization'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try early metrics handling before full plugin initialization.
|
||||||
|
wp_prometheus_early_metrics_check();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin version.
|
* Plugin version.
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
define( 'WP_PROMETHEUS_VERSION', '0.2.0' );
|
define( 'WP_PROMETHEUS_VERSION', '0.5.1' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin file path.
|
* Plugin file path.
|
||||||
|
|||||||
Reference in New Issue
Block a user