From f519b2c5123aec01f09ff4d8486dbe3111942f4c Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 00:13:43 +0200 Subject: [PATCH 01/40] feature: add predis to dev deps Signed-off-by: Mateusz Cholewka --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index fd5e594..88b7c89 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "phpstan/phpstan-phpunit": "^1.1.0", "phpstan/phpstan-strict-rules": "^1.1.0", "phpunit/phpunit": "^9.4", + "predis/predis": "^2.3", "squizlabs/php_codesniffer": "^3.6", "symfony/polyfill-apcu": "^1.6" }, From 6e34684e14fd9287d4c429a45fd4c85b3fd45ea4 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 00:14:19 +0200 Subject: [PATCH 02/40] feature: implement redis client interface Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/Redis.php | 282 +++++++----------- .../Storage/RedisClients/RedisClient.php | 36 +++ .../RedisClients/RedisClientException.php | 7 + 3 files changed, 152 insertions(+), 173 deletions(-) create mode 100644 src/Prometheus/Storage/RedisClients/RedisClient.php create mode 100644 src/Prometheus/Storage/RedisClients/RedisClientException.php diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 730fac8..2b11436 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\MetricJsonException; use Prometheus\Exception\StorageException; @@ -12,6 +13,10 @@ use Prometheus\Histogram; use Prometheus\Math; use Prometheus\MetricFamilySamples; +use Prometheus\Storage\RedisClients\PHPRedis; +use Prometheus\Storage\RedisClients\Predis; +use Prometheus\Storage\RedisClients\RedisClient; +use Prometheus\Storage\RedisClients\RedisClientException; use Prometheus\Summary; use RuntimeException; @@ -43,54 +48,51 @@ class Redis implements Adapter private $options = []; /** - * @var \Redis + * @var RedisClient */ private $redis; - /** - * @var boolean - */ - private $connectionInitialized = false; - /** * Redis constructor. - * @param mixed[] $options + * + * @param mixed[] $options */ public function __construct(array $options = []) { $this->options = array_merge(self::$defaultOptions, $options); - $this->redis = new \Redis(); + $this->redis = PHPRedis::create($this->options); } /** - * @param \Redis $redis - * @return self * @throws StorageException */ - public static function fromExistingConnection(\Redis $redis): self + public static function fromExistingConnection(\Redis|Client $redis): self { + $self = new self; + + if ($redis instanceof Client) { + $self->redis = new Predis($redis); + + return $self; + } + if ($redis->isConnected() === false) { throw new StorageException('Connection to Redis server not established'); } - $self = new self(); - $self->connectionInitialized = true; - $self->redis = $redis; + $self->redis = new PHPRedis($redis, self::$defaultOptions); return $self; } /** - * @param mixed[] $options + * @param mixed[] $options */ public static function setDefaultOptions(array $options): void { self::$defaultOptions = array_merge(self::$defaultOptions, $options); } - /** - * @param string $prefix - */ public static function setPrefix(string $prefix): void { self::$prefix = $prefix; @@ -98,6 +100,7 @@ public static function setPrefix(string $prefix): void /** * @throws StorageException + * * @deprecated use replacement method wipeStorage from Adapter interface */ public function flushRedis(): void @@ -106,15 +109,15 @@ public function flushRedis(): void } /** - * @inheritDoc + * {@inheritDoc} */ public function wipeStorage(): void { - $this->ensureOpenConnection(); + $this->redis->ensureOpenConnection(); - $searchPattern = ""; + $searchPattern = ''; - $globalPrefix = $this->redis->getOption(\Redis::OPT_PREFIX); + $globalPrefix = $this->redis->getOption(RedisClient::OPT_PREFIX); // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int if (is_string($globalPrefix)) { $searchPattern .= $globalPrefix; @@ -124,7 +127,7 @@ public function wipeStorage(): void $searchPattern .= '*'; $this->redis->eval( - <<encodeLabelValues($data['labelValues']), - 'value' + 'value', ]); } /** * @return MetricFamilySamples[] + * * @throws StorageException */ public function collect(bool $sortMetrics = true): array { - $this->ensureOpenConnection(); + $this->redis->ensureOpenConnection(); $metrics = $this->collectHistograms(); $metrics = array_merge($metrics, $this->collectGauges($sortMetrics)); $metrics = array_merge($metrics, $this->collectCounters($sortMetrics)); $metrics = array_merge($metrics, $this->collectSummaries()); + return array_map( function (array $metric): MetricFamilySamples { return new MetricFamilySamples($metric); @@ -188,76 +189,13 @@ function (array $metric): MetricFamilySamples { } /** - * @throws StorageException - */ - private function ensureOpenConnection(): void - { - if ($this->connectionInitialized === true) { - return; - } - - $this->connectToServer(); - $authParams = []; - - if (isset($this->options['user']) && $this->options['user'] !== '') { - $authParams[] = $this->options['user']; - } - - if (isset($this->options['password'])) { - $authParams[] = $this->options['password']; - } - - if ($authParams !== []) { - $this->redis->auth($authParams); - } - - if (isset($this->options['database'])) { - $this->redis->select($this->options['database']); - } - - $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $this->options['read_timeout']); - - $this->connectionInitialized = true; - } - - /** - * @throws StorageException - */ - private function connectToServer(): void - { - try { - $connection_successful = false; - if ($this->options['persistent_connections'] !== false) { - $connection_successful = $this->redis->pconnect( - $this->options['host'], - (int)$this->options['port'], - (float)$this->options['timeout'] - ); - } else { - $connection_successful = $this->redis->connect($this->options['host'], (int)$this->options['port'], (float)$this->options['timeout']); - } - if (!$connection_successful) { - throw new StorageException( - sprintf("Can't connect to Redis server. %s", $this->redis->getLastError()), - 0 - ); - } - } catch (\RedisException $e) { - throw new StorageException( - sprintf("Can't connect to Redis server. %s", $e->getMessage()), - $e->getCode(), - $e - ); - } - } - - /** - * @param mixed[] $data + * @param mixed[] $data + * * @throws StorageException */ public function updateHistogram(array $data): void { - $this->ensureOpenConnection(); + $this->redis->ensureOpenConnection(); $bucketToIncrease = '+Inf'; foreach ($data['buckets'] as $bucket) { if ($data['value'] <= $bucket) { @@ -269,7 +207,7 @@ public function updateHistogram(array $data): void unset($metaData['value'], $metaData['labelValues']); $this->redis->eval( - <<= tonumber(ARGV[3]) then @@ -281,7 +219,7 @@ public function updateHistogram(array $data): void , [ $this->toMetricKey($data), - self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + 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'], @@ -292,50 +230,51 @@ public function updateHistogram(array $data): void } /** - * @param mixed[] $data + * @param mixed[] $data + * * @throws StorageException */ public function updateSummary(array $data): void { - $this->ensureOpenConnection(); + $this->redis->ensureOpenConnection(); // store meta - $summaryKey = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $metaKey = $summaryKey . ':' . $this->metaKey($data); + $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $metaKey = $summaryKey.':'.$this->metaKey($data); $json = json_encode($this->metaData($data)); - if (false === $json) { + if ($json === false) { throw new RuntimeException(json_last_error_msg()); } - $this->redis->setNx($metaKey, $json);/** @phpstan-ignore-line */ - + $this->redis->setNx($metaKey, $json); /** @phpstan-ignore-line */ // store value key - $valueKey = $summaryKey . ':' . $this->valueKey($data); + $valueKey = $summaryKey.':'.$this->valueKey($data); $json = json_encode($this->encodeLabelValues($data['labelValues'])); - if (false === $json) { + if ($json === false) { throw new RuntimeException(json_last_error_msg()); } - $this->redis->setNx($valueKey, $json);/** @phpstan-ignore-line */ + $this->redis->setNx($valueKey, $json); /** @phpstan-ignore-line */ // trick to handle uniqid collision $done = false; - while (!$done) { - $sampleKey = $valueKey . ':' . uniqid('', true); + while (! $done) { + $sampleKey = $valueKey.':'.uniqid('', true); $done = $this->redis->set($sampleKey, $data['value'], ['NX', 'EX' => $data['maxAgeSeconds']]); } } /** - * @param mixed[] $data + * @param mixed[] $data + * * @throws StorageException */ public function updateGauge(array $data): void { - $this->ensureOpenConnection(); + $this->redis->ensureOpenConnection(); $metaData = $data; unset($metaData['value'], $metaData['labelValues'], $metaData['command']); $this->redis->eval( - <<toMetricKey($data), - self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + self::$prefix.Gauge::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX, $this->getRedisCommand($data['command']), json_encode($data['labelValues']), $data['value'], @@ -364,16 +303,17 @@ public function updateGauge(array $data): void } /** - * @param mixed[] $data + * @param mixed[] $data + * * @throws StorageException */ public function updateCounter(array $data): void { - $this->ensureOpenConnection(); + $this->redis->ensureOpenConnection(); $metaData = $data; unset($metaData['value'], $metaData['labelValues'], $metaData['command']); $this->redis->eval( - <<toMetricKey($data), - self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + self::$prefix.Counter::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX, $this->getRedisCommand($data['command']), $data['value'], json_encode($data['labelValues']), @@ -394,15 +334,15 @@ public function updateCounter(array $data): void ); } - /** - * @param mixed[] $data + * @param mixed[] $data * @return mixed[] */ private function metaData(array $data): array { $metricsMetaData = $data; unset($metricsMetaData['value'], $metricsMetaData['command'], $metricsMetaData['labelValues']); + return $metricsMetaData; } @@ -411,12 +351,12 @@ private function metaData(array $data): array */ private function collectHistograms(): array { - $keys = $this->redis->sMembers(self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + $keys = $this->redis->sMembers(self::$prefix.Histogram::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $histograms = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->_prefix(''))); - if (!isset($raw['__meta'])) { + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getPrefix())); + if (! isset($raw['__meta'])) { continue; } $histogram = json_decode($raw['__meta'], true); @@ -440,7 +380,7 @@ private function collectHistograms(): array // We need set semantics. // This is the equivalent of array_unique but for arrays of arrays. - $allLabelValues = array_map("unserialize", array_unique(array_map("serialize", $allLabelValues))); + $allLabelValues = array_map('unserialize', array_unique(array_map('serialize', $allLabelValues))); sort($allLabelValues); foreach ($allLabelValues as $labelValues) { @@ -450,9 +390,9 @@ private function collectHistograms(): array $acc = 0; foreach ($histogram['buckets'] as $bucket) { $bucketKey = json_encode(['b' => $bucket, 'labelValues' => $labelValues]); - if (!isset($raw[$bucketKey])) { + if (! isset($raw[$bucketKey])) { $histogram['samples'][] = [ - 'name' => $histogram['name'] . '_bucket', + 'name' => $histogram['name'].'_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($labelValues, [$bucket]), 'value' => $acc, @@ -460,7 +400,7 @@ private function collectHistograms(): array } else { $acc += $raw[$bucketKey]; $histogram['samples'][] = [ - 'name' => $histogram['name'] . '_bucket', + 'name' => $histogram['name'].'_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($labelValues, [$bucket]), 'value' => $acc, @@ -470,7 +410,7 @@ private function collectHistograms(): array // Add the count $histogram['samples'][] = [ - 'name' => $histogram['name'] . '_count', + 'name' => $histogram['name'].'_count', 'labelNames' => [], 'labelValues' => $labelValues, 'value' => $acc, @@ -478,7 +418,7 @@ private function collectHistograms(): array // Add the sum $histogram['samples'][] = [ - 'name' => $histogram['name'] . '_sum', + 'name' => $histogram['name'].'_sum', 'labelNames' => [], 'labelValues' => $labelValues, 'value' => $raw[json_encode(['b' => 'sum', 'labelValues' => $labelValues])], @@ -486,22 +426,19 @@ private function collectHistograms(): array } $histograms[] = $histogram; } + return $histograms; } - /** - * @param string $key - * - * @return string - */ 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->redis->getOption(RedisClient::OPT_PREFIX) === 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->redis->getOption(RedisClient::OPT_PREFIX))); } /** @@ -509,9 +446,9 @@ private function removePrefixFromKey(string $key): string */ private function collectSummaries(): array { - $math = new Math(); - $summaryKey = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $keys = $this->redis->keys($summaryKey . ':*:meta'); + $math = new Math; + $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $keys = $this->redis->keys($summaryKey.':*:meta'); $summaries = []; foreach ($keys as $metaKeyWithPrefix) { @@ -532,7 +469,7 @@ private function collectSummaries(): array 'samples' => [], ]; - $values = $this->redis->keys($summaryKey . ':' . $metaData['name'] . ':*:value'); + $values = $this->redis->keys($summaryKey.':'.$metaData['name'].':*:value'); foreach ($values as $valueKeyWithPrefix) { $valueKey = $this->removePrefixFromKey($valueKeyWithPrefix); $rawValue = $this->redis->get($valueKey); @@ -544,18 +481,19 @@ private function collectSummaries(): array $decodedLabelValues = $this->decodeLabelValues($encodedLabelValues); $samples = []; - $sampleValues = $this->redis->keys($summaryKey . ':' . $metaData['name'] . ':' . $encodedLabelValues . ':value:*'); + $sampleValues = $this->redis->keys($summaryKey.':'.$metaData['name'].':'.$encodedLabelValues.':value:*'); foreach ($sampleValues as $sampleValueWithPrefix) { $sampleValue = $this->removePrefixFromKey($sampleValueWithPrefix); - $samples[] = (float)$this->redis->get($sampleValue); + $samples[] = (float) $this->redis->get($sampleValue); } if (count($samples) === 0) { try { $this->redis->del($valueKey); - } catch (\RedisException $e) { + } catch (RedisClientException $e) { // ignore if we can't delete the key } + continue; } @@ -572,7 +510,7 @@ private function collectSummaries(): array // Add the count $data['samples'][] = [ - 'name' => $metaData['name'] . '_count', + 'name' => $metaData['name'].'_count', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => count($samples), @@ -580,7 +518,7 @@ private function collectSummaries(): array // Add the sum $data['samples'][] = [ - 'name' => $metaData['name'] . '_sum', + 'name' => $metaData['name'].'_sum', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => array_sum($samples), @@ -592,11 +530,12 @@ private function collectSummaries(): array } else { try { $this->redis->del($metaKey); - } catch (\RedisException $e) { + } catch (RedisClientException $e) { // ignore if we can't delete the key } } } + return $summaries; } @@ -605,12 +544,12 @@ private function collectSummaries(): array */ private function collectGauges(bool $sortMetrics = true): array { - $keys = $this->redis->sMembers(self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + $keys = $this->redis->sMembers(self::$prefix.Gauge::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $gauges = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->_prefix(''))); - if (!isset($raw['__meta'])) { + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getPrefix())); + if (! isset($raw['__meta'])) { continue; } $gauge = json_decode($raw['__meta'], true); @@ -630,27 +569,29 @@ private function collectGauges(bool $sortMetrics = true): array if ($sortMetrics) { usort($gauge['samples'], function ($a, $b): int { - return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); + return strcmp(implode('', $a['labelValues']), implode('', $b['labelValues'])); }); } $gauges[] = $gauge; } + return $gauges; } /** * @return mixed[] + * * @throws MetricJsonException */ private function collectCounters(bool $sortMetrics = true): array { - $keys = $this->redis->sMembers(self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + $keys = $this->redis->sMembers(self::$prefix.Counter::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $counters = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->_prefix(''))); - if (!isset($raw['__meta'])) { + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getPrefix())); + if (! isset($raw['__meta'])) { continue; } $counter = json_decode($raw['__meta'], true); @@ -672,19 +613,16 @@ private function collectCounters(bool $sortMetrics = true): array if ($sortMetrics) { usort($counter['samples'], function ($a, $b): int { - return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); + return strcmp(implode('', $a['labelValues']), implode('', $b['labelValues'])); }); } $counters[] = $counter; } + return $counters; } - /** - * @param int $cmd - * @return string - */ private function getRedisCommand(int $cmd): string { switch ($cmd) { @@ -695,13 +633,12 @@ private function getRedisCommand(int $cmd): string case Adapter::COMMAND_SET: return 'hSet'; default: - throw new InvalidArgumentException("Unknown command"); + throw new InvalidArgumentException('Unknown command'); } } /** - * @param mixed[] $data - * @return string + * @param mixed[] $data */ private function toMetricKey(array $data): string { @@ -709,47 +646,46 @@ private function toMetricKey(array $data): string } /** - * @param mixed[] $values - * @return string + * @param mixed[] $values + * * @throws RuntimeException */ private function encodeLabelValues(array $values): string { $json = json_encode($values); - if (false === $json) { + if ($json === false) { throw new RuntimeException(json_last_error_msg()); } + return base64_encode($json); } /** - * @param string $values * @return mixed[] + * * @throws RuntimeException */ private function decodeLabelValues(string $values): array { $json = base64_decode($values, true); - if (false === $json) { + if ($json === false) { throw new RuntimeException('Cannot base64 decode label values'); } $decodedValues = json_decode($json, true); - if (false === $decodedValues) { + if ($decodedValues === false) { throw new RuntimeException(json_last_error_msg()); } + return $decodedValues; } /** - * @param string $redisKey - * @param string|null $metricName - * @return void * @throws MetricJsonException */ private function throwMetricJsonException(string $redisKey, ?string $metricName = null): void { $metricName = $metricName ?? 'unknown'; - $message = 'Json error: ' . json_last_error_msg() . ' redis key : ' . $redisKey . ' metric name: ' . $metricName; + $message = 'Json error: '.json_last_error_msg().' redis key : '.$redisKey.' metric name: '.$metricName; throw new MetricJsonException($message, 0, null, $metricName); } } diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php new file mode 100644 index 0000000..5d53294 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -0,0 +1,36 @@ + Date: Wed, 30 Apr 2025 00:14:33 +0200 Subject: [PATCH 03/40] feature: implement predis and phpredis clients Signed-off-by: Mateusz Cholewka --- .../Storage/RedisClients/PHPRedis.php | 157 ++++++++++++++++++ .../Storage/RedisClients/Predis.php | 117 +++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 src/Prometheus/Storage/RedisClients/PHPRedis.php create mode 100644 src/Prometheus/Storage/RedisClients/Predis.php diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php new file mode 100644 index 0000000..a650f01 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -0,0 +1,157 @@ +redis = $redis; + $this->options = $options; + } + + public static function create(array $options): self + { + $redis = new \Redis; + + return new self($redis, $options); + } + + public function getOption(int $option): mixed + { + return $this->redis->getOption($option); + } + + public function eval(string $script, array $args = [], int $num_keys = 0): mixed + { + return $this->redis->eval($script, $args, $num_keys); + } + + public function set(string $key, mixed $value, mixed $options = null): string|bool + { + return $this->redis->set($key, $value, $options); + } + + public function setNx(string $key, mixed $value): bool + { + return $this->redis->setNx($key, $value); + } + + public function hSetNx(string $key, string $field, mixed $value): bool + { + return $this->redis->hSetNx($key, $field, $value); + } + + public function sMembers(string $key): array|false + { + return $this->redis->sMembers($key); + } + + public function hGetAll(string $key): array|false + { + return $this->redis->hGetAll($key); + } + + public function keys(string $pattern) + { + return $this->redis->keys($pattern); + } + + public function get(string $key): mixed + { + return $this->redis->get($key); + } + + public function del(array|string $key, string ...$other_keys): int|false + { + try { + return $this->redis->del($key, ...$other_keys); + } catch (\RedisException $e) { + throw new RedisClientException($e->getMessage()); + } + } + + public function getPrefix(): string + { + return $this->redis->_prefix(''); + } + + /** + * @throws StorageException + */ + public function ensureOpenConnection(): void + { + if ($this->connectionInitialized === true) { + return; + } + + $this->connectToServer(); + $authParams = []; + + if (isset($this->options['user']) && $this->options['user'] !== '') { + $authParams[] = $this->options['user']; + } + + if (isset($this->options['password'])) { + $authParams[] = $this->options['password']; + } + + if ($authParams !== []) { + $this->redis->auth($authParams); + } + + if (isset($this->options['database'])) { + $this->redis->select($this->options['database']); + } + + $this->redis->setOption(RedisClient::OPT_READ_TIMEOUT, $this->options['read_timeout']); + + $this->connectionInitialized = true; + } + + /** + * @throws StorageException + */ + private function connectToServer(): void + { + try { + $connection_successful = false; + if ($this->options['persistent_connections'] !== false) { + $connection_successful = $this->redis->pconnect( + $this->options['host'], + (int) $this->options['port'], + (float) $this->options['timeout'] + ); + } else { + $connection_successful = $this->redis->connect($this->options['host'], (int) $this->options['port'], (float) $this->options['timeout']); + } + if (! $connection_successful) { + throw new StorageException( + sprintf("Can't connect to Redis server. %s", $this->redis->getLastError()), + 0 + ); + } + } catch (\RedisException $e) { + throw new StorageException( + sprintf("Can't connect to Redis server. %s", $e->getMessage()), + $e->getCode(), + ); + } + } +} diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php new file mode 100644 index 0000000..d359640 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -0,0 +1,117 @@ + Prefix::class, + ]; + + private $client; + + private $prefix = ''; + + public function __construct(Client $redis) + { + $this->client = $redis; + } + + public static function create(array $options): self + { + $this->prefix = $options['prefix'] ?? ''; + $redisClient = new Client($options, ['prefix' => $options['prefix'] ?? '']); + + return new self($redisClient); + } + + public function getOption(int $option): mixed + { + if (! isset(self::OPTIONS_MAP[$option])) { + return null; + } + + $mappedOption = self::OPTIONS_MAP[$option]; + + return $this->client->getOptions()->$mappedOption; + } + + public function eval(string $script, array $args = [], int $num_keys = 0): mixed + { + return $this->client->eval($script, $num_keys, ...$args); + } + + public function set(string $key, mixed $value, mixed $options = null): string|bool + { + $result = $this->client->set($key, $value, ...$this->flattenFlags($options)); + + return (string) $result; + } + + private function flattenFlags(array $flags): array + { + $result = []; + foreach ($flags as $key => $value) { + if (is_int($key)) { + $result[] = $value; + } else { + $result[] = $key; + $result[] = $value; + } + } + + return $result; + } + + public function setNx(string $key, mixed $value): bool + { + return $this->client->setnx($key, $value) === 1; + } + + public function hSetNx(string $key, string $field, mixed $value): bool + { + return $this->hsetnx($key, $field, $value); + } + + public function sMembers(string $key): array|false + { + return $this->client->smembers($key); + } + + public function hGetAll(string $key): array|false + { + return $this->client->hgetall($key); + } + + public function keys(string $pattern) + { + return $this->client->keys($pattern); + } + + public function get(string $key): mixed + { + return $this->client->get($key); + } + + public function del(array|string $key, string ...$other_keys): int|false + { + return $this->client->del($key, ...$other_keys); + } + + public function getPrefix(): string + { + $key = RedisClient::OPT_PREFIX; + + return $this->prefix; + } + + public function ensureOpenConnection(): void + { + // Predis doesn't require to trigger connection + } +} From 1c1f8b11af80af0019839f87b31ddff9ba97088e Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 00:14:44 +0200 Subject: [PATCH 04/40] feature: add predis test Signed-off-by: Mateusz Cholewka --- .../Predis/CollectorRegistryTest.php | 23 ++++++++++++++ tests/Test/Prometheus/Predis/CounterTest.php | 25 +++++++++++++++ .../Predis/CounterWithPrefixTest.php | 25 +++++++++++++++ tests/Test/Prometheus/Predis/GaugeTest.php | 25 +++++++++++++++ .../Prometheus/Predis/GaugeWithPrefixTest.php | 25 +++++++++++++++ .../Test/Prometheus/Predis/HistogramTest.php | 25 +++++++++++++++ .../Predis/HistogramWithPrefixTest.php | 25 +++++++++++++++ tests/Test/Prometheus/Predis/SummaryTest.php | 31 +++++++++++++++++++ .../Predis/SummaryWithPrefixTest.php | 25 +++++++++++++++ 9 files changed, 229 insertions(+) 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/tests/Test/Prometheus/Predis/CollectorRegistryTest.php b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php new file mode 100644 index 0000000..2add921 --- /dev/null +++ b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php @@ -0,0 +1,23 @@ + REDIS_HOST]); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/CounterTest.php b/tests/Test/Prometheus/Predis/CounterTest.php new file mode 100644 index 0000000..a35b51c --- /dev/null +++ b/tests/Test/Prometheus/Predis/CounterTest.php @@ -0,0 +1,25 @@ + REDIS_HOST]); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php new file mode 100644 index 0000000..c4636b0 --- /dev/null +++ b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php @@ -0,0 +1,25 @@ + REDIS_HOST, 'prefix' => 'prefix:']); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/GaugeTest.php b/tests/Test/Prometheus/Predis/GaugeTest.php new file mode 100644 index 0000000..f141861 --- /dev/null +++ b/tests/Test/Prometheus/Predis/GaugeTest.php @@ -0,0 +1,25 @@ + REDIS_HOST]); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php new file mode 100644 index 0000000..dcc5748 --- /dev/null +++ b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php @@ -0,0 +1,25 @@ + REDIS_HOST, 'prefix' => 'prefix:']); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/HistogramTest.php b/tests/Test/Prometheus/Predis/HistogramTest.php new file mode 100644 index 0000000..2d2b670 --- /dev/null +++ b/tests/Test/Prometheus/Predis/HistogramTest.php @@ -0,0 +1,25 @@ + REDIS_HOST]); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php new file mode 100644 index 0000000..bf5f8ea --- /dev/null +++ b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php @@ -0,0 +1,25 @@ + REDIS_HOST, 'prefix' => 'prefix:']); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php new file mode 100644 index 0000000..430bf6b --- /dev/null +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -0,0 +1,31 @@ + REDIS_HOST]); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } + + /** @test */ + public function it_should_observe_with_labels(): void + { + parent::itShouldObserveWithLabels(); // TODO: Change the autogenerated stub + } +} diff --git a/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php new file mode 100644 index 0000000..d6bd40a --- /dev/null +++ b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php @@ -0,0 +1,25 @@ + REDIS_HOST, 'prefix' => 'prefix:']); + + $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} From 23b938e706f5012c4f13ad7f7e62591f4de8763a Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 14:30:39 +0200 Subject: [PATCH 05/40] refactor: yagni for interface Signed-off-by: Mateusz Cholewka --- .../Storage/RedisClients/PHPRedis.php | 21 ++++----- .../Storage/RedisClients/Predis.php | 43 ++++++++----------- .../Storage/RedisClients/RedisClient.php | 10 ++--- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index a650f01..9dcfbbf 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -38,19 +38,19 @@ public function getOption(int $option): mixed return $this->redis->getOption($option); } - public function eval(string $script, array $args = [], int $num_keys = 0): mixed + public function eval(string $script, array $args = [], int $num_keys = 0): void { - return $this->redis->eval($script, $args, $num_keys); + $this->redis->eval($script, $args, $num_keys); } - public function set(string $key, mixed $value, mixed $options = null): string|bool + public function set(string $key, mixed $value, mixed $options = null): void { - return $this->redis->set($key, $value, $options); + $this->redis->set($key, $value, $options); } - public function setNx(string $key, mixed $value): bool + public function setNx(string $key, mixed $value): void { - return $this->redis->setNx($key, $value); + $this->redis->setNx($key, $value); } public function hSetNx(string $key, string $field, mixed $value): bool @@ -78,20 +78,15 @@ public function get(string $key): mixed return $this->redis->get($key); } - public function del(array|string $key, string ...$other_keys): int|false + public function del(array|string $key, string ...$other_keys): void { try { - return $this->redis->del($key, ...$other_keys); + $this->redis->del($key, ...$other_keys); } catch (\RedisException $e) { throw new RedisClientException($e->getMessage()); } } - public function getPrefix(): string - { - return $this->redis->_prefix(''); - } - /** * @throws StorageException */ diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index d359640..c2d0c65 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -5,29 +5,29 @@ namespace Prometheus\Storage\RedisClients; use Predis\Client; -use Predis\Configuration\Option\Prefix; class Predis implements RedisClient { private const OPTIONS_MAP = [ - RedisClient::OPT_PREFIX => Prefix::class, + RedisClient::OPT_PREFIX => 'prefix', ]; private $client; - private $prefix = ''; + private $options = []; - public function __construct(Client $redis) + public function __construct(Client $redis, array $options) { $this->client = $redis; + + $this->options = $options; } - public static function create(array $options): self + public static function create(array $parameters, array $options): self { - $this->prefix = $options['prefix'] ?? ''; - $redisClient = new Client($options, ['prefix' => $options['prefix'] ?? '']); + $redisClient = new Client($parameters, $options); - return new self($redisClient); + return new self($redisClient, $options); } public function getOption(int $option): mixed @@ -38,19 +38,17 @@ public function getOption(int $option): mixed $mappedOption = self::OPTIONS_MAP[$option]; - return $this->client->getOptions()->$mappedOption; + return $this->options[$mappedOption] ?? null; } - public function eval(string $script, array $args = [], int $num_keys = 0): mixed + public function eval(string $script, array $args = [], int $num_keys = 0): void { - return $this->client->eval($script, $num_keys, ...$args); + $this->client->eval($script, $num_keys, ...$args); } - public function set(string $key, mixed $value, mixed $options = null): string|bool + public function set(string $key, mixed $value, mixed $options = null): void { - $result = $this->client->set($key, $value, ...$this->flattenFlags($options)); - - return (string) $result; + $this->client->set($key, $value, ...$this->flattenFlags($options)); } private function flattenFlags(array $flags): array @@ -68,9 +66,9 @@ private function flattenFlags(array $flags): array return $result; } - public function setNx(string $key, mixed $value): bool + public function setNx(string $key, mixed $value): void { - return $this->client->setnx($key, $value) === 1; + $this->client->setnx($key, $value) === 1; } public function hSetNx(string $key, string $field, mixed $value): bool @@ -98,16 +96,9 @@ public function get(string $key): mixed return $this->client->get($key); } - public function del(array|string $key, string ...$other_keys): int|false - { - return $this->client->del($key, ...$other_keys); - } - - public function getPrefix(): string + public function del(array|string $key, string ...$other_keys): void { - $key = RedisClient::OPT_PREFIX; - - return $this->prefix; + $this->client->del($key, ...$other_keys); } public function ensureOpenConnection(): void diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 5d53294..89eadcb 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -12,11 +12,11 @@ interface RedisClient public function getOption(int $option): mixed; - public function eval(string $script, array $args = [], int $num_keys = 0): mixed; + public function eval(string $script, array $args = [], int $num_keys = 0): void; - public function set(string $key, mixed $value, mixed $options = null): string|bool; + public function set(string $key, mixed $value, mixed $options = null): void; - public function setNx(string $key, mixed $value): bool; + public function setNx(string $key, mixed $value): void; public function hSetNx(string $key, string $field, mixed $value): bool; @@ -28,9 +28,7 @@ public function keys(string $pattern); public function get(string $key): mixed; - public function del(array|string $key, string ...$other_keys): int|false; - - public function getPrefix(): string; + public function del(array|string $key, string ...$other_keys): void; public function ensureOpenConnection(): void; } From 5e2e4d5caced9b8955ca7faaf795605d4ce71f3e Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 14:31:05 +0200 Subject: [PATCH 06/40] refactor: use abstract redis and keep full backward compatibility Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 629 +++++++++++++++++ src/Prometheus/Storage/Predis.php | 94 +++ src/Prometheus/Storage/Redis.php | 634 +----------------- .../Predis/CollectorRegistryTest.php | 7 +- tests/Test/Prometheus/Predis/CounterTest.php | 7 +- .../Predis/CounterWithPrefixTest.php | 7 +- tests/Test/Prometheus/Predis/GaugeTest.php | 7 +- .../Prometheus/Predis/GaugeWithPrefixTest.php | 7 +- .../Test/Prometheus/Predis/HistogramTest.php | 7 +- .../Predis/HistogramWithPrefixTest.php | 7 +- tests/Test/Prometheus/Predis/SummaryTest.php | 7 +- .../Predis/SummaryWithPrefixTest.php | 7 +- 12 files changed, 744 insertions(+), 676 deletions(-) create mode 100644 src/Prometheus/Storage/AbstractRedis.php create mode 100644 src/Prometheus/Storage/Predis.php diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php new file mode 100644 index 0000000..4cab84d --- /dev/null +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -0,0 +1,629 @@ +wipeStorage(); + } + + /** + * {@inheritDoc} + */ + public function wipeStorage(): void + { + $this->redis->ensureOpenConnection(); + + $searchPattern = ''; + + $globalPrefix = $this->redis->getOption(RedisClient::OPT_PREFIX); + // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int + if (is_string($globalPrefix)) { + $searchPattern .= $globalPrefix; + } + + $searchPattern .= self::$prefix; + $searchPattern .= '*'; + + $this->redis->eval( + <<<'LUA' +redis.replicate_commands() +local cursor = "0" +repeat + local results = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) + cursor = results[1] + for _, key in ipairs(results[2]) do + redis.call('DEL', key) + end +until cursor == "0" +LUA + , + [$searchPattern], + 0 + ); + } + + /** + * @param mixed[] $data + */ + protected function metaKey(array $data): string + { + return implode(':', [ + $data['name'], + 'meta', + ]); + } + + /** + * @param mixed[] $data + */ + protected function valueKey(array $data): string + { + return implode(':', [ + $data['name'], + $this->encodeLabelValues($data['labelValues']), + 'value', + ]); + } + + /** + * @return MetricFamilySamples[] + * + * @throws StorageException + */ + public function collect(bool $sortMetrics = true): array + { + $this->redis->ensureOpenConnection(); + $metrics = $this->collectHistograms(); + $metrics = array_merge($metrics, $this->collectGauges($sortMetrics)); + $metrics = array_merge($metrics, $this->collectCounters($sortMetrics)); + $metrics = array_merge($metrics, $this->collectSummaries()); + + return array_map( + function (array $metric): MetricFamilySamples { + return new MetricFamilySamples($metric); + }, + $metrics + ); + } + + /** + * @param mixed[] $data + * + * @throws StorageException + */ + public function updateHistogram(array $data): void + { + $this->redis->ensureOpenConnection(); + $bucketToIncrease = '+Inf'; + foreach ($data['buckets'] as $bucket) { + if ($data['value'] <= $bucket) { + $bucketToIncrease = $bucket; + break; + } + } + $metaData = $data; + unset($metaData['value'], $metaData['labelValues']); + + $this->redis->eval( + <<<'LUA' +local result = redis.call('hIncrByFloat', KEYS[1], ARGV[1], ARGV[3]) +redis.call('hIncrBy', KEYS[1], ARGV[2], 1) +if tonumber(result) >= tonumber(ARGV[3]) then + redis.call('hSet', KEYS[1], '__meta', ARGV[4]) + redis.call('sAdd', KEYS[2], KEYS[1]) +end +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 + ); + } + + /** + * @param mixed[] $data + * + * @throws StorageException + */ + public function updateSummary(array $data): void + { + $this->redis->ensureOpenConnection(); + + // store meta + $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $metaKey = $summaryKey.':'.$this->metaKey($data); + $json = json_encode($this->metaData($data)); + if ($json === false) { + throw new RuntimeException(json_last_error_msg()); + } + $this->redis->setNx($metaKey, $json); /** @phpstan-ignore-line */ + + // store value key + $valueKey = $summaryKey.':'.$this->valueKey($data); + $json = json_encode($this->encodeLabelValues($data['labelValues'])); + if ($json === false) { + throw new RuntimeException(json_last_error_msg()); + } + $this->redis->setNx($valueKey, $json); /** @phpstan-ignore-line */ + + // trick to handle uniqid collision + $done = false; + while (! $done) { + $sampleKey = $valueKey.':'.uniqid('', true); + $done = $this->redis->set($sampleKey, $data['value'], ['NX', 'EX' => $data['maxAgeSeconds']]); + } + } + + /** + * @param mixed[] $data + * + * @throws StorageException + */ + public function updateGauge(array $data): void + { + $this->redis->ensureOpenConnection(); + $metaData = $data; + unset($metaData['value'], $metaData['labelValues'], $metaData['command']); + $this->redis->eval( + <<<'LUA' +local result = redis.call(ARGV[1], KEYS[1], ARGV[2], ARGV[3]) + +if ARGV[1] == 'hSet' then + if result == 1 then + redis.call('hSet', KEYS[1], '__meta', ARGV[4]) + redis.call('sAdd', KEYS[2], KEYS[1]) + end +else + if result == ARGV[3] then + redis.call('hSet', KEYS[1], '__meta', ARGV[4]) + redis.call('sAdd', KEYS[2], KEYS[1]) + end +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 + ); + } + + /** + * @param mixed[] $data + * + * @throws StorageException + */ + public function updateCounter(array $data): void + { + $this->redis->ensureOpenConnection(); + $metaData = $data; + unset($metaData['value'], $metaData['labelValues'], $metaData['command']); + $this->redis->eval( + <<<'LUA' +local result = redis.call(ARGV[1], KEYS[1], ARGV[3], ARGV[2]) +local added = redis.call('sAdd', KEYS[2], KEYS[1]) +if added == 1 then + redis.call('hMSet', KEYS[1], '__meta', ARGV[4]) +end +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 + ); + } + + /** + * @param mixed[] $data + * @return mixed[] + */ + protected function metaData(array $data): array + { + $metricsMetaData = $data; + unset($metricsMetaData['value'], $metricsMetaData['command'], $metricsMetaData['labelValues']); + + return $metricsMetaData; + } + + /** + * @return mixed[] + */ + protected function collectHistograms(): array + { + $keys = $this->redis->sMembers(self::$prefix.Histogram::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $histograms = []; + foreach ($keys as $key) { + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX))); + if (! isset($raw['__meta'])) { + continue; + } + $histogram = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $histogram['samples'] = []; + + // Add the Inf bucket so we can compute it later on + $histogram['buckets'][] = '+Inf'; + + $allLabelValues = []; + foreach (array_keys($raw) as $k) { + $d = json_decode($k, true); + if ($d['b'] == 'sum') { + continue; + } + $allLabelValues[] = $d['labelValues']; + } + if (json_last_error() !== JSON_ERROR_NONE) { + $this->throwMetricJsonException($key); + } + + // We need set semantics. + // This is the equivalent of array_unique but for arrays of arrays. + $allLabelValues = array_map('unserialize', array_unique(array_map('serialize', $allLabelValues))); + sort($allLabelValues); + + foreach ($allLabelValues as $labelValues) { + // Fill up all buckets. + // If the bucket doesn't exist fill in values from + // the previous one. + $acc = 0; + foreach ($histogram['buckets'] as $bucket) { + $bucketKey = json_encode(['b' => $bucket, 'labelValues' => $labelValues]); + if (! isset($raw[$bucketKey])) { + $histogram['samples'][] = [ + 'name' => $histogram['name'].'_bucket', + 'labelNames' => ['le'], + 'labelValues' => array_merge($labelValues, [$bucket]), + 'value' => $acc, + ]; + } else { + $acc += $raw[$bucketKey]; + $histogram['samples'][] = [ + 'name' => $histogram['name'].'_bucket', + 'labelNames' => ['le'], + 'labelValues' => array_merge($labelValues, [$bucket]), + 'value' => $acc, + ]; + } + } + + // Add the count + $histogram['samples'][] = [ + 'name' => $histogram['name'].'_count', + 'labelNames' => [], + 'labelValues' => $labelValues, + 'value' => $acc, + ]; + + // Add the sum + $histogram['samples'][] = [ + 'name' => $histogram['name'].'_sum', + 'labelNames' => [], + 'labelValues' => $labelValues, + 'value' => $raw[json_encode(['b' => 'sum', 'labelValues' => $labelValues])], + ]; + } + $histograms[] = $histogram; + } + + return $histograms; + } + + protected function removePrefixFromKey(string $key): string + { + // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int + if ($this->redis->getOption(RedisClient::OPT_PREFIX) === null) { + return $key; + } + + // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int + return substr($key, strlen($this->redis->getOption(RedisClient::OPT_PREFIX))); + } + + /** + * @return mixed[] + */ + protected function collectSummaries(): array + { + $math = new Math; + $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $keys = $this->redis->keys($summaryKey.':*:meta'); + + $summaries = []; + foreach ($keys as $metaKeyWithPrefix) { + $metaKey = $this->removePrefixFromKey($metaKeyWithPrefix); + $rawSummary = $this->redis->get($metaKey); + if ($rawSummary === false) { + continue; + } + $summary = json_decode($rawSummary, true); + $metaData = $summary; + $data = [ + 'name' => $metaData['name'], + 'help' => $metaData['help'], + 'type' => $metaData['type'], + 'labelNames' => $metaData['labelNames'], + 'maxAgeSeconds' => $metaData['maxAgeSeconds'], + 'quantiles' => $metaData['quantiles'], + 'samples' => [], + ]; + + $values = $this->redis->keys($summaryKey.':'.$metaData['name'].':*:value'); + foreach ($values as $valueKeyWithPrefix) { + $valueKey = $this->removePrefixFromKey($valueKeyWithPrefix); + $rawValue = $this->redis->get($valueKey); + if ($rawValue === false) { + continue; + } + $value = json_decode($rawValue, true); + $encodedLabelValues = $value; + $decodedLabelValues = $this->decodeLabelValues($encodedLabelValues); + + $samples = []; + $sampleValues = $this->redis->keys($summaryKey.':'.$metaData['name'].':'.$encodedLabelValues.':value:*'); + foreach ($sampleValues as $sampleValueWithPrefix) { + $sampleValue = $this->removePrefixFromKey($sampleValueWithPrefix); + $samples[] = (float) $this->redis->get($sampleValue); + } + + if (count($samples) === 0) { + try { + $this->redis->del($valueKey); + } catch (RedisClientException $e) { + // ignore if we can't delete the key + } + + continue; + } + + // Compute quantiles + sort($samples); + foreach ($data['quantiles'] as $quantile) { + $data['samples'][] = [ + 'name' => $metaData['name'], + 'labelNames' => ['quantile'], + 'labelValues' => array_merge($decodedLabelValues, [$quantile]), + 'value' => $math->quantile($samples, $quantile), + ]; + } + + // Add the count + $data['samples'][] = [ + 'name' => $metaData['name'].'_count', + 'labelNames' => [], + 'labelValues' => $decodedLabelValues, + 'value' => count($samples), + ]; + + // Add the sum + $data['samples'][] = [ + 'name' => $metaData['name'].'_sum', + 'labelNames' => [], + 'labelValues' => $decodedLabelValues, + 'value' => array_sum($samples), + ]; + } + + if (count($data['samples']) > 0) { + $summaries[] = $data; + } else { + try { + $this->redis->del($metaKey); + } catch (RedisClientException $e) { + // ignore if we can't delete the key + } + } + } + + return $summaries; + } + + /** + * @return mixed[] + */ + protected function collectGauges(bool $sortMetrics = true): array + { + $keys = $this->redis->sMembers(self::$prefix.Gauge::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $gauges = []; + foreach ($keys as $key) { + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX))); + if (! isset($raw['__meta'])) { + continue; + } + $gauge = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $gauge['samples'] = []; + foreach ($raw as $k => $value) { + $gauge['samples'][] = [ + 'name' => $gauge['name'], + 'labelNames' => [], + 'labelValues' => json_decode($k, true), + 'value' => $value, + ]; + if (json_last_error() !== JSON_ERROR_NONE) { + $this->throwMetricJsonException($key, $gauge['name']); + } + } + + if ($sortMetrics) { + usort($gauge['samples'], function ($a, $b): int { + return strcmp(implode('', $a['labelValues']), implode('', $b['labelValues'])); + }); + } + + $gauges[] = $gauge; + } + + return $gauges; + } + + /** + * @return mixed[] + * + * @throws MetricJsonException + */ + protected function collectCounters(bool $sortMetrics = true): array + { + $keys = $this->redis->sMembers(self::$prefix.Counter::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $counters = []; + foreach ($keys as $key) { + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX))); + if (! isset($raw['__meta'])) { + continue; + } + $counter = json_decode($raw['__meta'], true); + + unset($raw['__meta']); + $counter['samples'] = []; + foreach ($raw as $k => $value) { + $counter['samples'][] = [ + 'name' => $counter['name'], + 'labelNames' => [], + 'labelValues' => json_decode($k, true), + 'value' => $value, + ]; + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->throwMetricJsonException($key, $counter['name']); + } + } + + if ($sortMetrics) { + usort($counter['samples'], function ($a, $b): int { + return strcmp(implode('', $a['labelValues']), implode('', $b['labelValues'])); + }); + } + + $counters[] = $counter; + } + + return $counters; + } + + protected function getRedisCommand(int $cmd): string + { + switch ($cmd) { + case Adapter::COMMAND_INCREMENT_INTEGER: + return 'hIncrBy'; + case Adapter::COMMAND_INCREMENT_FLOAT: + return 'hIncrByFloat'; + case Adapter::COMMAND_SET: + return 'hSet'; + default: + throw new InvalidArgumentException('Unknown command'); + } + } + + /** + * @param mixed[] $data + */ + protected function toMetricKey(array $data): string + { + return implode(':', [self::$prefix, $data['type'], $data['name']]); + } + + /** + * @param mixed[] $values + * + * @throws RuntimeException + */ + protected function encodeLabelValues(array $values): string + { + $json = json_encode($values); + if ($json === false) { + throw new RuntimeException(json_last_error_msg()); + } + + return base64_encode($json); + } + + /** + * @return mixed[] + * + * @throws RuntimeException + */ + protected function decodeLabelValues(string $values): array + { + $json = base64_decode($values, true); + if ($json === false) { + throw new RuntimeException('Cannot base64 decode label values'); + } + $decodedValues = json_decode($json, true); + if ($decodedValues === false) { + throw new RuntimeException(json_last_error_msg()); + } + + return $decodedValues; + } + + /** + * @throws MetricJsonException + */ + protected function throwMetricJsonException(string $redisKey, ?string $metricName = null): void + { + $metricName = $metricName ?? 'unknown'; + $message = 'Json error: '.json_last_error_msg().' redis key : '.$redisKey.' metric name: '.$metricName; + throw new MetricJsonException($message, 0, null, $metricName); + } +} diff --git a/src/Prometheus/Storage/Predis.php b/src/Prometheus/Storage/Predis.php new file mode 100644 index 0000000..758ec79 --- /dev/null +++ b/src/Prometheus/Storage/Predis.php @@ -0,0 +1,94 @@ + 'tcp', + 'host' => '127.0.0.1', + 'port' => 6379, + 'timeout' => 0.1, + 'read_write_timeout' => 10, + 'persistent' => false, + 'password' => null, + 'username' => null, + ]; + + /** + * @var mixed[] + */ + private static $defaultOptions = [ + 'prefix' => '', + 'throw_errors' => true, + ]; + + /** + * @var mixed[] + */ + private $parameters = []; + + /** + * @var mixed[] + */ + private $options = []; + + /** + * Redis constructor. + * + * @param mixed[] $options + */ + public function __construct(array $parameters = [], array $options = []) + { + $this->parameters = array_merge(self::$defaultParameters, $parameters); + $this->options = array_merge(self::$defaultOptions, $options); + $this->redis = PredisClient::create($this->parameters, $this->options); + } + + /** + * @throws StorageException + */ + public static function fromExistingConnection(Client $client): self + { + $options = $client->getOptions(); + $allOptions = [ + 'aggregate' => $options->aggregate, + 'cluster' => $options->cluster, + 'connections' => $options->connections, + 'exceptions' => $options->exceptions, + 'prefix' => $options->prefix, + 'commands' => $options->commands, + 'replication' => $options->replication, + ]; + + $self = new self; + $self->redis = new PredisClient($client, self::$defaultParameters, $allOptions); + + return $self; + } + + /** + * @param mixed[] $parameters + */ + public static function setDefaultParameters(array $parameters): void + { + self::$defaultParameters = array_merge(self::$defaultParameters, $parameters); + } + + /** + * @param mixed[] $options + */ + public static function setDefaultOptions(array $options): void + { + self::$defaultOptions = array_merge(self::$defaultOptions, $options); + } +} diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 2b11436..f13352a 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -4,26 +4,11 @@ namespace Prometheus\Storage; -use InvalidArgumentException; -use Predis\Client; -use Prometheus\Counter; -use Prometheus\Exception\MetricJsonException; use Prometheus\Exception\StorageException; -use Prometheus\Gauge; -use Prometheus\Histogram; -use Prometheus\Math; -use Prometheus\MetricFamilySamples; use Prometheus\Storage\RedisClients\PHPRedis; -use Prometheus\Storage\RedisClients\Predis; -use Prometheus\Storage\RedisClients\RedisClient; -use Prometheus\Storage\RedisClients\RedisClientException; -use Prometheus\Summary; -use RuntimeException; -class Redis implements Adapter +class Redis extends AbstractRedis { - const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; - /** * @var mixed[] */ @@ -37,21 +22,11 @@ class Redis implements Adapter 'user' => null, ]; - /** - * @var string - */ - private static $prefix = 'PROMETHEUS_'; - /** * @var mixed[] */ private $options = []; - /** - * @var RedisClient - */ - private $redis; - /** * Redis constructor. * @@ -66,20 +41,13 @@ public function __construct(array $options = []) /** * @throws StorageException */ - public static function fromExistingConnection(\Redis|Client $redis): self + public static function fromExistingConnection(\Redis $redis): self { - $self = new self; - - if ($redis instanceof Client) { - $self->redis = new Predis($redis); - - return $self; - } - if ($redis->isConnected() === false) { throw new StorageException('Connection to Redis server not established'); } + $self = new self; $self->redis = new PHPRedis($redis, self::$defaultOptions); return $self; @@ -92,600 +60,4 @@ public static function setDefaultOptions(array $options): void { self::$defaultOptions = array_merge(self::$defaultOptions, $options); } - - public static function setPrefix(string $prefix): void - { - self::$prefix = $prefix; - } - - /** - * @throws StorageException - * - * @deprecated use replacement method wipeStorage from Adapter interface - */ - public function flushRedis(): void - { - $this->wipeStorage(); - } - - /** - * {@inheritDoc} - */ - public function wipeStorage(): void - { - $this->redis->ensureOpenConnection(); - - $searchPattern = ''; - - $globalPrefix = $this->redis->getOption(RedisClient::OPT_PREFIX); - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int - if (is_string($globalPrefix)) { - $searchPattern .= $globalPrefix; - } - - $searchPattern .= self::$prefix; - $searchPattern .= '*'; - - $this->redis->eval( - <<<'LUA' -redis.replicate_commands() -local cursor = "0" -repeat - local results = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) - cursor = results[1] - for _, key in ipairs(results[2]) do - redis.call('DEL', key) - end -until cursor == "0" -LUA - , - [$searchPattern], - 0 - ); - } - - /** - * @param mixed[] $data - */ - private function metaKey(array $data): string - { - return implode(':', [ - $data['name'], - 'meta', - ]); - } - - /** - * @param mixed[] $data - */ - private function valueKey(array $data): string - { - return implode(':', [ - $data['name'], - $this->encodeLabelValues($data['labelValues']), - 'value', - ]); - } - - /** - * @return MetricFamilySamples[] - * - * @throws StorageException - */ - public function collect(bool $sortMetrics = true): array - { - $this->redis->ensureOpenConnection(); - $metrics = $this->collectHistograms(); - $metrics = array_merge($metrics, $this->collectGauges($sortMetrics)); - $metrics = array_merge($metrics, $this->collectCounters($sortMetrics)); - $metrics = array_merge($metrics, $this->collectSummaries()); - - return array_map( - function (array $metric): MetricFamilySamples { - return new MetricFamilySamples($metric); - }, - $metrics - ); - } - - /** - * @param mixed[] $data - * - * @throws StorageException - */ - public function updateHistogram(array $data): void - { - $this->redis->ensureOpenConnection(); - $bucketToIncrease = '+Inf'; - foreach ($data['buckets'] as $bucket) { - if ($data['value'] <= $bucket) { - $bucketToIncrease = $bucket; - break; - } - } - $metaData = $data; - unset($metaData['value'], $metaData['labelValues']); - - $this->redis->eval( - <<<'LUA' -local result = redis.call('hIncrByFloat', KEYS[1], ARGV[1], ARGV[3]) -redis.call('hIncrBy', KEYS[1], ARGV[2], 1) -if tonumber(result) >= tonumber(ARGV[3]) then - redis.call('hSet', KEYS[1], '__meta', ARGV[4]) - redis.call('sAdd', KEYS[2], KEYS[1]) -end -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 - ); - } - - /** - * @param mixed[] $data - * - * @throws StorageException - */ - public function updateSummary(array $data): void - { - $this->redis->ensureOpenConnection(); - - // store meta - $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $metaKey = $summaryKey.':'.$this->metaKey($data); - $json = json_encode($this->metaData($data)); - if ($json === false) { - throw new RuntimeException(json_last_error_msg()); - } - $this->redis->setNx($metaKey, $json); /** @phpstan-ignore-line */ - - // store value key - $valueKey = $summaryKey.':'.$this->valueKey($data); - $json = json_encode($this->encodeLabelValues($data['labelValues'])); - if ($json === false) { - throw new RuntimeException(json_last_error_msg()); - } - $this->redis->setNx($valueKey, $json); /** @phpstan-ignore-line */ - - // trick to handle uniqid collision - $done = false; - while (! $done) { - $sampleKey = $valueKey.':'.uniqid('', true); - $done = $this->redis->set($sampleKey, $data['value'], ['NX', 'EX' => $data['maxAgeSeconds']]); - } - } - - /** - * @param mixed[] $data - * - * @throws StorageException - */ - public function updateGauge(array $data): void - { - $this->redis->ensureOpenConnection(); - $metaData = $data; - unset($metaData['value'], $metaData['labelValues'], $metaData['command']); - $this->redis->eval( - <<<'LUA' -local result = redis.call(ARGV[1], KEYS[1], ARGV[2], ARGV[3]) - -if ARGV[1] == 'hSet' then - if result == 1 then - redis.call('hSet', KEYS[1], '__meta', ARGV[4]) - redis.call('sAdd', KEYS[2], KEYS[1]) - end -else - if result == ARGV[3] then - redis.call('hSet', KEYS[1], '__meta', ARGV[4]) - redis.call('sAdd', KEYS[2], KEYS[1]) - end -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 - ); - } - - /** - * @param mixed[] $data - * - * @throws StorageException - */ - public function updateCounter(array $data): void - { - $this->redis->ensureOpenConnection(); - $metaData = $data; - unset($metaData['value'], $metaData['labelValues'], $metaData['command']); - $this->redis->eval( - <<<'LUA' -local result = redis.call(ARGV[1], KEYS[1], ARGV[3], ARGV[2]) -local added = redis.call('sAdd', KEYS[2], KEYS[1]) -if added == 1 then - redis.call('hMSet', KEYS[1], '__meta', ARGV[4]) -end -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 - ); - } - - /** - * @param mixed[] $data - * @return mixed[] - */ - private function metaData(array $data): array - { - $metricsMetaData = $data; - unset($metricsMetaData['value'], $metricsMetaData['command'], $metricsMetaData['labelValues']); - - return $metricsMetaData; - } - - /** - * @return mixed[] - */ - private function collectHistograms(): array - { - $keys = $this->redis->sMembers(self::$prefix.Histogram::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); - sort($keys); - $histograms = []; - foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getPrefix())); - if (! isset($raw['__meta'])) { - continue; - } - $histogram = json_decode($raw['__meta'], true); - unset($raw['__meta']); - $histogram['samples'] = []; - - // Add the Inf bucket so we can compute it later on - $histogram['buckets'][] = '+Inf'; - - $allLabelValues = []; - foreach (array_keys($raw) as $k) { - $d = json_decode($k, true); - if ($d['b'] == 'sum') { - continue; - } - $allLabelValues[] = $d['labelValues']; - } - if (json_last_error() !== JSON_ERROR_NONE) { - $this->throwMetricJsonException($key); - } - - // We need set semantics. - // This is the equivalent of array_unique but for arrays of arrays. - $allLabelValues = array_map('unserialize', array_unique(array_map('serialize', $allLabelValues))); - sort($allLabelValues); - - foreach ($allLabelValues as $labelValues) { - // Fill up all buckets. - // If the bucket doesn't exist fill in values from - // the previous one. - $acc = 0; - foreach ($histogram['buckets'] as $bucket) { - $bucketKey = json_encode(['b' => $bucket, 'labelValues' => $labelValues]); - if (! isset($raw[$bucketKey])) { - $histogram['samples'][] = [ - 'name' => $histogram['name'].'_bucket', - 'labelNames' => ['le'], - 'labelValues' => array_merge($labelValues, [$bucket]), - 'value' => $acc, - ]; - } else { - $acc += $raw[$bucketKey]; - $histogram['samples'][] = [ - 'name' => $histogram['name'].'_bucket', - 'labelNames' => ['le'], - 'labelValues' => array_merge($labelValues, [$bucket]), - 'value' => $acc, - ]; - } - } - - // Add the count - $histogram['samples'][] = [ - 'name' => $histogram['name'].'_count', - 'labelNames' => [], - 'labelValues' => $labelValues, - 'value' => $acc, - ]; - - // Add the sum - $histogram['samples'][] = [ - 'name' => $histogram['name'].'_sum', - 'labelNames' => [], - 'labelValues' => $labelValues, - 'value' => $raw[json_encode(['b' => 'sum', 'labelValues' => $labelValues])], - ]; - } - $histograms[] = $histogram; - } - - return $histograms; - } - - private function removePrefixFromKey(string $key): string - { - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int - if ($this->redis->getOption(RedisClient::OPT_PREFIX) === null) { - return $key; - } - - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int - return substr($key, strlen($this->redis->getOption(RedisClient::OPT_PREFIX))); - } - - /** - * @return mixed[] - */ - private function collectSummaries(): array - { - $math = new Math; - $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $keys = $this->redis->keys($summaryKey.':*:meta'); - - $summaries = []; - foreach ($keys as $metaKeyWithPrefix) { - $metaKey = $this->removePrefixFromKey($metaKeyWithPrefix); - $rawSummary = $this->redis->get($metaKey); - if ($rawSummary === false) { - continue; - } - $summary = json_decode($rawSummary, true); - $metaData = $summary; - $data = [ - 'name' => $metaData['name'], - 'help' => $metaData['help'], - 'type' => $metaData['type'], - 'labelNames' => $metaData['labelNames'], - 'maxAgeSeconds' => $metaData['maxAgeSeconds'], - 'quantiles' => $metaData['quantiles'], - 'samples' => [], - ]; - - $values = $this->redis->keys($summaryKey.':'.$metaData['name'].':*:value'); - foreach ($values as $valueKeyWithPrefix) { - $valueKey = $this->removePrefixFromKey($valueKeyWithPrefix); - $rawValue = $this->redis->get($valueKey); - if ($rawValue === false) { - continue; - } - $value = json_decode($rawValue, true); - $encodedLabelValues = $value; - $decodedLabelValues = $this->decodeLabelValues($encodedLabelValues); - - $samples = []; - $sampleValues = $this->redis->keys($summaryKey.':'.$metaData['name'].':'.$encodedLabelValues.':value:*'); - foreach ($sampleValues as $sampleValueWithPrefix) { - $sampleValue = $this->removePrefixFromKey($sampleValueWithPrefix); - $samples[] = (float) $this->redis->get($sampleValue); - } - - if (count($samples) === 0) { - try { - $this->redis->del($valueKey); - } catch (RedisClientException $e) { - // ignore if we can't delete the key - } - - continue; - } - - // Compute quantiles - sort($samples); - foreach ($data['quantiles'] as $quantile) { - $data['samples'][] = [ - 'name' => $metaData['name'], - 'labelNames' => ['quantile'], - 'labelValues' => array_merge($decodedLabelValues, [$quantile]), - 'value' => $math->quantile($samples, $quantile), - ]; - } - - // Add the count - $data['samples'][] = [ - 'name' => $metaData['name'].'_count', - 'labelNames' => [], - 'labelValues' => $decodedLabelValues, - 'value' => count($samples), - ]; - - // Add the sum - $data['samples'][] = [ - 'name' => $metaData['name'].'_sum', - 'labelNames' => [], - 'labelValues' => $decodedLabelValues, - 'value' => array_sum($samples), - ]; - } - - if (count($data['samples']) > 0) { - $summaries[] = $data; - } else { - try { - $this->redis->del($metaKey); - } catch (RedisClientException $e) { - // ignore if we can't delete the key - } - } - } - - return $summaries; - } - - /** - * @return mixed[] - */ - private function collectGauges(bool $sortMetrics = true): array - { - $keys = $this->redis->sMembers(self::$prefix.Gauge::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); - sort($keys); - $gauges = []; - foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getPrefix())); - if (! isset($raw['__meta'])) { - continue; - } - $gauge = json_decode($raw['__meta'], true); - unset($raw['__meta']); - $gauge['samples'] = []; - foreach ($raw as $k => $value) { - $gauge['samples'][] = [ - 'name' => $gauge['name'], - 'labelNames' => [], - 'labelValues' => json_decode($k, true), - 'value' => $value, - ]; - if (json_last_error() !== JSON_ERROR_NONE) { - $this->throwMetricJsonException($key, $gauge['name']); - } - } - - if ($sortMetrics) { - usort($gauge['samples'], function ($a, $b): int { - return strcmp(implode('', $a['labelValues']), implode('', $b['labelValues'])); - }); - } - - $gauges[] = $gauge; - } - - return $gauges; - } - - /** - * @return mixed[] - * - * @throws MetricJsonException - */ - private function collectCounters(bool $sortMetrics = true): array - { - $keys = $this->redis->sMembers(self::$prefix.Counter::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); - sort($keys); - $counters = []; - foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getPrefix())); - if (! isset($raw['__meta'])) { - continue; - } - $counter = json_decode($raw['__meta'], true); - - unset($raw['__meta']); - $counter['samples'] = []; - foreach ($raw as $k => $value) { - $counter['samples'][] = [ - 'name' => $counter['name'], - 'labelNames' => [], - 'labelValues' => json_decode($k, true), - 'value' => $value, - ]; - - if (json_last_error() !== JSON_ERROR_NONE) { - $this->throwMetricJsonException($key, $counter['name']); - } - } - - if ($sortMetrics) { - usort($counter['samples'], function ($a, $b): int { - return strcmp(implode('', $a['labelValues']), implode('', $b['labelValues'])); - }); - } - - $counters[] = $counter; - } - - return $counters; - } - - private function getRedisCommand(int $cmd): string - { - switch ($cmd) { - case Adapter::COMMAND_INCREMENT_INTEGER: - return 'hIncrBy'; - case Adapter::COMMAND_INCREMENT_FLOAT: - return 'hIncrByFloat'; - case Adapter::COMMAND_SET: - return 'hSet'; - default: - throw new InvalidArgumentException('Unknown command'); - } - } - - /** - * @param mixed[] $data - */ - private function toMetricKey(array $data): string - { - return implode(':', [self::$prefix, $data['type'], $data['name']]); - } - - /** - * @param mixed[] $values - * - * @throws RuntimeException - */ - private function encodeLabelValues(array $values): string - { - $json = json_encode($values); - if ($json === false) { - throw new RuntimeException(json_last_error_msg()); - } - - return base64_encode($json); - } - - /** - * @return mixed[] - * - * @throws RuntimeException - */ - private function decodeLabelValues(string $values): array - { - $json = base64_decode($values, true); - if ($json === false) { - throw new RuntimeException('Cannot base64 decode label values'); - } - $decodedValues = json_decode($json, true); - if ($decodedValues === false) { - throw new RuntimeException(json_last_error_msg()); - } - - return $decodedValues; - } - - /** - * @throws MetricJsonException - */ - private function throwMetricJsonException(string $redisKey, ?string $metricName = null): void - { - $metricName = $metricName ?? 'unknown'; - $message = 'Json error: '.json_last_error_msg().' redis key : '.$redisKey.' metric name: '.$metricName; - throw new MetricJsonException($message, 0, null, $metricName); - } } diff --git a/tests/Test/Prometheus/Predis/CollectorRegistryTest.php b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php index 2add921..8a79852 100644 --- a/tests/Test/Prometheus/Predis/CollectorRegistryTest.php +++ b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractCollectorRegistryTest; /** @@ -15,9 +14,7 @@ class CollectorRegistryTest extends AbstractCollectorRegistryTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST]); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->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 index a35b51c..fa610e1 100644 --- a/tests/Test/Prometheus/Predis/CounterTest.php +++ b/tests/Test/Prometheus/Predis/CounterTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractCounterTest; /** @@ -17,9 +16,7 @@ class CounterTest extends AbstractCounterTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST]); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->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 index c4636b0..7283572 100644 --- a/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractCounterTest; /** @@ -17,9 +16,7 @@ class CounterWithPrefixTest extends AbstractCounterTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST, 'prefix' => 'prefix:']); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter = new Predis(['host' => REDIS_HOST], ['prefix' => 'prefix:']); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/Predis/GaugeTest.php b/tests/Test/Prometheus/Predis/GaugeTest.php index f141861..9d48ec1 100644 --- a/tests/Test/Prometheus/Predis/GaugeTest.php +++ b/tests/Test/Prometheus/Predis/GaugeTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractGaugeTest; /** @@ -17,9 +16,7 @@ class GaugeTest extends AbstractGaugeTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST]); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->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 index dcc5748..4f3dc21 100644 --- a/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractGaugeTest; /** @@ -17,9 +16,7 @@ class GaugeWithPrefixTest extends AbstractGaugeTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST, 'prefix' => 'prefix:']); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter = new Predis(['host' => REDIS_HOST], ['prefix' => 'prefix:']); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/Predis/HistogramTest.php b/tests/Test/Prometheus/Predis/HistogramTest.php index 2d2b670..f4148b9 100644 --- a/tests/Test/Prometheus/Predis/HistogramTest.php +++ b/tests/Test/Prometheus/Predis/HistogramTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractHistogramTest; /** @@ -17,9 +16,7 @@ class HistogramTest extends AbstractHistogramTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST]); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->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 index bf5f8ea..751a44a 100644 --- a/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractHistogramTest; /** @@ -17,9 +16,7 @@ class HistogramWithPrefixTest extends AbstractHistogramTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST, 'prefix' => 'prefix:']); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter = new Predis(['host' => REDIS_HOST], ['prefix' => 'prefix:']); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php index 430bf6b..3566769 100644 --- a/tests/Test/Prometheus/Predis/SummaryTest.php +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractSummaryTest; /** @@ -17,9 +16,7 @@ class SummaryTest extends AbstractSummaryTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST]); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->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 index d6bd40a..1c1d164 100644 --- a/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php @@ -4,8 +4,7 @@ namespace Test\Prometheus\Predis; -use Predis\Client; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Predis; use Test\Prometheus\AbstractSummaryTest; /** @@ -17,9 +16,7 @@ class SummaryWithPrefixTest extends AbstractSummaryTest { public function configureAdapter(): void { - $connection = new Client(['host' => REDIS_HOST, 'prefix' => 'prefix:']); - - $this->adapter = Redis::fromExistingConnection($connection); + $this->adapter = new Predis(['host' => REDIS_HOST], ['prefix' => 'prefix:']); $this->adapter->wipeStorage(); } } From 8bed4a4d4020baf3561a30d5aec3c571c14dfc48 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 15:05:18 +0200 Subject: [PATCH 07/40] fix: add phpdocs Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 7 ++--- src/Prometheus/Storage/Predis.php | 3 ++- .../Storage/RedisClients/PHPRedis.php | 17 +++++++++--- .../Storage/RedisClients/Predis.php | 27 ++++++++++++++++--- .../Storage/RedisClients/RedisClient.php | 19 +++++++++++-- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index 4cab84d..820d7d8 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -56,7 +56,6 @@ public function wipeStorage(): void $searchPattern = ''; $globalPrefix = $this->redis->getOption(RedisClient::OPT_PREFIX); - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int if (is_string($globalPrefix)) { $searchPattern .= $globalPrefix; } @@ -183,7 +182,7 @@ public function updateSummary(array $data): void if ($json === false) { throw new RuntimeException(json_last_error_msg()); } - $this->redis->setNx($metaKey, $json); /** @phpstan-ignore-line */ + $this->redis->setNx($metaKey, $json); // store value key $valueKey = $summaryKey.':'.$this->valueKey($data); @@ -191,7 +190,7 @@ public function updateSummary(array $data): void if ($json === false) { throw new RuntimeException(json_last_error_msg()); } - $this->redis->setNx($valueKey, $json); /** @phpstan-ignore-line */ + $this->redis->setNx($valueKey, $json); // trick to handle uniqid collision $done = false; @@ -370,12 +369,10 @@ protected function collectHistograms(): array protected function removePrefixFromKey(string $key): string { - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int if ($this->redis->getOption(RedisClient::OPT_PREFIX) === null) { return $key; } - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int return substr($key, strlen($this->redis->getOption(RedisClient::OPT_PREFIX))); } diff --git a/src/Prometheus/Storage/Predis.php b/src/Prometheus/Storage/Predis.php index 758ec79..a12e58b 100644 --- a/src/Prometheus/Storage/Predis.php +++ b/src/Prometheus/Storage/Predis.php @@ -45,6 +45,7 @@ class Predis extends AbstractRedis /** * Redis constructor. * + * @param mixed[] $parameters * @param mixed[] $options */ public function __construct(array $parameters = [], array $options = []) @@ -71,7 +72,7 @@ public static function fromExistingConnection(Client $client): self ]; $self = new self; - $self->redis = new PredisClient($client, self::$defaultParameters, $allOptions); + $self->redis = new PredisClient($client, $allOptions); return $self; } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 9dcfbbf..59c1da0 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -13,6 +13,9 @@ class PHPRedis implements RedisClient */ private $redis; + /** + * @var mixed[] + */ private $options = []; /** @@ -20,12 +23,18 @@ class PHPRedis implements RedisClient */ private $connectionInitialized = false; + /** + * @param mixed[] $options + */ public function __construct(\Redis $redis, array $options) { $this->redis = $redis; $this->options = $options; } + /** + * @param mixed[] $options + */ public static function create(array $options): self { $redis = new \Redis; @@ -43,14 +52,14 @@ public function eval(string $script, array $args = [], int $num_keys = 0): void $this->redis->eval($script, $args, $num_keys); } - public function set(string $key, mixed $value, mixed $options = null): void + public function set(string $key, mixed $value, mixed $options = null): bool { - $this->redis->set($key, $value, $options); + return $this->redis->set($key, $value, $options); } public function setNx(string $key, mixed $value): void { - $this->redis->setNx($key, $value); + $this->redis->setNx($key, $value); /** @phpstan-ignore-line */ } public function hSetNx(string $key, string $field, mixed $value): bool @@ -58,7 +67,7 @@ public function hSetNx(string $key, string $field, mixed $value): bool return $this->redis->hSetNx($key, $field, $value); } - public function sMembers(string $key): array|false + public function sMembers(string $key): array { return $this->redis->sMembers($key); } diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index c2d0c65..562df6d 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -12,10 +12,19 @@ class Predis implements RedisClient RedisClient::OPT_PREFIX => 'prefix', ]; + /** + * @var Client + */ private $client; + /** + * @var mixed[] + */ private $options = []; + /** + * @param mixed[] $options + */ public function __construct(Client $redis, array $options) { $this->client = $redis; @@ -23,6 +32,10 @@ public function __construct(Client $redis, array $options) $this->options = $options; } + /** + * @param mixed[] $parameters + * @param mixed[] $options + */ public static function create(array $parameters, array $options): self { $redisClient = new Client($parameters, $options); @@ -46,11 +59,17 @@ public function eval(string $script, array $args = [], int $num_keys = 0): void $this->client->eval($script, $num_keys, ...$args); } - public function set(string $key, mixed $value, mixed $options = null): void + public function set(string $key, mixed $value, mixed $options = null): bool { - $this->client->set($key, $value, ...$this->flattenFlags($options)); + $result = $this->client->set($key, $value, ...$this->flattenFlags($options)); + + return (string) $result === 'OK'; } + /** + * @param array $flags + * @return mixed[] + */ private function flattenFlags(array $flags): array { $result = []; @@ -73,10 +92,10 @@ public function setNx(string $key, mixed $value): void public function hSetNx(string $key, string $field, mixed $value): bool { - return $this->hsetnx($key, $field, $value); + return $this->hSetNx($key, $field, $value); } - public function sMembers(string $key): array|false + public function sMembers(string $key): array { return $this->client->smembers($key); } diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 89eadcb..e9149e4 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -12,22 +12,37 @@ interface RedisClient public function getOption(int $option): mixed; + /** + * @param mixed[] $args + */ public function eval(string $script, array $args = [], int $num_keys = 0): void; - public function set(string $key, mixed $value, mixed $options = null): void; + public function set(string $key, mixed $value, mixed $options = null): bool; public function setNx(string $key, mixed $value): void; public function hSetNx(string $key, string $field, mixed $value): bool; - public function sMembers(string $key): array|false; + /** + * @return string[] + */ + public function sMembers(string $key): array; + /** + * @return array|false + */ public function hGetAll(string $key): array|false; + /** + * @return string[] + */ public function keys(string $pattern); public function get(string $key): mixed; + /** + * @param string|string[] $key + */ public function del(array|string $key, string ...$other_keys): void; public function ensureOpenConnection(): void; From 35af9bda4fae18870adab882dc23ec00c44c8e83 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 15:11:51 +0200 Subject: [PATCH 08/40] fix: bring _prefix() back Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 8 ++++---- src/Prometheus/Storage/RedisClients/PHPRedis.php | 5 +++++ src/Prometheus/Storage/RedisClients/Predis.php | 5 +++++ src/Prometheus/Storage/RedisClients/RedisClient.php | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index 820d7d8..dedd2c7 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -292,7 +292,7 @@ protected function collectHistograms(): array sort($keys); $histograms = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX))); + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->prefix(''))); if (! isset($raw['__meta'])) { continue; } @@ -373,7 +373,7 @@ protected function removePrefixFromKey(string $key): string return $key; } - return substr($key, strlen($this->redis->getOption(RedisClient::OPT_PREFIX))); + return substr($key, strlen($this->redis->prefix(''))); } /** @@ -483,7 +483,7 @@ protected function collectGauges(bool $sortMetrics = true): array sort($keys); $gauges = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX))); + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->prefix(''))); if (! isset($raw['__meta'])) { continue; } @@ -525,7 +525,7 @@ protected function collectCounters(bool $sortMetrics = true): array sort($keys); $counters = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX))); + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->prefix(''))); if (! isset($raw['__meta'])) { continue; } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 59c1da0..f224806 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -96,6 +96,11 @@ public function del(array|string $key, string ...$other_keys): void } } + public function prefix(string $key): string + { + return $this->redis->_prefix($key); + } + /** * @throws StorageException */ diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 562df6d..9cfe7ee 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -120,6 +120,11 @@ public function del(array|string $key, string ...$other_keys): void $this->client->del($key, ...$other_keys); } + public function prefix(string $key): string + { + return $this->getOption(RedisClient::OPT_PREFIX).$key; + } + public function ensureOpenConnection(): void { // Predis doesn't require to trigger connection diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index e9149e4..cfc8b83 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -45,5 +45,7 @@ public function get(string $key): mixed; */ public function del(array|string $key, string ...$other_keys): void; + public function prefix(string $key): string; + public function ensureOpenConnection(): void; } From 07ca5fc4442400b193c7cdc4bd127ccc78d11687 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 15:30:37 +0200 Subject: [PATCH 09/40] fix: fix prefix for PHPRedis Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 8 ++++---- src/Prometheus/Storage/Redis.php | 2 +- src/Prometheus/Storage/RedisClients/PHPRedis.php | 16 +++++++++++----- src/Prometheus/Storage/RedisClients/Predis.php | 5 ----- .../Storage/RedisClients/RedisClient.php | 2 -- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index dedd2c7..fac0e96 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -292,7 +292,7 @@ protected function collectHistograms(): array sort($keys); $histograms = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->prefix(''))); + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX) ?? '')); if (! isset($raw['__meta'])) { continue; } @@ -373,7 +373,7 @@ protected function removePrefixFromKey(string $key): string return $key; } - return substr($key, strlen($this->redis->prefix(''))); + return substr($key, strlen($this->redis->getOption(RedisClient::OPT_PREFIX))); } /** @@ -483,7 +483,7 @@ protected function collectGauges(bool $sortMetrics = true): array sort($keys); $gauges = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->prefix(''))); + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX) ?? '')); if (! isset($raw['__meta'])) { continue; } @@ -525,7 +525,7 @@ protected function collectCounters(bool $sortMetrics = true): array sort($keys); $counters = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->prefix(''))); + $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX) ?? '')); if (! isset($raw['__meta'])) { continue; } diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index f13352a..64422b7 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -48,7 +48,7 @@ public static function fromExistingConnection(\Redis $redis): self } $self = new self; - $self->redis = new PHPRedis($redis, self::$defaultOptions); + $self->redis = PHPRedis::fromExistingConnection($redis, self::$defaultOptions); return $self; } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index f224806..b3edabf 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -42,6 +42,17 @@ public static function create(array $options): self return new self($redis, $options); } + /** + * @param mixed[] $options + */ + public static function fromExistingConnection(\Redis $redis, array $options): self + { + $self = new self($redis, $options); + $self->connectionInitialized = true; + + return $self; + } + public function getOption(int $option): mixed { return $this->redis->getOption($option); @@ -96,11 +107,6 @@ public function del(array|string $key, string ...$other_keys): void } } - public function prefix(string $key): string - { - return $this->redis->_prefix($key); - } - /** * @throws StorageException */ diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 9cfe7ee..562df6d 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -120,11 +120,6 @@ public function del(array|string $key, string ...$other_keys): void $this->client->del($key, ...$other_keys); } - public function prefix(string $key): string - { - return $this->getOption(RedisClient::OPT_PREFIX).$key; - } - public function ensureOpenConnection(): void { // Predis doesn't require to trigger connection diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index cfc8b83..e9149e4 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -45,7 +45,5 @@ public function get(string $key): mixed; */ public function del(array|string $key, string ...$other_keys): void; - public function prefix(string $key): string; - public function ensureOpenConnection(): void; } From b8c7f1ac5ddad6dd8b96c0e8da4210c75a28bfb0 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 16:03:37 +0200 Subject: [PATCH 10/40] fix: run phpcs Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 44 +++++++++---------- src/Prometheus/Storage/Predis.php | 2 +- src/Prometheus/Storage/Redis.php | 2 +- .../Storage/RedisClients/PHPRedis.php | 2 +- .../RedisClients/RedisClientException.php | 4 +- tests/Test/Prometheus/Predis/SummaryTest.php | 2 +- 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index fac0e96..8336c0c 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -156,7 +156,7 @@ public function updateHistogram(array $data): void , [ $this->toMetricKey($data), - self::$prefix.Histogram::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX, + 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'], @@ -176,8 +176,8 @@ public function updateSummary(array $data): void $this->redis->ensureOpenConnection(); // store meta - $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $metaKey = $summaryKey.':'.$this->metaKey($data); + $summaryKey = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $metaKey = $summaryKey . ':' . $this->metaKey($data); $json = json_encode($this->metaData($data)); if ($json === false) { throw new RuntimeException(json_last_error_msg()); @@ -185,7 +185,7 @@ public function updateSummary(array $data): void $this->redis->setNx($metaKey, $json); // store value key - $valueKey = $summaryKey.':'.$this->valueKey($data); + $valueKey = $summaryKey . ':' . $this->valueKey($data); $json = json_encode($this->encodeLabelValues($data['labelValues'])); if ($json === false) { throw new RuntimeException(json_last_error_msg()); @@ -195,7 +195,7 @@ public function updateSummary(array $data): void // trick to handle uniqid collision $done = false; while (! $done) { - $sampleKey = $valueKey.':'.uniqid('', true); + $sampleKey = $valueKey . ':' . uniqid('', true); $done = $this->redis->set($sampleKey, $data['value'], ['NX', 'EX' => $data['maxAgeSeconds']]); } } @@ -229,7 +229,7 @@ public function updateGauge(array $data): void , [ $this->toMetricKey($data), - self::$prefix.Gauge::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX, + self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, $this->getRedisCommand($data['command']), json_encode($data['labelValues']), $data['value'], @@ -261,7 +261,7 @@ public function updateCounter(array $data): void , [ $this->toMetricKey($data), - self::$prefix.Counter::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX, + self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, $this->getRedisCommand($data['command']), $data['value'], json_encode($data['labelValues']), @@ -288,7 +288,7 @@ protected function metaData(array $data): array */ protected function collectHistograms(): array { - $keys = $this->redis->sMembers(self::$prefix.Histogram::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); + $keys = $this->redis->sMembers(self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $histograms = []; foreach ($keys as $key) { @@ -329,7 +329,7 @@ protected function collectHistograms(): array $bucketKey = json_encode(['b' => $bucket, 'labelValues' => $labelValues]); if (! isset($raw[$bucketKey])) { $histogram['samples'][] = [ - 'name' => $histogram['name'].'_bucket', + 'name' => $histogram['name'] . '_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($labelValues, [$bucket]), 'value' => $acc, @@ -337,7 +337,7 @@ protected function collectHistograms(): array } else { $acc += $raw[$bucketKey]; $histogram['samples'][] = [ - 'name' => $histogram['name'].'_bucket', + 'name' => $histogram['name'] . '_bucket', 'labelNames' => ['le'], 'labelValues' => array_merge($labelValues, [$bucket]), 'value' => $acc, @@ -347,7 +347,7 @@ protected function collectHistograms(): array // Add the count $histogram['samples'][] = [ - 'name' => $histogram['name'].'_count', + 'name' => $histogram['name'] . '_count', 'labelNames' => [], 'labelValues' => $labelValues, 'value' => $acc, @@ -355,7 +355,7 @@ protected function collectHistograms(): array // Add the sum $histogram['samples'][] = [ - 'name' => $histogram['name'].'_sum', + 'name' => $histogram['name'] . '_sum', 'labelNames' => [], 'labelValues' => $labelValues, 'value' => $raw[json_encode(['b' => 'sum', 'labelValues' => $labelValues])], @@ -381,9 +381,9 @@ protected function removePrefixFromKey(string $key): string */ protected function collectSummaries(): array { - $math = new Math; - $summaryKey = self::$prefix.Summary::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $keys = $this->redis->keys($summaryKey.':*:meta'); + $math = new Math(); + $summaryKey = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $keys = $this->redis->keys($summaryKey . ':*:meta'); $summaries = []; foreach ($keys as $metaKeyWithPrefix) { @@ -404,7 +404,7 @@ protected function collectSummaries(): array 'samples' => [], ]; - $values = $this->redis->keys($summaryKey.':'.$metaData['name'].':*:value'); + $values = $this->redis->keys($summaryKey . ':' . $metaData['name'] . ':*:value'); foreach ($values as $valueKeyWithPrefix) { $valueKey = $this->removePrefixFromKey($valueKeyWithPrefix); $rawValue = $this->redis->get($valueKey); @@ -416,7 +416,7 @@ protected function collectSummaries(): array $decodedLabelValues = $this->decodeLabelValues($encodedLabelValues); $samples = []; - $sampleValues = $this->redis->keys($summaryKey.':'.$metaData['name'].':'.$encodedLabelValues.':value:*'); + $sampleValues = $this->redis->keys($summaryKey . ':' . $metaData['name'] . ':' . $encodedLabelValues . ':value:*'); foreach ($sampleValues as $sampleValueWithPrefix) { $sampleValue = $this->removePrefixFromKey($sampleValueWithPrefix); $samples[] = (float) $this->redis->get($sampleValue); @@ -445,7 +445,7 @@ protected function collectSummaries(): array // Add the count $data['samples'][] = [ - 'name' => $metaData['name'].'_count', + 'name' => $metaData['name'] . '_count', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => count($samples), @@ -453,7 +453,7 @@ protected function collectSummaries(): array // Add the sum $data['samples'][] = [ - 'name' => $metaData['name'].'_sum', + 'name' => $metaData['name'] . '_sum', 'labelNames' => [], 'labelValues' => $decodedLabelValues, 'value' => array_sum($samples), @@ -479,7 +479,7 @@ protected function collectSummaries(): array */ protected function collectGauges(bool $sortMetrics = true): array { - $keys = $this->redis->sMembers(self::$prefix.Gauge::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); + $keys = $this->redis->sMembers(self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $gauges = []; foreach ($keys as $key) { @@ -521,7 +521,7 @@ protected function collectGauges(bool $sortMetrics = true): array */ protected function collectCounters(bool $sortMetrics = true): array { - $keys = $this->redis->sMembers(self::$prefix.Counter::TYPE.self::PROMETHEUS_METRIC_KEYS_SUFFIX); + $keys = $this->redis->sMembers(self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); sort($keys); $counters = []; foreach ($keys as $key) { @@ -620,7 +620,7 @@ protected function decodeLabelValues(string $values): array protected function throwMetricJsonException(string $redisKey, ?string $metricName = null): void { $metricName = $metricName ?? 'unknown'; - $message = 'Json error: '.json_last_error_msg().' redis key : '.$redisKey.' metric name: '.$metricName; + $message = 'Json error: ' . json_last_error_msg() . ' redis key : ' . $redisKey . ' metric name: ' . $metricName; throw new MetricJsonException($message, 0, null, $metricName); } } diff --git a/src/Prometheus/Storage/Predis.php b/src/Prometheus/Storage/Predis.php index a12e58b..504b322 100644 --- a/src/Prometheus/Storage/Predis.php +++ b/src/Prometheus/Storage/Predis.php @@ -71,7 +71,7 @@ public static function fromExistingConnection(Client $client): self 'replication' => $options->replication, ]; - $self = new self; + $self = new self(); $self->redis = new PredisClient($client, $allOptions); return $self; diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 64422b7..78b0e37 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -47,7 +47,7 @@ public static function fromExistingConnection(\Redis $redis): self throw new StorageException('Connection to Redis server not established'); } - $self = new self; + $self = new self(); $self->redis = PHPRedis::fromExistingConnection($redis, self::$defaultOptions); return $self; diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index b3edabf..17f1912 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -37,7 +37,7 @@ public function __construct(\Redis $redis, array $options) */ public static function create(array $options): self { - $redis = new \Redis; + $redis = new \Redis(); return new self($redis, $options); } diff --git a/src/Prometheus/Storage/RedisClients/RedisClientException.php b/src/Prometheus/Storage/RedisClients/RedisClientException.php index 36d8cf3..1a81128 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClientException.php +++ b/src/Prometheus/Storage/RedisClients/RedisClientException.php @@ -4,4 +4,6 @@ namespace Prometheus\Storage\RedisClients; -class RedisClientException extends \Exception {} +class RedisClientException extends \Exception +{ +} diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php index 3566769..b3cc3b4 100644 --- a/tests/Test/Prometheus/Predis/SummaryTest.php +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -21,7 +21,7 @@ public function configureAdapter(): void } /** @test */ - public function it_should_observe_with_labels(): void + public function itShouldObserveWithLabels(): void { parent::itShouldObserveWithLabels(); // TODO: Change the autogenerated stub } From 7ddb21b130c2c1242d3a666c991a93f1b535521d Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 00:12:40 +0200 Subject: [PATCH 11/40] tests: add config for blackbox test Signed-off-by: Mateusz Cholewka --- composer.json | 1 + examples/flush_adapter.php | 4 ++++ examples/metrics.php | 2 ++ examples/some_counter.php | 2 ++ examples/some_gauge.php | 3 ++- examples/some_histogram.php | 2 ++ examples/some_summary.php | 2 ++ src/Prometheus/Storage/AbstractRedis.php | 2 +- 8 files changed, 16 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 88b7c89..ebe65fb 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ }, "suggest": { "ext-redis": "Required if using Redis.", + "predis/predis": "Required if using Predis.", "ext-apc": "Required if using APCu.", "ext-pdo": "Required if using PDO.", "promphp/prometheus_push_gateway_php": "An easy client for using Prometheus PushGateway.", diff --git a/examples/flush_adapter.php b/examples/flush_adapter.php index 1c00eab..82b73ae 100644 --- a/examples/flush_adapter.php +++ b/examples/flush_adapter.php @@ -10,6 +10,8 @@ define('REDIS_HOST', $_SERVER['REDIS_HOST'] ?? '127.0.0.1'); $adapter = new Prometheus\Storage\Redis(['host' => REDIS_HOST]); +} elseif ($adapterName === 'predis') { + $adapter = new Prometheus\Storage\Predis(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } elseif ($adapterName === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapterName === 'apcng') { @@ -18,4 +20,6 @@ $adapter = new Prometheus\Storage\InMemory(); } + + $adapter->wipeStorage(); diff --git a/examples/metrics.php b/examples/metrics.php index 9c0fdb8..844c2b2 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/examples/some_counter.php b/examples/some_counter.php index c7426ce..b861a18 100644 --- a/examples/some_counter.php +++ b/examples/some_counter.php @@ -10,6 +10,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/examples/some_gauge.php b/examples/some_gauge.php index 9e8b3da..a0a5894 100644 --- a/examples/some_gauge.php +++ b/examples/some_gauge.php @@ -5,7 +5,6 @@ use Prometheus\CollectorRegistry; use Prometheus\Storage\Redis; - error_log('c=' . $_GET['c']); $adapter = $_GET['adapter']; @@ -13,6 +12,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/examples/some_histogram.php b/examples/some_histogram.php index 2f1a5f9..b2d9135 100644 --- a/examples/some_histogram.php +++ b/examples/some_histogram.php @@ -12,6 +12,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/examples/some_summary.php b/examples/some_summary.php index 363f919..34b30ce 100644 --- a/examples/some_summary.php +++ b/examples/some_summary.php @@ -12,6 +12,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/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index 8336c0c..ce46400 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -67,7 +67,7 @@ public function wipeStorage(): void <<<'LUA' redis.replicate_commands() local cursor = "0" -repeat +repeat local results = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) cursor = results[1] for _, key in ipairs(results[2]) do From 45628012426c35d87301526b699830cdac7f1726 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 13:17:38 +0200 Subject: [PATCH 12/40] fix: remove all redis notes from predis Signed-off-by: Mateusz Cholewka --- examples/flush_adapter.php | 10 ++++------ tests/Test/Prometheus/Predis/CollectorRegistryTest.php | 3 --- tests/Test/Prometheus/Predis/CounterTest.php | 2 -- tests/Test/Prometheus/Predis/CounterWithPrefixTest.php | 2 -- tests/Test/Prometheus/Predis/GaugeTest.php | 2 -- tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php | 2 -- tests/Test/Prometheus/Predis/HistogramTest.php | 2 -- .../Test/Prometheus/Predis/HistogramWithPrefixTest.php | 2 -- tests/Test/Prometheus/Predis/SummaryTest.php | 4 +--- tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php | 2 -- 10 files changed, 5 insertions(+), 26 deletions(-) diff --git a/examples/flush_adapter.php b/examples/flush_adapter.php index 82b73ae..858e9b0 100644 --- a/examples/flush_adapter.php +++ b/examples/flush_adapter.php @@ -1,6 +1,6 @@ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } elseif ($adapterName === 'apc') { - $adapter = new Prometheus\Storage\APC(); + $adapter = new Prometheus\Storage\APC; } elseif ($adapterName === 'apcng') { - $adapter = new Prometheus\Storage\APCng(); + $adapter = new Prometheus\Storage\APCng; } elseif ($adapterName === 'in-memory') { - $adapter = new Prometheus\Storage\InMemory(); + $adapter = new Prometheus\Storage\InMemory; } - - $adapter->wipeStorage(); diff --git a/tests/Test/Prometheus/Predis/CollectorRegistryTest.php b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php index 8a79852..e1c54fb 100644 --- a/tests/Test/Prometheus/Predis/CollectorRegistryTest.php +++ b/tests/Test/Prometheus/Predis/CollectorRegistryTest.php @@ -7,9 +7,6 @@ use Prometheus\Storage\Predis; use Test\Prometheus\AbstractCollectorRegistryTest; -/** - * @requires extension redis - */ class CollectorRegistryTest extends AbstractCollectorRegistryTest { public function configureAdapter(): void diff --git a/tests/Test/Prometheus/Predis/CounterTest.php b/tests/Test/Prometheus/Predis/CounterTest.php index fa610e1..f8508dc 100644 --- a/tests/Test/Prometheus/Predis/CounterTest.php +++ b/tests/Test/Prometheus/Predis/CounterTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class CounterTest extends AbstractCounterTest { diff --git a/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php index 7283572..25e2d01 100644 --- a/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class CounterWithPrefixTest extends AbstractCounterTest { diff --git a/tests/Test/Prometheus/Predis/GaugeTest.php b/tests/Test/Prometheus/Predis/GaugeTest.php index 9d48ec1..e84e814 100644 --- a/tests/Test/Prometheus/Predis/GaugeTest.php +++ b/tests/Test/Prometheus/Predis/GaugeTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class GaugeTest extends AbstractGaugeTest { diff --git a/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php index 4f3dc21..d5a895e 100644 --- a/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class GaugeWithPrefixTest extends AbstractGaugeTest { diff --git a/tests/Test/Prometheus/Predis/HistogramTest.php b/tests/Test/Prometheus/Predis/HistogramTest.php index f4148b9..381ed19 100644 --- a/tests/Test/Prometheus/Predis/HistogramTest.php +++ b/tests/Test/Prometheus/Predis/HistogramTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class HistogramTest extends AbstractHistogramTest { diff --git a/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php index 751a44a..d4029a0 100644 --- a/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class HistogramWithPrefixTest extends AbstractHistogramTest { diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php index b3cc3b4..3d5a981 100644 --- a/tests/Test/Prometheus/Predis/SummaryTest.php +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class SummaryTest extends AbstractSummaryTest { @@ -21,7 +19,7 @@ public function configureAdapter(): void } /** @test */ - public function itShouldObserveWithLabels(): void + public function it_should_observe_with_labels(): void { parent::itShouldObserveWithLabels(); // TODO: Change the autogenerated stub } diff --git a/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php index 1c1d164..54ffd31 100644 --- a/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php +++ b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php @@ -9,8 +9,6 @@ /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ - * - * @requires extension redis */ class SummaryWithPrefixTest extends AbstractSummaryTest { From c9f0a977cddca481a99346ad7e8155af4cc306e4 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 13:18:19 +0200 Subject: [PATCH 13/40] fix: estabish connection after first call in predis Signed-off-by: Mateusz Cholewka --- examples/flush_adapter.php | 8 +++--- src/Prometheus/Storage/Predis.php | 26 +++++++++---------- .../Storage/RedisClients/Predis.php | 19 +++++++++----- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/examples/flush_adapter.php b/examples/flush_adapter.php index 858e9b0..712e2be 100644 --- a/examples/flush_adapter.php +++ b/examples/flush_adapter.php @@ -1,6 +1,6 @@ $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } elseif ($adapterName === 'apc') { - $adapter = new Prometheus\Storage\APC; + $adapter = new Prometheus\Storage\APC(); } elseif ($adapterName === 'apcng') { - $adapter = new Prometheus\Storage\APCng; + $adapter = new Prometheus\Storage\APCng(); } elseif ($adapterName === 'in-memory') { - $adapter = new Prometheus\Storage\InMemory; + $adapter = new Prometheus\Storage\InMemory(); } $adapter->wipeStorage(); diff --git a/src/Prometheus/Storage/Predis.php b/src/Prometheus/Storage/Predis.php index 504b322..c2d6b5b 100644 --- a/src/Prometheus/Storage/Predis.php +++ b/src/Prometheus/Storage/Predis.php @@ -4,8 +4,8 @@ namespace Prometheus\Storage; +use InvalidArgumentException; use Predis\Client; -use Prometheus\Exception\StorageException; use Prometheus\Storage\RedisClients\Predis as PredisClient; class Predis extends AbstractRedis @@ -43,7 +43,7 @@ class Predis extends AbstractRedis private $options = []; /** - * Redis constructor. + * Predis constructor. * * @param mixed[] $parameters * @param mixed[] $options @@ -56,23 +56,23 @@ public function __construct(array $parameters = [], array $options = []) } /** - * @throws StorageException + * @throws InvalidArgumentException */ public static function fromExistingConnection(Client $client): self { - $options = $client->getOptions(); - $allOptions = [ - 'aggregate' => $options->aggregate, - 'cluster' => $options->cluster, - 'connections' => $options->connections, - 'exceptions' => $options->exceptions, - 'prefix' => $options->prefix, - 'commands' => $options->commands, - 'replication' => $options->replication, + $clientOptions = $client->getOptions(); + $options = [ + 'aggregate' => $clientOptions->aggregate, + 'cluster' => $clientOptions->cluster, + 'connections' => $clientOptions->connections, + 'exceptions' => $clientOptions->exceptions, + 'prefix' => $clientOptions->prefix, + 'commands' => $clientOptions->commands, + 'replication' => $clientOptions->replication, ]; $self = new self(); - $self->redis = new PredisClient($client, $allOptions); + $self->redis = new PredisClient(self::$defaultParameters, $options, $client); return $self; } diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 562df6d..e8164b1 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -13,22 +13,29 @@ class Predis implements RedisClient ]; /** - * @var Client + * @var ?Client */ private $client; + /** + * @var mixed[] + */ + private $parameters = []; + /** * @var mixed[] */ private $options = []; /** + * @param mixed[] $parameters * @param mixed[] $options */ - public function __construct(Client $redis, array $options) + public function __construct(array $parameters, array $options, ?Client $redis = null) { $this->client = $redis; + $this->parameters = $parameters; $this->options = $options; } @@ -38,9 +45,7 @@ public function __construct(Client $redis, array $options) */ public static function create(array $parameters, array $options): self { - $redisClient = new Client($parameters, $options); - - return new self($redisClient, $options); + return new self($parameters, $options); } public function getOption(int $option): mixed @@ -122,6 +127,8 @@ public function del(array|string $key, string ...$other_keys): void public function ensureOpenConnection(): void { - // Predis doesn't require to trigger connection + if ($this->client === null) { + $this->client = new Client($this->parameters, $this->options); + } } } From c3080db9461a42dac1c47065d1298b2a52af24c6 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 13:24:58 +0200 Subject: [PATCH 14/40] fix: add exception handling for predis connection Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/Predis.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index e8164b1..1146a06 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -4,7 +4,9 @@ namespace Prometheus\Storage\RedisClients; +use InvalidArgumentException; use Predis\Client; +use Prometheus\Exception\StorageException; class Predis implements RedisClient { @@ -125,10 +127,17 @@ public function del(array|string $key, string ...$other_keys): void $this->client->del($key, ...$other_keys); } + /** + * @throws StorageException + */ public function ensureOpenConnection(): void { if ($this->client === null) { - $this->client = new Client($this->parameters, $this->options); + try { + $this->client = new Client($this->parameters, $this->options); + } catch (InvalidArgumentException $e) { + throw new StorageException('Cannot establish Redis Connection:' . $e->getMessage()); + } } } } From 880e7a27335de9c0d2f915b4835d334c3627cc70 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 21:38:06 +0100 Subject: [PATCH 15/40] chore: remove unecessary comment Signed-off-by: Mateusz Cholewka --- tests/Test/Prometheus/Predis/SummaryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php index 3d5a981..ac6f4d4 100644 --- a/tests/Test/Prometheus/Predis/SummaryTest.php +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -21,6 +21,6 @@ public function configureAdapter(): void /** @test */ public function it_should_observe_with_labels(): void { - parent::itShouldObserveWithLabels(); // TODO: Change the autogenerated stub + parent::itShouldObserveWithLabels(); } } From 8dd074a30a95eded56ac9588d47738778d8f05dd Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 21:38:19 +0100 Subject: [PATCH 16/40] fix: remove redundant replicate commands Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index ce46400..cd993f4 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -65,7 +65,6 @@ public function wipeStorage(): void $this->redis->eval( <<<'LUA' -redis.replicate_commands() local cursor = "0" repeat local results = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) From a019eb2b7e90f3ce00d91da66ac217a439caeea9 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 21:41:11 +0100 Subject: [PATCH 17/40] fix: remove unnecessary comparison Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/Predis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 1146a06..b264138 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -94,7 +94,7 @@ private function flattenFlags(array $flags): array public function setNx(string $key, mixed $value): void { - $this->client->setnx($key, $value) === 1; + $this->client->setnx($key, $value); } public function hSetNx(string $key, string $field, mixed $value): bool From ddb503bbedf9a6d9fc4ac913ed4797a2c9a870ad Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 21:42:57 +0100 Subject: [PATCH 18/40] fix: remove dead code This code has been mentioned as issue because of recurency call. Afeter quick analyse, I've realised that this is dead code, and it's not used anywhere. Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 5 ----- src/Prometheus/Storage/RedisClients/Predis.php | 5 ----- src/Prometheus/Storage/RedisClients/RedisClient.php | 6 ++---- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 17f1912..b5f6e76 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -73,11 +73,6 @@ public function setNx(string $key, mixed $value): void $this->redis->setNx($key, $value); /** @phpstan-ignore-line */ } - public function hSetNx(string $key, string $field, mixed $value): bool - { - return $this->redis->hSetNx($key, $field, $value); - } - public function sMembers(string $key): array { return $this->redis->sMembers($key); diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index b264138..3d48e5c 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -97,11 +97,6 @@ public function setNx(string $key, mixed $value): void $this->client->setnx($key, $value); } - public function hSetNx(string $key, string $field, mixed $value): bool - { - return $this->hSetNx($key, $field, $value); - } - public function sMembers(string $key): array { return $this->client->smembers($key); diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index e9149e4..84fb26f 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -6,9 +6,9 @@ interface RedisClient { - const OPT_PREFIX = 2; + public const OPT_PREFIX = 2; - const OPT_READ_TIMEOUT = 3; + public const OPT_READ_TIMEOUT = 3; public function getOption(int $option): mixed; @@ -21,8 +21,6 @@ public function set(string $key, mixed $value, mixed $options = null): bool; public function setNx(string $key, mixed $value): void; - public function hSetNx(string $key, string $field, mixed $value): bool; - /** * @return string[] */ From ef9c7dbd063a0bee2b4e7bed38ee281494c987c2 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 22:07:16 +0100 Subject: [PATCH 19/40] rafactor: remove OPT_READ_TIMEOUT as it's phpredis internals Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 2 +- src/Prometheus/Storage/RedisClients/RedisClient.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index b5f6e76..c98de49 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -130,7 +130,7 @@ public function ensureOpenConnection(): void $this->redis->select($this->options['database']); } - $this->redis->setOption(RedisClient::OPT_READ_TIMEOUT, $this->options['read_timeout']); + $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $this->options['read_timeout']); $this->connectionInitialized = true; } diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 84fb26f..fac6643 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -8,8 +8,6 @@ interface RedisClient { public const OPT_PREFIX = 2; - public const OPT_READ_TIMEOUT = 3; - public function getOption(int $option): mixed; /** From 6b9c8d67d5c39a20dc0a92a635014e765444001e Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 22:14:40 +0100 Subject: [PATCH 20/40] refactor: make opt truely abstract https://github.com/PromPHP/prometheus_client_php/pull/186/changes#r2953935115 Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 12 ++++++++++-- src/Prometheus/Storage/RedisClients/Predis.php | 2 +- src/Prometheus/Storage/RedisClients/RedisClient.php | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index c98de49..89f7a01 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -8,6 +8,10 @@ class PHPRedis implements RedisClient { + private const OPTIONS_MAP = [ + RedisClient::OPT_PREFIX => \Redis::OPT_PREFIX, + ]; + /** * @var \Redis */ @@ -53,9 +57,13 @@ public static function fromExistingConnection(\Redis $redis, array $options): se return $self; } - public function getOption(int $option): mixed + public function getOption(string $option): mixed { - return $this->redis->getOption($option); + if (!isset(self::OPTIONS_MAP[$option])) { + return null; + } + + return $this->redis->getOption(self::OPTIONS_MAP[$option]); } public function eval(string $script, array $args = [], int $num_keys = 0): void diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 3d48e5c..f172a53 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -50,7 +50,7 @@ public static function create(array $parameters, array $options): self return new self($parameters, $options); } - public function getOption(int $option): mixed + public function getOption(string $option): mixed { if (! isset(self::OPTIONS_MAP[$option])) { return null; diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index fac6643..2f1e5b2 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -6,9 +6,9 @@ interface RedisClient { - public const OPT_PREFIX = 2; + public const OPT_PREFIX = 'prefix'; - public function getOption(int $option): mixed; + public function getOption(string $option): mixed; /** * @param mixed[] $args From 55de57dbc2efceb0eaca38a72b9e720ca901692c Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 22:28:56 +0100 Subject: [PATCH 21/40] fix: apply stripPrefix only to phpredis implementation Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 6 +++--- src/Prometheus/Storage/RedisClients/PHPRedis.php | 10 ++++++++++ src/Prometheus/Storage/RedisClients/Predis.php | 5 +++++ src/Prometheus/Storage/RedisClients/RedisClient.php | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index cd993f4..9caba9d 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -291,7 +291,7 @@ protected function collectHistograms(): array sort($keys); $histograms = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX) ?? '')); + $raw = $this->redis->hGetAll($this->redis->stripKeyPrefix($key)); if (! isset($raw['__meta'])) { continue; } @@ -482,7 +482,7 @@ protected function collectGauges(bool $sortMetrics = true): array sort($keys); $gauges = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX) ?? '')); + $raw = $this->redis->hGetAll($this->redis->stripKeyPrefix($key)); if (! isset($raw['__meta'])) { continue; } @@ -524,7 +524,7 @@ protected function collectCounters(bool $sortMetrics = true): array sort($keys); $counters = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll(ltrim($key, $this->redis->getOption(RedisClient::OPT_PREFIX) ?? '')); + $raw = $this->redis->hGetAll($this->redis->stripKeyPrefix($key)); if (! isset($raw['__meta'])) { continue; } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 89f7a01..a7674a0 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -66,6 +66,16 @@ public function getOption(string $option): mixed return $this->redis->getOption(self::OPTIONS_MAP[$option]); } + public function stripKeyPrefix(string $key): string + { + $prefix = $this->redis->getOption(\Redis::OPT_PREFIX); + if (!is_string($prefix) || $prefix === '') { + return $key; + } + + return substr($key, strlen($prefix)); + } + public function eval(string $script, array $args = [], int $num_keys = 0): void { $this->redis->eval($script, $args, $num_keys); diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index f172a53..865aeb4 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -61,6 +61,11 @@ public function getOption(string $option): mixed return $this->options[$mappedOption] ?? null; } + public function stripKeyPrefix(string $key): string + { + return $key; + } + public function eval(string $script, array $args = [], int $num_keys = 0): void { $this->client->eval($script, $num_keys, ...$args); diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 2f1e5b2..72875e5 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -10,6 +10,8 @@ interface RedisClient public function getOption(string $option): mixed; + public function stripKeyPrefix(string $key): string; + /** * @param mixed[] $args */ From be4731573f199bfa3de3910ddb95901e9e0ceaf1 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 22:33:16 +0100 Subject: [PATCH 22/40] fix: normalise the return type of get for predis and phpredis Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 2 +- src/Prometheus/Storage/RedisClients/Predis.php | 4 ++-- src/Prometheus/Storage/RedisClients/RedisClient.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index a7674a0..d1b152f 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -106,7 +106,7 @@ public function keys(string $pattern) return $this->redis->keys($pattern); } - public function get(string $key): mixed + public function get(string $key): string|false { return $this->redis->get($key); } diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 865aeb4..e2dcde1 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -117,9 +117,9 @@ public function keys(string $pattern) return $this->client->keys($pattern); } - public function get(string $key): mixed + public function get(string $key): string|false { - return $this->client->get($key); + return $this->client->get($key) ?? false; } public function del(array|string $key, string ...$other_keys): void diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 72875e5..2eaedcd 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -36,7 +36,7 @@ public function hGetAll(string $key): array|false; */ public function keys(string $pattern); - public function get(string $key): mixed; + public function get(string $key): string|false; /** * @param string|string[] $key From 87a5f91cb6db9a8a79347002f6f7fa51458b1591 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 22:53:59 +0100 Subject: [PATCH 23/40] fix: always check if connection exists This is also required by PHPStan Signed-off-by: Mateusz Cholewka --- .../Storage/RedisClients/Predis.php | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index e2dcde1..c3d930a 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -66,14 +66,23 @@ public function stripKeyPrefix(string $key): string return $key; } + private function getClient(): Client + { + if ($this->client === null) { + throw new StorageException('Redis connection not initialized. Call ensureOpenConnection() first.'); + } + + return $this->client; + } + public function eval(string $script, array $args = [], int $num_keys = 0): void { - $this->client->eval($script, $num_keys, ...$args); + $this->getClient()->eval($script, $num_keys, ...$args); } public function set(string $key, mixed $value, mixed $options = null): bool { - $result = $this->client->set($key, $value, ...$this->flattenFlags($options)); + $result = $this->getClient()->set($key, $value, ...$this->flattenFlags($options)); return (string) $result === 'OK'; } @@ -99,32 +108,32 @@ private function flattenFlags(array $flags): array public function setNx(string $key, mixed $value): void { - $this->client->setnx($key, $value); + $this->getClient()->setnx($key, $value); } public function sMembers(string $key): array { - return $this->client->smembers($key); + return $this->getClient()->smembers($key); } public function hGetAll(string $key): array|false { - return $this->client->hgetall($key); + return $this->getClient()->hgetall($key); } public function keys(string $pattern) { - return $this->client->keys($pattern); + return $this->getClient()->keys($pattern); } public function get(string $key): string|false { - return $this->client->get($key) ?? false; + return $this->getClient()->get($key) ?? false; } public function del(array|string $key, string ...$other_keys): void { - $this->client->del($key, ...$other_keys); + $this->getClient()->del($key, ...$other_keys); } /** From 7c0ef783802b42f5e1cd0574775beb7700233014 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 23:00:25 +0100 Subject: [PATCH 24/40] fix: fix mixed type of getOption For some reason PHPStan cannot recognize that this method is returning mixed, and still trying to use int in this code, so this function would always fall in to if statement. Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index d1b152f..94490c3 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -68,6 +68,7 @@ public function getOption(string $option): mixed public function stripKeyPrefix(string $key): string { + /** @var mixed $prefix */ $prefix = $this->redis->getOption(\Redis::OPT_PREFIX); if (!is_string($prefix) || $prefix === '') { return $key; From 8f3f2a8f56e20672c473fe7022c18871811b88f3 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 23:03:47 +0100 Subject: [PATCH 25/40] cicd: add predis to blackbox tests Signed-off-by: Mateusz Cholewka --- .github/workflows/blackbox.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/blackbox.yml b/.github/workflows/blackbox.yml index afdce3c..225f2c1 100644 --- a/.github/workflows/blackbox.yml +++ b/.github/workflows/blackbox.yml @@ -39,5 +39,7 @@ jobs: run: docker compose run phpunit env ADAPTER=apc vendor/bin/phpunit tests/Test/ - name: Run Blackbox with APCng run: docker compose run phpunit env ADAPTER=apcng vendor/bin/phpunit tests/Test/ - - name: Run Blackbox with Redis + - name: Run Blackbox with PHPRedis run: docker compose run phpunit env ADAPTER=redis vendor/bin/phpunit tests/Test/ + - name: Run Blackbox with Predis + run: docker compose run phpunit env ADAPTER=predis vendor/bin/phpunit tests/Test/ From 5686202fe611dfbef92dc8e120a84939f11af6f1 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 23:07:46 +0100 Subject: [PATCH 26/40] fix: fix test name case Signed-off-by: Mateusz Cholewka --- tests/Test/Prometheus/Predis/SummaryTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php index ac6f4d4..5d0a9ef 100644 --- a/tests/Test/Prometheus/Predis/SummaryTest.php +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -18,8 +18,7 @@ public function configureAdapter(): void $this->adapter->wipeStorage(); } - /** @test */ - public function it_should_observe_with_labels(): void + public function itShouldObserveWithLabels(): void { parent::itShouldObserveWithLabels(); } From 72af24e3e8ef40b1f98c7e0d4d7056654bb5bee1 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 19 Mar 2026 23:48:38 +0100 Subject: [PATCH 27/40] fix: fix failing tests Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 6 +++--- src/Prometheus/Storage/RedisClients/PHPRedis.php | 11 ----------- src/Prometheus/Storage/RedisClients/Predis.php | 5 ----- src/Prometheus/Storage/RedisClients/RedisClient.php | 2 -- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index 9caba9d..fb4b077 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -291,7 +291,7 @@ protected function collectHistograms(): array sort($keys); $histograms = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll($this->redis->stripKeyPrefix($key)); + $raw = $this->redis->hGetAll($this->removePrefixFromKey($key)); if (! isset($raw['__meta'])) { continue; } @@ -482,7 +482,7 @@ protected function collectGauges(bool $sortMetrics = true): array sort($keys); $gauges = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll($this->redis->stripKeyPrefix($key)); + $raw = $this->redis->hGetAll($this->removePrefixFromKey($key)); if (! isset($raw['__meta'])) { continue; } @@ -524,7 +524,7 @@ protected function collectCounters(bool $sortMetrics = true): array sort($keys); $counters = []; foreach ($keys as $key) { - $raw = $this->redis->hGetAll($this->redis->stripKeyPrefix($key)); + $raw = $this->redis->hGetAll($this->removePrefixFromKey($key)); if (! isset($raw['__meta'])) { continue; } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 94490c3..5e7cc59 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -66,17 +66,6 @@ public function getOption(string $option): mixed return $this->redis->getOption(self::OPTIONS_MAP[$option]); } - public function stripKeyPrefix(string $key): string - { - /** @var mixed $prefix */ - $prefix = $this->redis->getOption(\Redis::OPT_PREFIX); - if (!is_string($prefix) || $prefix === '') { - return $key; - } - - return substr($key, strlen($prefix)); - } - public function eval(string $script, array $args = [], int $num_keys = 0): void { $this->redis->eval($script, $args, $num_keys); diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index c3d930a..156628b 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -61,11 +61,6 @@ public function getOption(string $option): mixed return $this->options[$mappedOption] ?? null; } - public function stripKeyPrefix(string $key): string - { - return $key; - } - private function getClient(): Client { if ($this->client === null) { diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 2eaedcd..3ffe301 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -10,8 +10,6 @@ interface RedisClient public function getOption(string $option): mixed; - public function stripKeyPrefix(string $key): string; - /** * @param mixed[] $args */ From c6692f493025b534e6c351f305ac808f5c28ff3e Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 17:47:08 +0100 Subject: [PATCH 28/40] fix: add return type of keys Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 2 +- src/Prometheus/Storage/RedisClients/Predis.php | 2 +- src/Prometheus/Storage/RedisClients/RedisClient.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 5e7cc59..f77f0b0 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -91,7 +91,7 @@ public function hGetAll(string $key): array|false return $this->redis->hGetAll($key); } - public function keys(string $pattern) + public function keys(string $pattern): array { return $this->redis->keys($pattern); } diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 156628b..77f527c 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -116,7 +116,7 @@ public function hGetAll(string $key): array|false return $this->getClient()->hgetall($key); } - public function keys(string $pattern) + public function keys(string $pattern): array { return $this->getClient()->keys($pattern); } diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 3ffe301..332e19b 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -32,7 +32,7 @@ public function hGetAll(string $key): array|false; /** * @return string[] */ - public function keys(string $pattern); + public function keys(string $pattern): array; public function get(string $key): string|false; From d2eda2cb8432882ed92e292f304570e087775d8b Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 17:59:58 +0100 Subject: [PATCH 29/40] style: add comment to document intentionally sharing prefix Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index fb4b077..f397429 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -22,6 +22,8 @@ abstract class AbstractRedis implements Adapter const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; /** + * Global prefix shared across all adapters. + * * @var string */ protected static $prefix = 'PROMETHEUS_'; From 8a4e308533255e2f4d603801e3a332455f42a099 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 18:49:36 +0100 Subject: [PATCH 30/40] fix: bring back replicate_commands() to keep support for older redis version Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index f397429..5f9dd20 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -67,6 +67,7 @@ public function wipeStorage(): void $this->redis->eval( <<<'LUA' +redis.replicate_commands() local cursor = "0" repeat local results = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) From e0afae3f64790a521372a9958601b9d1913f217a Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 19:15:39 +0100 Subject: [PATCH 31/40] fix: pass previous exception Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/Predis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 77f527c..2b80efb 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -140,7 +140,7 @@ public function ensureOpenConnection(): void try { $this->client = new Client($this->parameters, $this->options); } catch (InvalidArgumentException $e) { - throw new StorageException('Cannot establish Redis Connection:' . $e->getMessage()); + throw new StorageException('Cannot establish Redis Connection:' . $e->getMessage(), 0, $e); } } } From 7d0d27eb0e228ed45fe9d792fd44bed221116673 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 20:09:22 +0100 Subject: [PATCH 32/40] fix: connect predis earlier to fail fast same as phpredis Signed-off-by: Mateusz Cholewka --- .../Storage/RedisClients/Predis.php | 3 +- tests/Test/Prometheus/Storage/PredisTest.php | 61 +++++++++++++++++++ tests/Test/Prometheus/Storage/RedisTest.php | 2 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/Test/Prometheus/Storage/PredisTest.php diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 2b80efb..f5f65cf 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -139,7 +139,8 @@ public function ensureOpenConnection(): void if ($this->client === null) { try { $this->client = new Client($this->parameters, $this->options); - } catch (InvalidArgumentException $e) { + $this->client->connect(); + } catch (InvalidArgumentException|\Predis\Connection\ConnectionException $e) { throw new StorageException('Cannot establish Redis Connection:' . $e->getMessage(), 0, $e); } } diff --git a/tests/Test/Prometheus/Storage/PredisTest.php b/tests/Test/Prometheus/Storage/PredisTest.php new file mode 100644 index 0000000..82a63f0 --- /dev/null +++ b/tests/Test/Prometheus/Storage/PredisTest.php @@ -0,0 +1,61 @@ +predisConnection = new Client(['host' => REDIS_HOST]); + $this->predisConnection->flushall(); + } + + /** + * @test + */ + public function itShouldThrowAnExceptionOnConnectionFailure(): void + { + $predis = new Predis(['host' => '/dev/null']); + + $this->expectException(StorageException::class); + $this->expectExceptionMessage('Cannot establish Redis Connection'); + + $predis->wipeStorage(); + } + + /** + * @test + */ + public function itShouldNotClearWholeRedisOnFlush(): void + { + $this->predisConnection->set('not a prometheus metric key', 'data'); + + $predis = Predis::fromExistingConnection($this->predisConnection); + $registry = new CollectorRegistry($predis); + + for ($i = 0; $i < 1000; $i++) { + $registry->getOrRegisterCounter('namespace', "counter_$i", 'counter help')->inc(); + $registry->getOrRegisterGauge('namespace', "gauge_$i", 'gauge help')->inc(); + $registry->getOrRegisterHistogram('namespace', "histogram_$i", 'histogram help')->observe(1); + } + $predis->wipeStorage(); + + $redisKeys = $this->predisConnection->keys('*'); + self::assertThat( + $redisKeys, + self::equalTo(['not a prometheus metric key']) + ); + } +} diff --git a/tests/Test/Prometheus/Storage/RedisTest.php b/tests/Test/Prometheus/Storage/RedisTest.php index 90ca2d7..f8b3aa6 100644 --- a/tests/Test/Prometheus/Storage/RedisTest.php +++ b/tests/Test/Prometheus/Storage/RedisTest.php @@ -74,7 +74,7 @@ public function itShouldNotClearWholeRedisOnFlush(): void self::assertThat( $redisKeys, self::equalTo([ - 'not a prometheus metric key' + 'not a prometheus metric key', ]) ); } From bd428ff40cc5fbbdd9a2d96700f62461c5cc8a9a Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 23:16:52 +0100 Subject: [PATCH 33/40] refactor: create Predis\Client eagerly in constructor That allowed me to simplify this adapter as well as removing some confusing parts of code, where some settings are required but not used Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/Predis.php | 17 +---- .../Storage/RedisClients/PHPRedis.php | 3 + .../Storage/RedisClients/Predis.php | 73 +++++++------------ 3 files changed, 32 insertions(+), 61 deletions(-) diff --git a/src/Prometheus/Storage/Predis.php b/src/Prometheus/Storage/Predis.php index c2d6b5b..fbdc7ff 100644 --- a/src/Prometheus/Storage/Predis.php +++ b/src/Prometheus/Storage/Predis.php @@ -4,7 +4,6 @@ namespace Prometheus\Storage; -use InvalidArgumentException; use Predis\Client; use Prometheus\Storage\RedisClients\Predis as PredisClient; @@ -55,24 +54,10 @@ public function __construct(array $parameters = [], array $options = []) $this->redis = PredisClient::create($this->parameters, $this->options); } - /** - * @throws InvalidArgumentException - */ public static function fromExistingConnection(Client $client): self { - $clientOptions = $client->getOptions(); - $options = [ - 'aggregate' => $clientOptions->aggregate, - 'cluster' => $clientOptions->cluster, - 'connections' => $clientOptions->connections, - 'exceptions' => $clientOptions->exceptions, - 'prefix' => $clientOptions->prefix, - 'commands' => $clientOptions->commands, - 'replication' => $clientOptions->replication, - ]; - $self = new self(); - $self->redis = new PredisClient(self::$defaultParameters, $options, $client); + $self->redis = PredisClient::fromExistingConnection($client); return $self; } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index f77f0b0..940e1b8 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -101,6 +101,9 @@ public function get(string $key): string|false return $this->redis->get($key); } + /** + * @throws RedisClientException + */ public function del(array|string $key, string ...$other_keys): void { try { diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index f5f65cf..99a297d 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -14,40 +14,30 @@ class Predis implements RedisClient RedisClient::OPT_PREFIX => 'prefix', ]; - /** - * @var ?Client - */ - private $client; - - /** - * @var mixed[] - */ - private $parameters = []; - - /** - * @var mixed[] - */ - private $options = []; + private Client $client; - /** - * @param mixed[] $parameters - * @param mixed[] $options - */ - public function __construct(array $parameters, array $options, ?Client $redis = null) + public function __construct(Client $client) { - $this->client = $redis; - - $this->parameters = $parameters; - $this->options = $options; + $this->client = $client; } /** * @param mixed[] $parameters * @param mixed[] $options + * @throws StorageException */ public static function create(array $parameters, array $options): self { - return new self($parameters, $options); + try { + return new self(new Client($parameters, $options)); + } catch (InvalidArgumentException $e) { + throw new StorageException('Invalid Redis client configuration: ' . $e->getMessage(), 0, $e); + } + } + + public static function fromExistingConnection(Client $client): self + { + return new self($client); } public function getOption(string $option): mixed @@ -57,27 +47,21 @@ public function getOption(string $option): mixed } $mappedOption = self::OPTIONS_MAP[$option]; + $value = $this->client->getOptions()->$mappedOption; - return $this->options[$mappedOption] ?? null; - } - - private function getClient(): Client - { - if ($this->client === null) { - throw new StorageException('Redis connection not initialized. Call ensureOpenConnection() first.'); - } - - return $this->client; + return $value instanceof \Predis\Command\Processor\KeyPrefixProcessor + ? $value->getPrefix() + : $value; } public function eval(string $script, array $args = [], int $num_keys = 0): void { - $this->getClient()->eval($script, $num_keys, ...$args); + $this->client->eval($script, $num_keys, ...$args); } public function set(string $key, mixed $value, mixed $options = null): bool { - $result = $this->getClient()->set($key, $value, ...$this->flattenFlags($options)); + $result = $this->client->set($key, $value, ...$this->flattenFlags($options)); return (string) $result === 'OK'; } @@ -103,32 +87,32 @@ private function flattenFlags(array $flags): array public function setNx(string $key, mixed $value): void { - $this->getClient()->setnx($key, $value); + $this->client->setnx($key, $value); } public function sMembers(string $key): array { - return $this->getClient()->smembers($key); + return $this->client->smembers($key); } public function hGetAll(string $key): array|false { - return $this->getClient()->hgetall($key); + return $this->client->hgetall($key); } public function keys(string $pattern): array { - return $this->getClient()->keys($pattern); + return $this->client->keys($pattern); } public function get(string $key): string|false { - return $this->getClient()->get($key) ?? false; + return $this->client->get($key) ?? false; } public function del(array|string $key, string ...$other_keys): void { - $this->getClient()->del($key, ...$other_keys); + $this->client->del($key, ...$other_keys); } /** @@ -136,11 +120,10 @@ public function del(array|string $key, string ...$other_keys): void */ public function ensureOpenConnection(): void { - if ($this->client === null) { + if (!$this->client->isConnected()) { try { - $this->client = new Client($this->parameters, $this->options); $this->client->connect(); - } catch (InvalidArgumentException|\Predis\Connection\ConnectionException $e) { + } catch (\Predis\Connection\ConnectionException $e) { throw new StorageException('Cannot establish Redis Connection:' . $e->getMessage(), 0, $e); } } From 13068a3ad68e39630991a5bef8e1ccf3ea807c18 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 23:27:59 +0100 Subject: [PATCH 34/40] refactor: remove no-op override Signed-off-by: Mateusz Cholewka --- tests/Test/Prometheus/Predis/SummaryTest.php | 5 ----- tests/Test/Prometheus/Redis/SummaryTest.php | 5 ----- 2 files changed, 10 deletions(-) diff --git a/tests/Test/Prometheus/Predis/SummaryTest.php b/tests/Test/Prometheus/Predis/SummaryTest.php index 5d0a9ef..58fb479 100644 --- a/tests/Test/Prometheus/Predis/SummaryTest.php +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -17,9 +17,4 @@ public function configureAdapter(): void $this->adapter = new Predis(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } - - public function itShouldObserveWithLabels(): void - { - parent::itShouldObserveWithLabels(); - } } diff --git a/tests/Test/Prometheus/Redis/SummaryTest.php b/tests/Test/Prometheus/Redis/SummaryTest.php index d6b0c59..1446f65 100644 --- a/tests/Test/Prometheus/Redis/SummaryTest.php +++ b/tests/Test/Prometheus/Redis/SummaryTest.php @@ -18,9 +18,4 @@ public function configureAdapter(): void $this->adapter = new Redis(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } - /** @test */ - public function itShouldObserveWithLabels(): void - { - parent::itShouldObserveWithLabels(); // TODO: Change the autogenerated stub - } } From 19fb93fdc98a51a2cffc9e2ea73feaf176b3a0ba Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 23:37:28 +0100 Subject: [PATCH 35/40] refactor: replase universal getOption() to simple getPrefix() Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 10 ++++++---- src/Prometheus/Storage/RedisClients/PHPRedis.php | 12 ++---------- src/Prometheus/Storage/RedisClients/Predis.php | 15 +++------------ .../Storage/RedisClients/RedisClient.php | 4 +--- 4 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index 5f9dd20..f947308 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -57,8 +57,8 @@ public function wipeStorage(): void $searchPattern = ''; - $globalPrefix = $this->redis->getOption(RedisClient::OPT_PREFIX); - if (is_string($globalPrefix)) { + $globalPrefix = $this->redis->getPrefix(); + if ($globalPrefix !== null) { $searchPattern .= $globalPrefix; } @@ -371,11 +371,13 @@ protected function collectHistograms(): array protected function removePrefixFromKey(string $key): string { - if ($this->redis->getOption(RedisClient::OPT_PREFIX) === null) { + $prefix = $this->redis->getPrefix(); + + if ($prefix === null) { return $key; } - return substr($key, strlen($this->redis->getOption(RedisClient::OPT_PREFIX))); + return substr($key, strlen($prefix)); } /** diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 940e1b8..6d1c8e7 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -8,10 +8,6 @@ class PHPRedis implements RedisClient { - private const OPTIONS_MAP = [ - RedisClient::OPT_PREFIX => \Redis::OPT_PREFIX, - ]; - /** * @var \Redis */ @@ -57,13 +53,9 @@ public static function fromExistingConnection(\Redis $redis, array $options): se return $self; } - public function getOption(string $option): mixed + public function getPrefix(): ?string { - if (!isset(self::OPTIONS_MAP[$option])) { - return null; - } - - return $this->redis->getOption(self::OPTIONS_MAP[$option]); + return $this->redis->getOption(\Redis::OPT_PREFIX) ?: null; } public function eval(string $script, array $args = [], int $num_keys = 0): void diff --git a/src/Prometheus/Storage/RedisClients/Predis.php b/src/Prometheus/Storage/RedisClients/Predis.php index 99a297d..3dfe5a9 100644 --- a/src/Prometheus/Storage/RedisClients/Predis.php +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -10,10 +10,6 @@ class Predis implements RedisClient { - private const OPTIONS_MAP = [ - RedisClient::OPT_PREFIX => 'prefix', - ]; - private Client $client; public function __construct(Client $client) @@ -40,18 +36,13 @@ public static function fromExistingConnection(Client $client): self return new self($client); } - public function getOption(string $option): mixed + public function getPrefix(): ?string { - if (! isset(self::OPTIONS_MAP[$option])) { - return null; - } - - $mappedOption = self::OPTIONS_MAP[$option]; - $value = $this->client->getOptions()->$mappedOption; + $value = $this->client->getOptions()->prefix; return $value instanceof \Predis\Command\Processor\KeyPrefixProcessor ? $value->getPrefix() - : $value; + : null; } public function eval(string $script, array $args = [], int $num_keys = 0): void diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php index 332e19b..ae4cb6a 100644 --- a/src/Prometheus/Storage/RedisClients/RedisClient.php +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -6,9 +6,7 @@ interface RedisClient { - public const OPT_PREFIX = 'prefix'; - - public function getOption(string $option): mixed; + public function getPrefix(): ?string; /** * @param mixed[] $args From f101dd65c35f59fa6bef9c9333b457e5ea23dde0 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 23:44:34 +0100 Subject: [PATCH 36/40] fix: add type to inform phpstan Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/AbstractRedis.php | 4 +++- src/Prometheus/Storage/RedisClients/PHPRedis.php | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Prometheus/Storage/AbstractRedis.php b/src/Prometheus/Storage/AbstractRedis.php index f947308..35bbde7 100644 --- a/src/Prometheus/Storage/AbstractRedis.php +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -22,7 +22,9 @@ abstract class AbstractRedis implements Adapter const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; /** - * Global prefix shared across all adapters. + * Intentionally shared across all subclasses via self::$prefix. + * Prefix is a global concern and mixing adapters, + * with different prefixes in one application is not supported. * * @var string */ diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index 6d1c8e7..f7521b0 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -55,7 +55,10 @@ public static function fromExistingConnection(\Redis $redis, array $options): se public function getPrefix(): ?string { - return $this->redis->getOption(\Redis::OPT_PREFIX) ?: null; + /** @var mixed $prefix */ + $prefix = $this->redis->getOption(\Redis::OPT_PREFIX); + + return is_string($prefix) && $prefix !== '' ? $prefix : null; } public function eval(string $script, array $args = [], int $num_keys = 0): void From a6030860a9066d50e18df5c8f035a94589a8afac Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Sun, 22 Mar 2026 23:59:53 +0100 Subject: [PATCH 37/40] readme: add information about new predis adapter Signed-off-by: Mateusz Cholewka --- README.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f189b08..61b87f9 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ If using Redis, we recommend running a local Redis instance next to your PHP wor ## How does it work? Usually PHP worker processes don't share any state. -You can pick from four adapters. -Redis, APC, APCng, or an in-memory adapter. +You can pick from five adapters. +Redis, Predis, APC, APCng, or an in-memory adapter. While the first needs a separate binary running, the second and third just need the [APC](https://pecl.php.net/package/APCU) extension to be installed. If you don't need persistent metrics between requests (e.g. a long running cron job or script) the in-memory adapter might be suitable to use. ## Installation @@ -24,6 +24,7 @@ composer require promphp/prometheus_client_php ## Usage A simple counter: + ```php \Prometheus\CollectorRegistry::getDefault() ->getOrRegisterCounter('', 'some_quick_counter', 'just a quick measurement') @@ -31,6 +32,7 @@ A simple counter: ``` Write some enhanced metrics: + ```php $registry = \Prometheus\CollectorRegistry::getDefault(); @@ -48,6 +50,7 @@ $summary->observe(5, ['blue']); ``` Manually register and retrieve metrics (these steps are combined in the `getOrRegister...` methods): + ```php $registry = \Prometheus\CollectorRegistry::getDefault(); @@ -60,6 +63,7 @@ $counterB->incBy(2, ['red']); ``` Expose the metrics: + ```php $registry = \Prometheus\CollectorRegistry::getDefault(); @@ -70,7 +74,8 @@ header('Content-type: ' . RenderTextFormat::MIME_TYPE); echo $result; ``` -Change the Redis options (the example shows the defaults): +change the Redis options (the example shows the defaults): + ```php \Prometheus\Storage\Redis::setDefaultOptions( [ @@ -84,7 +89,23 @@ Change the Redis options (the example shows the defaults): ); ``` +Using the Predis storage (requires `predis/predis`): + +```php +$registry = new CollectorRegistry(new \Prometheus\Storage\Predis()); +``` + +Or with an existing connection: + +```php +$client = new \Predis\Client(['host' => '127.0.0.1']); +$registry = new CollectorRegistry(\Prometheus\Storage\Predis::fromExistingConnection($client)); +``` + +> **Note:** `Redis::setPrefix()` and `Predis::setPrefix()` share the same prefix. Using both adapters with different prefixes in the same application is not supported. + Using the InMemory storage: + ```php $registry = new CollectorRegistry(new InMemory()); @@ -96,14 +117,17 @@ $result = $renderer->render($registry->getMetricFamilySamples()); ``` Using the APC or APCng storage: + ```php $registry = new CollectorRegistry(new APCng()); or $registry = new CollectorRegistry(new APC()); ``` + (see the `README.APCng.md` file for more details) Using the PDO storage: + ```php $registry = new CollectorRegistry(new \PDO('mysql:host=localhost;dbname=prometheus', 'username', 'password')); or @@ -113,11 +137,13 @@ $registry = new CollectorRegistry(new \PDO('sqlite::memory:')); ### Advanced Usage #### Advanced Histogram Usage + On passing an empty array for the bucket parameter on instantiation, a set of default buckets will be used instead. Whilst this is a good base for a typical web application, there is named constructor to assist in the generation of exponential / geometric buckets. Eg: + ``` Histogram::exponentialBuckets(0.05, 1.5, 10); ``` @@ -127,7 +153,9 @@ This will start your buckets with a value of 0.05, grow them by a factor of 1.5 Also look at the [examples](examples). #### PushGateway Support -As of Version 2.0.0 this library doesn't support the Prometheus PushGateway anymore because we want to have this package as small als possible. If you need Prometheus PushGateway support, you could use the companion library: https://github.com/PromPHP/prometheus_push_gateway_php + +As of Version 2.0.0 this library doesn't support the Prometheus PushGateway anymore because we want to have this package as small als possible. If you need Prometheus PushGateway support, you could use the companion library: + ``` composer require promphp/prometheus_push_gateway_php ``` @@ -143,11 +171,13 @@ composer require promphp/prometheus_push_gateway_php * Redis Start a Redis instance: + ``` docker-compose up redis ``` Run the tests: + ``` composer install @@ -159,9 +189,11 @@ composer install ## Black box testing Just start the nginx, fpm & Redis setup with docker-compose: + ``` docker-compose up ``` + Pick the adapter you want to test. ``` @@ -173,11 +205,13 @@ docker-compose run phpunit env ADAPTER=redis vendor/bin/phpunit tests/Test/ ## Performance testing This currently tests the APC and APCng adapters head-to-head and reports if the APCng adapter is slower for any actions. + ``` phpunit vendor/bin/phpunit tests/Test/ --group Performance ``` The test can also be run inside a container. + ``` docker-compose up docker-compose run phpunit vendor/bin/phpunit tests/Test/ --group Performance From 339f666c6d84d5ed341e099b83c418a0a9379a22 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Tue, 24 Mar 2026 20:07:39 +0100 Subject: [PATCH 38/40] chore: fix regression in passing previous exception if thrown Signed-off-by: Mateusz Cholewka --- src/Prometheus/Storage/RedisClients/PHPRedis.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php index f7521b0..35c7b01 100644 --- a/src/Prometheus/Storage/RedisClients/PHPRedis.php +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -104,7 +104,7 @@ public function del(array|string $key, string ...$other_keys): void try { $this->redis->del($key, ...$other_keys); } catch (\RedisException $e) { - throw new RedisClientException($e->getMessage()); + throw new RedisClientException($e->getMessage(), $e->getCode(), $e); } } @@ -167,6 +167,7 @@ private function connectToServer(): void throw new StorageException( sprintf("Can't connect to Redis server. %s", $e->getMessage()), $e->getCode(), + $e, ); } } From 75b501772d7e45ccfb0513bca3a2369eb994aa30 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Tue, 24 Mar 2026 20:15:18 +0100 Subject: [PATCH 39/40] docs: capital letter typo Signed-off-by: Mateusz Cholewka --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61b87f9..65a2b15 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ header('Content-type: ' . RenderTextFormat::MIME_TYPE); echo $result; ``` -change the Redis options (the example shows the defaults): +Change the Redis options (the example shows the defaults): ```php \Prometheus\Storage\Redis::setDefaultOptions( From ae19f8413d25829ca1efcea315c9ad5b42b7a2ff Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Tue, 24 Mar 2026 21:07:14 +0100 Subject: [PATCH 40/40] docs: update shared prefix note Signed-off-by: Mateusz Cholewka --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65a2b15..925a284 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ $client = new \Predis\Client(['host' => '127.0.0.1']); $registry = new CollectorRegistry(\Prometheus\Storage\Predis::fromExistingConnection($client)); ``` -> **Note:** `Redis::setPrefix()` and `Predis::setPrefix()` share the same prefix. Using both adapters with different prefixes in the same application is not supported. +> **Note:** Using `Redis::setPrefix()` and `Predis::setPrefix()` share the same prefix. Using both adapters with different prefixes in the same application is not supported. Using the InMemory storage: