6 Commits

Author SHA1 Message Date
bad977bef0 feat: Add custom metric builder, export/import, and Grafana dashboards (v0.3.0)
All checks were successful
Create Release Package / build-release (push) Successful in 59s
- Custom Metrics Builder with admin UI for gauge metrics
- Support for static values and WordPress option-based values
- JSON-based export/import with skip/overwrite/rename modes
- Three Grafana dashboard templates (overview, runtime, WooCommerce)
- New tabs: Custom Metrics and Dashboards
- Reset runtime metrics button

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 15:27:16 +01:00
da6d5081f7 fix: Localhost license bypass, rewrite rules flush, WooCommerce orders (v0.2.2)
All checks were successful
Create Release Package / build-release (push) Successful in 56s
- Add localhost license bypass for development environments
- Flush rewrite rules when license status changes to fix 404 on /metrics
- Fix wc_orders_count() missing required status parameter

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:57:43 +01:00
3eb66b0ebe feat: Add WooCommerce, cron, and transient metrics (v0.2.0)
All checks were successful
Create Release Package / build-release (push) Successful in 56s
- WooCommerce integration metrics (products, orders, revenue, customers)
- Cron job metrics (events by hook, overdue count, next run timestamp)
- Transient cache metrics (total, expiring, expired)
- Support for WooCommerce HPOS storage
- Updated settings page with new metric categories
- Updated translations and documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:41:09 +01:00
bc108f6bd5 feat: Reorganize settings page with tabbed interface (v0.1.1)
All checks were successful
Create Release Package / build-release (push) Successful in 57s
- Add tabbed navigation (License, Metrics, Help)
- Move Prometheus configuration to dedicated Help tab
- Separate static and runtime metrics with descriptions
- Add admin CSS for tab styling
- Add endpoint info, curl examples, and metrics reference in Help tab

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:30:11 +01:00
6256ba777c feat: Add runtime metrics for HTTP requests and database queries (v0.1.0)
All checks were successful
Create Release Package / build-release (push) Successful in 59s
- Add RuntimeCollector class for tracking request lifecycle metrics
- Add wordpress_http_requests_total counter (method, status, endpoint)
- Add wordpress_http_request_duration_seconds histogram
- Add wordpress_db_queries_total counter (endpoint)
- Add wordpress_db_query_duration_seconds histogram (requires SAVEQUERIES)
- Update Collector to expose stored runtime metrics
- Add new settings options for enabling/disabling runtime metrics
- Create translation files (.pot, .po, .mo) for internationalization
- Update documentation and changelog

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:24:05 +01:00
f1748727ce docs: Update CLAUDE.md with v0.0.2 session learnings
- Document composer path repository configuration with version aliases
- Document Gitea CI/CD fix for handling re-releases
- Add key learnings about submodule handling in CI environments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:52:40 +01:00
21 changed files with 8185 additions and 419 deletions

View File

@@ -5,6 +5,105 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.0] - 2026-02-02
### Added
- Custom Metrics Builder:
- Admin UI to define custom gauge metrics
- Support for static values and WordPress option-based values
- Label support with up to 5 labels and 50 value combinations
- Metric validation following Prometheus naming conventions
- Metric Export/Import:
- JSON-based configuration export for backup
- Import with three modes: skip existing, overwrite, or rename duplicates
- Version tracking in export format
- Grafana Dashboard Templates:
- WordPress Overview dashboard (users, posts, comments, cron, transients)
- WordPress Runtime dashboard (HTTP requests, database queries)
- WordPress WooCommerce dashboard (orders, revenue, products, customers)
- Easy download and import instructions
- New "Custom Metrics" tab in admin settings
- New "Dashboards" tab in admin settings
- Reset runtime metrics button to clear accumulated data
### Changed
- Settings page now has 5 tabs: License, Metrics, Custom Metrics, Dashboards, Help
- Updated translations with all new strings
## [0.2.2] - 2026-02-02
### Fixed
- Fixed `wc_orders_count()` call missing required status parameter in WooCommerce orders metrics
## [0.2.1] - 2026-02-02
### Added
- Localhost license bypass for development environments (localhost, 127.0.0.1, ::1, \*.localhost, \*.local)
- Automatic rewrite rules flush when license status changes
### Fixed
- Fixed 404 error on `/metrics` endpoint when license becomes valid after plugin activation
## [0.2.0] - 2026-02-02
### Added
- WooCommerce integration metrics (when WooCommerce is active):
- `wordpress_woocommerce_products_total` - Products by status and type
- `wordpress_woocommerce_orders_total` - Orders by status
- `wordpress_woocommerce_revenue_total` - Revenue (all time, today, month)
- `wordpress_woocommerce_customers_total` - Customers (registered, guest)
- Cron job metrics:
- `wordpress_cron_events_total` - Scheduled cron events by hook
- `wordpress_cron_overdue_total` - Number of overdue cron events
- `wordpress_cron_next_run_timestamp` - Unix timestamp of next scheduled cron
- Transient cache metrics:
- `wordpress_transients_total` - Transients by type (total, with_expiration, persistent, expired)
- WooCommerce metrics section in settings (only visible when WooCommerce is active)
- Support for WooCommerce HPOS (High-Performance Order Storage)
### Changed
- Updated Help tab with new metrics reference
## [0.1.1] - 2026-02-02
### Changed
- Reorganized settings page with tabbed interface (License, Metrics, Help tabs)
- Moved Prometheus configuration help to dedicated Help tab
- Separated static and runtime metrics in settings with descriptions
- Added admin CSS for improved tab styling
### Added
- New Help tab with endpoint information, curl examples, and metrics reference table
- Custom code examples section in Help tab
## [0.1.0] - 2026-02-02
### Added
- HTTP request metrics:
- `wordpress_http_requests_total` - Counter of HTTP requests by method, status code, and endpoint
- `wordpress_http_request_duration_seconds` - Histogram of request durations
- Database query metrics:
- `wordpress_db_queries_total` - Counter of database queries by endpoint
- `wordpress_db_query_duration_seconds` - Histogram of query durations (requires SAVEQUERIES)
- RuntimeCollector class for collecting metrics during WordPress request lifecycle
- New settings options for enabling/disabling runtime metrics
- Translation files (.pot, .po, .mo) for German (Switzerland)
### Changed
- Metrics are now categorized into static metrics (users, posts, etc.) and runtime metrics (HTTP, database)
- Runtime metrics only collected when explicitly enabled and license is valid
## [0.0.2] - 2026-02-01
### Fixed

120
CLAUDE.md
View File

@@ -14,6 +14,13 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a
- Prometheus compatible authenticated `/metrics` endpoint
- Optional default metrics (users, posts, comments, plugins)
- Runtime metrics (HTTP requests, request duration, database queries)
- Cron job metrics (scheduled events, overdue, next run)
- Transient cache metrics (total, expiring, expired)
- WooCommerce integration (products, orders, revenue, customers)
- Custom metric builder with admin UI (gauges with static or option-based values)
- Metric export/import for backup and site migration
- Grafana dashboard templates for easy visualization
- Dedicated plugin settings under 'Settings/Metrics' menu
- Extensible by other plugins using `wp_prometheus_collect_metrics` action hook
- License management integration
@@ -26,11 +33,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
### Version 0.1.0 (Planned)
- Add request/response timing metrics
- Add HTTP status code counters
- Add database query metrics
*No planned features at this time.*
## Technical Stack
@@ -210,6 +213,10 @@ wp-prometheus/
│ └── release.yml # CI/CD pipeline
├── assets/
│ ├── css/ # Admin/Frontend styles
│ ├── dashboards/ # Grafana dashboard templates
│ │ ├── wordpress-overview.json
│ │ ├── wordpress-runtime.json
│ │ └── wordpress-woocommerce.json
│ └── js/
│ └── admin.js # Admin JavaScript
├── languages/ # Translation files
@@ -218,13 +225,16 @@ wp-prometheus/
├── releases/ # Release packages
├── src/
│ ├── Admin/
│ │ ├── DashboardProvider.php # Grafana dashboard provider
│ │ └── Settings.php # Settings page
│ ├── Endpoint/
│ │ └── MetricsEndpoint.php # /metrics endpoint
│ ├── License/
│ │ └── Manager.php # License management
│ ├── Metrics/
│ │ ── Collector.php # Prometheus metrics collector
│ │ ── Collector.php # Prometheus metrics collector
│ │ ├── CustomMetricBuilder.php # Custom metric CRUD
│ │ └── RuntimeCollector.php # Runtime metrics collector
│ ├── Installer.php # Activation/Deactivation
│ ├── Plugin.php # Main plugin class
│ └── index.php
@@ -280,6 +290,104 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
## Session History
### 2026-02-02 - Custom Metrics & Dashboards (v0.3.0)
- Added Custom Metric Builder with full admin UI:
- `CustomMetricBuilder.php` - CRUD operations, validation, export/import
- Support for static values and WordPress option-based values
- Label support (max 5 labels, 50 value combinations)
- Prometheus naming convention validation (`[a-zA-Z_:][a-zA-Z0-9_:]*`)
- Added Grafana Dashboard Templates:
- `DashboardProvider.php` - Dashboard file provider with path traversal protection
- `wordpress-overview.json` - General WordPress metrics
- `wordpress-runtime.json` - HTTP/DB performance metrics
- `wordpress-woocommerce.json` - WooCommerce store metrics
- Added export/import functionality:
- JSON-based configuration export
- Three import modes: skip, overwrite, rename duplicates
- Version tracking in export format
- Updated Settings page with new tabs:
- "Custom Metrics" tab with metric form and table
- "Dashboards" tab with download buttons
- "Reset Runtime Metrics" button in Metrics tab
- Updated `Collector.php` to integrate custom metrics
- Updated translation files with all new strings
- **Key Learning**: Dynamic form handling in WordPress admin
- Use `wp_create_nonce()` with unique nonce names per AJAX action
- Localize script with `wp_localize_script()` for nonces and AJAX URL
- Always verify `current_user_can('manage_options')` in AJAX handlers
- **Key Learning**: Grafana dashboard JSON format
- Use `${DS_PROMETHEUS}` for data source variable
- Schema version 39 for current Grafana compatibility
- Panels use `gridPos` for layout positioning
### 2026-02-02 - Extended Metrics (v0.2.0)
- Added WooCommerce integration metrics (only when WooCommerce is active):
- `wordpress_woocommerce_products_total` - Products by status and type
- `wordpress_woocommerce_orders_total` - Orders by status
- `wordpress_woocommerce_revenue_total` - Revenue (all time, today, month)
- `wordpress_woocommerce_customers_total` - Customers (registered, guest)
- Added cron job metrics:
- `wordpress_cron_events_total` - Scheduled cron events by hook (top 20)
- `wordpress_cron_overdue_total` - Number of overdue cron events
- `wordpress_cron_next_run_timestamp` - Unix timestamp of next scheduled cron
- Added transient cache metrics:
- `wordpress_transients_total` - Transients by type (total, with_expiration, persistent, expired)
- Updated Settings page with new metric categories
- Updated Help tab with new metrics reference
- **Key Learning**: WooCommerce HPOS (High-Performance Order Storage) requires different queries
- Check `OrderUtil::custom_orders_table_usage_is_enabled()` to determine storage type
- HPOS uses `wc_orders` table instead of `posts` and `postmeta`
- **Key Learning**: Cron event labeling requires cardinality control
- Limit to top 20 hooks to prevent label explosion
- Use `arsort()` to get most frequent hooks first
### 2026-02-02 - Runtime Metrics (v0.1.0)
- Implemented runtime metrics collection for HTTP requests and database queries
- Created `RuntimeCollector` class that hooks into WordPress request lifecycle
- Added new metrics:
- `wordpress_http_requests_total` - Counter by method, status, endpoint
- `wordpress_http_request_duration_seconds` - Histogram of request durations
- `wordpress_db_queries_total` - Counter by endpoint
- `wordpress_db_query_duration_seconds` - Histogram (requires SAVEQUERIES)
- Updated `Collector` class to expose stored runtime metrics
- Added new settings options in admin for enabling/disabling runtime metrics
- Created translation files (.pot, .po, .mo) for internationalization
- **Key Learning**: With InMemory Prometheus storage, counters/histograms reset per request
- Solution: Store aggregated data in WordPress options, read during metrics collection
- Histograms exposed as gauge metrics following Prometheus naming conventions (`_bucket`, `_sum`, `_count`)
- **Key Learning**: Endpoint normalization is important for cardinality control
- Group requests into categories (admin, ajax, cron, rest-api, frontend, etc.)
- Avoid high-cardinality labels like full URL paths
### 2026-02-01 - CI/CD Fixes (v0.0.2)
- Fixed composer.json dependency configuration for CI compatibility
- **Key Learning**: Git submodules with path repositories need explicit version aliases for CI:
```json
"repositories": [
{
"type": "path",
"url": "lib/wc-licensed-product-client",
"options": {
"symlink": false,
"versions": {
"magdev/wc-licensed-product-client": "0.2.2"
}
}
}
]
```
- Using `dev-main` constraints with `minimum-stability: dev` causes issues in CI
- Path repository with `symlink: false` and explicit `versions` mapping works reliably
- **Key Learning**: Gitea API returns "Release has no Tag" error when re-releasing existing tags
- Fixed release.yml to check for and delete existing releases before creating new ones
- Changed minimum-stability back to stable
### 2026-02-01 - Initial Setup (v0.0.1)
- Created initial plugin structure based on wp-fedistream blueprint

23
PLAN.md
View File

@@ -91,6 +91,8 @@ wp-prometheus/
The plugin provides the following default metrics (can be toggled in settings):
### Static Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_info | Gauge | version, php_version, multisite | WordPress installation info |
@@ -99,6 +101,15 @@ The plugin provides the following default metrics (can be toggled in settings):
| wordpress_comments_total | Gauge | status | Total comments by status |
| wordpress_plugins_total | Gauge | status | Total plugins (active/inactive) |
### Runtime Metrics
| Metric | Type | Labels | Description |
| ---------------------------------------- | --------- | ------------------------ | ------------------------------------- |
| wordpress_http_requests_total | Counter | method, status, endpoint | Total HTTP requests |
| wordpress_http_request_duration_seconds | Histogram | method, endpoint | Request duration distribution |
| wordpress_db_queries_total | Counter | endpoint | Total database queries |
| wordpress_db_query_duration_seconds | Histogram | endpoint | Query duration (requires SAVEQUERIES) |
## Extensibility
### Adding Custom Metrics
@@ -150,18 +161,6 @@ https://example.com/metrics/?token=your-auth-token
## Future Enhancements
### Version 0.1.0
- Request/Response timing metrics
- HTTP status code counters
- Database query metrics
### Version 0.2.0
- WooCommerce integration metrics
- Cron job metrics
- Transient cache metrics
### Version 0.3.0
- Custom metric builder in admin

View File

@@ -71,6 +71,8 @@ scrape_configs:
## Default Metrics
### Static Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_info | Gauge | version, php_version, multisite | WordPress installation info |
@@ -79,6 +81,44 @@ scrape_configs:
| wordpress_comments_total | Gauge | status | Total comments by status |
| wordpress_plugins_total | Gauge | status | Total plugins (active/inactive) |
### Runtime Metrics (v0.1.0+)
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_http_requests_total | Counter | method, status, endpoint | Total HTTP requests |
| wordpress_http_request_duration_seconds | Histogram | method, endpoint | Request duration distribution |
| wordpress_db_queries_total | Counter | endpoint | Total database queries |
| wordpress_db_query_duration_seconds | Histogram | endpoint | Query duration (requires SAVEQUERIES) |
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact.
### Cron Metrics (v0.2.0+)
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_cron_events_total | Gauge | hook | Scheduled cron events by hook |
| wordpress_cron_overdue_total | Gauge | - | Number of overdue cron events |
| wordpress_cron_next_run_timestamp | Gauge | - | Unix timestamp of next scheduled cron |
### Transient Metrics (v0.2.0+)
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_transients_total | Gauge | type | Transients by type (total, with_expiration, persistent, expired) |
### WooCommerce Metrics (v0.2.0+)
These metrics are only available when WooCommerce is active.
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_woocommerce_products_total | Gauge | status, type | Products by status and type |
| wordpress_woocommerce_orders_total | Gauge | status | Orders by status |
| wordpress_woocommerce_revenue_total | Gauge | period, currency | Revenue (all_time, today, month) |
| wordpress_woocommerce_customers_total | Gauge | type | Customers (registered, guest) |
**Note:** WooCommerce metrics support both legacy post-based orders and HPOS (High-Performance Order Storage).
## Extending with Custom Metrics
Add your own metrics using the `wp_prometheus_collect_metrics` action:

207
assets/css/admin.css Normal file
View File

@@ -0,0 +1,207 @@
/**
* WP Prometheus Admin Styles
*
* @package WP_Prometheus
*/
/* Tab content styling */
.wp-prometheus-tab-content {
margin-top: 20px;
}
/* License status box */
.wp-prometheus-license-status {
margin: 15px 0;
}
/* Help tab code blocks */
.wp-prometheus-tab-content pre {
background: #f1f1f1;
padding: 15px;
overflow-x: auto;
margin: 15px 0;
border-radius: 3px;
border: 1px solid #ddd;
}
/* Help tab tables */
.wp-prometheus-tab-content .widefat {
margin: 15px 0;
}
.wp-prometheus-tab-content .widefat code {
background: none;
padding: 0;
}
/* Metrics fieldset */
.wp-prometheus-tab-content fieldset p strong {
display: block;
margin-bottom: 8px;
font-size: 14px;
}
/* Form table adjustments for tabs */
.wp-prometheus-tab-content .form-table {
margin-top: 10px;
}
/* Custom metrics form */
.wp-prometheus-metric-form {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 20px;
margin: 20px 0;
}
.wp-prometheus-metric-form h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.wp-prometheus-metric-form .required {
color: #d63638;
}
/* Label rows */
.metric-label-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.metric-label-row input {
flex: 1;
max-width: 300px;
}
.metric-label-row .remove-label {
padding: 0 8px;
min-width: auto;
}
/* Value rows */
.metric-value-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.metric-value-row input {
width: 120px;
}
.metric-value-row .remove-value-row {
padding: 0 8px;
min-width: auto;
}
/* Custom metrics table */
.wp-prometheus-custom-metrics .wp-list-table code {
background: #f0f0f1;
padding: 2px 6px;
border-radius: 3px;
}
.wp-prometheus-custom-metrics .wp-list-table td {
vertical-align: middle;
}
.wp-prometheus-custom-metrics .wp-list-table .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
}
/* Dashboard grid */
.wp-prometheus-dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin: 20px 0;
}
.wp-prometheus-dashboard-card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 20px;
text-align: center;
transition: box-shadow 0.2s ease;
}
.wp-prometheus-dashboard-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.wp-prometheus-dashboard-card .dashboard-icon {
margin-bottom: 15px;
}
.wp-prometheus-dashboard-card .dashboard-icon .dashicons {
font-size: 48px;
width: 48px;
height: 48px;
color: #2271b1;
}
.wp-prometheus-dashboard-card h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
.wp-prometheus-dashboard-card p {
color: #646970;
margin: 0 0 15px 0;
font-size: 13px;
}
/* Import options panel */
#import-options {
border-radius: 4px;
}
#import-options label {
display: block;
margin-bottom: 8px;
}
/* Spinner alignment */
.spinner {
float: none;
vertical-align: middle;
}
/* Button groups */
.wp-prometheus-tab-content .button + .button {
margin-left: 5px;
}
/* Notice messages in forms */
.wp-prometheus-metric-form .notice,
.wp-prometheus-custom-metrics .notice {
margin: 10px 0;
padding: 10px 15px;
}
/* Responsive adjustments */
@media screen and (max-width: 782px) {
.wp-prometheus-dashboard-grid {
grid-template-columns: 1fr;
}
.metric-value-row {
flex-direction: column;
align-items: flex-start;
}
.metric-value-row input {
width: 100%;
max-width: none;
}
}

View 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": ""
}

View File

@@ -0,0 +1,987 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"panels": [],
"title": "HTTP Requests",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(wordpress_http_requests_total[5m])) by (endpoint)",
"legendFormat": "{{endpoint}}",
"range": true,
"refId": "A"
}
],
"title": "Requests per Second by Endpoint",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "reqps"
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": "/2..$/"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "green",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/4..$/"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "orange",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/5..$/"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 1
},
"id": 3,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(wordpress_http_requests_total[5m])) by (status)",
"legendFormat": "{{status}}",
"range": true,
"refId": "A"
}
],
"title": "Requests per Second by Status",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": []
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 9
},
"id": 4,
"options": {
"legend": {
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"pieType": "donut",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(wordpress_http_requests_total) by (method)",
"instant": true,
"legendFormat": "{{method}}",
"refId": "A"
}
],
"title": "Requests by Method",
"type": "piechart"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": []
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 9
},
"id": 5,
"options": {
"legend": {
"displayMode": "list",
"placement": "right",
"showLegend": true
},
"pieType": "donut",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(wordpress_http_requests_total) by (endpoint)",
"instant": true,
"legendFormat": "{{endpoint}}",
"refId": "A"
}
],
"title": "Requests by Endpoint",
"type": "piechart"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 9
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(wordpress_http_requests_total)",
"instant": true,
"legendFormat": "__auto",
"refId": "A"
}
],
"title": "Total Requests (All Time)",
"type": "stat"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 17
},
"id": 7,
"panels": [],
"title": "Request Duration",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 18
},
"id": 8,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "wordpress_http_request_duration_seconds_sum / wordpress_http_request_duration_seconds_count",
"legendFormat": "{{method}} {{endpoint}}",
"range": true,
"refId": "A"
}
],
"title": "Average Request Duration",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 1,
"scaleDistribution": {
"type": "linear"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 18
},
"id": 9,
"options": {
"barRadius": 0,
"barWidth": 0.97,
"fullHighlight": false,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"orientation": "horizontal",
"showValue": "auto",
"stacking": "none",
"tooltip": {
"mode": "single",
"sort": "none"
},
"xTickLabelRotation": 0,
"xTickLabelSpacing": 0
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(wordpress_http_request_duration_seconds_bucket) by (le)",
"format": "table",
"instant": true,
"legendFormat": "{{le}}",
"refId": "A"
}
],
"title": "Request Duration Distribution (Buckets)",
"type": "barchart"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 26
},
"id": 10,
"panels": [],
"title": "Database Queries",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 27
},
"id": 11,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "rate(wordpress_db_queries_total[5m])",
"legendFormat": "{{endpoint}}",
"range": true,
"refId": "A"
}
],
"title": "Database Queries per Second",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 27
},
"id": 12,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "wordpress_db_query_duration_seconds_sum / wordpress_db_query_duration_seconds_count",
"legendFormat": "{{endpoint}}",
"range": true,
"refId": "A"
}
],
"title": "Average Query Duration",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 35
},
"id": 13,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(wordpress_db_queries_total)",
"instant": true,
"legendFormat": "__auto",
"refId": "A"
}
],
"title": "Total Database Queries (All Time)",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 0.1
},
{
"color": "red",
"value": 0.5
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 35
},
"id": 14,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(wordpress_db_query_duration_seconds_sum) / sum(wordpress_db_query_duration_seconds_count)",
"instant": true,
"legendFormat": "__auto",
"refId": "A"
}
],
"title": "Average Query Duration (Overall)",
"type": "stat"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": ["wordpress", "prometheus", "performance"],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"label": "Data Source",
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"queryValue": "",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "WordPress Runtime Performance",
"uid": "wp-prometheus-runtime",
"version": 1,
"weekStart": ""
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,26 @@
(function($) {
'use strict';
var importFileContent = null;
$(document).ready(function() {
// License tab handlers.
initLicenseHandlers();
// Custom metrics tab handlers.
initCustomMetricsHandlers();
// Dashboards tab handlers.
initDashboardsHandlers();
// Runtime metrics reset handler.
initResetRuntimeHandler();
});
/**
* Initialize license tab handlers.
*/
function initLicenseHandlers() {
// Validate license button.
$('#wp-prometheus-validate-license').on('click', function(e) {
e.preventDefault();
@@ -23,78 +42,575 @@
// Regenerate token button.
$('#wp-prometheus-regenerate-token').on('click', function(e) {
e.preventDefault();
if (confirm('Are you sure you want to regenerate the auth token? You will need to update your Prometheus configuration.')) {
if (confirm(wpPrometheus.confirmRegenerateToken)) {
var newToken = generateToken(32);
$('#wp_prometheus_auth_token').val(newToken);
}
});
}
/**
* Perform a license action via AJAX.
*
* @param {string} action AJAX action name.
* @param {string} message Loading message.
*/
function performLicenseAction(action, message) {
var $spinner = $('#wp-prometheus-license-spinner');
var $message = $('#wp-prometheus-license-message');
/**
* Initialize custom metrics tab handlers.
*/
function initCustomMetricsHandlers() {
var $formContainer = $('#wp-prometheus-metric-form-container');
var $form = $('#wp-prometheus-metric-form');
var $showFormBtn = $('#show-metric-form');
$spinner.addClass('is-active');
$message.hide();
// Show metric form.
$showFormBtn.on('click', function() {
resetMetricForm();
$formContainer.slideDown();
$(this).hide();
});
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: action,
nonce: wpPrometheus.nonce
},
success: function(response) {
$spinner.removeClass('is-active');
// Cancel metric form.
$('#cancel-metric-form').on('click', function() {
$formContainer.slideUp();
$showFormBtn.show();
// Remove edit parameter from URL.
if (window.location.search.indexOf('edit=') > -1) {
window.history.pushState({}, '', window.location.pathname + '?page=wp-prometheus&tab=custom');
}
});
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
// Value type toggle.
$('input[name="value_type"]').on('change', function() {
var valueType = $(this).val();
if (valueType === 'option') {
$('#option-config-row').show();
$('#static-values-row').hide();
} else {
$('#option-config-row').hide();
$('#static-values-row').show();
}
});
// Reload page after successful validation/activation.
setTimeout(function() {
location.reload();
}, 1500);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
}
},
error: function() {
$spinner.removeClass('is-active');
// Add label.
$('#add-label').on('click', function() {
var $container = $('#metric-labels-container');
var labelCount = $container.find('.metric-label-row').length;
if (labelCount >= 5) {
alert('Maximum 5 labels allowed per metric.');
return;
}
var $row = $('<div class="metric-label-row">' +
'<input type="text" name="labels[]" class="regular-text" placeholder="label_name" pattern="[a-zA-Z_][a-zA-Z0-9_]*">' +
'<button type="button" class="button remove-label">&times;</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">&times;</button>');
$container.append($row);
});
// Remove value row.
$(document).on('click', '.remove-value-row', function() {
$(this).closest('.metric-value-row').remove();
});
// Submit metric form.
$form.on('submit', function(e) {
e.preventDefault();
saveCustomMetric();
});
// Delete metric.
$(document).on('click', '.delete-metric', function() {
var id = $(this).data('id');
if (confirm(wpPrometheus.confirmDelete)) {
deleteCustomMetric(id);
}
});
// Export metrics.
$('#export-metrics').on('click', function() {
exportMetrics();
});
// Import metrics - trigger file input.
$('#import-metrics-btn').on('click', function() {
$('#import-metrics-file').click();
});
// Import file selected.
$('#import-metrics-file').on('change', function(e) {
var file = e.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
importFileContent = e.target.result;
$('#import-options').slideDown();
};
reader.readAsText(file);
}
});
// Confirm import.
$('#confirm-import').on('click', function() {
var mode = $('input[name="import_mode"]:checked').val();
importMetrics(importFileContent, mode);
});
// Cancel import.
$('#cancel-import').on('click', function() {
$('#import-options').slideUp();
$('#import-metrics-file').val('');
importFileContent = null;
});
}
/**
* Initialize dashboards tab handlers.
*/
function initDashboardsHandlers() {
$(document).on('click', '.download-dashboard', function() {
var slug = $(this).data('slug');
downloadDashboard(slug);
});
}
/**
* Initialize reset runtime metrics handler.
*/
function initResetRuntimeHandler() {
$('#wp-prometheus-reset-runtime').on('click', function() {
if (confirm(wpPrometheus.confirmReset)) {
resetRuntimeMetrics();
}
});
}
/**
* Perform a license action via AJAX.
*
* @param {string} action AJAX action name.
* @param {string} message Loading message.
*/
function performLicenseAction(action, message) {
var $spinner = $('#wp-prometheus-license-spinner');
var $message = $('#wp-prometheus-license-message');
$spinner.addClass('is-active');
$message.hide();
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: action,
nonce: wpPrometheus.nonce
},
success: function(response) {
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
// Reload page after successful validation/activation.
setTimeout(function() {
location.reload();
}, 1500);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
}
});
}
/**
* Generate a random token.
*
* @param {number} length Token length.
* @return {string} Generated token.
*/
function generateToken(length) {
var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var token = '';
for (var i = 0; i < length; i++) {
token += charset.charAt(Math.floor(Math.random() * charset.length));
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
}
return token;
});
}
/**
* Save custom metric via AJAX.
*/
function saveCustomMetric() {
var $spinner = $('#wp-prometheus-metric-spinner');
var $message = $('#wp-prometheus-metric-message');
var $form = $('#wp-prometheus-metric-form');
$spinner.addClass('is-active');
$message.hide();
var formData = $form.serialize();
formData += '&action=wp_prometheus_save_custom_metric';
formData += '&nonce=' + wpPrometheus.customMetricNonce;
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: formData,
success: function(response) {
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
setTimeout(function() {
window.location.href = window.location.pathname + '?page=wp-prometheus&tab=custom';
}, 1000);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
}
});
}
/**
* Delete custom metric via AJAX.
*
* @param {string} id Metric ID.
*/
function deleteCustomMetric(id) {
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: 'wp_prometheus_delete_custom_metric',
nonce: wpPrometheus.customMetricNonce,
id: id
},
success: function(response) {
if (response.success) {
$('tr[data-metric-id="' + id + '"]').fadeOut(function() {
$(this).remove();
// Check if table is empty.
if ($('.wp-prometheus-custom-metrics tbody tr').length === 0) {
location.reload();
}
});
} else {
alert(response.data.message || 'An error occurred.');
}
},
error: function() {
alert('Connection error. Please try again.');
}
});
}
/**
* Export metrics via AJAX.
*/
function exportMetrics() {
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: 'wp_prometheus_export_metrics',
nonce: wpPrometheus.exportNonce
},
success: function(response) {
if (response.success) {
downloadFile(response.data.json, response.data.filename, 'application/json');
} else {
alert(response.data.message || 'An error occurred.');
}
},
error: function() {
alert('Connection error. Please try again.');
}
});
}
/**
* Import metrics via AJAX.
*
* @param {string} json JSON content.
* @param {string} mode Import mode.
*/
function importMetrics(json, mode) {
var $spinner = $('#wp-prometheus-import-spinner');
var $message = $('#wp-prometheus-import-message');
$spinner.addClass('is-active');
$message.hide();
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: 'wp_prometheus_import_metrics',
nonce: wpPrometheus.importNonce,
json: json,
mode: mode
},
success: function(response) {
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
$('#import-options').slideUp();
$('#import-metrics-file').val('');
importFileContent = null;
setTimeout(function() {
location.reload();
}, 1500);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
}
});
}
/**
* Download dashboard via AJAX.
*
* @param {string} slug Dashboard slug.
*/
function downloadDashboard(slug) {
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: 'wp_prometheus_download_dashboard',
nonce: wpPrometheus.dashboardNonce,
slug: slug
},
success: function(response) {
if (response.success) {
downloadFile(response.data.json, response.data.filename, 'application/json');
} else {
alert(response.data.message || 'An error occurred.');
}
},
error: function() {
alert('Connection error. Please try again.');
}
});
}
/**
* Reset runtime metrics via AJAX.
*/
function resetRuntimeMetrics() {
var $spinner = $('#wp-prometheus-reset-spinner');
var $message = $('#wp-prometheus-reset-message');
$spinner.addClass('is-active');
$message.hide();
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: 'wp_prometheus_reset_runtime_metrics',
nonce: wpPrometheus.resetRuntimeNonce
},
success: function(response) {
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
}
});
}
/**
* Reset the metric form to default state.
*/
function resetMetricForm() {
var $form = $('#wp-prometheus-metric-form');
$form[0].reset();
$('#metric-id').val('');
$('#wp-prometheus-form-title').text('Add New Metric');
$('#option-config-row').hide();
$('#static-values-row').show();
$('#wp-prometheus-metric-message').hide();
// Reset labels to single empty row.
$('#metric-labels-container').html(
'<div class="metric-label-row">' +
'<input type="text" name="labels[]" class="regular-text" placeholder="label_name" pattern="[a-zA-Z_][a-zA-Z0-9_]*">' +
'<button type="button" class="button remove-label">&times;</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">&times;</button>' +
'</div>'
);
}
/**
* Update value rows when labels change.
*/
function updateValueRows() {
var labels = getLabels();
var labelCount = labels.length;
$('#metric-values-container .metric-value-row').each(function(rowIndex) {
var $row = $(this);
var inputs = $row.find('input').toArray();
var currentValues = inputs.map(function(input) { return input.value; });
// Remove all inputs except the value and button.
$row.find('input').remove();
// Re-add label inputs.
for (var i = 0; i < labelCount; i++) {
var val = currentValues[i] || '';
$row.prepend('<input type="text" name="label_values[' + rowIndex + '][]" class="small-text" placeholder="' + (labels[i] || 'value') + '" value="' + val + '">');
}
// Re-add value input.
var metricVal = currentValues[currentValues.length - 1] || '';
$row.find('.remove-value-row').before('<input type="number" name="label_values[' + rowIndex + '][]" class="small-text" step="any" placeholder="Value" value="' + metricVal + '">');
});
}
/**
* Get current label names.
*
* @return {string[]} Array of label names.
*/
function getLabels() {
var labels = [];
$('#metric-labels-container input[name="labels[]"]').each(function() {
var val = $(this).val().trim();
if (val) {
labels.push(val);
}
});
return labels;
}
/**
* Get current label count.
*
* @return {number} Number of labels.
*/
function getLabelCount() {
return getLabels().length;
}
/**
* Generate a random token.
*
* @param {number} length Token length.
* @return {string} Generated token.
*/
function generateToken(length) {
var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var token = '';
for (var i = 0; i < length; i++) {
token += charset.charAt(Math.floor(Math.random() * charset.length));
}
});
return token;
}
/**
* Download a file.
*
* @param {string} content File content.
* @param {string} filename Filename.
* @param {string} type MIME type.
*/
function downloadFile(content, filename, type) {
var blob = new Blob([content], { type: type });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
})(jQuery);

Binary file not shown.

View File

@@ -0,0 +1,619 @@
# German (Switzerland, formal) translation for WP Prometheus.
# Copyright (C) 2026 Marco Graetsch
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
"Project-Id-Version: WP Prometheus 0.3.0\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
"PO-Revision-Date: 2026-02-02T00:00:00+00:00\n"
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
"Language: de_CH\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/Admin/Settings.php
msgid "Metrics Settings"
msgstr "Metriken-Einstellungen"
#: src/Admin/Settings.php
msgid "Metrics"
msgstr "Metriken"
#: src/Admin/Settings.php
msgid "License"
msgstr "Lizenz"
#: src/Admin/Settings.php
msgid "Help"
msgstr "Hilfe"
#: src/Admin/Settings.php
msgid "Custom Metrics"
msgstr "Eigene Metriken"
#: src/Admin/Settings.php
msgid "Dashboards"
msgstr "Dashboards"
#: src/Admin/Settings.php
msgid "License settings saved."
msgstr "Lizenz-Einstellungen gespeichert."
#: src/Admin/Settings.php
msgid "License is active and valid."
msgstr "Lizenz ist aktiv und gueltig."
#: src/Admin/Settings.php
msgid "License is invalid."
msgstr "Lizenz ist ungueltig."
#: src/Admin/Settings.php
msgid "License has expired."
msgstr "Lizenz ist abgelaufen."
#: src/Admin/Settings.php
msgid "License has been revoked."
msgstr "Lizenz wurde widerrufen."
#: src/Admin/Settings.php
msgid "License is inactive."
msgstr "Lizenz ist inaktiv."
#: src/Admin/Settings.php
msgid "License has not been validated yet."
msgstr "Lizenz wurde noch nicht validiert."
#: src/Admin/Settings.php
msgid "License server is not configured."
msgstr "Lizenz-Server ist nicht konfiguriert."
#: src/Admin/Settings.php
msgid "Unknown status."
msgstr "Unbekannter Status."
#. translators: %s: Expiration date
#: src/Admin/Settings.php
msgid "Expires: %s"
msgstr "Laeuft ab: %s"
#. translators: %s: Time ago
#: src/Admin/Settings.php
msgid "Last checked: %s ago"
msgstr "Zuletzt geprueft: vor %s"
#: src/Admin/Settings.php
msgid "License Server URL"
msgstr "Lizenz-Server URL"
#: src/Admin/Settings.php
msgid "License Key"
msgstr "Lizenzschluessel"
#: src/Admin/Settings.php
msgid "Server Secret"
msgstr "Server-Geheimnis"
#: src/Admin/Settings.php
msgid "Leave empty to keep existing."
msgstr "Leer lassen, um bestehenden Wert zu behalten."
#: src/Admin/Settings.php
msgid "Save License Settings"
msgstr "Lizenz-Einstellungen speichern"
#: src/Admin/Settings.php
msgid "Validate License"
msgstr "Lizenz validieren"
#: src/Admin/Settings.php
msgid "Activate License"
msgstr "Lizenz aktivieren"
#: src/Admin/Settings.php
msgid "Authentication"
msgstr "Authentifizierung"
#: src/Admin/Settings.php
msgid "Enabled Metrics"
msgstr "Aktivierte Metriken"
#: src/Admin/Settings.php
msgid "Configure authentication for the /metrics endpoint."
msgstr "Authentifizierung fuer den /metrics-Endpunkt konfigurieren."
#: src/Admin/Settings.php
msgid "Select which metrics to expose on the /metrics endpoint."
msgstr "Waehlen Sie, welche Metriken auf dem /metrics-Endpunkt bereitgestellt werden sollen."
#: src/Admin/Settings.php
msgid "Auth Token"
msgstr "Auth-Token"
#: src/Admin/Settings.php
msgid "Select Metrics"
msgstr "Metriken auswaehlen"
#: src/Admin/Settings.php
msgid "Regenerate"
msgstr "Neu generieren"
#: src/Admin/Settings.php
msgid "Use this token to authenticate Prometheus scrape requests."
msgstr "Verwenden Sie diesen Token zur Authentifizierung von Prometheus-Abfragen."
#: src/Admin/Settings.php
msgid "Static Metrics"
msgstr "Statische Metriken"
#: src/Admin/Settings.php
msgid "WordPress Info (version, PHP version, multisite)"
msgstr "WordPress-Info (Version, PHP-Version, Multisite)"
#: src/Admin/Settings.php
msgid "Total Users by Role"
msgstr "Benutzer nach Rolle"
#: src/Admin/Settings.php
msgid "Total Posts by Type and Status"
msgstr "Beitraege nach Typ und Status"
#: src/Admin/Settings.php
msgid "Total Comments by Status"
msgstr "Kommentare nach Status"
#: src/Admin/Settings.php
msgid "Total Plugins (active/inactive)"
msgstr "Plugins (aktiv/inaktiv)"
#: src/Admin/Settings.php
msgid "Cron Events (scheduled tasks, overdue, next run)"
msgstr "Cron-Ereignisse (geplante Aufgaben, ueberfaellig, naechste Ausfuehrung)"
#: src/Admin/Settings.php
msgid "Transients (total, expiring, expired)"
msgstr "Transienten (gesamt, ablaufend, abgelaufen)"
#: src/Admin/Settings.php
msgid "Runtime Metrics"
msgstr "Laufzeit-Metriken"
#: src/Admin/Settings.php
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."
#: src/Admin/Settings.php
msgid "HTTP Requests Total (by method, status, endpoint)"
msgstr "HTTP-Anfragen gesamt (nach Methode, Status, Endpunkt)"
#: src/Admin/Settings.php
msgid "HTTP Request Duration (histogram)"
msgstr "HTTP-Anfragedauer (Histogramm)"
#: src/Admin/Settings.php
msgid "Database Queries Total (by endpoint)"
msgstr "Datenbank-Abfragen gesamt (nach Endpunkt)"
#: src/Admin/Settings.php
msgid "WooCommerce Metrics"
msgstr "WooCommerce-Metriken"
#: src/Admin/Settings.php
msgid "Metrics specific to WooCommerce stores. Only available when WooCommerce is active."
msgstr "Metriken speziell fuer WooCommerce-Shops. Nur verfuegbar, wenn WooCommerce aktiv ist."
#: src/Admin/Settings.php
msgid "WooCommerce Products (by status and type)"
msgstr "WooCommerce-Produkte (nach Status und Typ)"
#: src/Admin/Settings.php
msgid "WooCommerce Orders (by status)"
msgstr "WooCommerce-Bestellungen (nach Status)"
#: src/Admin/Settings.php
msgid "WooCommerce Revenue (all time, today, month)"
msgstr "WooCommerce-Umsatz (gesamt, heute, Monat)"
#: src/Admin/Settings.php
msgid "WooCommerce Customers (registered, guest)"
msgstr "WooCommerce-Kunden (registriert, Gast)"
#: src/Admin/Settings.php
msgid "Reset Runtime Metrics"
msgstr "Laufzeit-Metriken zuruecksetzen"
#: src/Admin/Settings.php
msgid "Clear all accumulated runtime metric data."
msgstr "Alle gesammelten Laufzeit-Metrikdaten loeschen."
#: src/Admin/Settings.php
msgid "Reset Metrics"
msgstr "Metriken zuruecksetzen"
#: src/Admin/Settings.php
msgid "Prometheus Configuration"
msgstr "Prometheus-Konfiguration"
#: src/Admin/Settings.php
msgid "Add the following to your prometheus.yml:"
msgstr "Fuegen Sie Folgendes zu Ihrer prometheus.yml hinzu:"
#: src/Admin/Settings.php
msgid "Endpoint Information"
msgstr "Endpunkt-Informationen"
#: src/Admin/Settings.php
msgid "Metrics URL"
msgstr "Metriken-URL"
#: src/Admin/Settings.php
msgid "Testing the Endpoint"
msgstr "Endpunkt testen"
#: src/Admin/Settings.php
msgid "You can test the endpoint using curl:"
msgstr "Sie koennen den Endpunkt mit curl testen:"
#: src/Admin/Settings.php
msgid "Available Metrics"
msgstr "Verfuegbare Metriken"
#: src/Admin/Settings.php
msgid "Metric"
msgstr "Metrik"
#: src/Admin/Settings.php
msgid "Type"
msgstr "Typ"
#: src/Admin/Settings.php
msgid "Description"
msgstr "Beschreibung"
#: src/Admin/Settings.php
msgid "Gauge"
msgstr "Gauge"
#: src/Admin/Settings.php
msgid "Counter"
msgstr "Counter"
#: src/Admin/Settings.php
msgid "Histogram"
msgstr "Histogramm"
#: src/Admin/Settings.php
msgid "WordPress installation info"
msgstr "WordPress-Installationsinformationen"
#: src/Admin/Settings.php
msgid "Total users by role"
msgstr "Benutzer gesamt nach Rolle"
#: src/Admin/Settings.php
msgid "Total posts by type and status"
msgstr "Beitraege gesamt nach Typ und Status"
#: src/Admin/Settings.php
msgid "Total comments by status"
msgstr "Kommentare gesamt nach Status"
#: src/Admin/Settings.php
msgid "Total plugins by status"
msgstr "Plugins gesamt nach Status"
#: src/Admin/Settings.php
msgid "HTTP requests by method, status, endpoint"
msgstr "HTTP-Anfragen nach Methode, Status, Endpunkt"
#: src/Admin/Settings.php
msgid "HTTP request duration distribution"
msgstr "HTTP-Anfragedauer-Verteilung"
#: src/Admin/Settings.php
msgid "Database queries by endpoint"
msgstr "Datenbank-Abfragen nach Endpunkt"
#: src/Admin/Settings.php
msgid "Scheduled cron events by hook"
msgstr "Geplante Cron-Ereignisse nach Hook"
#: src/Admin/Settings.php
msgid "Number of overdue cron events"
msgstr "Anzahl ueberfaelliger Cron-Ereignisse"
#: src/Admin/Settings.php
msgid "Unix timestamp of next scheduled cron"
msgstr "Unix-Zeitstempel des naechsten geplanten Crons"
#: src/Admin/Settings.php
msgid "Total transients by type"
msgstr "Transienten gesamt nach Typ"
#: src/Admin/Settings.php
msgid "WooCommerce products by status and type"
msgstr "WooCommerce-Produkte nach Status und Typ"
#: src/Admin/Settings.php
msgid "WooCommerce orders by status"
msgstr "WooCommerce-Bestellungen nach Status"
#: src/Admin/Settings.php
msgid "WooCommerce revenue by period"
msgstr "WooCommerce-Umsatz nach Zeitraum"
#: src/Admin/Settings.php
msgid "WooCommerce customers by type"
msgstr "WooCommerce-Kunden nach Typ"
#: src/Admin/Settings.php
msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:"
msgstr "Sie koennen benutzerdefinierte Metriken mit der wp_prometheus_collect_metrics-Aktion hinzufuegen:"
#: src/Admin/Settings.php
msgid "Add Custom Metric"
msgstr "Eigene Metrik hinzufuegen"
#: src/Admin/Settings.php
msgid "Edit Custom Metric"
msgstr "Eigene Metrik bearbeiten"
#: src/Admin/Settings.php
msgid "Metric Name"
msgstr "Metrik-Name"
#: src/Admin/Settings.php
msgid "Must follow Prometheus naming conventions."
msgstr "Muss den Prometheus-Namenskonventionen entsprechen."
#: src/Admin/Settings.php
msgid "Help Text"
msgstr "Hilfetext"
#: src/Admin/Settings.php
msgid "Description shown in Prometheus output."
msgstr "Beschreibung in der Prometheus-Ausgabe."
#: src/Admin/Settings.php
msgid "Value Type"
msgstr "Werttyp"
#: src/Admin/Settings.php
msgid "Static Value"
msgstr "Statischer Wert"
#: src/Admin/Settings.php
msgid "WordPress Option"
msgstr "WordPress-Option"
#: src/Admin/Settings.php
msgid "Static Value:"
msgstr "Statischer Wert:"
#: src/Admin/Settings.php
msgid "Option Name:"
msgstr "Optionsname:"
#: src/Admin/Settings.php
msgid "The name of the WordPress option to read."
msgstr "Der Name der zu lesenden WordPress-Option."
#: src/Admin/Settings.php
msgid "Labels"
msgstr "Labels"
#: src/Admin/Settings.php
msgid "Label name"
msgstr "Label-Name"
#: src/Admin/Settings.php
msgid "Add Label"
msgstr "Label hinzufuegen"
#: src/Admin/Settings.php
msgid "Label Values"
msgstr "Label-Werte"
#: src/Admin/Settings.php
msgid "Value"
msgstr "Wert"
#: src/Admin/Settings.php
msgid "Add Value Row"
msgstr "Wertezeile hinzufuegen"
#: src/Admin/Settings.php
msgid "Enabled"
msgstr "Aktiviert"
#: src/Admin/Settings.php
msgid "Save Metric"
msgstr "Metrik speichern"
#: src/Admin/Settings.php
msgid "Cancel"
msgstr "Abbrechen"
#: src/Admin/Settings.php
msgid "Your Custom Metrics"
msgstr "Ihre eigenen Metriken"
#: src/Admin/Settings.php
msgid "Name"
msgstr "Name"
#: src/Admin/Settings.php
msgid "Status"
msgstr "Status"
#: src/Admin/Settings.php
msgid "Actions"
msgstr "Aktionen"
#: src/Admin/Settings.php
msgid "Active"
msgstr "Aktiv"
#: src/Admin/Settings.php
msgid "Inactive"
msgstr "Inaktiv"
#: src/Admin/Settings.php
msgid "Edit"
msgstr "Bearbeiten"
#: src/Admin/Settings.php
msgid "Delete"
msgstr "Loeschen"
#: src/Admin/Settings.php
msgid "No custom metrics defined yet."
msgstr "Noch keine eigenen Metriken definiert."
#: src/Admin/Settings.php
msgid "Export / Import"
msgstr "Export / Import"
#: src/Admin/Settings.php
msgid "Export your custom metrics configuration for backup or transfer to another site."
msgstr "Exportieren Sie Ihre Metriken-Konfiguration zur Sicherung oder Uebertragung auf eine andere Website."
#: src/Admin/Settings.php
msgid "Export Metrics"
msgstr "Metriken exportieren"
#: src/Admin/Settings.php
msgid "Import Metrics"
msgstr "Metriken importieren"
#: src/Admin/Settings.php
msgid "Import Options"
msgstr "Import-Optionen"
#: src/Admin/Settings.php
msgid "Skip existing metrics"
msgstr "Bestehende Metriken ueberspringen"
#: src/Admin/Settings.php
msgid "Overwrite existing metrics"
msgstr "Bestehende Metriken ueberschreiben"
#: src/Admin/Settings.php
msgid "Rename duplicates"
msgstr "Duplikate umbenennen"
#: src/Admin/Settings.php
msgid "Import"
msgstr "Importieren"
#: src/Admin/Settings.php
msgid "Grafana Dashboard Templates"
msgstr "Grafana Dashboard-Vorlagen"
#: src/Admin/Settings.php
msgid "Download pre-built Grafana dashboards for visualizing your WordPress metrics."
msgstr "Laden Sie vorgefertigte Grafana-Dashboards zur Visualisierung Ihrer WordPress-Metriken herunter."
#: src/Admin/Settings.php
msgid "Download"
msgstr "Herunterladen"
#: src/Admin/Settings.php
msgid "Import instructions:"
msgstr "Import-Anleitung:"
#: src/Admin/Settings.php
msgid "Download the desired dashboard JSON file"
msgstr "Laden Sie die gewuenschte Dashboard-JSON-Datei herunter"
#: src/Admin/Settings.php
msgid "In Grafana, go to Dashboards > Import"
msgstr "Gehen Sie in Grafana zu Dashboards > Import"
#: src/Admin/Settings.php
msgid "Upload the JSON file or paste its contents"
msgstr "Laden Sie die JSON-Datei hoch oder fuegen Sie deren Inhalt ein"
#: src/Admin/Settings.php
msgid "Select your Prometheus data source"
msgstr "Waehlen Sie Ihre Prometheus-Datenquelle"
#: src/Admin/Settings.php
msgid "Click Import"
msgstr "Klicken Sie auf Import"
#: src/Metrics/CustomMetricBuilder.php
msgid "Metric name is required."
msgstr "Metrik-Name ist erforderlich."
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid metric name format."
msgstr "Ungueltiges Metrik-Namensformat."
#: src/Metrics/CustomMetricBuilder.php
msgid "A metric with this name already exists."
msgstr "Eine Metrik mit diesem Namen existiert bereits."
#: src/Metrics/CustomMetricBuilder.php
msgid "Help text is required."
msgstr "Hilfetext ist erforderlich."
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid value type."
msgstr "Ungueltiger Werttyp."
#: src/Metrics/CustomMetricBuilder.php
msgid "Static value must be numeric."
msgstr "Statischer Wert muss numerisch sein."
#: src/Metrics/CustomMetricBuilder.php
msgid "Option name is required for option value type."
msgstr "Optionsname ist fuer den Options-Werttyp erforderlich."
#. translators: %d: Maximum number of labels
#: src/Metrics/CustomMetricBuilder.php
msgid "Maximum %d labels allowed."
msgstr "Maximal %d Labels erlaubt."
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid label name format."
msgstr "Ungueltiges Label-Namensformat."
#. translators: %d: Maximum number of label value combinations
#: src/Metrics/CustomMetricBuilder.php
msgid "Maximum %d label value combinations allowed."
msgstr "Maximal %d Label-Wert-Kombinationen erlaubt."
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid JSON format."
msgstr "Ungueltiges JSON-Format."
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid export format."
msgstr "Ungueltiges Export-Format."
#: src/Plugin.php
msgid "Settings"
msgstr "Einstellungen"
#. translators: 1: Required PHP version, 2: Current PHP version
#: wp-prometheus.php
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."
#. translators: 1: Required WordPress version, 2: Current WordPress version
#: wp-prometheus.php
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."
#: wp-prometheus.php
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."
#. translators: %s: Required PHP version
#: wp-prometheus.php
msgid "WP Prometheus requires PHP version %s or higher."
msgstr "WP Prometheus erfordert PHP-Version %s oder hoeher."

616
languages/wp-prometheus.pot Normal file
View File

@@ -0,0 +1,616 @@
# Copyright (C) 2026 Marco Graetsch
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
"Project-Id-Version: WP Prometheus 0.3.0\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2026-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
#: src/Admin/Settings.php
msgid "Metrics Settings"
msgstr ""
#: src/Admin/Settings.php
msgid "Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "License"
msgstr ""
#: src/Admin/Settings.php
msgid "Help"
msgstr ""
#: src/Admin/Settings.php
msgid "Custom Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Dashboards"
msgstr ""
#: src/Admin/Settings.php
msgid "License settings saved."
msgstr ""
#: src/Admin/Settings.php
msgid "License is active and valid."
msgstr ""
#: src/Admin/Settings.php
msgid "License is invalid."
msgstr ""
#: src/Admin/Settings.php
msgid "License has expired."
msgstr ""
#: src/Admin/Settings.php
msgid "License has been revoked."
msgstr ""
#: src/Admin/Settings.php
msgid "License is inactive."
msgstr ""
#: src/Admin/Settings.php
msgid "License has not been validated yet."
msgstr ""
#: src/Admin/Settings.php
msgid "License server is not configured."
msgstr ""
#: src/Admin/Settings.php
msgid "Unknown status."
msgstr ""
#. translators: %s: Expiration date
#: src/Admin/Settings.php
msgid "Expires: %s"
msgstr ""
#. translators: %s: Time ago
#: src/Admin/Settings.php
msgid "Last checked: %s ago"
msgstr ""
#: src/Admin/Settings.php
msgid "License Server URL"
msgstr ""
#: src/Admin/Settings.php
msgid "License Key"
msgstr ""
#: src/Admin/Settings.php
msgid "Server Secret"
msgstr ""
#: src/Admin/Settings.php
msgid "Leave empty to keep existing."
msgstr ""
#: src/Admin/Settings.php
msgid "Save License Settings"
msgstr ""
#: src/Admin/Settings.php
msgid "Validate License"
msgstr ""
#: src/Admin/Settings.php
msgid "Activate License"
msgstr ""
#: src/Admin/Settings.php
msgid "Authentication"
msgstr ""
#: src/Admin/Settings.php
msgid "Enabled Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Configure authentication for the /metrics endpoint."
msgstr ""
#: src/Admin/Settings.php
msgid "Select which metrics to expose on the /metrics endpoint."
msgstr ""
#: src/Admin/Settings.php
msgid "Auth Token"
msgstr ""
#: src/Admin/Settings.php
msgid "Select Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Regenerate"
msgstr ""
#: src/Admin/Settings.php
msgid "Use this token to authenticate Prometheus scrape requests."
msgstr ""
#: src/Admin/Settings.php
msgid "Static Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "WordPress Info (version, PHP version, multisite)"
msgstr ""
#: src/Admin/Settings.php
msgid "Total Users by Role"
msgstr ""
#: src/Admin/Settings.php
msgid "Total Posts by Type and Status"
msgstr ""
#: src/Admin/Settings.php
msgid "Total Comments by Status"
msgstr ""
#: src/Admin/Settings.php
msgid "Total Plugins (active/inactive)"
msgstr ""
#: src/Admin/Settings.php
msgid "Cron Events (scheduled tasks, overdue, next run)"
msgstr ""
#: src/Admin/Settings.php
msgid "Transients (total, expiring, expired)"
msgstr ""
#: src/Admin/Settings.php
msgid "Runtime Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Runtime metrics track data across requests. Enable only what you need to minimize performance impact."
msgstr ""
#: src/Admin/Settings.php
msgid "HTTP Requests Total (by method, status, endpoint)"
msgstr ""
#: src/Admin/Settings.php
msgid "HTTP Request Duration (histogram)"
msgstr ""
#: src/Admin/Settings.php
msgid "Database Queries Total (by endpoint)"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Metrics specific to WooCommerce stores. Only available when WooCommerce is active."
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce Products (by status and type)"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce Orders (by status)"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce Revenue (all time, today, month)"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce Customers (registered, guest)"
msgstr ""
#: src/Admin/Settings.php
msgid "Reset Runtime Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Clear all accumulated runtime metric data."
msgstr ""
#: src/Admin/Settings.php
msgid "Reset Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Prometheus Configuration"
msgstr ""
#: src/Admin/Settings.php
msgid "Add the following to your prometheus.yml:"
msgstr ""
#: src/Admin/Settings.php
msgid "Endpoint Information"
msgstr ""
#: src/Admin/Settings.php
msgid "Metrics URL"
msgstr ""
#: src/Admin/Settings.php
msgid "Testing the Endpoint"
msgstr ""
#: src/Admin/Settings.php
msgid "You can test the endpoint using curl:"
msgstr ""
#: src/Admin/Settings.php
msgid "Available Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Metric"
msgstr ""
#: src/Admin/Settings.php
msgid "Type"
msgstr ""
#: src/Admin/Settings.php
msgid "Description"
msgstr ""
#: src/Admin/Settings.php
msgid "Gauge"
msgstr ""
#: src/Admin/Settings.php
msgid "Counter"
msgstr ""
#: src/Admin/Settings.php
msgid "Histogram"
msgstr ""
#: src/Admin/Settings.php
msgid "WordPress installation info"
msgstr ""
#: src/Admin/Settings.php
msgid "Total users by role"
msgstr ""
#: src/Admin/Settings.php
msgid "Total posts by type and status"
msgstr ""
#: src/Admin/Settings.php
msgid "Total comments by status"
msgstr ""
#: src/Admin/Settings.php
msgid "Total plugins by status"
msgstr ""
#: src/Admin/Settings.php
msgid "HTTP requests by method, status, endpoint"
msgstr ""
#: src/Admin/Settings.php
msgid "HTTP request duration distribution"
msgstr ""
#: src/Admin/Settings.php
msgid "Database queries by endpoint"
msgstr ""
#: src/Admin/Settings.php
msgid "Scheduled cron events by hook"
msgstr ""
#: src/Admin/Settings.php
msgid "Number of overdue cron events"
msgstr ""
#: src/Admin/Settings.php
msgid "Unix timestamp of next scheduled cron"
msgstr ""
#: src/Admin/Settings.php
msgid "Total transients by type"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce products by status and type"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce orders by status"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce revenue by period"
msgstr ""
#: src/Admin/Settings.php
msgid "WooCommerce customers by type"
msgstr ""
#: src/Admin/Settings.php
msgid "You can add custom metrics using the wp_prometheus_collect_metrics action:"
msgstr ""
#: src/Admin/Settings.php
msgid "Add Custom Metric"
msgstr ""
#: src/Admin/Settings.php
msgid "Edit Custom Metric"
msgstr ""
#: src/Admin/Settings.php
msgid "Metric Name"
msgstr ""
#: src/Admin/Settings.php
msgid "Must follow Prometheus naming conventions."
msgstr ""
#: src/Admin/Settings.php
msgid "Help Text"
msgstr ""
#: src/Admin/Settings.php
msgid "Description shown in Prometheus output."
msgstr ""
#: src/Admin/Settings.php
msgid "Value Type"
msgstr ""
#: src/Admin/Settings.php
msgid "Static Value"
msgstr ""
#: src/Admin/Settings.php
msgid "WordPress Option"
msgstr ""
#: src/Admin/Settings.php
msgid "Static Value:"
msgstr ""
#: src/Admin/Settings.php
msgid "Option Name:"
msgstr ""
#: src/Admin/Settings.php
msgid "The name of the WordPress option to read."
msgstr ""
#: src/Admin/Settings.php
msgid "Labels"
msgstr ""
#: src/Admin/Settings.php
msgid "Label name"
msgstr ""
#: src/Admin/Settings.php
msgid "Add Label"
msgstr ""
#: src/Admin/Settings.php
msgid "Label Values"
msgstr ""
#: src/Admin/Settings.php
msgid "Value"
msgstr ""
#: src/Admin/Settings.php
msgid "Add Value Row"
msgstr ""
#: src/Admin/Settings.php
msgid "Enabled"
msgstr ""
#: src/Admin/Settings.php
msgid "Save Metric"
msgstr ""
#: src/Admin/Settings.php
msgid "Cancel"
msgstr ""
#: src/Admin/Settings.php
msgid "Your Custom Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Name"
msgstr ""
#: src/Admin/Settings.php
msgid "Status"
msgstr ""
#: src/Admin/Settings.php
msgid "Actions"
msgstr ""
#: src/Admin/Settings.php
msgid "Active"
msgstr ""
#: src/Admin/Settings.php
msgid "Inactive"
msgstr ""
#: src/Admin/Settings.php
msgid "Edit"
msgstr ""
#: src/Admin/Settings.php
msgid "Delete"
msgstr ""
#: src/Admin/Settings.php
msgid "No custom metrics defined yet."
msgstr ""
#: src/Admin/Settings.php
msgid "Export / Import"
msgstr ""
#: src/Admin/Settings.php
msgid "Export your custom metrics configuration for backup or transfer to another site."
msgstr ""
#: src/Admin/Settings.php
msgid "Export Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Import Metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Import Options"
msgstr ""
#: src/Admin/Settings.php
msgid "Skip existing metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Overwrite existing metrics"
msgstr ""
#: src/Admin/Settings.php
msgid "Rename duplicates"
msgstr ""
#: src/Admin/Settings.php
msgid "Import"
msgstr ""
#: src/Admin/Settings.php
msgid "Grafana Dashboard Templates"
msgstr ""
#: src/Admin/Settings.php
msgid "Download pre-built Grafana dashboards for visualizing your WordPress metrics."
msgstr ""
#: src/Admin/Settings.php
msgid "Download"
msgstr ""
#: src/Admin/Settings.php
msgid "Import instructions:"
msgstr ""
#: src/Admin/Settings.php
msgid "Download the desired dashboard JSON file"
msgstr ""
#: src/Admin/Settings.php
msgid "In Grafana, go to Dashboards > Import"
msgstr ""
#: src/Admin/Settings.php
msgid "Upload the JSON file or paste its contents"
msgstr ""
#: src/Admin/Settings.php
msgid "Select your Prometheus data source"
msgstr ""
#: src/Admin/Settings.php
msgid "Click Import"
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Metric name is required."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid metric name format."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "A metric with this name already exists."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Help text is required."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid value type."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Static value must be numeric."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Option name is required for option value type."
msgstr ""
#. translators: %d: Maximum number of labels
#: src/Metrics/CustomMetricBuilder.php
msgid "Maximum %d labels allowed."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid label name format."
msgstr ""
#. translators: %d: Maximum number of label value combinations
#: src/Metrics/CustomMetricBuilder.php
msgid "Maximum %d label value combinations allowed."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid JSON format."
msgstr ""
#: src/Metrics/CustomMetricBuilder.php
msgid "Invalid export format."
msgstr ""
#: src/Plugin.php
msgid "Settings"
msgstr ""
#. translators: 1: Required PHP version, 2: Current PHP version
#: wp-prometheus.php
msgid "WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s."
msgstr ""
#. translators: 1: Required WordPress version, 2: Current WordPress version
#: wp-prometheus.php
msgid "WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s."
msgstr ""
#: wp-prometheus.php
msgid "WP Prometheus requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
msgstr ""
#. translators: %s: Required PHP version
#: wp-prometheus.php
msgid "WP Prometheus requires PHP version %s or higher."
msgstr ""

View File

@@ -0,0 +1,151 @@
<?php
/**
* Dashboard provider class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Admin;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* DashboardProvider class.
*
* Provides Grafana dashboard templates for download.
*/
class DashboardProvider {
/**
* Dashboard directory path.
*
* @var string
*/
private string $dashboard_dir;
/**
* Available dashboard definitions.
*
* @var array
*/
private array $dashboards = array();
/**
* Constructor.
*/
public function __construct() {
$this->dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/';
$this->dashboards = array(
'wordpress-overview' => array(
'title' => __( 'WordPress Overview', 'wp-prometheus' ),
'description' => __( 'General WordPress metrics including users, posts, comments, and plugins.', 'wp-prometheus' ),
'file' => 'wordpress-overview.json',
'icon' => 'dashicons-wordpress',
),
'wordpress-runtime' => array(
'title' => __( 'Runtime Performance', 'wp-prometheus' ),
'description' => __( 'HTTP request metrics, database query performance, and response times.', 'wp-prometheus' ),
'file' => 'wordpress-runtime.json',
'icon' => 'dashicons-performance',
),
'wordpress-woocommerce' => array(
'title' => __( 'WooCommerce Store', 'wp-prometheus' ),
'description' => __( 'WooCommerce metrics including products, orders, revenue, and customers.', 'wp-prometheus' ),
'file' => 'wordpress-woocommerce.json',
'icon' => 'dashicons-cart',
),
);
}
/**
* Get list of available dashboards.
*
* @return array
*/
public function get_available(): array {
$available = array();
foreach ( $this->dashboards as $slug => $dashboard ) {
$file_path = $this->dashboard_dir . $dashboard['file'];
if ( file_exists( $file_path ) ) {
$available[ $slug ] = $dashboard;
}
}
return $available;
}
/**
* Get dashboard content by slug.
*
* @param string $slug Dashboard slug.
* @return string|null JSON content or null if not found.
*/
public function get_dashboard( string $slug ): ?string {
// Validate slug to prevent directory traversal.
$slug = sanitize_file_name( $slug );
if ( ! isset( $this->dashboards[ $slug ] ) ) {
return null;
}
$file_path = $this->dashboard_dir . $this->dashboards[ $slug ]['file'];
// Security: Ensure file is within dashboard directory.
$real_path = realpath( $file_path );
$real_dir = realpath( $this->dashboard_dir );
if ( false === $real_path || false === $real_dir || strpos( $real_path, $real_dir ) !== 0 ) {
return null;
}
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
return null;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$content = file_get_contents( $file_path );
if ( false === $content ) {
return null;
}
return $content;
}
/**
* Get dashboard metadata by slug.
*
* @param string $slug Dashboard slug.
* @return array|null Dashboard metadata or null if not found.
*/
public function get_metadata( string $slug ): ?array {
$slug = sanitize_file_name( $slug );
if ( ! isset( $this->dashboards[ $slug ] ) ) {
return null;
}
return $this->dashboards[ $slug ];
}
/**
* Get filename for download.
*
* @param string $slug Dashboard slug.
* @return string|null Filename or null if not found.
*/
public function get_filename( string $slug ): ?string {
$slug = sanitize_file_name( $slug );
if ( ! isset( $this->dashboards[ $slug ] ) ) {
return null;
}
return $this->dashboards[ $slug ]['file'];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -63,6 +63,8 @@ final class Installer {
'wp_prometheus_auth_token',
'wp_prometheus_enable_default_metrics',
'wp_prometheus_enabled_metrics',
'wp_prometheus_runtime_metrics',
'wp_prometheus_custom_metrics',
);
foreach ( $options as $option ) {

View File

@@ -302,10 +302,47 @@ final class Manager {
* @return bool
*/
public static function is_license_valid(): bool {
// Bypass license check on localhost for development.
if ( self::is_localhost() ) {
return true;
}
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
return 'valid' === $status;
}
/**
* Check if the current site is running on localhost.
*
* @return bool
*/
public static function is_localhost(): bool {
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$localhost_patterns = array(
'localhost',
'127.0.0.1',
'::1',
);
// Check exact matches.
if ( in_array( $host, $localhost_patterns, true ) ) {
return true;
}
// Check .localhost TLD (e.g., mysite.localhost).
if ( str_ends_with( $host, '.localhost' ) ) {
return true;
}
// Check .local TLD (common for local development).
if ( str_ends_with( $host, '.local' ) ) {
return true;
}
return false;
}
/**
* Get the license key.
*
@@ -396,9 +433,16 @@ final class Manager {
* @return void
*/
private function update_cached_status( string $status, array $data = array() ): void {
$previous_status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
update_option( self::OPTION_LICENSE_STATUS, $status );
update_option( self::OPTION_LICENSE_DATA, $data );
update_option( self::OPTION_LAST_CHECK, time() );
// Flush rewrite rules when license becomes valid to register the /metrics endpoint.
if ( 'valid' === $status && 'valid' !== $previous_status ) {
flush_rewrite_rules();
}
}
/**
@@ -465,6 +509,9 @@ final class Manager {
update_option( self::OPTION_LICENSE_DATA, array() );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
// Flush rewrite rules to remove the /metrics endpoint.
flush_rewrite_rules();
wp_send_json_success( array(
'success' => true,
'message' => __( 'License deactivated.', 'wp-prometheus' ),

View File

@@ -10,6 +10,7 @@ namespace Magdev\WpPrometheus\Metrics;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\InMemory;
use Prometheus\RenderTextFormat;
use Magdev\WpPrometheus\Metrics\CustomMetricBuilder;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
@@ -95,6 +96,28 @@ class Collector {
$this->collect_plugins_total();
}
// Collect cron metrics.
if ( in_array( 'wordpress_cron_events_total', $enabled_metrics, true ) ) {
$this->collect_cron_metrics();
}
// Collect transient metrics.
if ( in_array( 'wordpress_transients_total', $enabled_metrics, true ) ) {
$this->collect_transient_metrics();
}
// Collect WooCommerce metrics (if WooCommerce is active).
if ( $this->is_woocommerce_active() ) {
$this->collect_woocommerce_metrics( $enabled_metrics );
}
// Collect runtime metrics (HTTP requests, DB queries).
$this->collect_runtime_metrics( $enabled_metrics );
// Collect custom user-defined metrics.
$custom_builder = new CustomMetricBuilder();
$custom_builder->register_with_collector( $this );
/**
* Fires after default metrics are collected.
*
@@ -233,6 +256,522 @@ class Collector {
$gauge->set( count( $all_plugins ) - count( $active_plugins ), array( 'inactive' ) );
}
/**
* Collect cron metrics.
*
* @return void
*/
private function collect_cron_metrics(): void {
$cron_array = _get_cron_array();
if ( ! is_array( $cron_array ) ) {
return;
}
// Events total gauge.
$events_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'cron_events_total',
'Total number of scheduled cron events',
array( 'hook' )
);
// Count events by hook.
$hook_counts = array();
$total_events = 0;
$overdue_count = 0;
$current_time = time();
$next_run = PHP_INT_MAX;
foreach ( $cron_array as $timestamp => $cron ) {
if ( $timestamp < $next_run ) {
$next_run = $timestamp;
}
foreach ( $cron as $hook => $events ) {
$event_count = count( $events );
$total_events += $event_count;
if ( ! isset( $hook_counts[ $hook ] ) ) {
$hook_counts[ $hook ] = 0;
}
$hook_counts[ $hook ] += $event_count;
// Check if overdue.
if ( $timestamp < $current_time ) {
$overdue_count += $event_count;
}
}
}
// Set events by hook (limit to top 20 to avoid cardinality explosion).
arsort( $hook_counts );
$hook_counts = array_slice( $hook_counts, 0, 20, true );
foreach ( $hook_counts as $hook => $count ) {
$events_gauge->set( $count, array( $hook ) );
}
// Overdue events gauge.
$overdue_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'cron_overdue_total',
'Number of overdue cron events',
array()
);
$overdue_gauge->set( $overdue_count, array() );
// Next run timestamp.
$next_run_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'cron_next_run_timestamp',
'Unix timestamp of next scheduled cron event',
array()
);
if ( $next_run !== PHP_INT_MAX ) {
$next_run_gauge->set( $next_run, array() );
}
}
/**
* Collect transient metrics.
*
* @return void
*/
private function collect_transient_metrics(): void {
global $wpdb;
// Count all transients.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$transient_count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%'"
);
// Count transients with expiration.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$expiring_count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%'"
);
// Count expired transients.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$expired_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%%' AND option_value < %d",
time()
)
);
// Transients total gauge.
$transients_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'transients_total',
'Total number of transients in database',
array( 'type' )
);
$transients_gauge->set( (int) $transient_count, array( 'total' ) );
$transients_gauge->set( (int) $expiring_count, array( 'with_expiration' ) );
$transients_gauge->set( (int) $transient_count - (int) $expiring_count, array( 'persistent' ) );
$transients_gauge->set( (int) $expired_count, array( 'expired' ) );
// Site transients (for multisite).
if ( is_multisite() ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$site_transient_count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->sitemeta} WHERE meta_key LIKE '_site_transient_%' AND meta_key NOT LIKE '_site_transient_timeout_%'"
);
$transients_gauge->set( (int) $site_transient_count, array( 'site_transients' ) );
}
}
/**
* Check if WooCommerce is active.
*
* @return bool
*/
private function is_woocommerce_active(): bool {
return class_exists( 'WooCommerce' );
}
/**
* Collect WooCommerce metrics.
*
* @param array $enabled_metrics List of enabled metrics.
* @return void
*/
private function collect_woocommerce_metrics( array $enabled_metrics ): void {
// Products total.
if ( in_array( 'wordpress_woocommerce_products_total', $enabled_metrics, true ) ) {
$this->collect_woocommerce_products();
}
// Orders total.
if ( in_array( 'wordpress_woocommerce_orders_total', $enabled_metrics, true ) ) {
$this->collect_woocommerce_orders();
}
// Revenue.
if ( in_array( 'wordpress_woocommerce_revenue_total', $enabled_metrics, true ) ) {
$this->collect_woocommerce_revenue();
}
// Customers.
if ( in_array( 'wordpress_woocommerce_customers_total', $enabled_metrics, true ) ) {
$this->collect_woocommerce_customers();
}
}
/**
* Collect WooCommerce products metrics.
*
* @return void
*/
private function collect_woocommerce_products(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'woocommerce_products_total',
'Total number of WooCommerce products by status and type',
array( 'status', 'type' )
);
// Get product counts by status.
$product_counts = wp_count_posts( 'product' );
$product_types = wc_get_product_types();
foreach ( get_object_vars( $product_counts ) as $status => $count ) {
if ( (int) $count > 0 ) {
$gauge->set( (int) $count, array( $status, 'all' ) );
}
}
// Count by product type (for published products only).
foreach ( array_keys( $product_types ) as $type ) {
$args = array(
'status' => 'publish',
'type' => $type,
'limit' => -1,
'return' => 'ids',
);
$products = wc_get_products( $args );
$gauge->set( count( $products ), array( 'publish', $type ) );
}
}
/**
* Collect WooCommerce orders metrics.
*
* @return void
*/
private function collect_woocommerce_orders(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'woocommerce_orders_total',
'Total number of WooCommerce orders by status',
array( 'status' )
);
// Get all registered order statuses and count each.
$statuses = wc_get_order_statuses();
foreach ( array_keys( $statuses ) as $status ) {
// Remove 'wc-' prefix for the label.
$status_label = str_replace( 'wc-', '', $status );
$count = wc_orders_count( $status );
$gauge->set( (int) $count, array( $status_label ) );
}
}
/**
* Collect WooCommerce revenue metrics.
*
* @return void
*/
private function collect_woocommerce_revenue(): void {
global $wpdb;
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'woocommerce_revenue_total',
'Total WooCommerce revenue',
array( 'period', 'currency' )
);
$currency = get_woocommerce_currency();
// Check if HPOS (High-Performance Order Storage) is enabled.
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
if ( $hpos_enabled ) {
$orders_table = $wpdb->prefix . 'wc_orders';
// Total revenue (all time) - completed and processing orders.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$total_revenue = $wpdb->get_var(
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing')"
);
// Today's revenue.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$today_revenue = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing') AND DATE(date_created_gmt) = %s",
gmdate( 'Y-m-d' )
)
);
// This month's revenue.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$month_revenue = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(total_amount) FROM {$orders_table} WHERE status IN ('wc-completed', 'wc-processing') AND YEAR(date_created_gmt) = %d AND MONTH(date_created_gmt) = %d",
gmdate( 'Y' ),
gmdate( 'm' )
)
);
} else {
// Legacy post-based orders.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$total_revenue = $wpdb->get_var(
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_order_total'
AND p.post_type = 'shop_order'
AND p.post_status IN ('wc-completed', 'wc-processing')"
);
// Today's revenue.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$today_revenue = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_order_total'
AND p.post_type = 'shop_order'
AND p.post_status IN ('wc-completed', 'wc-processing')
AND DATE(p.post_date_gmt) = %s",
gmdate( 'Y-m-d' )
)
);
// This month's revenue.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$month_revenue = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(meta_value) FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_order_total'
AND p.post_type = 'shop_order'
AND p.post_status IN ('wc-completed', 'wc-processing')
AND YEAR(p.post_date_gmt) = %d
AND MONTH(p.post_date_gmt) = %d",
gmdate( 'Y' ),
gmdate( 'm' )
)
);
}
$gauge->set( (float) ( $total_revenue ?? 0 ), array( 'all_time', $currency ) );
$gauge->set( (float) ( $today_revenue ?? 0 ), array( 'today', $currency ) );
$gauge->set( (float) ( $month_revenue ?? 0 ), array( 'month', $currency ) );
}
/**
* Collect WooCommerce customers metrics.
*
* @return void
*/
private function collect_woocommerce_customers(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'woocommerce_customers_total',
'Total number of WooCommerce customers',
array( 'type' )
);
// Count users with customer role.
$customer_count = count_users();
$customers = $customer_count['avail_roles']['customer'] ?? 0;
$gauge->set( $customers, array( 'registered' ) );
// Count guest orders (orders without user_id).
global $wpdb;
// Check if HPOS is enabled.
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
if ( $hpos_enabled ) {
$orders_table = $wpdb->prefix . 'wc_orders';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$guest_orders = $wpdb->get_var(
"SELECT COUNT(DISTINCT billing_email) FROM {$orders_table} WHERE customer_id = 0 AND billing_email != ''"
);
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$guest_orders = $wpdb->get_var(
"SELECT COUNT(DISTINCT pm.meta_value) FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
LEFT JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_customer_user'
WHERE pm.meta_key = '_billing_email'
AND p.post_type = 'shop_order'
AND (pm2.meta_value = '0' OR pm2.meta_value IS NULL)"
);
}
$gauge->set( (int) $guest_orders, array( 'guest' ) );
}
/**
* Collect runtime metrics from stored data.
*
* @param array $enabled_metrics List of enabled metrics.
* @return void
*/
private function collect_runtime_metrics( array $enabled_metrics ): void {
$runtime_collector = RuntimeCollector::get_instance();
$stored_metrics = $runtime_collector->get_stored_metrics();
// HTTP requests total counter.
if ( in_array( 'wordpress_http_requests_total', $enabled_metrics, true ) && ! empty( $stored_metrics['counters'] ) ) {
foreach ( $stored_metrics['counters'] as $counter_data ) {
if ( 'http_requests_total' !== $counter_data['name'] ) {
continue;
}
$counter = $this->registry->getOrRegisterCounter(
$this->namespace,
'http_requests_total',
'Total number of HTTP requests',
array( 'method', 'status', 'endpoint' )
);
$counter->incBy(
(int) $counter_data['value'],
array(
$counter_data['labels']['method'] ?? 'GET',
$counter_data['labels']['status'] ?? '200',
$counter_data['labels']['endpoint'] ?? 'unknown',
)
);
}
}
// HTTP request duration histogram.
if ( in_array( 'wordpress_http_request_duration_seconds', $enabled_metrics, true ) && ! empty( $stored_metrics['histograms'] ) ) {
foreach ( $stored_metrics['histograms'] as $histogram_data ) {
if ( 'http_request_duration_seconds' !== $histogram_data['name'] ) {
continue;
}
// For histograms, we expose as a gauge with pre-aggregated bucket counts.
// This is a workaround since we can't directly populate histogram buckets.
$this->expose_histogram_as_gauges(
'http_request_duration_seconds',
'HTTP request duration in seconds',
$histogram_data,
array( 'method', 'endpoint' )
);
}
}
// Database queries total counter.
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) && ! empty( $stored_metrics['counters'] ) ) {
foreach ( $stored_metrics['counters'] as $counter_data ) {
if ( 'db_queries_total' !== $counter_data['name'] ) {
continue;
}
$counter = $this->registry->getOrRegisterCounter(
$this->namespace,
'db_queries_total',
'Total number of database queries',
array( 'endpoint' )
);
$counter->incBy(
(int) $counter_data['value'],
array(
$counter_data['labels']['endpoint'] ?? 'unknown',
)
);
}
}
// Database query duration histogram (if SAVEQUERIES is enabled).
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) && ! empty( $stored_metrics['histograms'] ) ) {
foreach ( $stored_metrics['histograms'] as $histogram_data ) {
if ( 'db_query_duration_seconds' !== $histogram_data['name'] ) {
continue;
}
$this->expose_histogram_as_gauges(
'db_query_duration_seconds',
'Database query duration in seconds',
$histogram_data,
array( 'endpoint' )
);
}
}
}
/**
* Expose pre-aggregated histogram data as gauge metrics.
*
* Since we store histogram data externally, we expose it using gauges
* that follow Prometheus histogram naming conventions.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $histogram_data Stored histogram data.
* @param array $label_names Label names.
* @return void
*/
private function expose_histogram_as_gauges( string $name, string $help, array $histogram_data, array $label_names ): void {
$label_values = array();
foreach ( $label_names as $label_name ) {
$label_values[] = $histogram_data['labels'][ $label_name ] ?? 'unknown';
}
// Expose bucket counts.
$bucket_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
$name . '_bucket',
$help . ' (bucket)',
array_merge( $label_names, array( 'le' ) )
);
$cumulative_count = 0;
foreach ( $histogram_data['buckets'] as $le => $count ) {
$cumulative_count += $count;
$bucket_gauge->set(
$cumulative_count,
array_merge( $label_values, array( $le ) )
);
}
// Expose sum.
$sum_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
$name . '_sum',
$help . ' (sum)',
$label_names
);
$sum_gauge->set( $histogram_data['sum'], $label_values );
// Expose count.
$count_gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
$name . '_count',
$help . ' (count)',
$label_names
);
$count_gauge->set( $histogram_data['count'], $label_values );
}
/**
* Register a custom gauge metric.
*

View File

@@ -0,0 +1,496 @@
<?php
/**
* Custom metric builder class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Metrics;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* CustomMetricBuilder class.
*
* Manages custom user-defined Prometheus metrics.
*/
class CustomMetricBuilder {
/**
* Option name for storing custom metrics.
*
* @var string
*/
const OPTION_NAME = 'wp_prometheus_custom_metrics';
/**
* Export format version.
*
* @var string
*/
const EXPORT_VERSION = '1.0.0';
/**
* Maximum number of labels per metric.
*
* @var int
*/
const MAX_LABELS = 5;
/**
* Maximum number of label value combinations per metric.
*
* @var int
*/
const MAX_LABEL_VALUES = 50;
/**
* Get all custom metrics.
*
* @return array
*/
public function get_all(): array {
$metrics = get_option( self::OPTION_NAME, array() );
return is_array( $metrics ) ? $metrics : array();
}
/**
* Get a single metric by ID.
*
* @param string $id Metric ID.
* @return array|null
*/
public function get( string $id ): ?array {
$metrics = $this->get_all();
return $metrics[ $id ] ?? null;
}
/**
* Save a metric (create or update).
*
* @param array $metric Metric data.
* @return string Metric ID.
* @throws \InvalidArgumentException If validation fails.
*/
public function save( array $metric ): string {
$errors = $this->validate( $metric );
if ( ! empty( $errors ) ) {
throw new \InvalidArgumentException( implode( ', ', $errors ) );
}
$metrics = $this->get_all();
// Generate ID if not provided.
if ( empty( $metric['id'] ) ) {
$metric['id'] = wp_generate_uuid4();
$metric['created_at'] = time();
}
$metric['updated_at'] = time();
// Sanitize and normalize the metric data.
$metric = $this->sanitize_metric( $metric );
$metrics[ $metric['id'] ] = $metric;
update_option( self::OPTION_NAME, $metrics );
return $metric['id'];
}
/**
* Delete a metric.
*
* @param string $id Metric ID.
* @return bool True if deleted, false if not found.
*/
public function delete( string $id ): bool {
$metrics = $this->get_all();
if ( ! isset( $metrics[ $id ] ) ) {
return false;
}
unset( $metrics[ $id ] );
update_option( self::OPTION_NAME, $metrics );
return true;
}
/**
* Validate a Prometheus metric name.
*
* @param string $name Metric name.
* @return bool True if valid.
*/
public function validate_name( string $name ): bool {
// Prometheus metric names must match: [a-zA-Z_:][a-zA-Z0-9_:]*
return (bool) preg_match( '/^[a-zA-Z_:][a-zA-Z0-9_:]*$/', $name );
}
/**
* Validate a Prometheus label name.
*
* @param string $name Label name.
* @return bool True if valid.
*/
public function validate_label_name( string $name ): bool {
// Prometheus label names must match: [a-zA-Z_][a-zA-Z0-9_]*
// Labels starting with __ are reserved.
if ( strpos( $name, '__' ) === 0 ) {
return false;
}
return (bool) preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name );
}
/**
* Validate a complete metric definition.
*
* @param array $metric Metric data.
* @return array Array of error messages (empty if valid).
*/
public function validate( array $metric ): array {
$errors = array();
// Name is required.
if ( empty( $metric['name'] ) ) {
$errors[] = __( 'Metric name is required.', 'wp-prometheus' );
} elseif ( ! $this->validate_name( $metric['name'] ) ) {
$errors[] = __( 'Metric name must start with a letter, underscore, or colon, and contain only letters, numbers, underscores, and colons.', 'wp-prometheus' );
}
// Check for reserved prefixes.
if ( ! empty( $metric['name'] ) ) {
$reserved_prefixes = array( 'wordpress_', 'go_', 'process_', 'promhttp_' );
foreach ( $reserved_prefixes as $prefix ) {
if ( strpos( $metric['name'], $prefix ) === 0 ) {
$errors[] = sprintf(
/* translators: %s: Reserved prefix */
__( 'Metric name cannot start with reserved prefix "%s".', 'wp-prometheus' ),
$prefix
);
break;
}
}
}
// Check for duplicate names (excluding current metric if editing).
if ( ! empty( $metric['name'] ) ) {
$existing = $this->get_all();
foreach ( $existing as $id => $existing_metric ) {
if ( $existing_metric['name'] === $metric['name'] && ( empty( $metric['id'] ) || $metric['id'] !== $id ) ) {
$errors[] = __( 'A metric with this name already exists.', 'wp-prometheus' );
break;
}
}
}
// Help text is required.
if ( empty( $metric['help'] ) ) {
$errors[] = __( 'Help text is required.', 'wp-prometheus' );
}
// Validate type.
$valid_types = array( 'gauge' );
if ( empty( $metric['type'] ) || ! in_array( $metric['type'], $valid_types, true ) ) {
$errors[] = __( 'Invalid metric type. Only gauge is supported.', 'wp-prometheus' );
}
// Validate labels.
if ( ! empty( $metric['labels'] ) ) {
if ( ! is_array( $metric['labels'] ) ) {
$errors[] = __( 'Labels must be an array.', 'wp-prometheus' );
} elseif ( count( $metric['labels'] ) > self::MAX_LABELS ) {
$errors[] = sprintf(
/* translators: %d: Maximum labels */
__( 'Maximum %d labels allowed per metric.', 'wp-prometheus' ),
self::MAX_LABELS
);
} else {
foreach ( $metric['labels'] as $label ) {
if ( ! $this->validate_label_name( $label ) ) {
$errors[] = sprintf(
/* translators: %s: Label name */
__( 'Invalid label name: %s', 'wp-prometheus' ),
$label
);
}
}
}
}
// Validate value type.
$valid_value_types = array( 'static', 'option' );
if ( empty( $metric['value_type'] ) || ! in_array( $metric['value_type'], $valid_value_types, true ) ) {
$errors[] = __( 'Invalid value type. Must be "static" or "option".', 'wp-prometheus' );
}
// Validate value config based on type.
if ( ! empty( $metric['value_type'] ) ) {
if ( 'static' === $metric['value_type'] ) {
// Static values validated in label_values.
} elseif ( 'option' === $metric['value_type'] ) {
if ( empty( $metric['value_config']['option_name'] ) ) {
$errors[] = __( 'Option name is required for option-based metrics.', 'wp-prometheus' );
}
}
}
// Validate label values count.
if ( ! empty( $metric['label_values'] ) && is_array( $metric['label_values'] ) ) {
if ( count( $metric['label_values'] ) > self::MAX_LABEL_VALUES ) {
$errors[] = sprintf(
/* translators: %d: Maximum label combinations */
__( 'Maximum %d label value combinations allowed.', 'wp-prometheus' ),
self::MAX_LABEL_VALUES
);
}
// Validate each row has correct number of values.
$label_count = count( $metric['labels'] ?? array() );
foreach ( $metric['label_values'] as $row ) {
if ( is_array( $row ) && count( $row ) !== $label_count + 1 ) { // +1 for value.
$errors[] = __( 'Each label value row must have values for all labels plus a metric value.', 'wp-prometheus' );
break;
}
}
}
return $errors;
}
/**
* Sanitize metric data.
*
* @param array $metric Raw metric data.
* @return array Sanitized metric data.
*/
private function sanitize_metric( array $metric ): array {
$sanitized = array(
'id' => sanitize_key( $metric['id'] ?? '' ),
'name' => sanitize_key( $metric['name'] ?? '' ),
'help' => sanitize_text_field( $metric['help'] ?? '' ),
'type' => sanitize_key( $metric['type'] ?? 'gauge' ),
'labels' => array(),
'value_type' => sanitize_key( $metric['value_type'] ?? 'static' ),
'value_config' => array(),
'label_values' => array(),
'enabled' => ! empty( $metric['enabled'] ),
'created_at' => absint( $metric['created_at'] ?? time() ),
'updated_at' => absint( $metric['updated_at'] ?? time() ),
);
// Sanitize labels.
if ( ! empty( $metric['labels'] ) && is_array( $metric['labels'] ) ) {
foreach ( $metric['labels'] as $label ) {
$sanitized['labels'][] = sanitize_key( $label );
}
}
// Sanitize value config.
if ( 'static' === $sanitized['value_type'] ) {
$sanitized['value_config'] = array();
} elseif ( 'option' === $sanitized['value_type'] ) {
$sanitized['value_config'] = array(
'option_name' => sanitize_key( $metric['value_config']['option_name'] ?? '' ),
'default' => floatval( $metric['value_config']['default'] ?? 0 ),
);
}
// Sanitize label values.
if ( ! empty( $metric['label_values'] ) && is_array( $metric['label_values'] ) ) {
foreach ( $metric['label_values'] as $row ) {
if ( is_array( $row ) ) {
$sanitized_row = array();
foreach ( $row as $index => $value ) {
// Last value is the metric value (numeric).
if ( $index === count( $row ) - 1 ) {
$sanitized_row[] = floatval( $value );
} else {
$sanitized_row[] = sanitize_text_field( $value );
}
}
$sanitized['label_values'][] = $sanitized_row;
}
}
}
return $sanitized;
}
/**
* Export all metrics to JSON.
*
* @return string JSON string.
*/
public function export(): string {
$metrics = $this->get_all();
$export_data = array(
'version' => self::EXPORT_VERSION,
'plugin_version' => WP_PROMETHEUS_VERSION,
'exported_at' => gmdate( 'c' ),
'site_url' => home_url(),
'metrics' => array_values( $metrics ),
);
return wp_json_encode( $export_data, JSON_PRETTY_PRINT );
}
/**
* Import metrics from JSON.
*
* @param string $json JSON string.
* @param string $mode Import mode: 'skip', 'overwrite', or 'rename'.
* @return array Result with 'imported', 'skipped', 'errors' counts.
* @throws \InvalidArgumentException If JSON is invalid.
*/
public function import( string $json, string $mode = 'skip' ): array {
$data = json_decode( $json, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
throw new \InvalidArgumentException( __( 'Invalid JSON format.', 'wp-prometheus' ) );
}
if ( empty( $data['metrics'] ) || ! is_array( $data['metrics'] ) ) {
throw new \InvalidArgumentException( __( 'No metrics found in import file.', 'wp-prometheus' ) );
}
$result = array(
'imported' => 0,
'skipped' => 0,
'errors' => 0,
'messages' => array(),
);
$existing_metrics = $this->get_all();
$existing_names = array_column( $existing_metrics, 'name', 'id' );
foreach ( $data['metrics'] as $metric ) {
if ( empty( $metric['name'] ) ) {
$result['errors']++;
continue;
}
// Check for name collision.
$name_exists = in_array( $metric['name'], $existing_names, true );
if ( $name_exists ) {
if ( 'skip' === $mode ) {
$result['skipped']++;
$result['messages'][] = sprintf(
/* translators: %s: Metric name */
__( 'Skipped "%s" (already exists).', 'wp-prometheus' ),
$metric['name']
);
continue;
} elseif ( 'rename' === $mode ) {
// Generate unique name.
$base_name = $metric['name'];
$counter = 1;
while ( in_array( $metric['name'], $existing_names, true ) ) {
$metric['name'] = $base_name . '_imported_' . $counter;
$counter++;
}
}
// 'overwrite' mode: continue with same name, will overwrite below.
}
// Clear ID to create new metric (unless overwriting).
if ( 'overwrite' === $mode && $name_exists ) {
// Find existing ID by name.
$metric['id'] = array_search( $metric['name'], $existing_names, true );
} else {
unset( $metric['id'] );
}
try {
$this->save( $metric );
$result['imported']++;
// Update existing names for subsequent collision checks.
$existing_names = array_column( $this->get_all(), 'name', 'id' );
} catch ( \InvalidArgumentException $e ) {
$result['errors']++;
$result['messages'][] = sprintf(
/* translators: 1: Metric name, 2: Error message */
__( 'Error importing "%1$s": %2$s', 'wp-prometheus' ),
$metric['name'],
$e->getMessage()
);
}
}
return $result;
}
/**
* Register custom metrics with the Collector.
*
* @param Collector $collector The metrics collector instance.
* @return void
*/
public function register_with_collector( Collector $collector ): void {
$metrics = $this->get_all();
foreach ( $metrics as $metric ) {
if ( empty( $metric['enabled'] ) ) {
continue;
}
try {
$gauge = $collector->register_gauge(
$metric['name'],
$metric['help'],
$metric['labels'] ?? array()
);
// Set values based on value type.
if ( 'option' === $metric['value_type'] ) {
// Option-based metric: read from WordPress option.
$option_name = $metric['value_config']['option_name'] ?? '';
$default = $metric['value_config']['default'] ?? 0;
if ( ! empty( $option_name ) ) {
$value = get_option( $option_name, $default );
$value = is_numeric( $value ) ? floatval( $value ) : $default;
// For option-based, use empty labels if no labels defined.
$label_values = array();
if ( ! empty( $metric['labels'] ) && ! empty( $metric['label_values'][0] ) ) {
// Use first row of labels (without the value).
$label_values = array_slice( $metric['label_values'][0], 0, count( $metric['labels'] ) );
}
$gauge->set( $value, $label_values );
}
} elseif ( 'static' === $metric['value_type'] ) {
// Static metric: use predefined label values.
if ( ! empty( $metric['label_values'] ) ) {
foreach ( $metric['label_values'] as $row ) {
if ( ! is_array( $row ) || count( $row ) < 1 ) {
continue;
}
// Last element is the value.
$value = array_pop( $row );
// Remaining elements are label values.
$gauge->set( floatval( $value ), $row );
}
} else {
// No labels, single value.
$gauge->set( 0, array() );
}
}
} catch ( \Exception $e ) {
// Log error but don't break metric collection.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( sprintf( 'WP Prometheus: Failed to register custom metric "%s": %s', $metric['name'], $e->getMessage() ) );
}
}
}
}
}

View File

@@ -0,0 +1,371 @@
<?php
/**
* Runtime metrics collector class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Metrics;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* RuntimeCollector class.
*
* Collects runtime metrics during WordPress request lifecycle.
* Stores aggregated data for later retrieval by the Prometheus endpoint.
*/
class RuntimeCollector {
/**
* Singleton instance.
*
* @var RuntimeCollector|null
*/
private static ?RuntimeCollector $instance = null;
/**
* Request start time.
*
* @var float
*/
private float $request_start_time;
/**
* Option name for stored metrics.
*
* @var string
*/
private const OPTION_NAME = 'wp_prometheus_runtime_metrics';
/**
* Histogram buckets for request duration (in seconds).
*
* @var array
*/
private const DURATION_BUCKETS = array( 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 );
/**
* Get singleton instance.
*
* @return RuntimeCollector
*/
public static function get_instance(): RuntimeCollector {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor.
*/
private function __construct() {
$this->request_start_time = microtime( true );
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
// Record metrics at the end of the request.
add_action( 'shutdown', array( $this, 'record_request_metrics' ), 9999 );
}
/**
* Record request metrics at shutdown.
*
* @return void
*/
public function record_request_metrics(): void {
// Skip metrics endpoint requests to avoid self-referential metrics.
if ( $this->is_metrics_request() ) {
return;
}
// Skip AJAX requests for license validation etc. from this plugin.
if ( $this->is_plugin_ajax_request() ) {
return;
}
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
$metrics = $this->get_stored_metrics();
$duration = microtime( true ) - $this->request_start_time;
$status_code = http_response_code() ?: 200;
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET';
$endpoint = $this->get_normalized_endpoint();
// Record HTTP request count and duration.
if ( in_array( 'wordpress_http_requests_total', $enabled_metrics, true ) ) {
$this->increment_counter(
$metrics,
'http_requests_total',
array(
'method' => $method,
'status' => (string) $status_code,
'endpoint' => $endpoint,
)
);
}
// Record request duration histogram.
if ( in_array( 'wordpress_http_request_duration_seconds', $enabled_metrics, true ) ) {
$this->observe_histogram(
$metrics,
'http_request_duration_seconds',
$duration,
array(
'method' => $method,
'endpoint' => $endpoint,
),
self::DURATION_BUCKETS
);
}
// Record database query metrics.
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) ) {
global $wpdb;
$query_count = $wpdb->num_queries;
$this->increment_counter(
$metrics,
'db_queries_total',
array( 'endpoint' => $endpoint ),
$query_count
);
// Track query time if SAVEQUERIES is enabled.
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES && ! empty( $wpdb->queries ) ) {
$total_query_time = 0;
foreach ( $wpdb->queries as $query ) {
$total_query_time += $query[1]; // Query time is the second element.
}
$this->observe_histogram(
$metrics,
'db_query_duration_seconds',
$total_query_time,
array( 'endpoint' => $endpoint ),
self::DURATION_BUCKETS
);
}
}
$this->save_stored_metrics( $metrics );
}
/**
* Check if current request is for the metrics endpoint.
*
* @return bool
*/
private function is_metrics_request(): bool {
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
return strpos( $request_uri, '/metrics' ) !== false;
}
/**
* Check if current request is a plugin AJAX request.
*
* @return bool
*/
private function is_plugin_ajax_request(): bool {
if ( ! wp_doing_ajax() ) {
return false;
}
$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '';
return strpos( $action, 'wp_prometheus_' ) === 0;
}
/**
* Get normalized endpoint for labeling.
*
* @return string
*/
private function get_normalized_endpoint(): string {
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
// Remove query string.
$path = strtok( $request_uri, '?' );
// Normalize common patterns.
if ( is_admin() ) {
return 'admin';
}
if ( wp_doing_ajax() ) {
return 'ajax';
}
if ( wp_doing_cron() ) {
return 'cron';
}
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return 'rest-api';
}
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
return 'xmlrpc';
}
// Check for common WordPress patterns.
if ( preg_match( '#^/wp-json/#', $path ) ) {
return 'rest-api';
}
if ( preg_match( '#^/wp-login\.php#', $path ) ) {
return 'login';
}
if ( preg_match( '#^/wp-cron\.php#', $path ) ) {
return 'cron';
}
if ( preg_match( '#^/feed/#', $path ) || preg_match( '#/feed/?$#', $path ) ) {
return 'feed';
}
// Generic frontend.
return 'frontend';
}
/**
* Get stored metrics from database.
*
* @return array
*/
public function get_stored_metrics(): array {
$metrics = get_option( self::OPTION_NAME, array() );
if ( ! is_array( $metrics ) ) {
return array(
'counters' => array(),
'histograms' => array(),
'last_reset' => time(),
);
}
return $metrics;
}
/**
* Save stored metrics to database.
*
* @param array $metrics Metrics data.
* @return void
*/
private function save_stored_metrics( array $metrics ): void {
update_option( self::OPTION_NAME, $metrics, false );
}
/**
* Increment a counter metric.
*
* @param array $metrics Reference to metrics array.
* @param string $name Counter name.
* @param array $labels Label values.
* @param int $increment Amount to increment (default 1).
* @return void
*/
private function increment_counter( array &$metrics, string $name, array $labels, int $increment = 1 ): void {
if ( ! isset( $metrics['counters'] ) ) {
$metrics['counters'] = array();
}
$key = $this->make_label_key( $name, $labels );
if ( ! isset( $metrics['counters'][ $key ] ) ) {
$metrics['counters'][ $key ] = array(
'name' => $name,
'labels' => $labels,
'value' => 0,
);
}
$metrics['counters'][ $key ]['value'] += $increment;
}
/**
* Observe a value in a histogram metric.
*
* @param array $metrics Reference to metrics array.
* @param string $name Histogram name.
* @param float $value Observed value.
* @param array $labels Label values.
* @param array $buckets Bucket boundaries.
* @return void
*/
private function observe_histogram( array &$metrics, string $name, float $value, array $labels, array $buckets ): void {
if ( ! isset( $metrics['histograms'] ) ) {
$metrics['histograms'] = array();
}
$key = $this->make_label_key( $name, $labels );
if ( ! isset( $metrics['histograms'][ $key ] ) ) {
$bucket_counts = array();
foreach ( $buckets as $bucket ) {
$bucket_counts[ (string) $bucket ] = 0;
}
$bucket_counts['+Inf'] = 0;
$metrics['histograms'][ $key ] = array(
'name' => $name,
'labels' => $labels,
'buckets' => $bucket_counts,
'sum' => 0.0,
'count' => 0,
);
}
// Increment bucket counts.
foreach ( $buckets as $bucket ) {
if ( $value <= $bucket ) {
$metrics['histograms'][ $key ]['buckets'][ (string) $bucket ]++;
}
}
$metrics['histograms'][ $key ]['buckets']['+Inf']++;
// Update sum and count.
$metrics['histograms'][ $key ]['sum'] += $value;
$metrics['histograms'][ $key ]['count']++;
}
/**
* Create a unique key from name and labels.
*
* @param string $name Metric name.
* @param array $labels Label values.
* @return string
*/
private function make_label_key( string $name, array $labels ): string {
ksort( $labels );
return $name . ':' . wp_json_encode( $labels );
}
/**
* Reset stored metrics.
*
* @return void
*/
public static function reset_metrics(): void {
delete_option( self::OPTION_NAME );
}
/**
* Get histogram buckets.
*
* @return array
*/
public static function get_duration_buckets(): array {
return self::DURATION_BUCKETS;
}
}

View File

@@ -11,6 +11,7 @@ use Magdev\WpPrometheus\Admin\Settings;
use Magdev\WpPrometheus\Endpoint\MetricsEndpoint;
use Magdev\WpPrometheus\License\Manager as LicenseManager;
use Magdev\WpPrometheus\Metrics\Collector;
use Magdev\WpPrometheus\Metrics\RuntimeCollector;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
@@ -90,6 +91,20 @@ final class Plugin {
new Settings();
}
// Initialize runtime collector for request metrics (always runs to collect data).
// Only collect if at least one runtime metric is enabled.
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
$runtime_metrics = array(
'wordpress_http_requests_total',
'wordpress_http_request_duration_seconds',
'wordpress_db_queries_total',
);
$has_runtime_metrics = ! empty( array_intersect( $runtime_metrics, $enabled_metrics ) );
if ( $has_runtime_metrics && LicenseManager::is_license_valid() ) {
RuntimeCollector::get_instance();
}
// Initialize metrics endpoint (only if licensed).
if ( LicenseManager::is_license_valid() ) {
$this->collector = new Collector();

View File

@@ -3,7 +3,7 @@
* Plugin Name: WP Prometheus
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus
* Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics.
* Version: 0.0.2
* Version: 0.3.0
* Requires at least: 6.4
* Requires PHP: 8.3
* Author: Marco Graetsch
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* @var string
*/
define( 'WP_PROMETHEUS_VERSION', '0.0.2' );
define( 'WP_PROMETHEUS_VERSION', '0.3.0' );
/**
* Plugin file path.