From f560f2c742b3ca72b18f31defbab49f6e709188a Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 00:13:43 +0200 Subject: [PATCH 01/14] 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 dec37f8..15efa9e 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 92f4beb0530934515c281d785feb4bb486668196 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 00:14:19 +0200 Subject: [PATCH 02/14] 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/14] 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 fb4ab038775c39c493353d6b91586357f42be1de Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 00:14:44 +0200 Subject: [PATCH 04/14] 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 fb7913e0b0b0ac4280fc344767d670f5d1486bce Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 14:30:39 +0200 Subject: [PATCH 05/14] 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 4f2c22c264559b9ed58cd4bab888204d1397baf2 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 14:31:05 +0200 Subject: [PATCH 06/14] 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 30387342d3b70c6d7ac45b9bdc533d26c1d9c118 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 15:05:18 +0200 Subject: [PATCH 07/14] 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 d3b122251ca88070b3c6c20b4ed12f82e5273085 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 15:11:51 +0200 Subject: [PATCH 08/14] 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 004381bf5a29a6217a2062ded77ce043af8a9310 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 15:30:37 +0200 Subject: [PATCH 09/14] 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 f0dbbb9c805693ec0b9ffefab5307e284873f5c2 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Wed, 30 Apr 2025 16:03:37 +0200 Subject: [PATCH 10/14] 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 a3c7a3e0963cc839ee44d9b68168ceea2ccb4973 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 00:12:40 +0200 Subject: [PATCH 11/14] 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 15efa9e..b0f35bf 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 fb527ba8032ffcb5e670d5174406f83d13c44c5c Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 13:17:38 +0200 Subject: [PATCH 12/14] 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 8d1c694f6e445935b7b5fc621f92859968cf98c4 Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 13:18:19 +0200 Subject: [PATCH 13/14] 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 b984afa034d93bf6c9bd4faaf2d3fc54e9a1f8ad Mon Sep 17 00:00:00 2001 From: Mateusz Cholewka Date: Thu, 1 May 2025 13:24:58 +0200 Subject: [PATCH 14/14] 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()); + } } } }