From 898af5e9d2a9830561d3a55d2b9dea01410651a3 Mon Sep 17 00:00:00 2001 From: magdev Date: Mon, 2 Feb 2026 16:15:53 +0100 Subject: [PATCH] feat: Add persistent storage support for Redis and APCu (v0.4.0) - Add StorageFactory class for storage adapter selection with fallback - Support Redis storage for shared metrics across instances - Support APCu storage for high-performance single-server deployments - Add Storage tab in admin settings with configuration UI - Add connection testing for Redis and APCu adapters - Support environment variables for Docker/containerized deployments - Update Collector to use StorageFactory instead of hardcoded InMemory - Add all translations (English and German) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 32 ++ CLAUDE.md | 31 +- PLAN.md | 61 +++- assets/js/admin.js | 130 ++++++++ languages/wp-prometheus-de_CH.mo | Bin 12266 -> 18211 bytes languages/wp-prometheus-de_CH.po | 264 +++++++++++++++- languages/wp-prometheus.pot | 264 +++++++++++++++- src/Admin/Settings.php | 414 +++++++++++++++++++++++++ src/Metrics/Collector.php | 4 +- src/Metrics/StorageFactory.php | 502 +++++++++++++++++++++++++++++++ wp-prometheus.php | 4 +- 11 files changed, 1693 insertions(+), 13 deletions(-) create mode 100644 src/Metrics/StorageFactory.php 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 b3f880875135b9624f6e5b6c604d699bed8e51ad..94f2636acd300111c1b56c1eedef8c4ec5706627 100644 GIT binary patch literal 18211 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 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.