You've already forked wp-prometheus
feat: Add persistent storage support for Redis and APCu (v0.4.0)
All checks were successful
Create Release Package / build-release (push) Successful in 56s
All checks were successful
Create Release Package / build-release (push) Successful in 56s
- 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 <noreply@anthropic.com>
This commit is contained in:
502
src/Metrics/StorageFactory.php
Normal file
502
src/Metrics/StorageFactory.php
Normal file
@@ -0,0 +1,502 @@
|
||||
<?php
|
||||
/**
|
||||
* Storage factory for Prometheus metrics.
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
namespace Magdev\WpPrometheus\Metrics;
|
||||
|
||||
use Prometheus\Storage\Adapter;
|
||||
use Prometheus\Storage\InMemory;
|
||||
use Prometheus\Storage\Redis;
|
||||
use Prometheus\Storage\APC;
|
||||
use Prometheus\Exception\StorageException;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* StorageFactory class.
|
||||
*
|
||||
* Creates and configures storage adapters for Prometheus metrics.
|
||||
* Supports configuration via WordPress options or environment variables.
|
||||
*/
|
||||
class StorageFactory {
|
||||
|
||||
/**
|
||||
* Available storage adapters.
|
||||
*/
|
||||
public const ADAPTER_INMEMORY = 'inmemory';
|
||||
public const ADAPTER_REDIS = 'redis';
|
||||
public const ADAPTER_APCU = 'apcu';
|
||||
|
||||
/**
|
||||
* Environment variable names.
|
||||
*/
|
||||
private const ENV_STORAGE_ADAPTER = 'WP_PROMETHEUS_STORAGE_ADAPTER';
|
||||
private const ENV_REDIS_HOST = 'WP_PROMETHEUS_REDIS_HOST';
|
||||
private const ENV_REDIS_PORT = 'WP_PROMETHEUS_REDIS_PORT';
|
||||
private const ENV_REDIS_PASSWORD = 'WP_PROMETHEUS_REDIS_PASSWORD';
|
||||
private const ENV_REDIS_DATABASE = 'WP_PROMETHEUS_REDIS_DATABASE';
|
||||
private const ENV_REDIS_PREFIX = 'WP_PROMETHEUS_REDIS_PREFIX';
|
||||
private const ENV_APCU_PREFIX = 'WP_PROMETHEUS_APCU_PREFIX';
|
||||
|
||||
/**
|
||||
* Default Redis prefix.
|
||||
*/
|
||||
private const DEFAULT_REDIS_PREFIX = 'WORDPRESS_PROMETHEUS_';
|
||||
|
||||
/**
|
||||
* Default APCu prefix.
|
||||
*/
|
||||
private const DEFAULT_APCU_PREFIX = 'wp_prom';
|
||||
|
||||
/**
|
||||
* Singleton instance of the storage adapter.
|
||||
*
|
||||
* @var Adapter|null
|
||||
*/
|
||||
private static ?Adapter $instance = null;
|
||||
|
||||
/**
|
||||
* Last error message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $last_error = '';
|
||||
|
||||
/**
|
||||
* Get the storage adapter instance.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
public static function get_adapter(): Adapter {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = self::create_adapter();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the singleton instance (useful for testing or config changes).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function reset(): void {
|
||||
self::$instance = null;
|
||||
self::$last_error = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_last_error(): string {
|
||||
return self::$last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available storage adapters.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function get_available_adapters(): array {
|
||||
return array(
|
||||
self::ADAPTER_INMEMORY => __( 'In-Memory (default, no persistence)', 'wp-prometheus' ),
|
||||
self::ADAPTER_REDIS => __( 'Redis (requires PHP Redis extension)', 'wp-prometheus' ),
|
||||
self::ADAPTER_APCU => __( 'APCu (requires APCu extension)', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a storage adapter is available on this system.
|
||||
*
|
||||
* @param string $adapter Adapter name.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_adapter_available( string $adapter ): bool {
|
||||
switch ( $adapter ) {
|
||||
case self::ADAPTER_INMEMORY:
|
||||
return true;
|
||||
|
||||
case self::ADAPTER_REDIS:
|
||||
return extension_loaded( 'redis' );
|
||||
|
||||
case self::ADAPTER_APCU:
|
||||
return extension_loaded( 'apcu' ) && apcu_enabled();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured storage adapter name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_configured_adapter(): string {
|
||||
// Check environment variable first.
|
||||
$env_adapter = getenv( self::ENV_STORAGE_ADAPTER );
|
||||
if ( false !== $env_adapter && ! empty( $env_adapter ) ) {
|
||||
return strtolower( $env_adapter );
|
||||
}
|
||||
|
||||
// Fall back to WordPress option.
|
||||
return get_option( 'wp_prometheus_storage_adapter', self::ADAPTER_INMEMORY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active storage adapter name (may differ from configured if fallback occurred).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_active_adapter(): string {
|
||||
// Ensure adapter is created.
|
||||
self::get_adapter();
|
||||
|
||||
$configured = self::get_configured_adapter();
|
||||
if ( self::is_adapter_available( $configured ) && empty( self::$last_error ) ) {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
return self::ADAPTER_INMEMORY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the storage adapter based on configuration.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
private static function create_adapter(): Adapter {
|
||||
$adapter = self::get_configured_adapter();
|
||||
self::$last_error = '';
|
||||
|
||||
switch ( $adapter ) {
|
||||
case self::ADAPTER_REDIS:
|
||||
return self::create_redis_adapter();
|
||||
|
||||
case self::ADAPTER_APCU:
|
||||
return self::create_apcu_adapter();
|
||||
|
||||
case self::ADAPTER_INMEMORY:
|
||||
default:
|
||||
return new InMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Redis storage adapter.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
private static function create_redis_adapter(): Adapter {
|
||||
if ( ! extension_loaded( 'redis' ) ) {
|
||||
self::$last_error = __( 'PHP Redis extension is not installed.', 'wp-prometheus' );
|
||||
return new InMemory();
|
||||
}
|
||||
|
||||
$config = self::get_redis_config();
|
||||
|
||||
try {
|
||||
Redis::setPrefix( $config['prefix'] );
|
||||
|
||||
$redis = new Redis( array(
|
||||
'host' => $config['host'],
|
||||
'port' => $config['port'],
|
||||
'password' => $config['password'] ?: null,
|
||||
'timeout' => 0.5,
|
||||
'read_timeout' => 10,
|
||||
'persistent_connections' => true,
|
||||
) );
|
||||
|
||||
// Test connection by triggering initialization.
|
||||
// The Redis adapter connects lazily, so we need to check it works.
|
||||
return $redis;
|
||||
} catch ( StorageException $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Redis connection failed: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
} catch ( \RedisException $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Redis error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
} catch ( \Exception $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Storage error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create APCu storage adapter.
|
||||
*
|
||||
* @return Adapter
|
||||
*/
|
||||
private static function create_apcu_adapter(): Adapter {
|
||||
if ( ! extension_loaded( 'apcu' ) ) {
|
||||
self::$last_error = __( 'APCu extension is not installed.', 'wp-prometheus' );
|
||||
return new InMemory();
|
||||
}
|
||||
|
||||
if ( ! apcu_enabled() ) {
|
||||
self::$last_error = __( 'APCu is installed but not enabled.', 'wp-prometheus' );
|
||||
return new InMemory();
|
||||
}
|
||||
|
||||
$prefix = self::get_apcu_prefix();
|
||||
|
||||
try {
|
||||
return new APC( $prefix );
|
||||
} catch ( StorageException $e ) {
|
||||
self::$last_error = sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'APCu error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
);
|
||||
return new InMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redis configuration.
|
||||
*
|
||||
* @return array{host: string, port: int, password: string, database: int, prefix: string}
|
||||
*/
|
||||
public static function get_redis_config(): array {
|
||||
// Check environment variables first.
|
||||
$env_host = getenv( self::ENV_REDIS_HOST );
|
||||
$env_port = getenv( self::ENV_REDIS_PORT );
|
||||
$env_password = getenv( self::ENV_REDIS_PASSWORD );
|
||||
$env_database = getenv( self::ENV_REDIS_DATABASE );
|
||||
$env_prefix = getenv( self::ENV_REDIS_PREFIX );
|
||||
|
||||
// Get WordPress options as fallback.
|
||||
$options = get_option( 'wp_prometheus_redis_config', array() );
|
||||
|
||||
return array(
|
||||
'host' => ( false !== $env_host && ! empty( $env_host ) ) ? $env_host : ( $options['host'] ?? '127.0.0.1' ),
|
||||
'port' => ( false !== $env_port && ! empty( $env_port ) ) ? (int) $env_port : ( (int) ( $options['port'] ?? 6379 ) ),
|
||||
'password' => ( false !== $env_password ) ? $env_password : ( $options['password'] ?? '' ),
|
||||
'database' => ( false !== $env_database && ! empty( $env_database ) ) ? (int) $env_database : ( (int) ( $options['database'] ?? 0 ) ),
|
||||
'prefix' => ( false !== $env_prefix && ! empty( $env_prefix ) ) ? $env_prefix : ( $options['prefix'] ?? self::DEFAULT_REDIS_PREFIX ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get APCu prefix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_apcu_prefix(): string {
|
||||
$env_prefix = getenv( self::ENV_APCU_PREFIX );
|
||||
if ( false !== $env_prefix && ! empty( $env_prefix ) ) {
|
||||
return $env_prefix;
|
||||
}
|
||||
|
||||
return get_option( 'wp_prometheus_apcu_prefix', self::DEFAULT_APCU_PREFIX );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save storage configuration.
|
||||
*
|
||||
* @param array $config Configuration array.
|
||||
* @return void
|
||||
*/
|
||||
public static function save_config( array $config ): void {
|
||||
if ( isset( $config['adapter'] ) ) {
|
||||
update_option( 'wp_prometheus_storage_adapter', sanitize_key( $config['adapter'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $config['redis'] ) && is_array( $config['redis'] ) ) {
|
||||
$redis_config = array(
|
||||
'host' => sanitize_text_field( $config['redis']['host'] ?? '127.0.0.1' ),
|
||||
'port' => absint( $config['redis']['port'] ?? 6379 ),
|
||||
'password' => sanitize_text_field( $config['redis']['password'] ?? '' ),
|
||||
'database' => absint( $config['redis']['database'] ?? 0 ),
|
||||
'prefix' => sanitize_key( $config['redis']['prefix'] ?? self::DEFAULT_REDIS_PREFIX ),
|
||||
);
|
||||
update_option( 'wp_prometheus_redis_config', $redis_config );
|
||||
}
|
||||
|
||||
if ( isset( $config['apcu_prefix'] ) ) {
|
||||
update_option( 'wp_prometheus_apcu_prefix', sanitize_key( $config['apcu_prefix'] ) );
|
||||
}
|
||||
|
||||
// Reset the singleton to apply new configuration.
|
||||
self::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test storage adapter connection.
|
||||
*
|
||||
* @param string $adapter Adapter name.
|
||||
* @param array $config Optional configuration to test.
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
public static function test_connection( string $adapter, array $config = array() ): array {
|
||||
switch ( $adapter ) {
|
||||
case self::ADAPTER_REDIS:
|
||||
return self::test_redis_connection( $config );
|
||||
|
||||
case self::ADAPTER_APCU:
|
||||
return self::test_apcu_connection( $config );
|
||||
|
||||
case self::ADAPTER_INMEMORY:
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'In-Memory storage is always available.', 'wp-prometheus' ),
|
||||
);
|
||||
|
||||
default:
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Unknown storage adapter.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Redis connection.
|
||||
*
|
||||
* @param array $config Redis configuration.
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
private static function test_redis_connection( array $config ): array {
|
||||
if ( ! extension_loaded( 'redis' ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'PHP Redis extension is not installed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
$redis_config = ! empty( $config ) ? $config : self::get_redis_config();
|
||||
|
||||
try {
|
||||
$redis = new \Redis();
|
||||
|
||||
$connected = $redis->connect(
|
||||
$redis_config['host'],
|
||||
$redis_config['port'],
|
||||
0.5 // timeout
|
||||
);
|
||||
|
||||
if ( ! $connected ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Could not connect to Redis server.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $redis_config['password'] ) ) {
|
||||
$authenticated = $redis->auth( $redis_config['password'] );
|
||||
if ( ! $authenticated ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Redis authentication failed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( $redis_config['database'] > 0 ) {
|
||||
$redis->select( $redis_config['database'] );
|
||||
}
|
||||
|
||||
// Test with a ping.
|
||||
$pong = $redis->ping();
|
||||
$redis->close();
|
||||
|
||||
if ( $pong ) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Redis host:port */
|
||||
__( 'Successfully connected to Redis at %s.', 'wp-prometheus' ),
|
||||
$redis_config['host'] . ':' . $redis_config['port']
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Redis ping failed.', 'wp-prometheus' ),
|
||||
);
|
||||
} catch ( \RedisException $e ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Error message */
|
||||
__( 'Redis error: %s', 'wp-prometheus' ),
|
||||
$e->getMessage()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test APCu connection.
|
||||
*
|
||||
* @param array $config APCu configuration.
|
||||
* @return array{success: bool, message: string}
|
||||
*/
|
||||
private static function test_apcu_connection( array $config ): array {
|
||||
if ( ! extension_loaded( 'apcu' ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu extension is not installed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! apcu_enabled() ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu is installed but not enabled. Check your php.ini settings.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Test with a simple store/fetch.
|
||||
$test_key = 'wp_prometheus_test_' . time();
|
||||
$test_value = 'test_' . wp_rand();
|
||||
|
||||
$stored = apcu_store( $test_key, $test_value, 5 );
|
||||
if ( ! $stored ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu store operation failed.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
|
||||
$fetched = apcu_fetch( $test_key );
|
||||
apcu_delete( $test_key );
|
||||
|
||||
if ( $fetched === $test_value ) {
|
||||
$info = apcu_cache_info( true );
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Memory info */
|
||||
__( 'APCu is working. Memory: %s used.', 'wp-prometheus' ),
|
||||
size_format( $info['mem_size'] ?? 0 )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'APCu fetch operation returned unexpected value.', 'wp-prometheus' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user