From 257bf8bdf7cda7ca47b0b6275eed9f321e8074d9 Mon Sep 17 00:00:00 2001 From: Luke Adamczewski Date: Tue, 12 Sep 2023 17:54:02 +0200 Subject: [PATCH] Added support of Predis library as storage adapter (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ɓukasz Adamczewski --- composer.json | 2 + examples/metrics.php | 2 + src/Prometheus/Storage/Predis.php | 111 ++++++++++++++++ src/Prometheus/Storage/Redis.php | 119 +++++++++++------- .../Predis/CollectorRegistryTest.php | 21 ++++ tests/Test/Prometheus/Predis/CounterTest.php | 22 ++++ .../Predis/CounterWithPrefixTest.php | 30 +++++ tests/Test/Prometheus/Predis/GaugeTest.php | 23 ++++ .../Prometheus/Predis/GaugeWithPrefixTest.php | 29 +++++ .../Test/Prometheus/Predis/HistogramTest.php | 23 ++++ .../Predis/HistogramWithPrefixTest.php | 29 +++++ tests/Test/Prometheus/Predis/SummaryTest.php | 22 ++++ .../Predis/SummaryWithPrefixTest.php | 30 +++++ 13 files changed, 420 insertions(+), 43 deletions(-) create mode 100644 src/Prometheus/Storage/Predis.php create mode 100644 tests/Test/Prometheus/Predis/CollectorRegistryTest.php create mode 100644 tests/Test/Prometheus/Predis/CounterTest.php create mode 100644 tests/Test/Prometheus/Predis/CounterWithPrefixTest.php create mode 100644 tests/Test/Prometheus/Predis/GaugeTest.php create mode 100644 tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php create mode 100644 tests/Test/Prometheus/Predis/HistogramTest.php create mode 100644 tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php create mode 100644 tests/Test/Prometheus/Predis/SummaryTest.php create mode 100644 tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php diff --git a/composer.json b/composer.json index 320ad059..21b3830d 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,13 @@ "phpstan/phpstan-phpunit": "^1.1.0", "phpstan/phpstan-strict-rules": "^1.1.0", "phpunit/phpunit": "^9.4", + "predis/predis": "^2.0", "squizlabs/php_codesniffer": "^3.6", "symfony/polyfill-apcu": "^1.6" }, "suggest": { "ext-redis": "Required if using Redis.", + "predis/predis": "Required if using Predis.", "ext-apc": "Required if using APCu.", "promphp/prometheus_push_gateway_php": "An easy client for using Prometheus PushGateway.", "symfony/polyfill-apcu": "Required if you use APCu on PHP8.0+" diff --git a/examples/metrics.php b/examples/metrics.php index 9c0fdb80..844c2b24 100644 --- a/examples/metrics.php +++ b/examples/metrics.php @@ -11,6 +11,8 @@ if ($adapter === 'redis') { Redis::setDefaultOptions(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); +} elseif ($adapter === 'predis') { + $adapter = new Prometheus\Storage\Predis(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'apcng') { diff --git a/src/Prometheus/Storage/Predis.php b/src/Prometheus/Storage/Predis.php new file mode 100644 index 00000000..19b1070c --- /dev/null +++ b/src/Prometheus/Storage/Predis.php @@ -0,0 +1,111 @@ + '127.0.0.1', + 'port' => 6379, + 'scheme' => 'tcp', + 'timeout' => 0.1, + 'read_timeout' => '10', + 'persistent' => 0, + 'password' => null, + ]; + + public function __construct(array $options = []) + { + $this->options = array_merge(self::$defaultOptions, $options); + + parent::__construct($options); + + $this->redis = new Client($this->options); + } + + public static function fromClient(Client $redis): self + { + if ($redis->isConnected() === false) { + throw new StorageException('Connection to Redis server not established'); + } + + $self = new self(); + $self->redis = $redis; + + return $self; + } + + protected function ensureOpenConnection(): void + { + if ($this->redis->isConnected() === false) { + $this->redis->connect(); + } + } + + public static function fromExistingConnection(\Redis $redis): Redis + { + throw new \RuntimeException('This method is not supported by predis adapter'); + } + + protected function getGlobalPrefix(): ?string + { + if ($this->redis->getOptions()->prefix === null) { + return null; + } + + if ($this->redis->getOptions()->prefix instanceof Prefix) { + return $this->redis->getOptions()->prefix->getPrefix(); + } + + return null; + } + + /** + * @param mixed[] $args + * @param int $keysCount + * @return mixed[] + */ + protected function evalParams(array $args, int $keysCount): array + { + return [$keysCount, ...$args]; + } + + + protected function prefix(string $key): string + { + // the predis is doing key prefixing on its own + return ''; + } + + protected function setParams(array $input): array + { + $values = array_values($input); + $params = []; + + if (isset($input['EX'])) { + $params[] = 'EX'; + $params[] = $input['EX']; + } + + if (isset($input['PX'])) { + $params[] = 'PX'; + $params[] = $input['PX']; + } + + $params[] = $values[0]; + + return $params; + } +} diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index f8f67431..b681cf7a 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -5,6 +5,7 @@ namespace Prometheus\Storage; use InvalidArgumentException; +use Predis\Client; use Prometheus\Counter; use Prometheus\Exception\StorageException; use Prometheus\Gauge; @@ -38,12 +39,12 @@ class Redis implements Adapter /** * @var mixed[] */ - private $options = []; + protected $options = []; /** - * @var \Redis + * @var \Redis|Client */ - private $redis; + protected $redis; /** * @var boolean @@ -112,8 +113,7 @@ public function wipeStorage(): void $searchPattern = ""; - $globalPrefix = $this->redis->getOption(\Redis::OPT_PREFIX); - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int + $globalPrefix = $this->getGlobalPrefix(); if (is_string($globalPrefix)) { $searchPattern .= $globalPrefix; } @@ -133,8 +133,10 @@ public function wipeStorage(): void until cursor == "0" LUA , - [$searchPattern], - 0 + ...$this->evalParams( + [$searchPattern], + 0 + ) ); } @@ -187,7 +189,7 @@ function (array $metric): MetricFamilySamples { /** * @throws StorageException */ - private function ensureOpenConnection(): void + protected function ensureOpenConnection(): void { if ($this->connectionInitialized === true) { return; @@ -260,15 +262,17 @@ public function updateHistogram(array $data): void return result LUA , - [ - $this->toMetricKey($data), - self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, - json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]), - json_encode(['b' => $bucketToIncrease, 'labelValues' => $data['labelValues']]), - $data['value'], - json_encode($metaData), - ], - 2 + ...$this->evalParams( + [ + $this->toMetricKey($data), + self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]), + json_encode(['b' => $bucketToIncrease, 'labelValues' => $data['labelValues']]), + $data['value'], + json_encode($metaData), + ], + 2 + ) ); } @@ -301,7 +305,7 @@ public function updateSummary(array $data): void $done = false; while (!$done) { $sampleKey = $valueKey . ':' . uniqid('', true); - $done = $this->redis->set($sampleKey, $data['value'], ['NX', 'EX' => $data['maxAgeSeconds']]); + $done = $this->redis->set($sampleKey, $data['value'], ...$this->setParams(['NX', 'EX' => $data['maxAgeSeconds']])); } } @@ -331,15 +335,17 @@ public function updateGauge(array $data): void end LUA , - [ - $this->toMetricKey($data), - self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, - $this->getRedisCommand($data['command']), - json_encode($data['labelValues']), - $data['value'], - json_encode($metaData), - ], - 2 + ...$this->evalParams( + [ + $this->toMetricKey($data), + self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + $this->getRedisCommand($data['command']), + json_encode($data['labelValues']), + $data['value'], + json_encode($metaData), + ], + 2 + ) ); } @@ -362,15 +368,17 @@ public function updateCounter(array $data): void return result LUA , - [ - $this->toMetricKey($data), - self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, - $this->getRedisCommand($data['command']), - $data['value'], - json_encode($data['labelValues']), - json_encode($metaData), - ], - 2 + ...$this->evalParams( + [ + $this->toMetricKey($data), + self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + $this->getRedisCommand($data['command']), + $data['value'], + json_encode($data['labelValues']), + json_encode($metaData), + ], + 2 + ) ); } @@ -395,7 +403,7 @@ private function collectHistograms(): array sort($keys); $histograms = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); + $raw = $this->redis->hGetAll(str_replace($this->prefix(''), '', $key)); if (!isset($raw['__meta'])) { continue; } @@ -473,12 +481,11 @@ private function collectHistograms(): array */ private function removePrefixFromKey(string $key): string { - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int - if ($this->redis->getOption(\Redis::OPT_PREFIX) === null) { + if ($this->getGlobalPrefix() === null) { return $key; } - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int - return substr($key, strlen($this->redis->getOption(\Redis::OPT_PREFIX))); + + return substr($key, strlen($this->getGlobalPrefix())); } /** @@ -578,7 +585,7 @@ private function collectGauges(bool $sortMetrics = true): array sort($keys); $gauges = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); + $raw = $this->redis->hGetAll(str_replace($this->prefix(''), '', $key)); if (!isset($raw['__meta'])) { continue; } @@ -614,7 +621,7 @@ private function collectCounters(bool $sortMetrics = true): array sort($keys); $counters = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); + $raw = $this->redis->hGetAll(str_replace($this->prefix(''), '', $key)); if (!isset($raw['__meta'])) { continue; } @@ -699,4 +706,30 @@ private function decodeLabelValues(string $values): array } return $decodedValues; } + + protected function getGlobalPrefix(): ?string + { + // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int + return $this->redis->getOption(\Redis::OPT_PREFIX); + } + + /** + * @param mixed[] $args + * @param int $keysCount + * @return mixed[] + */ + protected function evalParams(array $args, int $keysCount): array + { + return [$args, $keysCount]; + } + + protected function prefix(string $key): string + { + return $this->redis->_prefix($key); + } + + protected function setParams(array $params): array + { + return [$params]; + } } diff --git a/tests/Test/Prometheus/Predis/CollectorRegistryTest.php b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php new file mode 100644 index 00000000..7d8a8f77 --- /dev/null +++ b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php @@ -0,0 +1,21 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/CounterTest.php b/tests/Test/Prometheus/Predis/CounterTest.php new file mode 100644 index 00000000..fdbe19af --- /dev/null +++ b/tests/Test/Prometheus/Predis/CounterTest.php @@ -0,0 +1,22 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php new file mode 100644 index 00000000..c2e47f2e --- /dev/null +++ b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php @@ -0,0 +1,30 @@ + REDIS_HOST, + 'prefix' => 'prefix:', + ]); + + $client->connect(); + + $this->adapter = Predis::fromClient($client); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/GaugeTest.php b/tests/Test/Prometheus/Predis/GaugeTest.php new file mode 100644 index 00000000..69b4598c --- /dev/null +++ b/tests/Test/Prometheus/Predis/GaugeTest.php @@ -0,0 +1,23 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php new file mode 100644 index 00000000..f12885b1 --- /dev/null +++ b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php @@ -0,0 +1,29 @@ + REDIS_HOST, + 'prefix' => 'prefix:', + ]); + + $client->connect(); + + $this->adapter = Predis::fromClient($client); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/HistogramTest.php b/tests/Test/Prometheus/Predis/HistogramTest.php new file mode 100644 index 00000000..583c5ac0 --- /dev/null +++ b/tests/Test/Prometheus/Predis/HistogramTest.php @@ -0,0 +1,23 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php new file mode 100644 index 00000000..0e3e37e4 --- /dev/null +++ b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php @@ -0,0 +1,29 @@ + REDIS_HOST, + 'prefix' => 'prefix:', + ]); + + $client->connect(); + + $this->adapter = Predis::fromClient($client); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php new file mode 100644 index 00000000..a0776a75 --- /dev/null +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -0,0 +1,22 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php new file mode 100644 index 00000000..11022f06 --- /dev/null +++ b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php @@ -0,0 +1,30 @@ + REDIS_HOST, + 'prefix' => 'prefix:', + ]); + + $client->connect(); + + $this->adapter = Predis::fromClient($client); + $this->adapter->wipeStorage(); + } +}