diff --git a/composer.json b/composer.json index dec37f80..b0f35bf3 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,13 @@ "phpstan/phpstan-phpunit": "^1.1.0", "phpstan/phpstan-strict-rules": "^1.1.0", "phpunit/phpunit": "^9.4", + "predis/predis": "^2.3", "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.", "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 1c00eab7..712e2bec 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') { diff --git a/examples/metrics.php b/examples/metrics.php index 9c0fdb80..844c2b24 100644 --- a/examples/metrics.php +++ b/examples/metrics.php @@ -11,6 +11,8 @@ if ($adapter === 'redis') { Redis::setDefaultOptions(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); $adapter = new Prometheus\Storage\Redis(); +} elseif ($adapter === 'predis') { + $adapter = new Prometheus\Storage\Predis(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } elseif ($adapter === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapter === 'apcng') { diff --git a/examples/some_counter.php b/examples/some_counter.php index c7426ce8..b861a18b 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 9e8b3da2..a0a5894c 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 2f1a5f98..b2d9135b 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 363f9190..34b30ce0 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 new file mode 100644 index 00000000..ce46400d --- /dev/null +++ b/src/Prometheus/Storage/AbstractRedis.php @@ -0,0 +1,626 @@ +wipeStorage(); + } + + /** + * {@inheritDoc} + */ + public function wipeStorage(): void + { + $this->redis->ensureOpenConnection(); + + $searchPattern = ''; + + $globalPrefix = $this->redis->getOption(RedisClient::OPT_PREFIX); + 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); + + // 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); + + // 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 + { + if ($this->redis->getOption(RedisClient::OPT_PREFIX) === null) { + return $key; + } + + 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 00000000..c2d6b5b4 --- /dev/null +++ b/src/Prometheus/Storage/Predis.php @@ -0,0 +1,95 @@ + '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 = []; + + /** + * Predis constructor. + * + * @param mixed[] $parameters + * @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 InvalidArgumentException + */ + public static function fromExistingConnection(Client $client): self + { + $clientOptions = $client->getOptions(); + $options = [ + 'aggregate' => $clientOptions->aggregate, + 'cluster' => $clientOptions->cluster, + 'connections' => $clientOptions->connections, + 'exceptions' => $clientOptions->exceptions, + 'prefix' => $clientOptions->prefix, + 'commands' => $clientOptions->commands, + 'replication' => $clientOptions->replication, + ]; + + $self = new self(); + $self->redis = new PredisClient(self::$defaultParameters, $options, $client); + + 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 730fac87..78b0e375 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -4,21 +4,11 @@ namespace Prometheus\Storage; -use InvalidArgumentException; -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\Summary; -use RuntimeException; +use Prometheus\Storage\RedisClients\PHPRedis; -class Redis implements Adapter +class Redis extends AbstractRedis { - const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; - /** * @var mixed[] */ @@ -32,39 +22,23 @@ class Redis implements Adapter 'user' => null, ]; - /** - * @var string - */ - private static $prefix = 'PROMETHEUS_'; - /** * @var mixed[] */ private $options = []; - /** - * @var \Redis - */ - 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 @@ -74,682 +48,16 @@ public static function fromExistingConnection(\Redis $redis): self } $self = new self(); - $self->connectionInitialized = true; - $self->redis = $redis; + $self->redis = PHPRedis::fromExistingConnection($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; - } - - /** - * @throws StorageException - * @deprecated use replacement method wipeStorage from Adapter interface - */ - public function flushRedis(): void - { - $this->wipeStorage(); - } - - /** - * @inheritDoc - */ - public function wipeStorage(): void - { - $this->ensureOpenConnection(); - - $searchPattern = ""; - - $globalPrefix = $this->redis->getOption(\Redis::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( - <<encodeLabelValues($data['labelValues']), - 'value' - ]); - } - - /** - * @return MetricFamilySamples[] - * @throws StorageException - */ - public function collect(bool $sortMetrics = true): array - { - $this->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 - ); - } - - /** - * @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 - * @throws StorageException - */ - public function updateHistogram(array $data): void - { - $this->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( - <<= 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->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 (false === $json) { - 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 (false === $json) { - 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->ensureOpenConnection(); - $metaData = $data; - unset($metaData['value'], $metaData['labelValues'], $metaData['command']); - $this->redis->eval( - <<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->ensureOpenConnection(); - $metaData = $data; - unset($metaData['value'], $metaData['labelValues'], $metaData['command']); - $this->redis->eval( - <<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->_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; - } - - /** - * @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) { - return $key; - } - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int - return substr($key, strlen($this->redis->getOption(\Redis::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 (\RedisException $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 (\RedisException $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->_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 - */ - 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->_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; - } - - /** - * @param int $cmd - * @return string - */ - 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 - * @return string - */ - private function toMetricKey(array $data): string - { - return implode(':', [self::$prefix, $data['type'], $data['name']]); - } - - /** - * @param mixed[] $values - * @return string - * @throws RuntimeException - */ - private function encodeLabelValues(array $values): string - { - $json = json_encode($values); - if (false === $json) { - 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) { - throw new RuntimeException('Cannot base64 decode label values'); - } - $decodedValues = json_decode($json, true); - if (false === $decodedValues) { - 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; - throw new MetricJsonException($message, 0, null, $metricName); - } } diff --git a/src/Prometheus/Storage/RedisClients/PHPRedis.php b/src/Prometheus/Storage/RedisClients/PHPRedis.php new file mode 100644 index 00000000..17f19129 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/PHPRedis.php @@ -0,0 +1,172 @@ +redis = $redis; + $this->options = $options; + } + + /** + * @param mixed[] $options + */ + public static function create(array $options): self + { + $redis = new \Redis(); + + 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); + } + + 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): bool + { + return $this->redis->set($key, $value, $options); + } + + public function setNx(string $key, mixed $value): void + { + $this->redis->setNx($key, $value); /** @phpstan-ignore-line */ + } + + public function hSetNx(string $key, string $field, mixed $value): bool + { + return $this->redis->hSetNx($key, $field, $value); + } + + public function sMembers(string $key): array + { + return $this->redis->sMembers($key); + } + + 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): void + { + try { + $this->redis->del($key, ...$other_keys); + } catch (\RedisException $e) { + throw new RedisClientException($e->getMessage()); + } + } + + /** + * @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 00000000..1146a067 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/Predis.php @@ -0,0 +1,143 @@ + 'prefix', + ]; + + /** + * @var ?Client + */ + private $client; + + /** + * @var mixed[] + */ + private $parameters = []; + + /** + * @var mixed[] + */ + private $options = []; + + /** + * @param mixed[] $parameters + * @param mixed[] $options + */ + public function __construct(array $parameters, array $options, ?Client $redis = null) + { + $this->client = $redis; + + $this->parameters = $parameters; + $this->options = $options; + } + + /** + * @param mixed[] $parameters + * @param mixed[] $options + */ + public static function create(array $parameters, array $options): self + { + return new self($parameters, $options); + } + + public function getOption(int $option): mixed + { + if (! isset(self::OPTIONS_MAP[$option])) { + return null; + } + + $mappedOption = self::OPTIONS_MAP[$option]; + + return $this->options[$mappedOption] ?? null; + } + + 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): bool + { + $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 = []; + 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): void + { + $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 + { + 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): void + { + $this->client->del($key, ...$other_keys); + } + + /** + * @throws StorageException + */ + public function ensureOpenConnection(): void + { + if ($this->client === null) { + try { + $this->client = new Client($this->parameters, $this->options); + } catch (InvalidArgumentException $e) { + throw new StorageException('Cannot establish Redis Connection:' . $e->getMessage()); + } + } + } +} diff --git a/src/Prometheus/Storage/RedisClients/RedisClient.php b/src/Prometheus/Storage/RedisClients/RedisClient.php new file mode 100644 index 00000000..e9149e41 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/RedisClient.php @@ -0,0 +1,49 @@ +|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; +} diff --git a/src/Prometheus/Storage/RedisClients/RedisClientException.php b/src/Prometheus/Storage/RedisClients/RedisClientException.php new file mode 100644 index 00000000..1a811285 --- /dev/null +++ b/src/Prometheus/Storage/RedisClients/RedisClientException.php @@ -0,0 +1,9 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/CounterTest.php b/tests/Test/Prometheus/Predis/CounterTest.php new file mode 100644 index 00000000..f8508dcf --- /dev/null +++ b/tests/Test/Prometheus/Predis/CounterTest.php @@ -0,0 +1,20 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php new file mode 100644 index 00000000..25e2d01a --- /dev/null +++ b/tests/Test/Prometheus/Predis/CounterWithPrefixTest.php @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000..e84e8142 --- /dev/null +++ b/tests/Test/Prometheus/Predis/GaugeTest.php @@ -0,0 +1,20 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php new file mode 100644 index 00000000..d5a895ec --- /dev/null +++ b/tests/Test/Prometheus/Predis/GaugeWithPrefixTest.php @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000..381ed196 --- /dev/null +++ b/tests/Test/Prometheus/Predis/HistogramTest.php @@ -0,0 +1,20 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php new file mode 100644 index 00000000..d4029a0c --- /dev/null +++ b/tests/Test/Prometheus/Predis/HistogramWithPrefixTest.php @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000..3d5a9810 --- /dev/null +++ b/tests/Test/Prometheus/Predis/SummaryTest.php @@ -0,0 +1,26 @@ +adapter = new Predis(['host' => REDIS_HOST]); + $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 00000000..54ffd31f --- /dev/null +++ b/tests/Test/Prometheus/Predis/SummaryWithPrefixTest.php @@ -0,0 +1,20 @@ +adapter = new Predis(['host' => REDIS_HOST], ['prefix' => 'prefix:']); + $this->adapter->wipeStorage(); + } +}