Skip to content

Commit

Permalink
Added support of Predis library as storage adapter (#127)
Browse files Browse the repository at this point in the history
Signed-off-by: Łukasz Adamczewski <tworzenieweb@gmail.com>
  • Loading branch information
tworzenieweb committed Sep 13, 2023
1 parent 82e42b5 commit 257bf8b
Show file tree
Hide file tree
Showing 13 changed files with 420 additions and 43 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
"phpstan/phpstan-phpunit": "^1.1.0",
"phpstan/phpstan-strict-rules": "^1.1.0",
"phpunit/phpunit": "^9.4",
"predis/predis": "^2.0",
"squizlabs/php_codesniffer": "^3.6",
"symfony/polyfill-apcu": "^1.6"
},
"suggest": {
"ext-redis": "Required if using Redis.",
"predis/predis": "Required if using Predis.",
"ext-apc": "Required if using APCu.",
"promphp/prometheus_push_gateway_php": "An easy client for using Prometheus PushGateway.",
"symfony/polyfill-apcu": "Required if you use APCu on PHP8.0+"
Expand Down
2 changes: 2 additions & 0 deletions examples/metrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
111 changes: 111 additions & 0 deletions src/Prometheus/Storage/Predis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace Prometheus\Storage;

use Predis\Configuration\Option\Prefix;
use Prometheus\Exception\StorageException;
use Predis\Client;

/**
* @property Client $redis
*/
final class Predis extends Redis
{
/**
* @var mixed[]
*/
private static array $defaultOptions = [
'host' => '127.0.0.1',
'port' => 6379,
'scheme' => 'tcp',
'timeout' => 0.1,
'read_timeout' => '10',
'persistent' => 0,
'password' => null,
];

public function __construct(array $options = [])
{
$this->options = array_merge(self::$defaultOptions, $options);

parent::__construct($options);

$this->redis = new Client($this->options);
}

public static function fromClient(Client $redis): self
{
if ($redis->isConnected() === false) {
throw new StorageException('Connection to Redis server not established');
}

$self = new self();
$self->redis = $redis;

return $self;
}

protected function ensureOpenConnection(): void
{
if ($this->redis->isConnected() === false) {
$this->redis->connect();
}
}

public static function fromExistingConnection(\Redis $redis): Redis
{
throw new \RuntimeException('This method is not supported by predis adapter');
}

protected function getGlobalPrefix(): ?string
{
if ($this->redis->getOptions()->prefix === null) {
return null;
}

if ($this->redis->getOptions()->prefix instanceof Prefix) {
return $this->redis->getOptions()->prefix->getPrefix();

Check failure on line 69 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.1

Call to an undefined method Predis\Command\Processor\ProcessorInterface&Predis\Configuration\Option\Prefix::getPrefix().

Check failure on line 69 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 7.4

Call to an undefined method Predis\Command\Processor\ProcessorInterface&Predis\Configuration\Option\Prefix::getPrefix().

Check failure on line 69 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.0

Call to an undefined method Predis\Command\Processor\ProcessorInterface&Predis\Configuration\Option\Prefix::getPrefix().

Check failure on line 69 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.2

Call to an undefined method Predis\Command\Processor\ProcessorInterface&Predis\Configuration\Option\Prefix::getPrefix().
}

return null;
}

/**
* @param mixed[] $args
* @param int $keysCount
* @return mixed[]
*/
protected function evalParams(array $args, int $keysCount): array
{
return [$keysCount, ...$args];
}


protected function prefix(string $key): string
{
// the predis is doing key prefixing on its own
return '';
}

protected function setParams(array $input): array

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.1

Method Prometheus\Storage\Predis::setParams() has parameter $input with no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.1

Method Prometheus\Storage\Predis::setParams() return type has no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 7.4

Method Prometheus\Storage\Predis::setParams() has parameter $input with no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 7.4

Method Prometheus\Storage\Predis::setParams() return type has no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.0

Method Prometheus\Storage\Predis::setParams() has parameter $input with no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.0

Method Prometheus\Storage\Predis::setParams() return type has no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.2

Method Prometheus\Storage\Predis::setParams() has parameter $input with no value type specified in iterable type array.

Check failure on line 92 in src/Prometheus/Storage/Predis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.2

Method Prometheus\Storage\Predis::setParams() return type has no value type specified in iterable type array.
{
$values = array_values($input);
$params = [];

if (isset($input['EX'])) {
$params[] = 'EX';
$params[] = $input['EX'];
}

if (isset($input['PX'])) {
$params[] = 'PX';
$params[] = $input['PX'];
}

$params[] = $values[0];

return $params;
}
}
119 changes: 76 additions & 43 deletions src/Prometheus/Storage/Redis.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Prometheus\Storage;

use InvalidArgumentException;
use Predis\Client;
use Prometheus\Counter;
use Prometheus\Exception\StorageException;
use Prometheus\Gauge;
Expand Down Expand Up @@ -38,12 +39,12 @@ class Redis implements Adapter
/**
* @var mixed[]
*/
private $options = [];
protected $options = [];

/**
* @var \Redis
* @var \Redis|Client
*/
private $redis;
protected $redis;

/**
* @var boolean
Expand Down Expand Up @@ -112,8 +113,7 @@ public function wipeStorage(): void

$searchPattern = "";

$globalPrefix = $this->redis->getOption(\Redis::OPT_PREFIX);
// @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int
$globalPrefix = $this->getGlobalPrefix();
if (is_string($globalPrefix)) {
$searchPattern .= $globalPrefix;
}
Expand All @@ -133,8 +133,10 @@ public function wipeStorage(): void
until cursor == "0"
LUA
,
[$searchPattern],
0
...$this->evalParams(
[$searchPattern],
0
)
);
}

Expand Down Expand Up @@ -187,7 +189,7 @@ function (array $metric): MetricFamilySamples {
/**
* @throws StorageException
*/
private function ensureOpenConnection(): void
protected function ensureOpenConnection(): void
{
if ($this->connectionInitialized === true) {
return;
Expand Down Expand Up @@ -260,15 +262,17 @@ public function updateHistogram(array $data): void
return result
LUA
,
[
$this->toMetricKey($data),
self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]),
json_encode(['b' => $bucketToIncrease, 'labelValues' => $data['labelValues']]),
$data['value'],
json_encode($metaData),
],
2
...$this->evalParams(
[
$this->toMetricKey($data),
self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]),
json_encode(['b' => $bucketToIncrease, 'labelValues' => $data['labelValues']]),
$data['value'],
json_encode($metaData),
],
2
)
);
}

Expand Down Expand Up @@ -301,7 +305,7 @@ public function updateSummary(array $data): void
$done = false;
while (!$done) {

Check failure on line 306 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.1

Only booleans are allowed in a negated boolean, bool|Predis\Response\Status given.

Check failure on line 306 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 7.4

Only booleans are allowed in a negated boolean, bool|Predis\Response\Status given.

Check failure on line 306 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.0

Only booleans are allowed in a negated boolean, bool|Predis\Response\Status given.

Check failure on line 306 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.2

Only booleans are allowed in a negated boolean, bool|Predis\Response\Status given.
$sampleKey = $valueKey . ':' . uniqid('', true);
$done = $this->redis->set($sampleKey, $data['value'], ['NX', 'EX' => $data['maxAgeSeconds']]);
$done = $this->redis->set($sampleKey, $data['value'], ...$this->setParams(['NX', 'EX' => $data['maxAgeSeconds']]));
}
}

Expand Down Expand Up @@ -331,15 +335,17 @@ public function updateGauge(array $data): void
end
LUA
,
[
$this->toMetricKey($data),
self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
$this->getRedisCommand($data['command']),
json_encode($data['labelValues']),
$data['value'],
json_encode($metaData),
],
2
...$this->evalParams(
[
$this->toMetricKey($data),
self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
$this->getRedisCommand($data['command']),
json_encode($data['labelValues']),
$data['value'],
json_encode($metaData),
],
2
)
);
}

Expand All @@ -362,15 +368,17 @@ public function updateCounter(array $data): void
return result
LUA
,
[
$this->toMetricKey($data),
self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
$this->getRedisCommand($data['command']),
$data['value'],
json_encode($data['labelValues']),
json_encode($metaData),
],
2
...$this->evalParams(
[
$this->toMetricKey($data),
self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
$this->getRedisCommand($data['command']),
$data['value'],
json_encode($data['labelValues']),
json_encode($metaData),
],
2
)
);
}

Expand All @@ -395,7 +403,7 @@ private function collectHistograms(): array
sort($keys);
$histograms = [];
foreach ($keys as $key) {
$raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key));
$raw = $this->redis->hGetAll(str_replace($this->prefix(''), '', $key));

Check failure on line 406 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.1

Call to an undefined method Predis\Client|Redis::hGetAll().

Check failure on line 406 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 7.4

Call to an undefined method Predis\Client|Redis::hGetAll().

Check failure on line 406 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.0

Call to an undefined method Predis\Client|Redis::hGetAll().

Check failure on line 406 in src/Prometheus/Storage/Redis.php

View workflow job for this annotation

GitHub Actions / PHPStan on PHP 8.2

Call to an undefined method Predis\Client|Redis::hGetAll().
if (!isset($raw['__meta'])) {
continue;
}
Expand Down Expand Up @@ -473,12 +481,11 @@ private function collectHistograms(): array
*/
private function removePrefixFromKey(string $key): string
{
// @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int
if ($this->redis->getOption(\Redis::OPT_PREFIX) === null) {
if ($this->getGlobalPrefix() === null) {
return $key;
}
// @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int
return substr($key, strlen($this->redis->getOption(\Redis::OPT_PREFIX)));

return substr($key, strlen($this->getGlobalPrefix()));
}

/**
Expand Down Expand Up @@ -578,7 +585,7 @@ private function collectGauges(bool $sortMetrics = true): array
sort($keys);
$gauges = [];
foreach ($keys as $key) {
$raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key));
$raw = $this->redis->hGetAll(str_replace($this->prefix(''), '', $key));
if (!isset($raw['__meta'])) {
continue;
}
Expand Down Expand Up @@ -614,7 +621,7 @@ private function collectCounters(bool $sortMetrics = true): array
sort($keys);
$counters = [];
foreach ($keys as $key) {
$raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key));
$raw = $this->redis->hGetAll(str_replace($this->prefix(''), '', $key));
if (!isset($raw['__meta'])) {
continue;
}
Expand Down Expand Up @@ -699,4 +706,30 @@ private function decodeLabelValues(string $values): array
}
return $decodedValues;
}

protected function getGlobalPrefix(): ?string
{
// @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int
return $this->redis->getOption(\Redis::OPT_PREFIX);
}

/**
* @param mixed[] $args
* @param int $keysCount
* @return mixed[]
*/
protected function evalParams(array $args, int $keysCount): array
{
return [$args, $keysCount];
}

protected function prefix(string $key): string
{
return $this->redis->_prefix($key);
}

protected function setParams(array $params): array
{
return [$params];
}
}
21 changes: 21 additions & 0 deletions tests/Test/Prometheus/Predis/CollectorRegistryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Test\Prometheus\Predis;

use Predis\Client;
use Prometheus\Storage\Predis;
use Test\Prometheus\AbstractCollectorRegistryTest;

/**
* @requires predis
*/
class CollectorRegistryTest extends AbstractCollectorRegistryTest
{
public function configureAdapter(): void
{
$this->adapter = new Predis(['host' => REDIS_HOST]);
$this->adapter->wipeStorage();
}
}
22 changes: 22 additions & 0 deletions tests/Test/Prometheus/Predis/CounterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Test\Prometheus\Predis;

use Predis\Client;
use Prometheus\Storage\Predis;
use Test\Prometheus\AbstractCounterTest;

/**
* See https://prometheus.io/docs/instrumenting/exposition_formats/
* @requires extension redis
*/
class CounterTest extends AbstractCounterTest
{
public function configureAdapter(): void
{
$this->adapter = new Predis(['host' => REDIS_HOST]);
$this->adapter->wipeStorage();
}
}
Loading

0 comments on commit 257bf8b

Please sign in to comment.