diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f46832..0ca1115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2026-02-02 + +### Added + +- Persistent Storage Support: + - Redis storage adapter for shared metrics across multiple instances + - APCu storage adapter for single-server high-performance caching + - StorageFactory class for automatic adapter selection and fallback + - Connection testing with detailed error messages +- New "Storage" tab in admin settings: + - Storage adapter selection (In-Memory, Redis, APCu) + - Redis configuration (host, port, password, database, key prefix) + - APCu configuration (key prefix) + - Connection test button + - Environment variables documentation +- Environment variable configuration for Docker/containerized environments: + - `WP_PROMETHEUS_STORAGE_ADAPTER` - Select storage adapter + - `WP_PROMETHEUS_REDIS_HOST` - Redis server hostname + - `WP_PROMETHEUS_REDIS_PORT` - Redis server port + - `WP_PROMETHEUS_REDIS_PASSWORD` - Redis authentication + - `WP_PROMETHEUS_REDIS_DATABASE` - Redis database index (0-15) + - `WP_PROMETHEUS_REDIS_PREFIX` - Redis key prefix + - `WP_PROMETHEUS_APCU_PREFIX` - APCu key prefix +- Automatic fallback to In-Memory storage if configured adapter fails +- Docker Compose example in admin settings + +### Changed + +- Settings page now has 6 tabs: License, Metrics, Storage, Custom Metrics, Dashboards, Help +- Updated translations with all new strings (English and German) +- Collector now uses StorageFactory for storage adapter instantiation + ## [0.3.0] - 2026-02-02 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 9fd0ae8..1b453b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,7 +234,8 @@ wp-prometheus/ │ ├── Metrics/ │ │ ├── Collector.php # Prometheus metrics collector │ │ ├── CustomMetricBuilder.php # Custom metric CRUD -│ │ └── RuntimeCollector.php # Runtime metrics collector +│ │ ├── RuntimeCollector.php # Runtime metrics collector +│ │ └── StorageFactory.php # Storage adapter factory │ ├── Installer.php # Activation/Deactivation │ ├── Plugin.php # Main plugin class │ └── index.php @@ -290,6 +291,34 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) { ## Session History +### 2026-02-02 - Persistent Storage (v0.4.0) + +- Added persistent storage support for metrics: + - `StorageFactory.php` - Factory class for storage adapter instantiation + - Redis storage adapter for shared metrics across multiple instances + - APCu storage adapter for single-server high-performance caching + - Automatic fallback to In-Memory if configured adapter fails +- Added new "Storage" tab in admin settings: + - Storage adapter selection (In-Memory, Redis, APCu) + - Redis configuration (host, port, password, database, key prefix) + - APCu configuration (key prefix) + - Connection test button with detailed error messages +- Added environment variable support for Docker deployments: + - `WP_PROMETHEUS_STORAGE_ADAPTER` - Adapter selection + - `WP_PROMETHEUS_REDIS_HOST`, `_PORT`, `_PASSWORD`, `_DATABASE`, `_PREFIX` + - `WP_PROMETHEUS_APCU_PREFIX` + - Environment variables take precedence over admin settings +- Updated `Collector.php` to use `StorageFactory::get_adapter()` +- Updated Help tab with storage backends documentation +- Updated translation files with all new strings +- **Key Learning**: promphp/prometheus_client_php storage adapters + - Redis adapter requires options array with host, port, password, timeout + - APCu adapter just needs a prefix string + - Use `Redis::setPrefix()` before instantiation for custom key prefixes +- **Key Learning**: Docker environment variable configuration + - Use `getenv()` with explicit false check (`false !== getenv()`) + - Environment variables should override WordPress options for containerized deployments + ### 2026-02-02 - Custom Metrics & Dashboards (v0.3.0) - Added Custom Metric Builder with full admin UI: diff --git a/PLAN.md b/PLAN.md index 54e8b89..b5a3168 100644 --- a/PLAN.md +++ b/PLAN.md @@ -59,6 +59,7 @@ wp-prometheus/ │ └── release.yml # CI/CD pipeline ├── assets/ │ ├── css/ # Admin/Frontend styles +│ ├── dashboards/ # Grafana dashboard templates │ └── js/ │ └── admin.js # Admin JavaScript ├── languages/ # Translation files @@ -67,13 +68,17 @@ wp-prometheus/ ├── releases/ # Release packages ├── src/ │ ├── Admin/ +│ │ ├── DashboardProvider.php │ │ └── Settings.php │ ├── Endpoint/ │ │ └── MetricsEndpoint.php │ ├── License/ │ │ └── Manager.php │ ├── Metrics/ -│ │ └── Collector.php +│ │ ├── Collector.php +│ │ ├── CustomMetricBuilder.php +│ │ ├── RuntimeCollector.php +│ │ └── StorageFactory.php │ ├── Installer.php │ ├── Plugin.php │ └── index.php @@ -159,13 +164,57 @@ Alternatively, the token can be passed as a query parameter (for testing): https://example.com/metrics/?token=your-auth-token ``` +## Storage Configuration + +The plugin supports multiple storage backends for Prometheus metrics: + +### Available Adapters + +| Adapter | Description | Use Case | +| --------- | ------------------------------- | ------------------------------------- | +| In-Memory | Default, no persistence | Development, single request metrics | +| Redis | Shared storage across instances | Production, load-balanced environments| +| APCu | High-performance local cache | Production, single-server deployments | + +### Environment Variables + +For Docker or containerized environments, configure storage via environment variables: + +```bash +# Storage adapter selection +WP_PROMETHEUS_STORAGE_ADAPTER=redis + +# Redis configuration +WP_PROMETHEUS_REDIS_HOST=redis +WP_PROMETHEUS_REDIS_PORT=6379 +WP_PROMETHEUS_REDIS_PASSWORD=secret +WP_PROMETHEUS_REDIS_DATABASE=0 +WP_PROMETHEUS_REDIS_PREFIX=WORDPRESS_PROMETHEUS_ + +# APCu configuration +WP_PROMETHEUS_APCU_PREFIX=wp_prom +``` + +### Docker Compose Example + +```yaml +services: + wordpress: + image: wordpress:latest + environment: + WP_PROMETHEUS_STORAGE_ADAPTER: redis + WP_PROMETHEUS_REDIS_HOST: redis + WP_PROMETHEUS_REDIS_PORT: 6379 + depends_on: + - redis + + redis: + image: redis:alpine +``` + ## Future Enhancements -### Version 0.3.0 - -- Custom metric builder in admin -- Metric export/import -- Grafana dashboard templates +*No planned features at this time.* ## Dependencies diff --git a/assets/js/admin.js b/assets/js/admin.js index c22693f..8f98aff 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -21,6 +21,9 @@ // Runtime metrics reset handler. initResetRuntimeHandler(); + + // Storage tab handlers. + initStorageHandlers(); }); /** @@ -613,4 +616,131 @@ document.body.removeChild(a); URL.revokeObjectURL(url); } + + /** + * Initialize storage tab handlers. + */ + function initStorageHandlers() { + var $form = $('#wp-prometheus-storage-form'); + var $adapterSelect = $('#storage-adapter'); + + // Show/hide adapter-specific config. + $adapterSelect.on('change', function() { + var adapter = $(this).val(); + $('#redis-config').toggle(adapter === 'redis'); + $('#apcu-config').toggle(adapter === 'apcu'); + }); + + // Save storage settings. + $form.on('submit', function(e) { + e.preventDefault(); + saveStorageSettings(); + }); + + // Test storage connection. + $('#test-storage').on('click', function() { + testStorageConnection(); + }); + } + + /** + * Save storage settings via AJAX. + */ + function saveStorageSettings() { + var $spinner = $('#wp-prometheus-storage-spinner'); + var $message = $('#wp-prometheus-storage-message'); + var $form = $('#wp-prometheus-storage-form'); + + $spinner.addClass('is-active'); + $message.hide(); + + var formData = $form.serialize(); + formData += '&action=wp_prometheus_save_storage'; + formData += '&nonce=' + wpPrometheus.storageNonce; + + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: formData, + success: function(response) { + $spinner.removeClass('is-active'); + + if (response.success) { + var noticeClass = response.data.warning ? 'notice-warning' : 'notice-success'; + $message + .removeClass('notice-error notice-success notice-warning') + .addClass('notice ' + noticeClass) + .html('

' + response.data.message + '

') + .show(); + + if (!response.data.warning) { + setTimeout(function() { + location.reload(); + }, 1500); + } + } else { + $message + .removeClass('notice-success notice-warning') + .addClass('notice notice-error') + .html('

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

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

Connection error. Please try again.

') + .show(); + } + }); + } + + /** + * Test storage connection via AJAX. + */ + function testStorageConnection() { + var $spinner = $('#wp-prometheus-storage-spinner'); + var $message = $('#wp-prometheus-storage-message'); + var $form = $('#wp-prometheus-storage-form'); + + $spinner.addClass('is-active'); + $message.hide(); + + var formData = $form.serialize(); + formData += '&action=wp_prometheus_test_storage'; + formData += '&nonce=' + wpPrometheus.storageNonce; + + $.ajax({ + url: wpPrometheus.ajaxUrl, + type: 'POST', + data: formData, + success: function(response) { + $spinner.removeClass('is-active'); + + if (response.success) { + $message + .removeClass('notice-error notice-warning') + .addClass('notice notice-success') + .html('

' + response.data.message + '

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

' + (response.data.message || 'Connection test failed.') + '

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

Connection error. Please try again.

') + .show(); + } + }); + } })(jQuery); diff --git a/languages/wp-prometheus-de_CH.mo b/languages/wp-prometheus-de_CH.mo index b3f8808..94f2636 100644 Binary files a/languages/wp-prometheus-de_CH.mo and b/languages/wp-prometheus-de_CH.mo differ diff --git a/languages/wp-prometheus-de_CH.po b/languages/wp-prometheus-de_CH.po index 706b046..aa79840 100644 --- a/languages/wp-prometheus-de_CH.po +++ b/languages/wp-prometheus-de_CH.po @@ -3,7 +3,7 @@ # This file is distributed under the GPL v2 or later. msgid "" msgstr "" -"Project-Id-Version: WP Prometheus 0.3.0\n" +"Project-Id-Version: WP Prometheus 0.4.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" @@ -617,3 +617,265 @@ msgstr "WP Prometheus erfordert installierte Composer-Abhaengigkeiten. Bitte fue #: wp-prometheus.php msgid "WP Prometheus requires PHP version %s or higher." msgstr "WP Prometheus erfordert PHP-Version %s oder hoeher." + +#: src/Admin/Settings.php +msgid "Storage" +msgstr "Speicher" + +#: src/Admin/Settings.php +msgid "Metrics Storage Configuration" +msgstr "Metriken-Speicherkonfiguration" + +#: src/Admin/Settings.php +msgid "Configure how Prometheus metrics are stored. Persistent storage (Redis, APCu) allows metrics to survive between requests and aggregate data over time." +msgstr "Konfigurieren Sie, wie Prometheus-Metriken gespeichert werden. Persistenter Speicher (Redis, APCu) ermoeglicht es, Metriken zwischen Anfragen zu erhalten und Daten ueber Zeit zu aggregieren." + +#: src/Admin/Settings.php +msgid "Environment Override Active" +msgstr "Umgebungsvariablen-Ueberschreibung aktiv" + +#: src/Admin/Settings.php +msgid "Storage adapter is configured via environment variable. Admin settings will be ignored." +msgstr "Speicher-Adapter ist ueber Umgebungsvariable konfiguriert. Admin-Einstellungen werden ignoriert." + +#: src/Admin/Settings.php +msgid "Storage Fallback Active" +msgstr "Speicher-Fallback aktiv" + +#: src/Admin/Settings.php +msgid "Falling back to In-Memory storage." +msgstr "Faellt zurueck auf In-Memory-Speicher." + +#: src/Admin/Settings.php +msgid "Current Status:" +msgstr "Aktueller Status:" + +#. translators: %s: Active adapter name +#: src/Admin/Settings.php +msgid "Using %s storage." +msgstr "Verwende %s-Speicher." + +#: src/Admin/Settings.php +msgid "Storage Adapter" +msgstr "Speicher-Adapter" + +#: src/Admin/Settings.php +msgid "unavailable" +msgstr "nicht verfuegbar" + +#: src/Admin/Settings.php +msgid "Select the storage backend for metrics. Redis and APCu require their respective PHP extensions." +msgstr "Waehlen Sie das Speicher-Backend fuer Metriken. Redis und APCu erfordern ihre jeweiligen PHP-Erweiterungen." + +#: src/Admin/Settings.php +msgid "Redis Configuration" +msgstr "Redis-Konfiguration" + +#: src/Admin/Settings.php +msgid "Host" +msgstr "Host" + +#. translators: %s: Environment variable name +#: src/Admin/Settings.php +msgid "Can be overridden with %s environment variable." +msgstr "Kann mit Umgebungsvariable %s ueberschrieben werden." + +#: src/Admin/Settings.php +msgid "Port" +msgstr "Port" + +#: src/Admin/Settings.php +msgid "Password" +msgstr "Passwort" + +#: src/Admin/Settings.php +msgid "Leave empty if not required" +msgstr "Leer lassen, falls nicht erforderlich" + +#: src/Admin/Settings.php +msgid "Database" +msgstr "Datenbank" + +#. translators: %s: Environment variable name +#: src/Admin/Settings.php +msgid "Redis database index (0-15). Can be overridden with %s." +msgstr "Redis-Datenbankindex (0-15). Kann mit %s ueberschrieben werden." + +#: src/Admin/Settings.php +msgid "Key Prefix" +msgstr "Schluessel-Praefix" + +#: src/Admin/Settings.php +msgid "Prefix for Redis keys. Useful when sharing Redis with other applications." +msgstr "Praefix fuer Redis-Schluessel. Nuetzlich bei gemeinsamer Redis-Nutzung mit anderen Anwendungen." + +#: src/Admin/Settings.php +msgid "APCu Configuration" +msgstr "APCu-Konfiguration" + +#. translators: %s: Environment variable name +#: src/Admin/Settings.php +msgid "Prefix for APCu keys. Can be overridden with %s." +msgstr "Praefix fuer APCu-Schluessel. Kann mit %s ueberschrieben werden." + +#: src/Admin/Settings.php +msgid "Save Storage Settings" +msgstr "Speicher-Einstellungen speichern" + +#: src/Admin/Settings.php +msgid "Test Connection" +msgstr "Verbindung testen" + +#: src/Admin/Settings.php +msgid "Environment Variables" +msgstr "Umgebungsvariablen" + +#: src/Admin/Settings.php +msgid "For Docker or containerized environments, you can configure storage using environment variables. These take precedence over admin settings." +msgstr "Fuer Docker- oder Container-Umgebungen koennen Sie den Speicher ueber Umgebungsvariablen konfigurieren. Diese haben Vorrang vor Admin-Einstellungen." + +#: src/Admin/Settings.php +msgid "Variable" +msgstr "Variable" + +#: src/Admin/Settings.php +msgid "Example" +msgstr "Beispiel" + +#: src/Admin/Settings.php +msgid "Storage adapter to use" +msgstr "Zu verwendender Speicher-Adapter" + +#: src/Admin/Settings.php +msgid "Redis server hostname" +msgstr "Redis-Server-Hostname" + +#: src/Admin/Settings.php +msgid "Redis server port" +msgstr "Redis-Server-Port" + +#: src/Admin/Settings.php +msgid "Redis authentication password" +msgstr "Redis-Authentifizierungspasswort" + +#: src/Admin/Settings.php +msgid "Redis database index" +msgstr "Redis-Datenbankindex" + +#: src/Admin/Settings.php +msgid "Redis key prefix" +msgstr "Redis-Schluessel-Praefix" + +#: src/Admin/Settings.php +msgid "APCu key prefix" +msgstr "APCu-Schluessel-Praefix" + +#: src/Admin/Settings.php +msgid "Docker Compose Example" +msgstr "Docker Compose-Beispiel" + +#: src/Admin/Settings.php +msgid "Permission denied." +msgstr "Zugriff verweigert." + +#: src/Admin/Settings.php +msgid "Storage adapter is configured via environment variable and cannot be changed." +msgstr "Speicher-Adapter ist ueber Umgebungsvariable konfiguriert und kann nicht geaendert werden." + +#: src/Admin/Settings.php +msgid "Invalid storage adapter." +msgstr "Ungueltiger Speicher-Adapter." + +#: src/Admin/Settings.php +msgid "Storage settings saved successfully." +msgstr "Speicher-Einstellungen erfolgreich gespeichert." + +#: src/Admin/Settings.php +msgid "Storage settings saved, but connection test failed:" +msgstr "Speicher-Einstellungen gespeichert, aber Verbindungstest fehlgeschlagen:" + +#: src/Metrics/StorageFactory.php +msgid "In-Memory (default, no persistence)" +msgstr "In-Memory (Standard, keine Persistenz)" + +#: src/Metrics/StorageFactory.php +msgid "Redis (requires PHP Redis extension)" +msgstr "Redis (erfordert PHP-Redis-Erweiterung)" + +#: src/Metrics/StorageFactory.php +msgid "APCu (requires APCu extension)" +msgstr "APCu (erfordert APCu-Erweiterung)" + +#: src/Metrics/StorageFactory.php +msgid "PHP Redis extension is not installed." +msgstr "PHP-Redis-Erweiterung ist nicht installiert." + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "Redis connection failed: %s" +msgstr "Redis-Verbindung fehlgeschlagen: %s" + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "Redis error: %s" +msgstr "Redis-Fehler: %s" + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "Storage error: %s" +msgstr "Speicherfehler: %s" + +#: src/Metrics/StorageFactory.php +msgid "APCu extension is not installed." +msgstr "APCu-Erweiterung ist nicht installiert." + +#: src/Metrics/StorageFactory.php +msgid "APCu is installed but not enabled." +msgstr "APCu ist installiert, aber nicht aktiviert." + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "APCu error: %s" +msgstr "APCu-Fehler: %s" + +#: src/Metrics/StorageFactory.php +msgid "In-Memory storage is always available." +msgstr "In-Memory-Speicher ist immer verfuegbar." + +#: src/Metrics/StorageFactory.php +msgid "Unknown storage adapter." +msgstr "Unbekannter Speicher-Adapter." + +#: src/Metrics/StorageFactory.php +msgid "Could not connect to Redis server." +msgstr "Verbindung zum Redis-Server konnte nicht hergestellt werden." + +#: src/Metrics/StorageFactory.php +msgid "Redis authentication failed." +msgstr "Redis-Authentifizierung fehlgeschlagen." + +#. translators: %s: Redis host:port +#: src/Metrics/StorageFactory.php +msgid "Successfully connected to Redis at %s." +msgstr "Erfolgreich mit Redis verbunden unter %s." + +#: src/Metrics/StorageFactory.php +msgid "Redis ping failed." +msgstr "Redis-Ping fehlgeschlagen." + +#: src/Metrics/StorageFactory.php +msgid "APCu is installed but not enabled. Check your php.ini settings." +msgstr "APCu ist installiert, aber nicht aktiviert. Pruefen Sie Ihre php.ini-Einstellungen." + +#: src/Metrics/StorageFactory.php +msgid "APCu store operation failed." +msgstr "APCu-Speicheroperation fehlgeschlagen." + +#. translators: %s: Memory info +#: src/Metrics/StorageFactory.php +msgid "APCu is working. Memory: %s used." +msgstr "APCu funktioniert. Speicher: %s belegt." + +#: src/Metrics/StorageFactory.php +msgid "APCu fetch operation returned unexpected value." +msgstr "APCu-Abrufoperation hat unerwarteten Wert zurueckgegeben." diff --git a/languages/wp-prometheus.pot b/languages/wp-prometheus.pot index 8712d47..52a8a1c 100644 --- a/languages/wp-prometheus.pot +++ b/languages/wp-prometheus.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL v2 or later. msgid "" msgstr "" -"Project-Id-Version: WP Prometheus 0.3.0\n" +"Project-Id-Version: WP Prometheus 0.4.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" @@ -614,3 +614,265 @@ msgstr "" #: wp-prometheus.php msgid "WP Prometheus requires PHP version %s or higher." msgstr "" + +#: src/Admin/Settings.php +msgid "Storage" +msgstr "" + +#: src/Admin/Settings.php +msgid "Metrics Storage Configuration" +msgstr "" + +#: src/Admin/Settings.php +msgid "Configure how Prometheus metrics are stored. Persistent storage (Redis, APCu) allows metrics to survive between requests and aggregate data over time." +msgstr "" + +#: src/Admin/Settings.php +msgid "Environment Override Active" +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage adapter is configured via environment variable. Admin settings will be ignored." +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage Fallback Active" +msgstr "" + +#: src/Admin/Settings.php +msgid "Falling back to In-Memory storage." +msgstr "" + +#: src/Admin/Settings.php +msgid "Current Status:" +msgstr "" + +#. translators: %s: Active adapter name +#: src/Admin/Settings.php +msgid "Using %s storage." +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage Adapter" +msgstr "" + +#: src/Admin/Settings.php +msgid "unavailable" +msgstr "" + +#: src/Admin/Settings.php +msgid "Select the storage backend for metrics. Redis and APCu require their respective PHP extensions." +msgstr "" + +#: src/Admin/Settings.php +msgid "Redis Configuration" +msgstr "" + +#: src/Admin/Settings.php +msgid "Host" +msgstr "" + +#. translators: %s: Environment variable name +#: src/Admin/Settings.php +msgid "Can be overridden with %s environment variable." +msgstr "" + +#: src/Admin/Settings.php +msgid "Port" +msgstr "" + +#: src/Admin/Settings.php +msgid "Password" +msgstr "" + +#: src/Admin/Settings.php +msgid "Leave empty if not required" +msgstr "" + +#: src/Admin/Settings.php +msgid "Database" +msgstr "" + +#. translators: %s: Environment variable name +#: src/Admin/Settings.php +msgid "Redis database index (0-15). Can be overridden with %s." +msgstr "" + +#: src/Admin/Settings.php +msgid "Key Prefix" +msgstr "" + +#: src/Admin/Settings.php +msgid "Prefix for Redis keys. Useful when sharing Redis with other applications." +msgstr "" + +#: src/Admin/Settings.php +msgid "APCu Configuration" +msgstr "" + +#. translators: %s: Environment variable name +#: src/Admin/Settings.php +msgid "Prefix for APCu keys. Can be overridden with %s." +msgstr "" + +#: src/Admin/Settings.php +msgid "Save Storage Settings" +msgstr "" + +#: src/Admin/Settings.php +msgid "Test Connection" +msgstr "" + +#: src/Admin/Settings.php +msgid "Environment Variables" +msgstr "" + +#: src/Admin/Settings.php +msgid "For Docker or containerized environments, you can configure storage using environment variables. These take precedence over admin settings." +msgstr "" + +#: src/Admin/Settings.php +msgid "Variable" +msgstr "" + +#: src/Admin/Settings.php +msgid "Example" +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage adapter to use" +msgstr "" + +#: src/Admin/Settings.php +msgid "Redis server hostname" +msgstr "" + +#: src/Admin/Settings.php +msgid "Redis server port" +msgstr "" + +#: src/Admin/Settings.php +msgid "Redis authentication password" +msgstr "" + +#: src/Admin/Settings.php +msgid "Redis database index" +msgstr "" + +#: src/Admin/Settings.php +msgid "Redis key prefix" +msgstr "" + +#: src/Admin/Settings.php +msgid "APCu key prefix" +msgstr "" + +#: src/Admin/Settings.php +msgid "Docker Compose Example" +msgstr "" + +#: src/Admin/Settings.php +msgid "Permission denied." +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage adapter is configured via environment variable and cannot be changed." +msgstr "" + +#: src/Admin/Settings.php +msgid "Invalid storage adapter." +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage settings saved successfully." +msgstr "" + +#: src/Admin/Settings.php +msgid "Storage settings saved, but connection test failed:" +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "In-Memory (default, no persistence)" +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "Redis (requires PHP Redis extension)" +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "APCu (requires APCu extension)" +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "PHP Redis extension is not installed." +msgstr "" + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "Redis connection failed: %s" +msgstr "" + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "Redis error: %s" +msgstr "" + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "Storage error: %s" +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "APCu extension is not installed." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "APCu is installed but not enabled." +msgstr "" + +#. translators: %s: Error message +#: src/Metrics/StorageFactory.php +msgid "APCu error: %s" +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "In-Memory storage is always available." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "Unknown storage adapter." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "Could not connect to Redis server." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "Redis authentication failed." +msgstr "" + +#. translators: %s: Redis host:port +#: src/Metrics/StorageFactory.php +msgid "Successfully connected to Redis at %s." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "Redis ping failed." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "APCu is installed but not enabled. Check your php.ini settings." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "APCu store operation failed." +msgstr "" + +#. translators: %s: Memory info +#: src/Metrics/StorageFactory.php +msgid "APCu is working. Memory: %s used." +msgstr "" + +#: src/Metrics/StorageFactory.php +msgid "APCu fetch operation returned unexpected value." +msgstr "" diff --git a/src/Admin/Settings.php b/src/Admin/Settings.php index 58520bf..8ee4167 100644 --- a/src/Admin/Settings.php +++ b/src/Admin/Settings.php @@ -10,6 +10,7 @@ namespace Magdev\WpPrometheus\Admin; use Magdev\WpPrometheus\License\Manager as LicenseManager; use Magdev\WpPrometheus\Metrics\CustomMetricBuilder; use Magdev\WpPrometheus\Metrics\RuntimeCollector; +use Magdev\WpPrometheus\Metrics\StorageFactory; // Prevent direct file access. if ( ! defined( 'ABSPATH' ) ) { @@ -51,6 +52,7 @@ class Settings { $this->tabs = array( 'license' => __( 'License', 'wp-prometheus' ), 'metrics' => __( 'Metrics', 'wp-prometheus' ), + 'storage' => __( 'Storage', 'wp-prometheus' ), 'custom' => __( 'Custom Metrics', 'wp-prometheus' ), 'dashboards' => __( 'Dashboards', 'wp-prometheus' ), 'help' => __( 'Help', 'wp-prometheus' ), @@ -70,6 +72,8 @@ class Settings { add_action( 'wp_ajax_wp_prometheus_import_metrics', array( $this, 'ajax_import_metrics' ) ); add_action( 'wp_ajax_wp_prometheus_download_dashboard', array( $this, 'ajax_download_dashboard' ) ); add_action( 'wp_ajax_wp_prometheus_reset_runtime_metrics', array( $this, 'ajax_reset_runtime_metrics' ) ); + add_action( 'wp_ajax_wp_prometheus_save_storage', array( $this, 'ajax_save_storage' ) ); + add_action( 'wp_ajax_wp_prometheus_test_storage', array( $this, 'ajax_test_storage' ) ); } /** @@ -183,6 +187,7 @@ class Settings { 'importNonce' => wp_create_nonce( 'wp_prometheus_import' ), 'dashboardNonce' => wp_create_nonce( 'wp_prometheus_dashboard' ), 'resetRuntimeNonce' => wp_create_nonce( 'wp_prometheus_reset_runtime' ), + 'storageNonce' => wp_create_nonce( 'wp_prometheus_storage' ), 'confirmDelete' => __( 'Are you sure you want to delete this metric?', 'wp-prometheus' ), 'confirmReset' => __( 'Are you sure you want to reset all runtime metrics? This cannot be undone.', 'wp-prometheus' ), 'confirmRegenerateToken' => __( 'Are you sure you want to regenerate the auth token? You will need to update your Prometheus configuration.', 'wp-prometheus' ), @@ -226,6 +231,9 @@ class Settings { case 'metrics': $this->render_metrics_tab(); break; + case 'storage': + $this->render_storage_tab(); + break; case 'custom': $this->render_custom_metrics_tab(); break; @@ -415,6 +423,282 @@ class Settings { +
+

+

+ +

+ + +
+ +

+
+ + + +
+ +

+

+
+ + +
+ + ' . esc_html( ucfirst( $active_adapter ) ) . '' + ); + ?> +
+ +
+ + + + + + + +
+

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

+ + + + + + +
+ +

+ + + +

+
+ + + +
+ +

+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WP_PROMETHEUS_STORAGE_ADAPTERredis
WP_PROMETHEUS_REDIS_HOSTredis
WP_PROMETHEUS_REDIS_PORT6379
WP_PROMETHEUS_REDIS_PASSWORDsecret123
WP_PROMETHEUS_REDIS_DATABASE0
WP_PROMETHEUS_REDIS_PREFIXMYSITE_PROM_
WP_PROMETHEUS_APCU_PREFIXwp_prom
+ +

+
services:
+  wordpress:
+    image: wordpress:latest
+    environment:
+      WP_PROMETHEUS_STORAGE_ADAPTER: redis
+      WP_PROMETHEUS_REDIS_HOST: redis
+      WP_PROMETHEUS_REDIS_PORT: 6379
+    depends_on:
+      - redis
+
+  redis:
+    image: redis:alpine
+
+ set( 42, array( 'value1', 'value2' ) ); } ); + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
In-Memory
Redis
APCu
+

+ +

__( 'Runtime metrics have been reset.', 'wp-prometheus' ) ) ); } + + /** + * AJAX handler for saving storage settings. + * + * @return void + */ + public function ajax_save_storage(): void { + check_ajax_referer( 'wp_prometheus_storage', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + // Check if environment variable override is active. + if ( false !== getenv( 'WP_PROMETHEUS_STORAGE_ADAPTER' ) ) { + wp_send_json_error( array( 'message' => __( 'Storage adapter is configured via environment variable and cannot be changed.', 'wp-prometheus' ) ) ); + } + + $adapter = isset( $_POST['adapter'] ) ? sanitize_key( $_POST['adapter'] ) : 'inmemory'; + + // Validate adapter. + $valid_adapters = array_keys( StorageFactory::get_available_adapters() ); + if ( ! in_array( $adapter, $valid_adapters, true ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid storage adapter.', 'wp-prometheus' ) ) ); + } + + // Build config array. + $config = array( + 'adapter' => $adapter, + ); + + // Redis config. + if ( 'redis' === $adapter ) { + $config['redis'] = array( + 'host' => isset( $_POST['redis_host'] ) ? sanitize_text_field( wp_unslash( $_POST['redis_host'] ) ) : '127.0.0.1', + 'port' => isset( $_POST['redis_port'] ) ? absint( $_POST['redis_port'] ) : 6379, + 'password' => isset( $_POST['redis_password'] ) ? sanitize_text_field( wp_unslash( $_POST['redis_password'] ) ) : '', + 'database' => isset( $_POST['redis_database'] ) ? absint( $_POST['redis_database'] ) : 0, + 'prefix' => isset( $_POST['redis_prefix'] ) ? sanitize_key( $_POST['redis_prefix'] ) : 'WORDPRESS_PROMETHEUS_', + ); + } + + // APCu config. + if ( 'apcu' === $adapter ) { + $config['apcu_prefix'] = isset( $_POST['apcu_prefix'] ) ? sanitize_key( $_POST['apcu_prefix'] ) : 'wp_prom'; + } + + // Save configuration. + StorageFactory::save_config( $config ); + + // Test if the new configuration works. + $test_result = StorageFactory::test_connection( $adapter, $config['redis'] ?? array() ); + + if ( $test_result['success'] ) { + wp_send_json_success( array( + 'message' => __( 'Storage settings saved successfully.', 'wp-prometheus' ) . ' ' . $test_result['message'], + ) ); + } else { + wp_send_json_success( array( + 'message' => __( 'Storage settings saved, but connection test failed:', 'wp-prometheus' ) . ' ' . $test_result['message'], + 'warning' => true, + ) ); + } + } + + /** + * AJAX handler for testing storage connection. + * + * @return void + */ + public function ajax_test_storage(): void { + check_ajax_referer( 'wp_prometheus_storage', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-prometheus' ) ) ); + } + + $adapter = isset( $_POST['adapter'] ) ? sanitize_key( $_POST['adapter'] ) : 'inmemory'; + + // Build test config from form data. + $config = array(); + if ( 'redis' === $adapter ) { + $config = array( + 'host' => isset( $_POST['redis_host'] ) ? sanitize_text_field( wp_unslash( $_POST['redis_host'] ) ) : '127.0.0.1', + 'port' => isset( $_POST['redis_port'] ) ? absint( $_POST['redis_port'] ) : 6379, + 'password' => isset( $_POST['redis_password'] ) ? sanitize_text_field( wp_unslash( $_POST['redis_password'] ) ) : '', + 'database' => isset( $_POST['redis_database'] ) ? absint( $_POST['redis_database'] ) : 0, + ); + } + + $result = StorageFactory::test_connection( $adapter, $config ); + + if ( $result['success'] ) { + wp_send_json_success( array( 'message' => $result['message'] ) ); + } else { + wp_send_json_error( array( 'message' => $result['message'] ) ); + } + } } diff --git a/src/Metrics/Collector.php b/src/Metrics/Collector.php index 512c697..e4640b1 100644 --- a/src/Metrics/Collector.php +++ b/src/Metrics/Collector.php @@ -8,9 +8,9 @@ namespace Magdev\WpPrometheus\Metrics; use Prometheus\CollectorRegistry; -use Prometheus\Storage\InMemory; use Prometheus\RenderTextFormat; use Magdev\WpPrometheus\Metrics\CustomMetricBuilder; +use Magdev\WpPrometheus\Metrics\StorageFactory; // Prevent direct file access. if ( ! defined( 'ABSPATH' ) ) { @@ -42,7 +42,7 @@ class Collector { * Constructor. */ public function __construct() { - $this->registry = new CollectorRegistry( new InMemory() ); + $this->registry = new CollectorRegistry( StorageFactory::get_adapter() ); } /** diff --git a/src/Metrics/StorageFactory.php b/src/Metrics/StorageFactory.php new file mode 100644 index 0000000..1eb12d2 --- /dev/null +++ b/src/Metrics/StorageFactory.php @@ -0,0 +1,502 @@ + + */ + public static function get_available_adapters(): array { + return array( + self::ADAPTER_INMEMORY => __( 'In-Memory (default, no persistence)', 'wp-prometheus' ), + self::ADAPTER_REDIS => __( 'Redis (requires PHP Redis extension)', 'wp-prometheus' ), + self::ADAPTER_APCU => __( 'APCu (requires APCu extension)', 'wp-prometheus' ), + ); + } + + /** + * Check if a storage adapter is available on this system. + * + * @param string $adapter Adapter name. + * @return bool + */ + public static function is_adapter_available( string $adapter ): bool { + switch ( $adapter ) { + case self::ADAPTER_INMEMORY: + return true; + + case self::ADAPTER_REDIS: + return extension_loaded( 'redis' ); + + case self::ADAPTER_APCU: + return extension_loaded( 'apcu' ) && apcu_enabled(); + + default: + return false; + } + } + + /** + * Get the configured storage adapter name. + * + * @return string + */ + public static function get_configured_adapter(): string { + // Check environment variable first. + $env_adapter = getenv( self::ENV_STORAGE_ADAPTER ); + if ( false !== $env_adapter && ! empty( $env_adapter ) ) { + return strtolower( $env_adapter ); + } + + // Fall back to WordPress option. + return get_option( 'wp_prometheus_storage_adapter', self::ADAPTER_INMEMORY ); + } + + /** + * Get the active storage adapter name (may differ from configured if fallback occurred). + * + * @return string + */ + public static function get_active_adapter(): string { + // Ensure adapter is created. + self::get_adapter(); + + $configured = self::get_configured_adapter(); + if ( self::is_adapter_available( $configured ) && empty( self::$last_error ) ) { + return $configured; + } + + return self::ADAPTER_INMEMORY; + } + + /** + * Create the storage adapter based on configuration. + * + * @return Adapter + */ + private static function create_adapter(): Adapter { + $adapter = self::get_configured_adapter(); + self::$last_error = ''; + + switch ( $adapter ) { + case self::ADAPTER_REDIS: + return self::create_redis_adapter(); + + case self::ADAPTER_APCU: + return self::create_apcu_adapter(); + + case self::ADAPTER_INMEMORY: + default: + return new InMemory(); + } + } + + /** + * Create Redis storage adapter. + * + * @return Adapter + */ + private static function create_redis_adapter(): Adapter { + if ( ! extension_loaded( 'redis' ) ) { + self::$last_error = __( 'PHP Redis extension is not installed.', 'wp-prometheus' ); + return new InMemory(); + } + + $config = self::get_redis_config(); + + try { + Redis::setPrefix( $config['prefix'] ); + + $redis = new Redis( array( + 'host' => $config['host'], + 'port' => $config['port'], + 'password' => $config['password'] ?: null, + 'timeout' => 0.5, + 'read_timeout' => 10, + 'persistent_connections' => true, + ) ); + + // Test connection by triggering initialization. + // The Redis adapter connects lazily, so we need to check it works. + return $redis; + } catch ( StorageException $e ) { + self::$last_error = sprintf( + /* translators: %s: Error message */ + __( 'Redis connection failed: %s', 'wp-prometheus' ), + $e->getMessage() + ); + return new InMemory(); + } catch ( \RedisException $e ) { + self::$last_error = sprintf( + /* translators: %s: Error message */ + __( 'Redis error: %s', 'wp-prometheus' ), + $e->getMessage() + ); + return new InMemory(); + } catch ( \Exception $e ) { + self::$last_error = sprintf( + /* translators: %s: Error message */ + __( 'Storage error: %s', 'wp-prometheus' ), + $e->getMessage() + ); + return new InMemory(); + } + } + + /** + * Create APCu storage adapter. + * + * @return Adapter + */ + private static function create_apcu_adapter(): Adapter { + if ( ! extension_loaded( 'apcu' ) ) { + self::$last_error = __( 'APCu extension is not installed.', 'wp-prometheus' ); + return new InMemory(); + } + + if ( ! apcu_enabled() ) { + self::$last_error = __( 'APCu is installed but not enabled.', 'wp-prometheus' ); + return new InMemory(); + } + + $prefix = self::get_apcu_prefix(); + + try { + return new APC( $prefix ); + } catch ( StorageException $e ) { + self::$last_error = sprintf( + /* translators: %s: Error message */ + __( 'APCu error: %s', 'wp-prometheus' ), + $e->getMessage() + ); + return new InMemory(); + } + } + + /** + * Get Redis configuration. + * + * @return array{host: string, port: int, password: string, database: int, prefix: string} + */ + public static function get_redis_config(): array { + // Check environment variables first. + $env_host = getenv( self::ENV_REDIS_HOST ); + $env_port = getenv( self::ENV_REDIS_PORT ); + $env_password = getenv( self::ENV_REDIS_PASSWORD ); + $env_database = getenv( self::ENV_REDIS_DATABASE ); + $env_prefix = getenv( self::ENV_REDIS_PREFIX ); + + // Get WordPress options as fallback. + $options = get_option( 'wp_prometheus_redis_config', array() ); + + return array( + 'host' => ( false !== $env_host && ! empty( $env_host ) ) ? $env_host : ( $options['host'] ?? '127.0.0.1' ), + 'port' => ( false !== $env_port && ! empty( $env_port ) ) ? (int) $env_port : ( (int) ( $options['port'] ?? 6379 ) ), + 'password' => ( false !== $env_password ) ? $env_password : ( $options['password'] ?? '' ), + 'database' => ( false !== $env_database && ! empty( $env_database ) ) ? (int) $env_database : ( (int) ( $options['database'] ?? 0 ) ), + 'prefix' => ( false !== $env_prefix && ! empty( $env_prefix ) ) ? $env_prefix : ( $options['prefix'] ?? self::DEFAULT_REDIS_PREFIX ), + ); + } + + /** + * Get APCu prefix. + * + * @return string + */ + public static function get_apcu_prefix(): string { + $env_prefix = getenv( self::ENV_APCU_PREFIX ); + if ( false !== $env_prefix && ! empty( $env_prefix ) ) { + return $env_prefix; + } + + return get_option( 'wp_prometheus_apcu_prefix', self::DEFAULT_APCU_PREFIX ); + } + + /** + * Save storage configuration. + * + * @param array $config Configuration array. + * @return void + */ + public static function save_config( array $config ): void { + if ( isset( $config['adapter'] ) ) { + update_option( 'wp_prometheus_storage_adapter', sanitize_key( $config['adapter'] ) ); + } + + if ( isset( $config['redis'] ) && is_array( $config['redis'] ) ) { + $redis_config = array( + 'host' => sanitize_text_field( $config['redis']['host'] ?? '127.0.0.1' ), + 'port' => absint( $config['redis']['port'] ?? 6379 ), + 'password' => sanitize_text_field( $config['redis']['password'] ?? '' ), + 'database' => absint( $config['redis']['database'] ?? 0 ), + 'prefix' => sanitize_key( $config['redis']['prefix'] ?? self::DEFAULT_REDIS_PREFIX ), + ); + update_option( 'wp_prometheus_redis_config', $redis_config ); + } + + if ( isset( $config['apcu_prefix'] ) ) { + update_option( 'wp_prometheus_apcu_prefix', sanitize_key( $config['apcu_prefix'] ) ); + } + + // Reset the singleton to apply new configuration. + self::reset(); + } + + /** + * Test storage adapter connection. + * + * @param string $adapter Adapter name. + * @param array $config Optional configuration to test. + * @return array{success: bool, message: string} + */ + public static function test_connection( string $adapter, array $config = array() ): array { + switch ( $adapter ) { + case self::ADAPTER_REDIS: + return self::test_redis_connection( $config ); + + case self::ADAPTER_APCU: + return self::test_apcu_connection( $config ); + + case self::ADAPTER_INMEMORY: + return array( + 'success' => true, + 'message' => __( 'In-Memory storage is always available.', 'wp-prometheus' ), + ); + + default: + return array( + 'success' => false, + 'message' => __( 'Unknown storage adapter.', 'wp-prometheus' ), + ); + } + } + + /** + * Test Redis connection. + * + * @param array $config Redis configuration. + * @return array{success: bool, message: string} + */ + private static function test_redis_connection( array $config ): array { + if ( ! extension_loaded( 'redis' ) ) { + return array( + 'success' => false, + 'message' => __( 'PHP Redis extension is not installed.', 'wp-prometheus' ), + ); + } + + $redis_config = ! empty( $config ) ? $config : self::get_redis_config(); + + try { + $redis = new \Redis(); + + $connected = $redis->connect( + $redis_config['host'], + $redis_config['port'], + 0.5 // timeout + ); + + if ( ! $connected ) { + return array( + 'success' => false, + 'message' => __( 'Could not connect to Redis server.', 'wp-prometheus' ), + ); + } + + if ( ! empty( $redis_config['password'] ) ) { + $authenticated = $redis->auth( $redis_config['password'] ); + if ( ! $authenticated ) { + return array( + 'success' => false, + 'message' => __( 'Redis authentication failed.', 'wp-prometheus' ), + ); + } + } + + if ( $redis_config['database'] > 0 ) { + $redis->select( $redis_config['database'] ); + } + + // Test with a ping. + $pong = $redis->ping(); + $redis->close(); + + if ( $pong ) { + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: Redis host:port */ + __( 'Successfully connected to Redis at %s.', 'wp-prometheus' ), + $redis_config['host'] . ':' . $redis_config['port'] + ), + ); + } + + return array( + 'success' => false, + 'message' => __( 'Redis ping failed.', 'wp-prometheus' ), + ); + } catch ( \RedisException $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: Error message */ + __( 'Redis error: %s', 'wp-prometheus' ), + $e->getMessage() + ), + ); + } + } + + /** + * Test APCu connection. + * + * @param array $config APCu configuration. + * @return array{success: bool, message: string} + */ + private static function test_apcu_connection( array $config ): array { + if ( ! extension_loaded( 'apcu' ) ) { + return array( + 'success' => false, + 'message' => __( 'APCu extension is not installed.', 'wp-prometheus' ), + ); + } + + if ( ! apcu_enabled() ) { + return array( + 'success' => false, + 'message' => __( 'APCu is installed but not enabled. Check your php.ini settings.', 'wp-prometheus' ), + ); + } + + // Test with a simple store/fetch. + $test_key = 'wp_prometheus_test_' . time(); + $test_value = 'test_' . wp_rand(); + + $stored = apcu_store( $test_key, $test_value, 5 ); + if ( ! $stored ) { + return array( + 'success' => false, + 'message' => __( 'APCu store operation failed.', 'wp-prometheus' ), + ); + } + + $fetched = apcu_fetch( $test_key ); + apcu_delete( $test_key ); + + if ( $fetched === $test_value ) { + $info = apcu_cache_info( true ); + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: Memory info */ + __( 'APCu is working. Memory: %s used.', 'wp-prometheus' ), + size_format( $info['mem_size'] ?? 0 ) + ), + ); + } + + return array( + 'success' => false, + 'message' => __( 'APCu fetch operation returned unexpected value.', 'wp-prometheus' ), + ); + } +} diff --git a/wp-prometheus.php b/wp-prometheus.php index 2de83fe..ccd0b7c 100644 --- a/wp-prometheus.php +++ b/wp-prometheus.php @@ -3,7 +3,7 @@ * Plugin Name: WP Prometheus * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus * Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics. - * Version: 0.3.0 + * Version: 0.4.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.3.0' ); +define( 'WP_PROMETHEUS_VERSION', '0.4.0' ); /** * Plugin file path.