From d2dae04117c953dc03d0e66df6413621cd9dbb84 Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Mon, 16 Jan 2023 16:23:33 -0500 Subject: [PATCH 01/14] Vendor Composer dependencies in php-fpm Docker image. Signed-off-by: Arris Ray Signed-off-by: Arris Ray --- php-fpm/Dockerfile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/php-fpm/Dockerfile b/php-fpm/Dockerfile index f0d50190..7a65252b 100644 --- a/php-fpm/Dockerfile +++ b/php-fpm/Dockerfile @@ -1,7 +1,14 @@ FROM php:8.1-fpm -RUN pecl install redis && docker-php-ext-enable redis -RUN pecl install apcu && docker-php-ext-enable apcu +RUN apt-get -y install git libzip-dev zip unzip php-zip +RUN pecl install redis && docker-php-ext-enable redis \ + && pecl install apcu && docker-php-ext-enable apcu \ + && pecl install zip && docker-php-ext-enable zip +RUN cd /var/www/html \ + && curl -sS https://getcomposer.org/installer -o composer-setup.php \ + && php composer-setup.php \ + && rm composer-setup.php \ + && composer.phar install COPY www.conf /usr/local/etc/php-fpm.d/ COPY docker-php-ext-apcu-cli.ini /usr/local/etc/php/conf.d/ From 313342dedec1decd2968ce396f59ff6dd308552d Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Mon, 16 Jan 2023 17:38:21 -0500 Subject: [PATCH 02/14] Add RedisTxn storage adapter. Signed-off-by: Arris Ray Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 742 ++++++++++++++++++ src/Prometheus/Storage/RedisTxn/Metadata.php | 173 ++++ .../Storage/RedisTxn/MetadataBuilder.php | 133 ++++ src/Prometheus/Storage/RedisTxn/Metric.php | 65 ++ .../Storage/RedisTxn/MetricBuilder.php | 140 ++++ src/Prometheus/Storage/RedisTxn/Sample.php | 80 ++ .../Storage/RedisTxn/SampleBuilder.php | 100 +++ 7 files changed, 1433 insertions(+) create mode 100644 src/Prometheus/Storage/RedisTxn.php create mode 100644 src/Prometheus/Storage/RedisTxn/Metadata.php create mode 100644 src/Prometheus/Storage/RedisTxn/MetadataBuilder.php create mode 100644 src/Prometheus/Storage/RedisTxn/Metric.php create mode 100644 src/Prometheus/Storage/RedisTxn/MetricBuilder.php create mode 100644 src/Prometheus/Storage/RedisTxn/Sample.php create mode 100644 src/Prometheus/Storage/RedisTxn/SampleBuilder.php diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php new file mode 100644 index 00000000..cb4e11e5 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn.php @@ -0,0 +1,742 @@ + '127.0.0.1', + 'port' => 6379, + 'timeout' => 0.1, + 'read_timeout' => '10', + 'persistent_connections' => false, + 'password' => 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 + */ + public function __construct(array $options = []) + { + $this->options = array_merge(self::$defaultOptions, $options); + $this->redis = new \Redis(); + } + + /** + * @param \Redis $redis + * @return self + * @throws StorageException + */ + public static function fromExistingConnection(\Redis $redis): self + { + if ($redis->isConnected() === false) { + throw new StorageException('Connection to Redis server not established'); + } + + $self = new self(); + $self->connectionInitialized = true; + $self->redis = $redis; + + return $self; + } + + /** + * @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(): array + { + $this->ensureOpenConnection(); + $metrics = $this->collectHistograms(); + $metrics = array_merge($metrics, $this->collectGauges()); + $metrics = array_merge($metrics, $this->collectCounters()); + $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(); + + if ($this->options['password'] !== null) { + $this->redis->auth($this->options['password']); + } + + 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("Can't connect to Redis server", 0); + } + } catch (\RedisException $e) { + throw new StorageException("Can't connect to Redis server", 0, $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 array $data + * @return void + * @throws StorageException + * @throws RedisException + */ + public function updateSummary(array $data): void + { + $this->ensureOpenConnection(); + + // Prepare summary metadata + $metaHashKey = self::$prefix . self::PROMETHEUS_METRIC_META_SUFFIX; + $summaryMetadata = $this->toMetadata($data); + $ttl = $summaryMetadata->getMaxAgeSeconds(); + + // Create summary key + $keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $summaryKey = implode(':', [$keyPrefix, $data['name'], $summaryMetadata->getLabelValuesEncoded()]); + $summaryRegistryKey = implode(':', [$keyPrefix, 'keys']); + + // Get summary sample + // + // NOTE: When we persist a summary metric sample into Redis, we write it into a Redis sorted set. + // We append the current time in microseconds as a suffix on the observed value to make each observed value + // durable and unique in the sorted set in accordance with best-practice guidelines described in the article, + // "Redis Best Practices: Sorted Set Time Series" [1]. + // + // See MetricBuilder::processSamples() for the complementary part of this operation. + // + // [1] https://redis.com/redis-best-practices/time-series/sorted-set-time-series/ + $value = implode(':', [$data['value'], microtime(true)]); + $currentTime = time(); + + // Commit the observed metric value + $this->redis->eval(<<toJson(), + $value, + $currentTime, + $ttl, + ], + 3 + ); + } + + /** + * @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 Metadata + */ + private function toMetadata(array $data): Metadata + { + return Metadata::newBuilder() + ->withName($data['name']) + ->withHelp($data['help']) + ->withLabelNames($data['labelNames']) + ->withLabelValues($data['labelValues']) + ->withQuantiles($data['quantiles']) + ->withMaxAgeSeconds($data['maxAgeSeconds']) + ->build(); + } + + /** + * @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(str_replace($this->redis->_prefix(''), '', $key)); + $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']; + } + + // 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 array + */ + private function collectSummaries(): array + { + // Register summary key + $keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + $summaryRegistryKey = implode(':', [$keyPrefix, 'keys']); + $metaHashKey = self::$prefix . self::PROMETHEUS_METRIC_META_SUFFIX; + $currentTime = time(); + + $result = $this->redis->eval(<< 0 and summaryTtl < currentTime then + local startScore = currentTime - summaryTtl + redis.call("zremrangebyscore", summaryKey, "-inf", startScore) + end + + -- Retrieve the set of remaining metric samples + local numSamples = redis.call('zcard', summaryKey) + local summaryMetadata = {} + local summarySamples = {} + if numSamples > 0 then + -- Configure results + summaryMetadata = redis.call("hget", metaHashKey, summaryKey) + summarySamples = redis.call("zrange", summaryKey, startScore, "+inf", "byscore") + else + -- Remove the metric's associated metadata if there are no associated samples remaining + redis.call('srem', summaryRegistryKey, summaryKey) + redis.call('hdel', metaHashKey, summaryKey) + redis.call('hdel', metaHashKey, ttlFieldName) + end + + -- Add the processed metric to the set of results + result[summaryKey] = {} + result[summaryKey]["metadata"] = summaryMetadata + result[summaryKey]["samples"] = summarySamples +end + +-- Return the set of summary metrics +return cjson.encode(result) +LUA + , + [ + $summaryRegistryKey, + $metaHashKey, + $currentTime, + ], + 2 + ); + + // Format summary metrics and hand them off to the calling collector + $summaries = []; + $redisSummaries = json_decode($result, true); + foreach ($redisSummaries as $summary) { + $serializedSummary = Metric::newBuilder() + ->withMetadata($summary['metadata']) + ->withSamples($summary['samples']) + ->build() + ->toArray(); + $summaries[] = $serializedSummary; + } + return $summaries; + } + + /** + * @return mixed[] + */ + private function collectGauges(): 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(str_replace($this->redis->_prefix(''), '', $key)); + $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, + ]; + } + usort($gauge['samples'], function ($a, $b): int { + return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); + }); + $gauges[] = $gauge; + } + return $gauges; + } + + /** + * @return mixed[] + */ + private function collectCounters(): 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(str_replace($this->redis->_prefix(''), '', $key)); + $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, + ]; + } + 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; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Metadata.php b/src/Prometheus/Storage/RedisTxn/Metadata.php new file mode 100644 index 00000000..0c2e3554 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Metadata.php @@ -0,0 +1,173 @@ +name = $name; + $this->help = $help; + $this->labelNames = $labelNames; + $this->labelValues = $labelValues; + $this->maxAgeSeconds = $maxAgeSeconds; + $this->quantiles = $quantiles; + } + + /** + * Prometheus metric name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Prometheus metric help description. + * + * @internal Optional. + * @return string + */ + public function getHelp(): string + { + return $this->help; + } + + /** + * Prometheus metric label names. + * + * Note that each label introduces a degree of cardinality for a given metric. + * + * @internal Optional. It is permissible to have no label names. + * @return string[] + */ + public function getLabelNames(): array + { + return $this->labelNames; + } + + /** + * Prometheus metric label values. + * + * Note that each label value should correspond to a label name. + * + * @internal Optional. + * @return mixed[] + */ + public function getLabelValues(): array + { + return $this->labelValues; + } + + /** + * Prometheus metric label values encoded for storage in Redis. + * + * This property is used internally by the storage adapter and is not served to a Prometheus scraper. Instead, + * the scraper receives the result from the {@see Metadata::getLabelValues()} accessor. + * + * @return string + */ + public function getLabelValuesEncoded(): string + { + return base64_encode(json_encode($this->labelValues)); + } + + /** + * Prometheus metric time-to-live (TTL) in seconds. + * + * This property is used internally by the storage adapter to enforce a TTL for metrics stored in Redis. + * + * @return int + */ + public function getMaxAgeSeconds(): int + { + return $this->maxAgeSeconds; + } + + /** + * Prometheus metric metadata that describes the set of quantiles to report for a summary-type metric. + * + * @return array + */ + public function getQuantiles(): array + { + return $this->quantiles; + } + + /** + * Represents this data structure as a JSON object. + * + * @return string + */ + public function toJson(): string + { + return json_encode([ + 'name' => $this->getName(), + 'help' => $this->getHelp(), + 'labelNames' => $this->getLabelNames(), + 'labelValues' => $this->getLabelValuesEncoded(), + 'maxAgeSeconds' => $this->getMaxAgeSeconds(), + 'quantiles' => $this->getQuantiles(), + ]); + } +} diff --git a/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php b/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php new file mode 100644 index 00000000..6f507290 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php @@ -0,0 +1,133 @@ +name = $name; + return $this; + } + + /** + * @param string|null $help + * @return MetadataBuilder + */ + public function withHelp(?string $help): MetadataBuilder + { + $this->help = $help; + return $this; + } + + /** + * @param string[]|null $labelNames + * @return MetadataBuilder + */ + public function withLabelNames(?array $labelNames): MetadataBuilder + { + $this->labelNames = $labelNames; + return $this; + } + + /** + * @param string|array|null $labelValues + * @return MetadataBuilder + */ + public function withLabelValues($labelValues): MetadataBuilder + { + if (is_array($labelValues)) { + $this->labelValues = $labelValues; + } else { + // See Metadata::getLabelNamesEncoded() for the inverse operation on this data. + $this->labelValues = json_decode(base64_decode($labelValues)); + } + return $this; + } + + /** + * @param int|null $maxAgeSeconds + * @return MetadataBuilder + */ + public function withMaxAgeSeconds(?int $maxAgeSeconds): MetadataBuilder + { + $this->maxAgeSeconds = $maxAgeSeconds; + return $this; + } + + /** + * @param float[]|null $quantiles + * @return MetadataBuilder + */ + public function withQuantiles(?array $quantiles): MetadataBuilder + { + $this->quantiles = $quantiles; + return $this; + } + + /** + * @return Metadata + */ + public function build(): Metadata + { + $this->validate(); + return new Metadata( + $this->name, + $this->help ?? '', + $this->labelNames ?? [], + $this->labelValues ?? [], + $this->maxAgeSeconds ?? 0, + $this->quantiles ?? [] + ); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->name === null) { + throw new InvalidArgumentException('Metadata name field is required'); + } + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Metric.php b/src/Prometheus/Storage/RedisTxn/Metric.php new file mode 100644 index 00000000..ae008dce --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Metric.php @@ -0,0 +1,65 @@ +metadata = $metadata; + $this->samples = $samples; + } + + /** + * Represents this data structure as a PHP associative array. + * + * This array generally conforms to the expectations of the {@see \Prometheus\MetricFamilySamples} structure. + * + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->metadata->getName(), + 'help' => $this->metadata->getHelp(), + 'type' => PrometheusSummary::TYPE, + 'labelNames' => $this->metadata->getLabelNames(), + 'maxAgeSeconds' => $this->metadata->getMaxAgeSeconds(), + 'quantiles' => $this->metadata->getQuantiles(), + 'samples' => $this->samples, + ]; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/MetricBuilder.php b/src/Prometheus/Storage/RedisTxn/MetricBuilder.php new file mode 100644 index 00000000..ce6b0fe2 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/MetricBuilder.php @@ -0,0 +1,140 @@ +metadata = Metadata::newBuilder() + ->withName($metadata['name']) + ->withHelp($metadata['help'] ?? null) + ->withLabelNames($metadata['labelNames'] ?? null) + ->withLabelValues($metadata['labelValues'] ?? null) + ->withMaxAgeSeconds($metadata['maxAgeSeconds'] ?? null) + ->withQuantiles($metadata['quantiles'] ?? null) + ->build(); + return $this; + } + + /** + * @param array $samples + * @return MetricBuilder + */ + public function withSamples(array $samples): MetricBuilder + { + $this->samples = $this->processSamples($samples); + return $this; + } + + /** + * @return Metric + */ + public function build(): Metric + { + $this->validate(); + return new Metric($this->metadata, $this->samples); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->metadata === null) { + throw new InvalidArgumentException('Summary metadata field is required.'); + } + + if ($this->samples === null) { + throw new InvalidArgumentException('Summary samples field is required.'); + } + } + + /** + * Calculates the configured quantiles, count, and sum for a summary metric given a set of observed values. + * + * @param array $sourceSamples + * @return array + */ + private function processSamples(array $sourceSamples): array + { + // Return value + $samples = []; + + // Coerce sample values to numeric type and strip off their unique suffixes + // + // NOTE: When we persist a summary metric sample into Redis, we write it into a Redis sorted set. + // We append the current time in microseconds as a suffix on the observed value to make each observed value + // durable and unique in the sorted set in accordance with best-practice guidelines described in the article, + // "Redis Best Practices: Sorted Set Time Series" [1]. + // + // See RedixTxn::updateSummary() for the complementary part of this operation. + // + // [1] https://redis.com/redis-best-practices/time-series/sorted-set-time-series/ + $typedSamples = array_map(function ($sample) { + $tokens = explode(':', $sample); + $sample = $tokens[0]; + return doubleval($sample); + }, $sourceSamples); + + // Sort samples to calculate quantiles + sort($typedSamples); + + // Calculate quantiles + $math = new Math(); + foreach ($this->metadata->getQuantiles() as $quantile) { + $value = $math->quantile($typedSamples, $quantile); + $labelValues = array_merge($this->metadata->getLabelValues(), [$quantile]); + $samples[] = Sample::newBuilder() + ->withName($this->metadata->getName()) + ->withLabelNames(['quantile']) + ->withLabelValues($labelValues) + ->withValue($value) + ->build() + ->toArray(); + } + + // Calculate count + $samples[] = Sample::newBuilder() + ->withName($this->metadata->getName() . '_count') + ->withLabelNames([]) + ->withLabelValues($this->metadata->getLabelValues()) + ->withValue(count($typedSamples)) + ->build() + ->toArray(); + + // Calculate sum + $samples[] = Sample::newBuilder() + ->withName($this->metadata->getName() . '_sum') + ->withLabelNames([]) + ->withLabelValues($this->metadata->getLabelValues()) + ->withValue(array_sum($typedSamples)) + ->build() + ->toArray(); + + // Return processed samples + return $samples; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Sample.php b/src/Prometheus/Storage/RedisTxn/Sample.php new file mode 100644 index 00000000..51b18d54 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Sample.php @@ -0,0 +1,80 @@ +name = $name; + $this->labelNames = $labelNames; + $this->labelValues = $labelValues; + $this->value = $value; + } + + /** + * Represents this structure as a PHP associative array. + * + * This array generally conforms to the expectations of the {@see \Prometheus\MetricFamilySamples} structure. + * + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'labelNames' => $this->labelNames, + 'labelValues' => $this->labelValues, + 'value' => $this->value, + ]; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/SampleBuilder.php b/src/Prometheus/Storage/RedisTxn/SampleBuilder.php new file mode 100644 index 00000000..fd8b59fb --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/SampleBuilder.php @@ -0,0 +1,100 @@ +name = $name; + return $this; + } + + /** + * @param string[] $labelNames + * @return SampleBuilder + */ + public function withLabelNames(array $labelNames): SampleBuilder + { + $this->labelNames = $labelNames; + return $this; + } + + /** + * @param float[]|int[] $labelValues + * @return SampleBuilder + */ + public function withLabelValues(array $labelValues): SampleBuilder + { + $this->labelValues = $labelValues; + return $this; + } + + /** + * @param float|int $value + * @return SampleBuilder + */ + public function withValue($value): SampleBuilder + { + $this->value = $value; + return $this; + } + + /** + * @return Sample + */ + public function build(): Sample + { + $this->validate(); + return new Sample( + $this->name, + $this->labelNames ?? [], + $this->labelValues ?? [], + $this->value + ); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->name === null) { + throw new InvalidArgumentException('Sample name field is required'); + } + + if ($this->value === null) { + throw new InvalidArgumentException('Sample name field is required'); + } + } +} From fdb97a55eee9fe48c60242b43310e13e70f4ad44 Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Mon, 16 Jan 2023 17:40:01 -0500 Subject: [PATCH 03/14] Add and update test coverage for new storage adapter. Signed-off-by: Arris Ray Signed-off-by: Arris Ray --- docker-compose.yml | 2 + examples/flush_adapter.php | 2 + examples/metrics.php | 2 + examples/some_counter.php | 2 + examples/some_gauge.php | 2 + examples/some_histogram.php | 2 + examples/some_summary.php | 2 + .../RedisTxn/CollectorRegistryTest.php | 20 +++ .../Test/Prometheus/RedisTxn/CounterTest.php | 21 +++ .../RedisTxn/CounterWithPrefixTest.php | 26 +++ tests/Test/Prometheus/RedisTxn/GaugeTest.php | 21 +++ .../RedisTxn/GaugeWithPrefixTest.php | 26 +++ .../Prometheus/RedisTxn/HistogramTest.php | 21 +++ .../RedisTxn/HistogramWithPrefixTest.php | 26 +++ .../Test/Prometheus/RedisTxn/SummaryTest.php | 21 +++ .../RedisTxn/SummaryWithPrefixTest.php | 26 +++ .../Test/Prometheus/Storage/RedisTxnTest.php | 150 ++++++++++++++++++ tests/bootstrap.php | 1 + 18 files changed, 373 insertions(+) create mode 100644 tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/CounterTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/GaugeTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/HistogramTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/SummaryTest.php create mode 100644 tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php create mode 100644 tests/Test/Prometheus/Storage/RedisTxnTest.php diff --git a/docker-compose.yml b/docker-compose.yml index c6a794b0..fb8ef342 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: - php-fpm ports: - 8080:80 + environment: + - REDIS_HOST=redis php-fpm: build: php-fpm/ diff --git a/examples/flush_adapter.php b/examples/flush_adapter.php index 1c00eab7..037f9142 100644 --- a/examples/flush_adapter.php +++ b/examples/flush_adapter.php @@ -16,6 +16,8 @@ $adapter = new Prometheus\Storage\APCng(); } elseif ($adapterName === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); +} elseif ($adapterName === 'redistxn') { + $adapter = new Prometheus\Storage\RedisTxn(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } $adapter->wipeStorage(); diff --git a/examples/metrics.php b/examples/metrics.php index 9c0fdb80..051caf38 100644 --- a/examples/metrics.php +++ b/examples/metrics.php @@ -17,6 +17,8 @@ $adapter = new Prometheus\Storage\APCng(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); +} elseif ($adapter === 'redistxn') { + $adapter = new Prometheus\Storage\RedisTxn(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } $registry = new CollectorRegistry($adapter); $renderer = new RenderTextFormat(); diff --git a/examples/some_counter.php b/examples/some_counter.php index c7426ce8..05ad9429 100644 --- a/examples/some_counter.php +++ b/examples/some_counter.php @@ -16,6 +16,8 @@ $adapter = new Prometheus\Storage\APCng(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); +} elseif ($adapter === 'redistxn') { + $adapter = new Prometheus\Storage\RedisTxn(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } $registry = new CollectorRegistry($adapter); diff --git a/examples/some_gauge.php b/examples/some_gauge.php index 9e8b3da2..05b8175e 100644 --- a/examples/some_gauge.php +++ b/examples/some_gauge.php @@ -19,6 +19,8 @@ $adapter = new Prometheus\Storage\APCng(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); +} elseif ($adapter === 'redistxn') { + $adapter = new Prometheus\Storage\RedisTxn(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } $registry = new CollectorRegistry($adapter); diff --git a/examples/some_histogram.php b/examples/some_histogram.php index 2f1a5f98..8970c849 100644 --- a/examples/some_histogram.php +++ b/examples/some_histogram.php @@ -18,6 +18,8 @@ $adapter = new Prometheus\Storage\APCng(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); +} elseif ($adapter === 'redistxn') { + $adapter = new Prometheus\Storage\RedisTxn(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } $registry = new CollectorRegistry($adapter); diff --git a/examples/some_summary.php b/examples/some_summary.php index 363f9190..8dabaadc 100644 --- a/examples/some_summary.php +++ b/examples/some_summary.php @@ -18,6 +18,8 @@ $adapter = new Prometheus\Storage\APCng(); } elseif ($adapter === 'in-memory') { $adapter = new Prometheus\Storage\InMemory(); +} elseif ($adapter === 'redistxn') { + $adapter = new Prometheus\Storage\RedisTxn(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']); } $registry = new CollectorRegistry($adapter); diff --git a/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php b/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php new file mode 100644 index 00000000..5150c6ac --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php @@ -0,0 +1,20 @@ +adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/CounterTest.php b/tests/Test/Prometheus/RedisTxn/CounterTest.php new file mode 100644 index 00000000..5067e841 --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/CounterTest.php @@ -0,0 +1,21 @@ +adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php new file mode 100644 index 00000000..364ce659 --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php @@ -0,0 +1,26 @@ +connect(REDIS_HOST); + + $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); + + $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/GaugeTest.php b/tests/Test/Prometheus/RedisTxn/GaugeTest.php new file mode 100644 index 00000000..1b298f39 --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/GaugeTest.php @@ -0,0 +1,21 @@ +adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php new file mode 100644 index 00000000..20fd764c --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php @@ -0,0 +1,26 @@ +connect(REDIS_HOST); + + $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); + + $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/HistogramTest.php b/tests/Test/Prometheus/RedisTxn/HistogramTest.php new file mode 100644 index 00000000..9d831ada --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/HistogramTest.php @@ -0,0 +1,21 @@ +adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php new file mode 100644 index 00000000..d768eeb7 --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php @@ -0,0 +1,26 @@ +connect(REDIS_HOST); + + $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); + + $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/SummaryTest.php b/tests/Test/Prometheus/RedisTxn/SummaryTest.php new file mode 100644 index 00000000..3be153d2 --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/SummaryTest.php @@ -0,0 +1,21 @@ +adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php new file mode 100644 index 00000000..94bab09c --- /dev/null +++ b/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php @@ -0,0 +1,26 @@ +connect(REDIS_HOST); + + $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); + + $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter->wipeStorage(); + } +} diff --git a/tests/Test/Prometheus/Storage/RedisTxnTest.php b/tests/Test/Prometheus/Storage/RedisTxnTest.php new file mode 100644 index 00000000..3cd1603f --- /dev/null +++ b/tests/Test/Prometheus/Storage/RedisTxnTest.php @@ -0,0 +1,150 @@ +redisConnection = new \Redis(); + $this->redisConnection->connect(REDIS_HOST); + $this->redisConnection->flushAll(); + } + + /** + * @test + */ + public function itShouldThrowAnExceptionOnConnectionFailure(): void + { + $redis = new RedixTxn(['host' => '/dev/null']); + + $this->expectException(StorageException::class); + $this->expectExceptionMessage("Can't connect to Redis server"); + + $redis->collect(); + $redis->wipeStorage(); + } + + /** + * @test + */ + public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected(): void + { + $connection = new \Redis(); + + self::expectException(StorageException::class); + self::expectExceptionMessage('Connection to Redis server not established'); + + RedixTxn::fromExistingConnection($connection); + } + + /** + * @test + */ + public function itShouldNotClearWholeRedisOnFlush(): void + { + $this->redisConnection->set('not a prometheus metric key', 'data'); + + $redis = RedixTxn::fromExistingConnection($this->redisConnection); + $registry = new CollectorRegistry($redis); + + // ensure flush is working correctly on large number of metrics + for ($i = 0; $i < 1000; $i++) { + $registry->getOrRegisterCounter('namespace', "counter_$i", 'counter help')->inc(); + $registry->getOrRegisterGauge('namespace', "gauge_$i", 'gauge help')->inc(); + $registry->getOrRegisterHistogram('namespace', "histogram_$i", 'histogram help')->observe(1); + } + $redis->wipeStorage(); + + $redisKeys = $this->redisConnection->keys("*"); + self::assertThat( + $redisKeys, + self::equalTo([ + 'not a prometheus metric key' + ]) + ); + } + + /** + * @test + */ + public function itShouldOnlyConnectOnceOnSubsequentCalls(): void + { + $clientId = $this->redisConnection->rawCommand('client', 'id'); + $expectedClientId = 'id=' . ($clientId + 1) . ' '; + $notExpectedClientId = 'id=' . ($clientId + 2) . ' '; + + $redis = new RedixTxn(['host' => REDIS_HOST]); + + $redis->collect(); + + self::assertStringContainsString( + $expectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + self::assertStringNotContainsString( + $notExpectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + + $redis->collect(); + + self::assertStringContainsString( + $expectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + self::assertStringNotContainsString( + $notExpectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + } + + /** + * @test + */ + public function itShouldOnlyConnectOnceForInjectedRedisConnectionOnSubsequentCalls(): void + { + $clientId = $this->redisConnection->rawCommand('client', 'id'); + $expectedClientId = 'id=' . $clientId . ' '; + $notExpectedClientId = 'id=' . ($clientId + 1) . ' '; + + $redis = RedixTxn::fromExistingConnection($this->redisConnection); + + $redis->collect(); + + self::assertStringContainsString( + $expectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + self::assertStringNotContainsString( + $notExpectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + + $redis->collect(); + + self::assertStringContainsString( + $expectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + self::assertStringNotContainsString( + $notExpectedClientId, + $this->redisConnection->rawCommand('client', 'list') + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 12456d29..7f9207f9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -15,5 +15,6 @@ $loader = require $autoload; $loader->add('Test\\Prometheus', __DIR__); $loader->add('Test\\Performance', __DIR__); +$loader->add('Test\\Benchmark', __DIR__); define('REDIS_HOST', isset($_ENV['REDIS_HOST']) ? $_ENV['REDIS_HOST'] : '127.0.0.1'); From fb0e0db84e62ff6bac464d95e29618a8dca6110c Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Mon, 16 Jan 2023 17:40:20 -0500 Subject: [PATCH 04/14] Add benchmark test. Signed-off-by: Arris Ray Signed-off-by: Arris Ray --- .gitignore | 1 + phpunit.xml.dist | 1 + tests/Test/Benchmark/AdapterType.php | 30 ++ tests/Test/Benchmark/BenchmarkTest.php | 98 +++++++ tests/Test/Benchmark/MetricType.php | 33 +++ tests/Test/Benchmark/ReportType.php | 9 + tests/Test/Benchmark/TestCase.php | 276 ++++++++++++++++++ tests/Test/Benchmark/TestCaseBuilder.php | 115 ++++++++ tests/Test/Benchmark/TestCaseResult.php | 161 ++++++++++ .../Test/Benchmark/TestCaseResultBuilder.php | 111 +++++++ tests/Test/Benchmark/TestType.php | 9 + 11 files changed, 844 insertions(+) create mode 100644 tests/Test/Benchmark/AdapterType.php create mode 100644 tests/Test/Benchmark/BenchmarkTest.php create mode 100644 tests/Test/Benchmark/MetricType.php create mode 100644 tests/Test/Benchmark/ReportType.php create mode 100644 tests/Test/Benchmark/TestCase.php create mode 100644 tests/Test/Benchmark/TestCaseBuilder.php create mode 100644 tests/Test/Benchmark/TestCaseResult.php create mode 100644 tests/Test/Benchmark/TestCaseResultBuilder.php create mode 100644 tests/Test/Benchmark/TestType.php diff --git a/.gitignore b/.gitignore index fc951dd5..50ce1c70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /vendor/ *.iml /.idea/ +benchmark.csv composer.lock composer.phar .phpunit.result.cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 88981644..68244721 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,6 +25,7 @@ Performance + Benchmark diff --git a/tests/Test/Benchmark/AdapterType.php b/tests/Test/Benchmark/AdapterType.php new file mode 100644 index 00000000..4c398df6 --- /dev/null +++ b/tests/Test/Benchmark/AdapterType.php @@ -0,0 +1,30 @@ +withAdapterType($adapter) + ->withMetricType($metric) + ->withReportType(ReportType::CSV) + ->withNumKeys($numKeys) + ->withNumSamples($numSamples) + ->build(); + + // Sanity check test structure + $this->assertEquals($adapter, $testCase->getAdapterType()); + $this->assertEquals($metric, $testCase->getMetricType()); + $this->assertEquals($numKeys, $testCase->getNumKeys()); + $this->assertEquals($numSamples, $testCase->getNumSamples()); + + // Record results + $result = $testCase->execute(); + file_put_contents(self::RESULT_FILENAME, $result->report() . PHP_EOL, FILE_APPEND); + } +} diff --git a/tests/Test/Benchmark/MetricType.php b/tests/Test/Benchmark/MetricType.php new file mode 100644 index 00000000..b8a05160 --- /dev/null +++ b/tests/Test/Benchmark/MetricType.php @@ -0,0 +1,33 @@ +adapterType = $adapterType; + $this->metricType = $metricType; + $this->reportType = $reportType; + $this->numKeys = $numKeys; + $this->numSamples = $numSamples; + $this->getRegistry(); + } + + /** + * @return int + */ + public function getAdapterType(): int + { + return $this->adapterType; + } + + /** + * @return int + */ + public function getMetricType(): int + { + return $this->metricType; + } + + /** + * @return int + */ + public function getNumKeys(): int + { + return $this->numKeys; + } + + /** + * @return int + */ + public function getNumSamples(): int + { + return $this->numSamples; + } + + /** + * @return TestCaseResult + */ + public function execute(): TestCaseResult + { + // Setup test + $this->executeSeed(); + + // Create result builder + $builder = TestCaseResult::newBuilder() + ->withAdapterType($this->adapterType) + ->withMetricType($this->metricType) + ->withNumKeys($this->numKeys) + ->withReportType($this->reportType); + + // Run render tests + for ($i = 0; $i < $this->numSamples; $i++) { + $result = $this->executeRender(); + $builder->withRenderResult($result); + } + + // Run write tests + for ($i = 0; $i < $this->numSamples; $i++) { + $result = $this->executeWrite(); + $builder->withWriteResult($result); + } + + // Build result + return $builder->build(); + } + + /** + * @return Adapter + */ + private function getAdapter(): Adapter + { + if ($this->adapter === null) { + switch ($this->adapterType) { + case AdapterType::REDIS: + $config = $this->getRedisConfig(); + $this->adapter = new Redis($config); + break; + case AdapterType::REDISNG: + $config = $this->getRedisConfig(); + $this->adapter = new RedisNg($config); + break; + case AdapterType::REDISTXN: + $config = $this->getRedisConfig(); + $this->adapter = new RedixTxn($config); + break; + default: + break; + } + } + return $this->adapter; + } + + /** + * @return RegistryInterface + */ + private function getRegistry(): RegistryInterface + { + if ($this->registry === null) { + $this->registry = new CollectorRegistry($this->getAdapter(), false); + } + return $this->registry; + } + + /** + * @return array + */ + private function getRedisConfig(): array + { + return [ + 'host' => $_SERVER['REDIS_HOST'] ?? self::REDIS_HOST, + 'port' => self::REDIS_PORT, + 'database' => self::REDIS_DB, + ]; + } + + /** + * @return void + */ + private function executeSeed(): void + { + $this->getAdapter()->wipeStorage(); + for ($i = 0; $i < $this->numKeys; $i++) { + $this->emitMetric(); + } + } + + /** + * @return float + * @throws Exception + */ + private function executeWrite(): float + { + // Write test key + $start = microtime(true); + $this->emitMetric(); + return microtime(true) - $start; + } + + /** + * @return float + */ + private function executeRender(): float + { + $start = microtime(true); + $this->render(); + return microtime(true) - $start; + } + + /** + * @return string + * @throws MetricsRegistrationException + * @throws Exception + */ + private function emitMetric(): string + { + $key = ''; + $registry = $this->getRegistry(); + switch ($this->metricType) { + case MetricType::COUNTER: + $key = uniqid('counter_', false); + $registry->getOrRegisterCounter(self::DEFAULT_METRIC_NAMESPACE, $key, self::DEFAULT_METRIC_HELP)->inc(); + break; + case MetricType::GAUGE: + $key = uniqid('gauge_', false); + $registry->getOrRegisterGauge(self::DEFAULT_METRIC_NAMESPACE, $key, self::DEFAULT_METRIC_HELP)->inc();; + break; + case MetricType::HISTOGRAM: + $key = uniqid('histogram_', false); + $value = random_int(1, PHP_INT_MAX); + $registry->getOrRegisterHistogram(self::DEFAULT_METRIC_NAMESPACE, $key, self::DEFAULT_METRIC_HELP)->observe($value); + break; + case MetricType::SUMMARY: + $key = uniqid('timer_', false); + $value = random_int(1, PHP_INT_MAX); + $registry->getOrRegisterSummary(self::DEFAULT_METRIC_NAMESPACE, $key, self::DEFAULT_METRIC_HELP)->observe($value); + break; + } + return $key; + } + + /** + * @return string + */ + private function render(): string + { + $renderer = new RenderTextFormat(); + return $renderer->render($this->getRegistry()->getMetricFamilySamples()); + } +} diff --git a/tests/Test/Benchmark/TestCaseBuilder.php b/tests/Test/Benchmark/TestCaseBuilder.php new file mode 100644 index 00000000..5bba584f --- /dev/null +++ b/tests/Test/Benchmark/TestCaseBuilder.php @@ -0,0 +1,115 @@ +adapterType = $adapterType; + return $this; + } + + /** + * @param int $metricType + * @return TestCaseBuilder + */ + public function withMetricType(int $metricType): TestCaseBuilder + { + $this->metricType = $metricType; + return $this; + } + + /** + * @param int $reportType + * @return TestCaseResultBuilder + */ + public function withReportType(int $reportType): TestCaseBuilder + { + $this->reportType = $reportType; + return $this; + } + + /** + * @param int $numKeys + * @return $this + */ + public function withNumKeys(int $numKeys): TestCaseBuilder + { + $this->numKeys = $numKeys; + return $this; + } + + /** + * @param int $numSamples + * @return $this + */ + public function withNumSamples(int $numSamples): TestCaseBuilder + { + $this->numSamples = $numSamples; + return $this; + } + + /** + * @return BenchmarkTestCase + * @throws InvalidArgumentException + */ + public function build(): BenchmarkTestCase + { + $this->validate(); + return new BenchmarkTestCase( + $this->adapterType, + $this->metricType, + $this->reportType ?? ReportType::CSV, + $this->numKeys ?? BenchmarkTestCase::DEFAULT_NUM_KEYS, + $this->numSamples ?? BenchmarkTestCase::DEFAULT_NUM_SAMPLES + ); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->adapterType === null) { + throw new InvalidArgumentException('Missing adapter type'); + } + + if ($this->metricType === null) { + throw new InvalidArgumentException('Missing metric type'); + } + } +} diff --git a/tests/Test/Benchmark/TestCaseResult.php b/tests/Test/Benchmark/TestCaseResult.php new file mode 100644 index 00000000..0f5c79ea --- /dev/null +++ b/tests/Test/Benchmark/TestCaseResult.php @@ -0,0 +1,161 @@ +adapterType = $adapterType; + $this->metricType = $metricType; + $this->reportType = $reportType; + $this->numKeys = $numKeys; + $this->writeResults = $writeResults; + $this->renderResults = $renderResults; + } + + /** + * @return string + */ + public function report(): string + { + assert(count($this->writeResults) === count($this->renderResults)); + + sort($this->writeResults); + sort($this->renderResults); + + return ($this->reportType === ReportType::CSV) + ? $this->toCsv() + : $this->toJson(); + } + + private function toCsv(): string + { + return implode(',', [ + AdapterType::toString($this->adapterType), + MetricType::toString($this->metricType), + $this->numKeys, + count($this->writeResults), + $this->quantile($this->writeResults, 0.50), + $this->quantile($this->writeResults, 0.75), + $this->quantile($this->writeResults, 0.95), + $this->quantile($this->writeResults, 0.99), + min($this->writeResults), + max($this->writeResults), + array_sum($this->writeResults) / count($this->writeResults), + $this->quantile($this->renderResults, 0.50), + $this->quantile($this->renderResults, 0.75), + $this->quantile($this->renderResults, 0.95), + $this->quantile($this->renderResults, 0.99), + min($this->renderResults), + max($this->renderResults), + array_sum($this->renderResults) / count($this->renderResults), + ]); + } + + /** + * @return string + */ + private function toJson(): string + { + return json_encode([ + 'adapter' => AdapterType::toString($this->adapterType), + 'metric' => MetricType::toString($this->metricType), + 'num-keys' => $this->numKeys, + 'num-samples' => count($this->writeResults), + 'tests' => [ + 'write' => [ + '50' => $this->quantile($this->writeResults, 0.50), + '75' => $this->quantile($this->writeResults, 0.75), + '95' => $this->quantile($this->writeResults, 0.95), + '99' => $this->quantile($this->writeResults, 0.99), + 'min' => min($this->writeResults), + 'max' => max($this->writeResults), + 'avg' => array_sum($this->writeResults) / count($this->writeResults), + ], + 'render' => [ + '50' => $this->quantile($this->renderResults, 0.50), + '75' => $this->quantile($this->renderResults, 0.75), + '95' => $this->quantile($this->renderResults, 0.95), + '99' => $this->quantile($this->renderResults, 0.99), + 'min' => min($this->renderResults), + 'max' => max($this->renderResults), + 'avg' => array_sum($this->renderResults) / count($this->renderResults), + ], + ], + ]); + } + + /** + * @param array $data + * @param float $quantile + * @return float + */ + private function quantile(array $data, float $quantile): float + { + $count = count($data); + if ($count === 0) { + return 0; + } + + $j = floor($count * $quantile); + $r = $count * $quantile - $j; + if (0.0 === $r) { + return $data[$j - 1]; + } + return $data[$j]; + } +} diff --git a/tests/Test/Benchmark/TestCaseResultBuilder.php b/tests/Test/Benchmark/TestCaseResultBuilder.php new file mode 100644 index 00000000..bcc43c47 --- /dev/null +++ b/tests/Test/Benchmark/TestCaseResultBuilder.php @@ -0,0 +1,111 @@ +adapterType = $adapterType; + return $this; + } + + /** + * @param int $metricType + * @return TestCaseResultBuilder + */ + public function withMetricType(int $metricType): TestCaseResultBuilder + { + $this->metricType = $metricType; + return $this; + } + + /** + * @param int $reportType + * @return TestCaseResultBuilder + */ + public function withReportType(int $reportType): TestCaseResultBuilder + { + $this->reportType = $reportType; + return $this; + } + + /** + * @param int $numKeys + * @return TestCaseResultBuilder + */ + public function withNumKeys(int $numKeys): TestCaseResultBuilder + { + $this->numKeys = $numKeys; + return $this; + } + + /** + * @param float $result + * @return TestCaseResultBuilder + */ + public function withWriteResult(float $result): TestCaseResultBuilder + { + $this->writeResults[] = $result; + return $this; + } + + /** + * @param float $result + * @return TestCaseResultBuilder + */ + public function withRenderResult(float $result): TestCaseResultBuilder + { + $this->renderResults[] = $result; + return $this; + } + + /** + * @return TestCaseResult + */ + public function build(): TestCaseResult + { + return new TestCaseResult( + $this->adapterType, + $this->metricType, + $this->reportType, + $this->numKeys, + $this->writeResults, + $this->renderResults + ); + } +} diff --git a/tests/Test/Benchmark/TestType.php b/tests/Test/Benchmark/TestType.php new file mode 100644 index 00000000..67d3d0ab --- /dev/null +++ b/tests/Test/Benchmark/TestType.php @@ -0,0 +1,9 @@ + Date: Mon, 16 Jan 2023 18:10:20 -0500 Subject: [PATCH 05/14] Fix typo. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn/MetricBuilder.php | 2 +- tests/Test/Benchmark/TestCase.php | 2 +- .../Test/Prometheus/RedisTxn/CollectorRegistryTest.php | 2 +- tests/Test/Prometheus/RedisTxn/CounterTest.php | 2 +- .../Test/Prometheus/RedisTxn/CounterWithPrefixTest.php | 2 +- tests/Test/Prometheus/RedisTxn/GaugeTest.php | 2 +- tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php | 2 +- tests/Test/Prometheus/RedisTxn/HistogramTest.php | 2 +- .../Prometheus/RedisTxn/HistogramWithPrefixTest.php | 2 +- tests/Test/Prometheus/RedisTxn/SummaryTest.php | 2 +- .../Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php | 2 +- tests/Test/Prometheus/Storage/RedisTxnTest.php | 10 +++++----- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Prometheus/Storage/RedisTxn/MetricBuilder.php b/src/Prometheus/Storage/RedisTxn/MetricBuilder.php index ce6b0fe2..086dda85 100644 --- a/src/Prometheus/Storage/RedisTxn/MetricBuilder.php +++ b/src/Prometheus/Storage/RedisTxn/MetricBuilder.php @@ -90,7 +90,7 @@ private function processSamples(array $sourceSamples): array // durable and unique in the sorted set in accordance with best-practice guidelines described in the article, // "Redis Best Practices: Sorted Set Time Series" [1]. // - // See RedixTxn::updateSummary() for the complementary part of this operation. + // See RedisTxn::updateSummary() for the complementary part of this operation. // // [1] https://redis.com/redis-best-practices/time-series/sorted-set-time-series/ $typedSamples = array_map(function ($sample) { diff --git a/tests/Test/Benchmark/TestCase.php b/tests/Test/Benchmark/TestCase.php index 185009d8..c4902c0c 100644 --- a/tests/Test/Benchmark/TestCase.php +++ b/tests/Test/Benchmark/TestCase.php @@ -168,7 +168,7 @@ private function getAdapter(): Adapter break; case AdapterType::REDISTXN: $config = $this->getRedisConfig(); - $this->adapter = new RedixTxn($config); + $this->adapter = new RedisTxn($config); break; default: break; diff --git a/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php b/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php index 5150c6ac..4ae7d6e5 100644 --- a/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php +++ b/tests/Test/Prometheus/RedisTxn/CollectorRegistryTest.php @@ -14,7 +14,7 @@ class CollectorRegistryTest extends AbstractCollectorRegistryTest { public function configureAdapter(): void { - $this->adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter = new RedisTxn(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/CounterTest.php b/tests/Test/Prometheus/RedisTxn/CounterTest.php index 5067e841..cbd84f07 100644 --- a/tests/Test/Prometheus/RedisTxn/CounterTest.php +++ b/tests/Test/Prometheus/RedisTxn/CounterTest.php @@ -15,7 +15,7 @@ class CounterTest extends AbstractCounterTest { public function configureAdapter(): void { - $this->adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter = new RedisTxn(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php index 364ce659..67d5e715 100644 --- a/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php +++ b/tests/Test/Prometheus/RedisTxn/CounterWithPrefixTest.php @@ -20,7 +20,7 @@ public function configureAdapter(): void $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); - $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter = RedisTxn::fromExistingConnection($connection); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/GaugeTest.php b/tests/Test/Prometheus/RedisTxn/GaugeTest.php index 1b298f39..119d4513 100644 --- a/tests/Test/Prometheus/RedisTxn/GaugeTest.php +++ b/tests/Test/Prometheus/RedisTxn/GaugeTest.php @@ -15,7 +15,7 @@ class GaugeTest extends AbstractGaugeTest { public function configureAdapter(): void { - $this->adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter = new RedisTxn(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php index 20fd764c..178bc35d 100644 --- a/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php +++ b/tests/Test/Prometheus/RedisTxn/GaugeWithPrefixTest.php @@ -20,7 +20,7 @@ public function configureAdapter(): void $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); - $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter = RedisTxn::fromExistingConnection($connection); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/HistogramTest.php b/tests/Test/Prometheus/RedisTxn/HistogramTest.php index 9d831ada..d18db180 100644 --- a/tests/Test/Prometheus/RedisTxn/HistogramTest.php +++ b/tests/Test/Prometheus/RedisTxn/HistogramTest.php @@ -15,7 +15,7 @@ class HistogramTest extends AbstractHistogramTest { public function configureAdapter(): void { - $this->adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter = new RedisTxn(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php index d768eeb7..b198d410 100644 --- a/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php +++ b/tests/Test/Prometheus/RedisTxn/HistogramWithPrefixTest.php @@ -20,7 +20,7 @@ public function configureAdapter(): void $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); - $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter = RedisTxn::fromExistingConnection($connection); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/SummaryTest.php b/tests/Test/Prometheus/RedisTxn/SummaryTest.php index 3be153d2..4b15746f 100644 --- a/tests/Test/Prometheus/RedisTxn/SummaryTest.php +++ b/tests/Test/Prometheus/RedisTxn/SummaryTest.php @@ -15,7 +15,7 @@ class SummaryTest extends AbstractSummaryTest { public function configureAdapter(): void { - $this->adapter = new RedixTxn(['host' => REDIS_HOST]); + $this->adapter = new RedisTxn(['host' => REDIS_HOST]); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php b/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php index 94bab09c..9a5ec9ef 100644 --- a/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php +++ b/tests/Test/Prometheus/RedisTxn/SummaryWithPrefixTest.php @@ -20,7 +20,7 @@ public function configureAdapter(): void $connection->setOption(\Redis::OPT_PREFIX, 'prefix:'); - $this->adapter = RedixTxn::fromExistingConnection($connection); + $this->adapter = RedisTxn::fromExistingConnection($connection); $this->adapter->wipeStorage(); } } diff --git a/tests/Test/Prometheus/Storage/RedisTxnTest.php b/tests/Test/Prometheus/Storage/RedisTxnTest.php index 3cd1603f..b1e929a0 100644 --- a/tests/Test/Prometheus/Storage/RedisTxnTest.php +++ b/tests/Test/Prometheus/Storage/RedisTxnTest.php @@ -31,7 +31,7 @@ protected function setUp(): void */ public function itShouldThrowAnExceptionOnConnectionFailure(): void { - $redis = new RedixTxn(['host' => '/dev/null']); + $redis = new RedisTxn(['host' => '/dev/null']); $this->expectException(StorageException::class); $this->expectExceptionMessage("Can't connect to Redis server"); @@ -50,7 +50,7 @@ public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected(): void self::expectException(StorageException::class); self::expectExceptionMessage('Connection to Redis server not established'); - RedixTxn::fromExistingConnection($connection); + RedisTxn::fromExistingConnection($connection); } /** @@ -60,7 +60,7 @@ public function itShouldNotClearWholeRedisOnFlush(): void { $this->redisConnection->set('not a prometheus metric key', 'data'); - $redis = RedixTxn::fromExistingConnection($this->redisConnection); + $redis = RedisTxn::fromExistingConnection($this->redisConnection); $registry = new CollectorRegistry($redis); // ensure flush is working correctly on large number of metrics @@ -89,7 +89,7 @@ public function itShouldOnlyConnectOnceOnSubsequentCalls(): void $expectedClientId = 'id=' . ($clientId + 1) . ' '; $notExpectedClientId = 'id=' . ($clientId + 2) . ' '; - $redis = new RedixTxn(['host' => REDIS_HOST]); + $redis = new RedisTxn(['host' => REDIS_HOST]); $redis->collect(); @@ -123,7 +123,7 @@ public function itShouldOnlyConnectOnceForInjectedRedisConnectionOnSubsequentCal $expectedClientId = 'id=' . $clientId . ' '; $notExpectedClientId = 'id=' . ($clientId + 1) . ' '; - $redis = RedixTxn::fromExistingConnection($this->redisConnection); + $redis = RedisTxn::fromExistingConnection($this->redisConnection); $redis->collect(); From b028f368897fe4c781ab5e6deebdde8c8d32292c Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Mon, 16 Jan 2023 19:50:05 -0500 Subject: [PATCH 06/14] Improve benchmark test. Signed-off-by: Arris Ray --- tests/Test/Benchmark/BenchmarkTest.php | 50 ++-- tests/Test/Benchmark/TestCaseResult.php | 335 +++++++++++++----------- 2 files changed, 211 insertions(+), 174 deletions(-) diff --git a/tests/Test/Benchmark/BenchmarkTest.php b/tests/Test/Benchmark/BenchmarkTest.php index 0d1fdd9b..9fea97b0 100644 --- a/tests/Test/Benchmark/BenchmarkTest.php +++ b/tests/Test/Benchmark/BenchmarkTest.php @@ -19,26 +19,10 @@ class BenchmarkTest extends TestCase */ public static function setUpBeforeClass(): void { - file_put_contents(self::RESULT_FILENAME, implode(',', [ - 'adapter', - 'metric', - 'num-keys', - 'num-samples', - 'write-p50', - 'write-p75', - 'write-p95', - 'write-p99', - 'write-min', - 'write-max', - 'write-avg', - 'render-p50', - 'render-p75', - 'render-p95', - 'render-p99', - 'render-min', - 'render-max', - 'render-avg', - ])); + file_put_contents( + self::RESULT_FILENAME, + implode(',', TestCaseResult::getCsvHeaders()) + ); parent::setUpBeforeClass(); } @@ -48,10 +32,34 @@ public static function setUpBeforeClass(): void public function benchmarkProvider(): array { return [ + [AdapterType::REDISNG, MetricType::COUNTER, 1000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 2000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 5000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 10000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 1000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 2000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 5000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 10000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 1000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 2000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 5000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 10000, 10], [AdapterType::REDISNG, MetricType::SUMMARY, 1000, 10], [AdapterType::REDISNG, MetricType::SUMMARY, 2000, 10], [AdapterType::REDISNG, MetricType::SUMMARY, 5000, 10], [AdapterType::REDISNG, MetricType::SUMMARY, 10000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 1000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 2000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 5000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 10000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 1000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 2000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 5000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 10000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 1000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 2000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 5000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 10000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 1000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 2000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 5000, 10], @@ -76,6 +84,8 @@ public function benchmark( int $numSamples ): void { + ini_set('memory_limit','1024M'); + // Create and execute test case $testCase = BenchmarkTestCase::newBuilder() ->withAdapterType($adapter) diff --git a/tests/Test/Benchmark/TestCaseResult.php b/tests/Test/Benchmark/TestCaseResult.php index 0f5c79ea..7a0c28a7 100644 --- a/tests/Test/Benchmark/TestCaseResult.php +++ b/tests/Test/Benchmark/TestCaseResult.php @@ -4,158 +4,185 @@ class TestCaseResult { - /** - * @var int - */ - private $adapterType; - - /** - * @var int - */ - private $metricType; - - /** - * @var int - */ - private $reportType; - - /** - * @var int - */ - private $numKeys; - - /** - * @var array - */ - private $writeResults; - - /** - * @var array - */ - private $renderResults; - - /** - * @return TestCaseResultBuilder - */ - public static function newBuilder(): TestCaseResultBuilder - { - return new TestCaseResultBuilder(); - } - - /** - * @param int $adapterType - * @param int $metricType - * @param int $reportType - * @param int $numKeys - * @param array $writeResults - * @param array $renderResults - */ - public function __construct( - int $adapterType, - int $metricType, - int $reportType, - int $numKeys, - array $writeResults, - array $renderResults - ) - { - $this->adapterType = $adapterType; - $this->metricType = $metricType; - $this->reportType = $reportType; - $this->numKeys = $numKeys; - $this->writeResults = $writeResults; - $this->renderResults = $renderResults; - } - - /** - * @return string - */ - public function report(): string - { - assert(count($this->writeResults) === count($this->renderResults)); - - sort($this->writeResults); - sort($this->renderResults); - - return ($this->reportType === ReportType::CSV) - ? $this->toCsv() - : $this->toJson(); - } - - private function toCsv(): string - { - return implode(',', [ - AdapterType::toString($this->adapterType), - MetricType::toString($this->metricType), - $this->numKeys, - count($this->writeResults), - $this->quantile($this->writeResults, 0.50), - $this->quantile($this->writeResults, 0.75), - $this->quantile($this->writeResults, 0.95), - $this->quantile($this->writeResults, 0.99), - min($this->writeResults), - max($this->writeResults), - array_sum($this->writeResults) / count($this->writeResults), - $this->quantile($this->renderResults, 0.50), - $this->quantile($this->renderResults, 0.75), - $this->quantile($this->renderResults, 0.95), - $this->quantile($this->renderResults, 0.99), - min($this->renderResults), - max($this->renderResults), - array_sum($this->renderResults) / count($this->renderResults), - ]); - } - - /** - * @return string - */ - private function toJson(): string - { - return json_encode([ - 'adapter' => AdapterType::toString($this->adapterType), - 'metric' => MetricType::toString($this->metricType), - 'num-keys' => $this->numKeys, - 'num-samples' => count($this->writeResults), - 'tests' => [ - 'write' => [ - '50' => $this->quantile($this->writeResults, 0.50), - '75' => $this->quantile($this->writeResults, 0.75), - '95' => $this->quantile($this->writeResults, 0.95), - '99' => $this->quantile($this->writeResults, 0.99), - 'min' => min($this->writeResults), - 'max' => max($this->writeResults), - 'avg' => array_sum($this->writeResults) / count($this->writeResults), - ], - 'render' => [ - '50' => $this->quantile($this->renderResults, 0.50), - '75' => $this->quantile($this->renderResults, 0.75), - '95' => $this->quantile($this->renderResults, 0.95), - '99' => $this->quantile($this->renderResults, 0.99), - 'min' => min($this->renderResults), - 'max' => max($this->renderResults), - 'avg' => array_sum($this->renderResults) / count($this->renderResults), - ], - ], - ]); - } - - /** - * @param array $data - * @param float $quantile - * @return float - */ - private function quantile(array $data, float $quantile): float - { - $count = count($data); - if ($count === 0) { - return 0; - } - - $j = floor($count * $quantile); - $r = $count * $quantile - $j; - if (0.0 === $r) { - return $data[$j - 1]; - } - return $data[$j]; - } + /** + * @var int + */ + private $adapterType; + + /** + * @var int + */ + private $metricType; + + /** + * @var int + */ + private $reportType; + + /** + * @var int + */ + private $numKeys; + + /** + * @var array + */ + private $updateResults; + + /** + * @var array + */ + private $collectResults; + + /** + * @return TestCaseResultBuilder + */ + public static function newBuilder(): TestCaseResultBuilder + { + return new TestCaseResultBuilder(); + } + + /** + * @return string[] + */ + public static function getCsvHeaders(): array + { + return [ + 'adapter', + 'metric', + 'num-keys', + 'num-samples', + 'update-p50', + 'update-p75', + 'update-p95', + 'update-p99', + 'update-min', + 'update-max', + 'update-avg', + 'collect-p50', + 'collect-p75', + 'collect-p95', + 'collect-p99', + 'collect-min', + 'collect-max', + 'collect-avg', + ]; + } + + /** + * @param int $adapterType + * @param int $metricType + * @param int $reportType + * @param int $numKeys + * @param array $updateResults + * @param array $collectResults + */ + public function __construct( + int $adapterType, + int $metricType, + int $reportType, + int $numKeys, + array $updateResults, + array $collectResults + ) + { + $this->adapterType = $adapterType; + $this->metricType = $metricType; + $this->reportType = $reportType; + $this->numKeys = $numKeys; + $this->updateResults = $updateResults; + $this->collectResults = $collectResults; + } + + /** + * @return string + */ + public function report(): string + { + assert(count($this->updateResults) === count($this->collectResults)); + + sort($this->updateResults); + sort($this->collectResults); + + return ($this->reportType === ReportType::CSV) + ? $this->toCsv() + : $this->toJson(); + } + + private function toCsv(): string + { + return implode(',', [ + AdapterType::toString($this->adapterType), + MetricType::toString($this->metricType), + $this->numKeys, + count($this->updateResults), + $this->quantile($this->updateResults, 0.50), + $this->quantile($this->updateResults, 0.75), + $this->quantile($this->updateResults, 0.95), + $this->quantile($this->updateResults, 0.99), + min($this->updateResults), + max($this->updateResults), + array_sum($this->updateResults) / count($this->updateResults), + $this->quantile($this->collectResults, 0.50), + $this->quantile($this->collectResults, 0.75), + $this->quantile($this->collectResults, 0.95), + $this->quantile($this->collectResults, 0.99), + min($this->collectResults), + max($this->collectResults), + array_sum($this->collectResults) / count($this->collectResults), + ]); + } + + /** + * @return string + */ + private function toJson(): string + { + return json_encode([ + 'adapter' => AdapterType::toString($this->adapterType), + 'metric' => MetricType::toString($this->metricType), + 'num-keys' => $this->numKeys, + 'num-samples' => count($this->updateResults), + 'tests' => [ + 'write' => [ + '50' => $this->quantile($this->updateResults, 0.50), + '75' => $this->quantile($this->updateResults, 0.75), + '95' => $this->quantile($this->updateResults, 0.95), + '99' => $this->quantile($this->updateResults, 0.99), + 'min' => min($this->updateResults), + 'max' => max($this->updateResults), + 'avg' => array_sum($this->updateResults) / count($this->updateResults), + ], + 'render' => [ + '50' => $this->quantile($this->collectResults, 0.50), + '75' => $this->quantile($this->collectResults, 0.75), + '95' => $this->quantile($this->collectResults, 0.95), + '99' => $this->quantile($this->collectResults, 0.99), + 'min' => min($this->collectResults), + 'max' => max($this->collectResults), + 'avg' => array_sum($this->collectResults) / count($this->collectResults), + ], + ], + ]); + } + + /** + * @param array $data + * @param float $quantile + * @return float + */ + private function quantile(array $data, float $quantile): float + { + $count = count($data); + if ($count === 0) { + return 0; + } + + $j = floor($count * $quantile); + $r = $count * $quantile - $j; + if (0.0 === $r) { + return $data[$j - 1]; + } + return $data[$j]; + } } From f1baf892a962ec6ed4ca58e9465a4a41c7f3951f Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 00:16:17 -0500 Subject: [PATCH 07/14] Optimize counter metric collection. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 227 ++++++++++---- src/Prometheus/Storage/RedisTxn/Metadata.php | 254 +++++++++------- .../Storage/RedisTxn/MetadataBuilder.php | 280 +++++++++++------- src/Prometheus/Storage/RedisTxn/Metric.php | 98 +++--- .../Storage/RedisTxn/SampleBuilder.php | 158 +++++----- .../Storage/RedisTxn/ScalarMetricBuilder.php | 94 ++++++ ...icBuilder.php => SummaryMetricBuilder.php} | 119 ++++---- 7 files changed, 759 insertions(+), 471 deletions(-) create mode 100644 src/Prometheus/Storage/RedisTxn/ScalarMetricBuilder.php rename src/Prometheus/Storage/RedisTxn/{MetricBuilder.php => SummaryMetricBuilder.php} (64%) diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php index cb4e11e5..fae00f10 100644 --- a/src/Prometheus/Storage/RedisTxn.php +++ b/src/Prometheus/Storage/RedisTxn.php @@ -11,6 +11,7 @@ use Prometheus\Histogram; use Prometheus\MetricFamilySamples; use Prometheus\Storage\RedisTxn\Metadata; +use Prometheus\Storage\RedisTxn\MetadataBuilder; use Prometheus\Storage\RedisTxn\Metric; use Prometheus\Summary; use RedisException; @@ -39,6 +40,8 @@ class RedisTxn implements Adapter const PROMETHEUS_METRIC_META_SUFFIX = '_METRIC_META'; + const DEFAULT_TTL_SECONDS = 600; + /** * @var mixed[] */ @@ -303,15 +306,14 @@ public function updateSummary(array $data): void { $this->ensureOpenConnection(); - // Prepare summary metadata - $metaHashKey = self::$prefix . self::PROMETHEUS_METRIC_META_SUFFIX; - $summaryMetadata = $this->toMetadata($data); - $ttl = $summaryMetadata->getMaxAgeSeconds(); + // Prepare metadata + $metadata = $this->toMetadata($data); + $ttl = $metadata->getMaxAgeSeconds(); - // Create summary key - $keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $summaryKey = implode(':', [$keyPrefix, $data['name'], $summaryMetadata->getLabelValuesEncoded()]); - $summaryRegistryKey = implode(':', [$keyPrefix, 'keys']); + // Create Redis keys + $metricKey = $this->getMetricKey($metadata); + $registryKey = $this->getMetricRegistryKey($metadata->getType()); + $metadataKey = $this->getMetadataKey($metadata->getType()); // Get summary sample // @@ -350,10 +352,10 @@ public function updateSummary(array $data): void LUA , [ - $summaryRegistryKey, - $metaHashKey, - $summaryKey, - $summaryMetadata->toJson(), + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), $value, $currentTime, $ttl, @@ -407,27 +409,50 @@ public function updateGauge(array $data): void public function updateCounter(array $data): void { $this->ensureOpenConnection(); - $metaData = $data; - unset($metaData['value'], $metaData['labelValues'], $metaData['command']); - $this->redis->eval( - <<toMetadata($data); + + // Create Redis keys + $metricKey = $this->getMetricKey($metadata); + $registryKey = $this->getMetricRegistryKey($metadata->getType()); + $metadataKey = $this->getMetadataKey($metadata->getType()); + + // Prepare script input + $command = $metadata->getCommand() === Adapter::COMMAND_INCREMENT_INTEGER ? 'incrby' : 'incrbyfloat'; + $value = $data['value']; + $ttl = time() + ($metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS); + + $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 + , [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $command, + $value, + $ttl + ], + 3 ); } @@ -440,11 +465,13 @@ private function toMetadata(array $data): Metadata { return Metadata::newBuilder() ->withName($data['name']) + ->withType($data['type']) ->withHelp($data['help']) ->withLabelNames($data['labelNames']) ->withLabelValues($data['labelValues']) - ->withQuantiles($data['quantiles']) - ->withMaxAgeSeconds($data['maxAgeSeconds']) + ->withQuantiles($data['quantiles'] ?? null) + ->withMaxAgeSeconds($data['maxAgeSeconds'] ?? null) + ->withCommand($data['command'] ?? null) ->build(); } @@ -548,23 +575,23 @@ private function collectSummaries(): array // Register summary key $keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; $summaryRegistryKey = implode(':', [$keyPrefix, 'keys']); - $metaHashKey = self::$prefix . self::PROMETHEUS_METRIC_META_SUFFIX; + $metadataKey = $this->getMetadataKey(Summary::TYPE); $currentTime = time(); $result = $this->redis->eval(<< 0 then -- Configure results - summaryMetadata = redis.call("hget", metaHashKey, summaryKey) + summaryMetadata = redis.call("hget", metadataKey, summaryKey) summarySamples = redis.call("zrange", summaryKey, startScore, "+inf", "byscore") else -- Remove the metric's associated metadata if there are no associated samples remaining redis.call('srem', summaryRegistryKey, summaryKey) - redis.call('hdel', metaHashKey, summaryKey) - redis.call('hdel', metaHashKey, ttlFieldName) + redis.call('hdel', metadataKey, summaryKey) + redis.call('hdel', metadataKey, ttlFieldName) end -- Add the processed metric to the set of results @@ -603,17 +630,17 @@ private function collectSummaries(): array , [ $summaryRegistryKey, - $metaHashKey, + $metadataKey, $currentTime, ], 2 ); - // Format summary metrics and hand them off to the calling collector + // Format metrics and hand them off to the calling collector $summaries = []; $redisSummaries = json_decode($result, true); foreach ($redisSummaries as $summary) { - $serializedSummary = Metric::newBuilder() + $serializedSummary = Metric::newSummaryMetricBuilder() ->withMetadata($summary['metadata']) ->withSamples($summary['samples']) ->build() @@ -657,26 +684,68 @@ private function collectGauges(): array */ private function collectCounters(): array { - $keys = $this->redis->sMembers(self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); - sort($keys); + // Create Redis keys + $registryKey = $this->getMetricRegistryKey(Counter::TYPE); + $metadataKey = $this->getMetadataKey(Counter::TYPE); + + // Execute transaction to collect metrics + $result = $this->redis->eval(<<build(); + + // Create or update metric + $metricName = $metadata->getName(); + $builder = $metrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata); + $builder->withSample($counter['samples'], $metadata->getLabelValues()); + $metrics[$metricName] = $builder; + } + + // Format metrics and hand them off to the calling collector $counters = []; - foreach ($keys as $key) { - $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); - $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, - ]; - } - usort($counter['samples'], function ($a, $b): int { - return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); - }); - $counters[] = $counter; + foreach ($metrics as $_ => $metric) { + $counters[] = $metric->build()->toArray(); } return $counters; } @@ -739,4 +808,36 @@ private function decodeLabelValues(string $values): array } return $decodedValues; } + + /** + * @param string $metricType + * @return string + */ + private function getMetricRegistryKey(string $metricType): string + { + $keyPrefix = self::$prefix . $metricType . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + return implode(':', [$keyPrefix, 'keys']); + } + + /** + * @param string $metricType + * @return string + */ + private function getMetadataKey(string $metricType): string + { + return self::$prefix . $metricType . self::PROMETHEUS_METRIC_META_SUFFIX; + } + + /** + * @param Metadata $metadata + * @return string + */ + private function getMetricKey(Metadata $metadata): string + { + $type = $metadata->getType(); + $name = $metadata->getName(); + $labelValues = $metadata->getLabelValuesEncoded(); + $keyPrefix = self::$prefix . $type . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + return implode(':', [$keyPrefix, $name, $labelValues]); + } } diff --git a/src/Prometheus/Storage/RedisTxn/Metadata.php b/src/Prometheus/Storage/RedisTxn/Metadata.php index 0c2e3554..c082eadd 100644 --- a/src/Prometheus/Storage/RedisTxn/Metadata.php +++ b/src/Prometheus/Storage/RedisTxn/Metadata.php @@ -9,115 +9,141 @@ */ class Metadata { - /** - * @var string - */ - private $name; - - /** - * @var string - */ - private $help; - - /** - * @var string[] - */ - private $labelNames; - - /** - * @var mixed[] - */ - private $labelValues; - - /** - * @var int - */ - private $maxAgeSeconds; - - /** - * @var float[] - */ - private $quantiles; - - /** - * @return MetadataBuilder - */ - public static function newBuilder(): MetadataBuilder - { - return new MetadataBuilder(); - } - - /** - * @param string $name - * @param string $help - * @param array $labelNames - * @param array $labelValues - * @param int $maxAgeSeconds - * @param array $quantiles - */ - public function __construct( - string $name, - string $help, - array $labelNames, - array $labelValues, - int $maxAgeSeconds, - array $quantiles - ) - { - $this->name = $name; - $this->help = $help; - $this->labelNames = $labelNames; - $this->labelValues = $labelValues; - $this->maxAgeSeconds = $maxAgeSeconds; - $this->quantiles = $quantiles; - } - - /** + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $help; + + /** + * @var string[] + */ + private $labelNames; + + /** + * @var mixed[] + */ + private $labelValues; + + /** + * @var int + */ + private $maxAgeSeconds; + + /** + * @var float[] + */ + private $quantiles; + + /** + * @var int + */ + private $command; + + /** + * @return MetadataBuilder + */ + public static function newBuilder(): MetadataBuilder + { + return new MetadataBuilder(); + } + + /** + * @param string $name + * @param string $type + * @param string $help + * @param array $labelNames + * @param array $labelValues + * @param int $maxAgeSeconds + * @param array $quantiles + * @param int $command + */ + public function __construct( + string $name, + string $type, + string $help, + array $labelNames, + array $labelValues, + int $maxAgeSeconds, + array $quantiles, + int $command + ) + { + $this->name = $name; + $this->type = $type; + $this->help = $help; + $this->labelNames = $labelNames; + $this->labelValues = $labelValues; + $this->maxAgeSeconds = $maxAgeSeconds; + $this->quantiles = $quantiles; + $this->command = $command; + } + + /** * Prometheus metric name. * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Prometheus metric type. + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** * Prometheus metric help description. * * @internal Optional. - * @return string - */ - public function getHelp(): string - { - return $this->help; - } - - /** + * @return string + */ + public function getHelp(): string + { + return $this->help; + } + + /** * Prometheus metric label names. * * Note that each label introduces a degree of cardinality for a given metric. * * @internal Optional. It is permissible to have no label names. - * @return string[] - */ - public function getLabelNames(): array - { - return $this->labelNames; - } - - /** + * @return string[] + */ + public function getLabelNames(): array + { + return $this->labelNames; + } + + /** * Prometheus metric label values. * * Note that each label value should correspond to a label name. * * @internal Optional. - * @return mixed[] - */ - public function getLabelValues(): array - { - return $this->labelValues; - } + * @return mixed[] + */ + public function getLabelValues(): array + { + return $this->labelValues; + } /** * Prometheus metric label values encoded for storage in Redis. @@ -132,27 +158,35 @@ public function getLabelValuesEncoded(): string return base64_encode(json_encode($this->labelValues)); } - /** + /** * Prometheus metric time-to-live (TTL) in seconds. * * This property is used internally by the storage adapter to enforce a TTL for metrics stored in Redis. * - * @return int - */ - public function getMaxAgeSeconds(): int - { - return $this->maxAgeSeconds; - } - - /** + * @return int + */ + public function getMaxAgeSeconds(): int + { + return $this->maxAgeSeconds; + } + + /** * Prometheus metric metadata that describes the set of quantiles to report for a summary-type metric. * - * @return array - */ - public function getQuantiles(): array - { - return $this->quantiles; - } + * @return array + */ + public function getQuantiles(): array + { + return $this->quantiles; + } + + /** + * @return int + */ + public function getCommand(): int + { + return $this->command; + } /** * Represents this data structure as a JSON object. @@ -163,11 +197,13 @@ public function toJson(): string { return json_encode([ 'name' => $this->getName(), + 'type' => $this->getType(), 'help' => $this->getHelp(), 'labelNames' => $this->getLabelNames(), 'labelValues' => $this->getLabelValuesEncoded(), 'maxAgeSeconds' => $this->getMaxAgeSeconds(), 'quantiles' => $this->getQuantiles(), + 'command' => $this->getCommand(), ]); } } diff --git a/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php b/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php index 6f507290..5eec2f7f 100644 --- a/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php +++ b/src/Prometheus/Storage/RedisTxn/MetadataBuilder.php @@ -3,131 +3,181 @@ namespace Prometheus\Storage\RedisTxn; use InvalidArgumentException; +use Prometheus\Storage\Adapter; /** * Fluent-builder for the {@see \Prometheus\Storage\RedisTxn\Metadata} structure. */ class MetadataBuilder { - /** - * @var string|null - */ - private $name; - - /** - * @var string|null - */ - private $help; - - /** - * @var string[]|null - */ - private $labelNames; - - /** - * @var mixed[]|null - */ - private $labelValues; - - /** - * @var int|null - */ - private $maxAgeSeconds; - - /** - * @var float[]|null - */ - private $quantiles; - - /** - * @param string $name - * @return MetadataBuilder - */ - public function withName(string $name): MetadataBuilder - { - $this->name = $name; - return $this; - } - - /** - * @param string|null $help - * @return MetadataBuilder - */ - public function withHelp(?string $help): MetadataBuilder - { - $this->help = $help; - return $this; - } - - /** - * @param string[]|null $labelNames - * @return MetadataBuilder - */ - public function withLabelNames(?array $labelNames): MetadataBuilder - { - $this->labelNames = $labelNames; - return $this; - } - - /** - * @param string|array|null $labelValues - * @return MetadataBuilder - */ - public function withLabelValues($labelValues): MetadataBuilder - { - if (is_array($labelValues)) { + /** + * @var string|null + */ + private $name; + + /** + * @var string|null + */ + private $type; + + /** + * @var string|null + */ + private $help; + + /** + * @var string[]|null + */ + private $labelNames; + + /** + * @var mixed[]|null + */ + private $labelValues; + + /** + * @var int|null + */ + private $maxAgeSeconds; + + /** + * @var float[]|null + */ + private $quantiles; + + /** + * @var int|null + */ + private $command; + + /** + * @param array $metadata + * @return MetadataBuilder + */ + public static function fromArray(array $metadata): MetadataBuilder + { + return Metadata::newBuilder() + ->withName($metadata['name']) + ->withType($metadata['type']) + ->withHelp($metadata['help'] ?? null) + ->withLabelNames($metadata['labelNames'] ?? null) + ->withLabelValues($metadata['labelValues'] ?? null) + ->withMaxAgeSeconds($metadata['maxAgeSeconds'] ?? null) + ->withQuantiles($metadata['quantiles'] ?? null) + ->withCommand($metadata['command'] ?? null); + } + + /** + * @param string $name + * @return MetadataBuilder + */ + public function withName(string $name): MetadataBuilder + { + $this->name = $name; + return $this; + } + + /** + * @param string $type + * @return MetadataBuilder + */ + public function withType(string $type): MetadataBuilder + { + $this->type = $type; + return $this; + } + + /** + * @param string|null $help + * @return MetadataBuilder + */ + public function withHelp(?string $help): MetadataBuilder + { + $this->help = $help; + return $this; + } + + /** + * @param string[]|null $labelNames + * @return MetadataBuilder + */ + public function withLabelNames(?array $labelNames): MetadataBuilder + { + $this->labelNames = $labelNames; + return $this; + } + + /** + * @param string|array|null $labelValues + * @return MetadataBuilder + */ + public function withLabelValues($labelValues): MetadataBuilder + { + if (($labelValues === null) || is_array($labelValues)) { $this->labelValues = $labelValues; } else { // See Metadata::getLabelNamesEncoded() for the inverse operation on this data. $this->labelValues = json_decode(base64_decode($labelValues)); } return $this; - } - - /** - * @param int|null $maxAgeSeconds - * @return MetadataBuilder - */ - public function withMaxAgeSeconds(?int $maxAgeSeconds): MetadataBuilder - { - $this->maxAgeSeconds = $maxAgeSeconds; - return $this; - } - - /** - * @param float[]|null $quantiles - * @return MetadataBuilder - */ - public function withQuantiles(?array $quantiles): MetadataBuilder - { - $this->quantiles = $quantiles; - return $this; - } - - /** - * @return Metadata - */ - public function build(): Metadata - { - $this->validate(); - return new Metadata( - $this->name, - $this->help ?? '', - $this->labelNames ?? [], - $this->labelValues ?? [], - $this->maxAgeSeconds ?? 0, - $this->quantiles ?? [] - ); - } - - /** - * @return void - * @throws InvalidArgumentException - */ - private function validate(): void - { - if ($this->name === null) { - throw new InvalidArgumentException('Metadata name field is required'); - } - } + } + + /** + * @param int|null $maxAgeSeconds + * @return MetadataBuilder + */ + public function withMaxAgeSeconds(?int $maxAgeSeconds): MetadataBuilder + { + $this->maxAgeSeconds = $maxAgeSeconds; + return $this; + } + + /** + * @param float[]|null $quantiles + * @return MetadataBuilder + */ + public function withQuantiles(?array $quantiles): MetadataBuilder + { + $this->quantiles = $quantiles; + return $this; + } + + /** + * @param int|null $command + * @return MetadataBuilder + */ + public function withCommand(?int $command): MetadataBuilder + { + $this->command = $command; + return $this; + } + + /** + * @return Metadata + */ + public function build(): Metadata + { + $this->validate(); + return new Metadata( + $this->name, + $this->type ?? '', + $this->help ?? '', + $this->labelNames ?? [], + $this->labelValues ?? [], + $this->maxAgeSeconds ?? 0, + $this->quantiles ?? [], + $this->command ?? Adapter::COMMAND_INCREMENT_FLOAT + ); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->name === null) { + throw new InvalidArgumentException('Metadata name field is required'); + } + } } diff --git a/src/Prometheus/Storage/RedisTxn/Metric.php b/src/Prometheus/Storage/RedisTxn/Metric.php index ae008dce..e3d8bcb5 100644 --- a/src/Prometheus/Storage/RedisTxn/Metric.php +++ b/src/Prometheus/Storage/RedisTxn/Metric.php @@ -2,10 +2,6 @@ namespace Prometheus\Storage\RedisTxn; -use Prometheus\Math; -use Prometheus\MetricFamilySamples; -use Prometheus\Summary as PrometheusSummary; - /** * This structure represents all the data associated with a single, unique metric that this library * should present to a Prometheus scraper. @@ -15,51 +11,67 @@ */ class Metric { - /** - * @var Metadata - */ - private $metadata; + /** + * @var Metadata + */ + private $metadata; + + /** + * @var double[]|float[]|int[] + */ + private $samples; + + /** + * @return SummaryMetricBuilder + */ + public static function newSummaryMetricBuilder(): SummaryMetricBuilder + { + return new SummaryMetricBuilder(); + } - /** - * @var double[]|float[]|int[] - */ - private $samples; + /** + * @return ScalarMetricBuilder + */ + public static function newScalarMetricBuilder(): ScalarMetricBuilder + { + return new ScalarMetricBuilder(); + } - /** - * @return MetricBuilder - */ - public static function newBuilder(): MetricBuilder - { - return new MetricBuilder(); - } + /** + * @param Metadata $metadata + * @param array|int|float $samples + */ + public function __construct(Metadata $metadata, $samples) + { + $this->metadata = $metadata; + $this->samples = $samples; + } - /** - * @param Metadata $metadata - * @param array $samples - */ - public function __construct(Metadata $metadata, array $samples) - { - $this->metadata = $metadata; - $this->samples = $samples; - } + /** + * @return Metadata + */ + public function getMetadata(): Metadata + { + return $this->metadata; + } - /** + /** * Represents this data structure as a PHP associative array. * * This array generally conforms to the expectations of the {@see \Prometheus\MetricFamilySamples} structure. * - * @return array - */ - public function toArray(): array - { - return [ - 'name' => $this->metadata->getName(), - 'help' => $this->metadata->getHelp(), - 'type' => PrometheusSummary::TYPE, - 'labelNames' => $this->metadata->getLabelNames(), - 'maxAgeSeconds' => $this->metadata->getMaxAgeSeconds(), - 'quantiles' => $this->metadata->getQuantiles(), - 'samples' => $this->samples, - ]; - } + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->metadata->getName(), + 'help' => $this->metadata->getHelp(), + 'type' => $this->metadata->getType(), + 'labelNames' => $this->metadata->getLabelNames(), + 'maxAgeSeconds' => $this->metadata->getMaxAgeSeconds(), + 'quantiles' => $this->metadata->getQuantiles() ?? [], + 'samples' => $this->samples, + ]; + } } diff --git a/src/Prometheus/Storage/RedisTxn/SampleBuilder.php b/src/Prometheus/Storage/RedisTxn/SampleBuilder.php index fd8b59fb..cc14f611 100644 --- a/src/Prometheus/Storage/RedisTxn/SampleBuilder.php +++ b/src/Prometheus/Storage/RedisTxn/SampleBuilder.php @@ -9,92 +9,94 @@ */ class SampleBuilder { - /** - * @var string|null - */ - private $name; + /** + * @var string|null + */ + private $name; - /** - * @var string[]|null - */ - private $labelNames; + /** + * @var string[]|null + */ + private $labelNames; - /** - * @var float[]|int[]|null - */ - private $labelValues; + /** + * @var float[]|int[]|null + */ + private $labelValues; - /** - * @var float|int|null - */ - private $value; + /** + * @var float|int|null + */ + private $value; - /** - * @param string $name - * @return SampleBuilder - */ - public function withName(string $name): SampleBuilder - { - $this->name = $name; - return $this; - } + /** + * @param string $name + * @return SampleBuilder + */ + public function withName(string $name): SampleBuilder + { + $this->name = $name; + return $this; + } - /** - * @param string[] $labelNames - * @return SampleBuilder - */ - public function withLabelNames(array $labelNames): SampleBuilder - { - $this->labelNames = $labelNames; - return $this; - } + /** + * @param string[] $labelNames + * @return SampleBuilder + */ + public function withLabelNames(array $labelNames): SampleBuilder + { + $this->labelNames = $labelNames; + return $this; + } - /** - * @param float[]|int[] $labelValues - * @return SampleBuilder - */ - public function withLabelValues(array $labelValues): SampleBuilder - { - $this->labelValues = $labelValues; - return $this; - } + /** + * @param float[]|int[] $labelValues + * @return SampleBuilder + */ + public function withLabelValues(array $labelValues): SampleBuilder + { + $this->labelValues = $labelValues; + return $this; + } - /** - * @param float|int $value - * @return SampleBuilder - */ - public function withValue($value): SampleBuilder - { - $this->value = $value; - return $this; - } + /** + * @param float|int $value + * @return SampleBuilder + */ + public function withValue($value): SampleBuilder + { + $this->value = floatval($value) && (floatval($value) != intval($value)) + ? floatval($value) + : intval($value); + return $this; + } - /** - * @return Sample - */ - public function build(): Sample - { - $this->validate(); - return new Sample( - $this->name, - $this->labelNames ?? [], - $this->labelValues ?? [], - $this->value - ); - } + /** + * @return Sample + */ + public function build(): Sample + { + $this->validate(); + return new Sample( + $this->name, + $this->labelNames ?? [], + $this->labelValues ?? [], + $this->value + ); + } - /** - * @return void - * @throws InvalidArgumentException - */ - private function validate(): void - { - if ($this->name === null) { - throw new InvalidArgumentException('Sample name field is required'); - } + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->name === null) { + throw new InvalidArgumentException('Sample name field is required'); + } - if ($this->value === null) { - throw new InvalidArgumentException('Sample name field is required'); - } - } + if ($this->value === null) { + throw new InvalidArgumentException('Sample name field is required'); + } + } } diff --git a/src/Prometheus/Storage/RedisTxn/ScalarMetricBuilder.php b/src/Prometheus/Storage/RedisTxn/ScalarMetricBuilder.php new file mode 100644 index 00000000..18fad273 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/ScalarMetricBuilder.php @@ -0,0 +1,94 @@ +metadata = $metadata; + return $this; + } + + /** + * @param string $sample + * @param array $labelValues + * @return ScalarMetricBuilder + */ + public function withSample(string $sample, array $labelValues): ScalarMetricBuilder + { + $sample = $this->coerceSampleType($sample); + $jsonLabelValues = json_encode($labelValues); + $this->samples[$jsonLabelValues] = $this->toSample($sample, $labelValues); + return $this; + } + + /** + * @return Metric + */ + public function build(): Metric + { + $this->validate(); + ksort($this->samples); + return new Metric($this->metadata, $this->samples); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->metadata === null) { + throw new InvalidArgumentException('Summary metadata field is required.'); + } + + if ($this->samples === null) { + throw new InvalidArgumentException('Summary samples field is required.'); + } + } + + + /** + * @param float|int $sourceSample + * @param array $labelValues + * @return array + */ + private function toSample($sourceSample, array $labelValues): array + { + return Sample::newBuilder() + ->withName($this->metadata->getName()) + ->withLabelNames([]) + ->withLabelValues($labelValues) + ->withValue($sourceSample) + ->build() + ->toArray(); + } + + /** + * @param string $sample + * @return float|int + */ + private function coerceSampleType(string $sample) + { + return (floatval($sample) && floatval($sample) != intval($sample)) + ? floatval($sample) + : intval($sample); + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/MetricBuilder.php b/src/Prometheus/Storage/RedisTxn/SummaryMetricBuilder.php similarity index 64% rename from src/Prometheus/Storage/RedisTxn/MetricBuilder.php rename to src/Prometheus/Storage/RedisTxn/SummaryMetricBuilder.php index 086dda85..2d747fe7 100644 --- a/src/Prometheus/Storage/RedisTxn/MetricBuilder.php +++ b/src/Prometheus/Storage/RedisTxn/SummaryMetricBuilder.php @@ -8,69 +8,62 @@ /** * Fluent-builder for the {@see \Prometheus\Storage\RedisTxn\Metric} data structure. */ -class MetricBuilder +class SummaryMetricBuilder { - /** - * @var Metadata|null - */ - private $metadata = null; - - /** - * @var array|null - */ - private $samples = null; - - /** - * @param string $jsonMetadata JSON-encoded array of metadata fields. - * @return MetricBuilder - */ - public function withMetadata(string $jsonMetadata): MetricBuilder - { - $metadata = json_decode($jsonMetadata, true); - $this->metadata = Metadata::newBuilder() - ->withName($metadata['name']) - ->withHelp($metadata['help'] ?? null) - ->withLabelNames($metadata['labelNames'] ?? null) - ->withLabelValues($metadata['labelValues'] ?? null) - ->withMaxAgeSeconds($metadata['maxAgeSeconds'] ?? null) - ->withQuantiles($metadata['quantiles'] ?? null) - ->build(); - return $this; - } - - /** - * @param array $samples - * @return MetricBuilder - */ - public function withSamples(array $samples): MetricBuilder - { - $this->samples = $this->processSamples($samples); - return $this; - } - - /** - * @return Metric - */ - public function build(): Metric - { - $this->validate(); - return new Metric($this->metadata, $this->samples); - } - - /** - * @return void - * @throws InvalidArgumentException - */ - private function validate(): void - { - if ($this->metadata === null) { - throw new InvalidArgumentException('Summary metadata field is required.'); - } - - if ($this->samples === null) { - throw new InvalidArgumentException('Summary samples field is required.'); - } - } + /** + * @var Metadata|null + */ + private $metadata = null; + + /** + * @var array|null + */ + private $samples = null; + + /** + * @param string $jsonMetadata JSON-encoded array of metadata fields. + * @return SummaryMetricBuilder + */ + public function withMetadata(string $jsonMetadata): SummaryMetricBuilder + { + $metadata = json_decode($jsonMetadata, true); + $this->metadata = MetadataBuilder::fromArray($metadata)->build(); + return $this; + } + + /** + * @param array $samples + * @return SummaryMetricBuilder + */ + public function withSamples(array $samples): SummaryMetricBuilder + { + $this->samples = $this->processSummarySamples($samples); + return $this; + } + + /** + * @return Metric + */ + public function build(): Metric + { + $this->validate(); + return new Metric($this->metadata, $this->samples); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->metadata === null) { + throw new InvalidArgumentException('Summary metadata field is required.'); + } + + if ($this->samples === null) { + throw new InvalidArgumentException('Summary samples field is required.'); + } + } /** * Calculates the configured quantiles, count, and sum for a summary metric given a set of observed values. @@ -78,7 +71,7 @@ private function validate(): void * @param array $sourceSamples * @return array */ - private function processSamples(array $sourceSamples): array + private function processSummarySamples(array $sourceSamples): array { // Return value $samples = []; From 7f7d05068111f86a7acb3e822247d587e6872bc6 Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 12:15:31 -0500 Subject: [PATCH 08/14] Optimize gauge metric collection. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 264 ++++++++++++++++++++-------- 1 file changed, 193 insertions(+), 71 deletions(-) diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php index fae00f10..e28cf0a2 100644 --- a/src/Prometheus/Storage/RedisTxn.php +++ b/src/Prometheus/Storage/RedisTxn.php @@ -33,6 +33,7 @@ * "collect" operations of each metric type within a single Redis transaction. * * @todo Only summary metrics have been refactored so far. Complete refactor for counter, gauge, and histogram metrics. + * @todo Reimplement all Redis scripts with redis.pcall() to trap runtime errors that are ignored by redis.call(). */ class RedisTxn implements Adapter { @@ -371,34 +372,74 @@ public function updateSummary(array $data): void public function updateGauge(array $data): void { $this->ensureOpenConnection(); - $metaData = $data; - unset($metaData['value'], $metaData['labelValues'], $metaData['command']); - $this->redis->eval( - <<toMetadata($data); + + // Create Redis keys + $metricKey = $this->getMetricKey($metadata); + $registryKey = $this->getMetricRegistryKey($metadata->getType()); + $metadataKey = $this->getMetadataKey($metadata->getType()); + + // Prepare script and input + $command = $this->getRedisCommand($metadata->getCommand()); + $value = $data['value']; + $ttl = $metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS; + $numKeys = 3; + $scriptArgs = [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $command, + $value, + $ttl + ]; + $script = << 0 then + redis.call('expire', metricKey, ttl) + else + redis.call('persist', metricKey) end + + -- Register metric value + redis.call('sadd', registryKey, metricKey) + + -- Register metric metadata + redis.call('hset', metadataKey, metricKey, metadata) 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 + +-- Report script result +return didUpdate +LUA; + + // Call script + $this->redis->eval( + $script, + $scriptArgs, + $numKeys ); } @@ -419,11 +460,54 @@ public function updateCounter(array $data): void $metadataKey = $this->getMetadataKey($metadata->getType()); // Prepare script input - $command = $metadata->getCommand() === Adapter::COMMAND_INCREMENT_INTEGER ? 'incrby' : 'incrbyfloat'; + $command = $this->getRedisCommand($metadata->getCommand()); $value = $data['value']; - $ttl = time() + ($metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS); + $ttl = $metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS; + $scriptArgs = [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $command, + $value, + $ttl + ]; + $numKeyArgs = 3; + $script = <<redis->eval(<< 0 then + redis.call('expire', metricKey, ttl) + else + redis.call('persist', metricKey) + end + + -- Register metric value + redis.call('sadd', registryKey, metricKey) + + -- Register metric metadata + redis.call('hset', metadataKey, metricKey, metadata) +end + +-- Report script result +return didUpdate +LUA; + + $oldScript = <<toJson(), - $command, - $value, - $ttl - ], - 3 +LUA; + + // Call script + $result = $this->redis->eval( + $script, + $scriptArgs, + $numKeyArgs ); } @@ -655,26 +735,68 @@ private function collectSummaries(): array */ private function collectGauges(): array { - $keys = $this->redis->sMembers(self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); - sort($keys); + // Create Redis keys + $registryKey = $this->getMetricRegistryKey(Gauge::TYPE); + $metadataKey = $this->getMetadataKey(Gauge::TYPE); + + // Execute transaction to collect metrics + $result = $this->redis->eval(<<build(); + + // Create or update metric + $metricName = $metadata->getName(); + $builder = $metrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata); + $builder->withSample($gauge['samples'], $metadata->getLabelValues()); + $metrics[$metricName] = $builder; + } + + // Format metrics and hand them off to the calling collector $gauges = []; - foreach ($keys as $key) { - $raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key)); - $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, - ]; - } - usort($gauge['samples'], function ($a, $b): int { - return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); - }); - $gauges[] = $gauge; + foreach ($metrics as $_ => $metric) { + $gauges[] = $metric->build()->toArray(); } return $gauges; } @@ -696,24 +818,24 @@ private function collectCounters(): array -- Process each registered counter metric local result = {} -local counterKeys = redis.call('smembers', registryKey) -for i, counterKey in ipairs(counterKeys) do - local doesExist = redis.call('exists', counterKey) +local metricKeys = redis.call('smembers', registryKey) +for i, metricKey in ipairs(metricKeys) do + local doesExist = redis.call('exists', metricKey) if doesExist then -- Get counter metadata - local metadata = redis.call('hget', metadataKey, counterKey) + local metadata = redis.call('hget', metadataKey, metricKey) -- Get counter sample - local sample = redis.call('get', counterKey) + local sample = redis.call('get', metricKey) -- Add the processed metric to the set of results - result[counterKey] = {} - result[counterKey]["metadata"] = metadata - result[counterKey]["samples"] = sample + result[metricKey] = {} + result[metricKey]["metadata"] = metadata + result[metricKey]["samples"] = sample else -- Remove metadata for expired key - redis.call('srem', registryKey, counterKey) - redis.call('hdel', metadataKey, counterKey) + redis.call('srem', registryKey, metricKey) + redis.call('hdel', metadataKey, metricKey) end end @@ -758,11 +880,11 @@ private function getRedisCommand(int $cmd): string { switch ($cmd) { case Adapter::COMMAND_INCREMENT_INTEGER: - return 'hIncrBy'; + return 'incrby'; case Adapter::COMMAND_INCREMENT_FLOAT: - return 'hIncrByFloat'; + return 'incrbyfloat'; case Adapter::COMMAND_SET: - return 'hSet'; + return 'set'; default: throw new InvalidArgumentException("Unknown command"); } From c54a8ec3a679b728e3c7d95edb2f665e39445d27 Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 15:39:53 -0500 Subject: [PATCH 09/14] Reorganize concepts. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 640 ++---------------- .../RedisTxn/Collecter/AbstractCollecter.php | 57 ++ .../RedisTxn/Collecter/CollecterInterface.php | 36 + .../RedisTxn/Collecter/CounterCollecter.php | 96 +++ .../RedisTxn/Collecter/GaugeCollecter.php | 96 +++ .../RedisTxn/Collecter/SummaryCollecter.php | 104 +++ .../RedisTxn/{ => Metric}/Metadata.php | 2 +- .../RedisTxn/{ => Metric}/MetadataBuilder.php | 4 +- .../Storage/RedisTxn/{ => Metric}/Metric.php | 16 +- .../Storage/RedisTxn/{ => Metric}/Sample.php | 2 +- .../RedisTxn/{ => Metric}/SampleBuilder.php | 4 +- .../{ => Metric}/ScalarMetricBuilder.php | 2 +- .../{ => Metric}/SummaryMetricBuilder.php | 6 +- .../RedisTxn/RedisScript/RedisScript.php | 84 +++ .../RedisScript/RedisScriptBuilder.php | 77 +++ .../RedisScript/RedisScriptHelper.php | 79 +++ .../RedisTxn/Updater/CounterUpdater.php | 126 ++++ .../Storage/RedisTxn/Updater/GaugeUpdater.php | 133 ++++ .../RedisTxn/Updater/SummaryUpdater.php | 123 ++++ .../RedisTxn/Updater/UpdaterInterface.php | 32 + 20 files changed, 1105 insertions(+), 614 deletions(-) create mode 100644 src/Prometheus/Storage/RedisTxn/Collecter/AbstractCollecter.php create mode 100644 src/Prometheus/Storage/RedisTxn/Collecter/CollecterInterface.php create mode 100644 src/Prometheus/Storage/RedisTxn/Collecter/CounterCollecter.php create mode 100644 src/Prometheus/Storage/RedisTxn/Collecter/GaugeCollecter.php create mode 100644 src/Prometheus/Storage/RedisTxn/Collecter/SummaryCollecter.php rename src/Prometheus/Storage/RedisTxn/{ => Metric}/Metadata.php (98%) rename src/Prometheus/Storage/RedisTxn/{ => Metric}/MetadataBuilder.php (98%) rename src/Prometheus/Storage/RedisTxn/{ => Metric}/Metric.php (83%) rename src/Prometheus/Storage/RedisTxn/{ => Metric}/Sample.php (97%) rename src/Prometheus/Storage/RedisTxn/{ => Metric}/SampleBuilder.php (93%) rename src/Prometheus/Storage/RedisTxn/{ => Metric}/ScalarMetricBuilder.php (97%) rename src/Prometheus/Storage/RedisTxn/{ => Metric}/SummaryMetricBuilder.php (97%) create mode 100644 src/Prometheus/Storage/RedisTxn/RedisScript/RedisScript.php create mode 100644 src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptBuilder.php create mode 100644 src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptHelper.php create mode 100644 src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php create mode 100644 src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php create mode 100644 src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php create mode 100644 src/Prometheus/Storage/RedisTxn/Updater/UpdaterInterface.php diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php index e28cf0a2..76caa024 100644 --- a/src/Prometheus/Storage/RedisTxn.php +++ b/src/Prometheus/Storage/RedisTxn.php @@ -4,18 +4,15 @@ namespace Prometheus\Storage; -use InvalidArgumentException; -use Prometheus\Counter; use Prometheus\Exception\StorageException; -use Prometheus\Gauge; use Prometheus\Histogram; use Prometheus\MetricFamilySamples; -use Prometheus\Storage\RedisTxn\Metadata; -use Prometheus\Storage\RedisTxn\MetadataBuilder; -use Prometheus\Storage\RedisTxn\Metric; -use Prometheus\Summary; -use RedisException; -use RuntimeException; +use Prometheus\Storage\RedisTxn\Collecter\CounterCollecter; +use Prometheus\Storage\RedisTxn\Collecter\GaugeCollecter; +use Prometheus\Storage\RedisTxn\Collecter\SummaryCollecter; +use Prometheus\Storage\RedisTxn\Updater\CounterUpdater; +use Prometheus\Storage\RedisTxn\Updater\GaugeUpdater; +use Prometheus\Storage\RedisTxn\Updater\SummaryUpdater; use function \sort; /** @@ -41,8 +38,6 @@ class RedisTxn implements Adapter const PROMETHEUS_METRIC_META_SUFFIX = '_METRIC_META'; - const DEFAULT_TTL_SECONDS = 600; - /** * @var mixed[] */ @@ -103,22 +98,6 @@ public static function fromExistingConnection(\Redis $redis): self return $self; } - /** - * @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 @@ -163,50 +142,33 @@ public function wipeStorage(): void ); } - /** - * @param mixed[] $data - * - * @return string - */ - private function metaKey(array $data): string - { - return implode(':', [ - $data['name'], - 'meta' - ]); - } - - /** - * @param mixed[] $data - * - * @return string - */ - private function valueKey(array $data): string - { - return implode(':', [ - $data['name'], - $this->encodeLabelValues($data['labelValues']), - 'value' - ]); - } - /** * @return MetricFamilySamples[] * @throws StorageException */ public function collect(): array { + // Ensure Redis connection $this->ensureOpenConnection(); + $metrics = $this->collectHistograms(); - $metrics = array_merge($metrics, $this->collectGauges()); - $metrics = array_merge($metrics, $this->collectCounters()); - $metrics = array_merge($metrics, $this->collectSummaries()); - return array_map( + $metricFamilySamples = array_map( function (array $metric): MetricFamilySamples { return new MetricFamilySamples($metric); }, $metrics ); + + // Collect all metrics + $counters = $this->collectCounters(); + $gauges = $this->collectGauges(); + $summaries = $this->collectSummaries(); + return array_merge( + $metricFamilySamples, + $counters, + $gauges, + $summaries + ); } /** @@ -298,261 +260,42 @@ public function updateHistogram(array $data): void } /** - * @param array $data - * @return void - * @throws StorageException - * @throws RedisException + * @inheritDoc */ public function updateSummary(array $data): void { + // Ensure Redis connection $this->ensureOpenConnection(); - // Prepare metadata - $metadata = $this->toMetadata($data); - $ttl = $metadata->getMaxAgeSeconds(); - - // Create Redis keys - $metricKey = $this->getMetricKey($metadata); - $registryKey = $this->getMetricRegistryKey($metadata->getType()); - $metadataKey = $this->getMetadataKey($metadata->getType()); - - // Get summary sample - // - // NOTE: When we persist a summary metric sample into Redis, we write it into a Redis sorted set. - // We append the current time in microseconds as a suffix on the observed value to make each observed value - // durable and unique in the sorted set in accordance with best-practice guidelines described in the article, - // "Redis Best Practices: Sorted Set Time Series" [1]. - // - // See MetricBuilder::processSamples() for the complementary part of this operation. - // - // [1] https://redis.com/redis-best-practices/time-series/sorted-set-time-series/ - $value = implode(':', [$data['value'], microtime(true)]); - $currentTime = time(); - - // Commit the observed metric value - $this->redis->eval(<<toJson(), - $value, - $currentTime, - $ttl, - ], - 3 - ); + // Update metric + $updater = new SummaryUpdater($this->redis); + $updater->update($data); } /** - * @param mixed[] $data - * @throws StorageException + * @inheritDoc */ public function updateGauge(array $data): void { + // Ensure Redis connection $this->ensureOpenConnection(); - // Prepare metadata - $metadata = $this->toMetadata($data); - - // Create Redis keys - $metricKey = $this->getMetricKey($metadata); - $registryKey = $this->getMetricRegistryKey($metadata->getType()); - $metadataKey = $this->getMetadataKey($metadata->getType()); - - // Prepare script and input - $command = $this->getRedisCommand($metadata->getCommand()); - $value = $data['value']; - $ttl = $metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS; - $numKeys = 3; - $scriptArgs = [ - $registryKey, - $metadataKey, - $metricKey, - $metadata->toJson(), - $command, - $value, - $ttl - ]; - $script = << 0 then - redis.call('expire', metricKey, ttl) - else - redis.call('persist', metricKey) - end - - -- Register metric value - redis.call('sadd', registryKey, metricKey) - - -- Register metric metadata - redis.call('hset', metadataKey, metricKey, metadata) -end - --- Report script result -return didUpdate -LUA; - - // Call script - $this->redis->eval( - $script, - $scriptArgs, - $numKeys - ); + // Update metric + $updater = new GaugeUpdater($this->redis); + $updater->update($data); } /** - * @param mixed[] $data - * @throws StorageException + * @inheritDoc */ public function updateCounter(array $data): void { + // Ensure Redis connection $this->ensureOpenConnection(); - // Prepare metadata - $metadata = $this->toMetadata($data); - - // Create Redis keys - $metricKey = $this->getMetricKey($metadata); - $registryKey = $this->getMetricRegistryKey($metadata->getType()); - $metadataKey = $this->getMetadataKey($metadata->getType()); - - // Prepare script input - $command = $this->getRedisCommand($metadata->getCommand()); - $value = $data['value']; - $ttl = $metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS; - $scriptArgs = [ - $registryKey, - $metadataKey, - $metricKey, - $metadata->toJson(), - $command, - $value, - $ttl - ]; - $numKeyArgs = 3; - $script = << 0 then - redis.call('expire', metricKey, ttl) - else - redis.call('persist', metricKey) - end - - -- Register metric value - redis.call('sadd', registryKey, metricKey) - - -- Register metric metadata - redis.call('hset', metadataKey, metricKey, metadata) -end - --- Report script result -return didUpdate -LUA; - - $oldScript = <<redis->eval( - $script, - $scriptArgs, - $numKeyArgs - ); - } - - - /** - * @param mixed[] $data - * @return Metadata - */ - private function toMetadata(array $data): Metadata - { - return Metadata::newBuilder() - ->withName($data['name']) - ->withType($data['type']) - ->withHelp($data['help']) - ->withLabelNames($data['labelNames']) - ->withLabelValues($data['labelValues']) - ->withQuantiles($data['quantiles'] ?? null) - ->withMaxAgeSeconds($data['maxAgeSeconds'] ?? null) - ->withCommand($data['command'] ?? null) - ->build(); + // Update metric + $updater = new CounterUpdater($this->redis); + $updater->update($data); } /** @@ -633,261 +376,30 @@ private function collectHistograms(): array } /** - * @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 array + * @return MetricFamilySamples[] */ private function collectSummaries(): array { - // Register summary key - $keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX; - $summaryRegistryKey = implode(':', [$keyPrefix, 'keys']); - $metadataKey = $this->getMetadataKey(Summary::TYPE); - $currentTime = time(); - - $result = $this->redis->eval(<< 0 and summaryTtl < currentTime then - local startScore = currentTime - summaryTtl - redis.call("zremrangebyscore", summaryKey, "-inf", startScore) - end - - -- Retrieve the set of remaining metric samples - local numSamples = redis.call('zcard', summaryKey) - local summaryMetadata = {} - local summarySamples = {} - if numSamples > 0 then - -- Configure results - summaryMetadata = redis.call("hget", metadataKey, summaryKey) - summarySamples = redis.call("zrange", summaryKey, startScore, "+inf", "byscore") - else - -- Remove the metric's associated metadata if there are no associated samples remaining - redis.call('srem', summaryRegistryKey, summaryKey) - redis.call('hdel', metadataKey, summaryKey) - redis.call('hdel', metadataKey, ttlFieldName) - end - - -- Add the processed metric to the set of results - result[summaryKey] = {} - result[summaryKey]["metadata"] = summaryMetadata - result[summaryKey]["samples"] = summarySamples -end - --- Return the set of summary metrics -return cjson.encode(result) -LUA - , - [ - $summaryRegistryKey, - $metadataKey, - $currentTime, - ], - 2 - ); - - // Format metrics and hand them off to the calling collector - $summaries = []; - $redisSummaries = json_decode($result, true); - foreach ($redisSummaries as $summary) { - $serializedSummary = Metric::newSummaryMetricBuilder() - ->withMetadata($summary['metadata']) - ->withSamples($summary['samples']) - ->build() - ->toArray(); - $summaries[] = $serializedSummary; - } - return $summaries; + $collector = new SummaryCollecter($this->redis); + return $collector->getMetricFamilySamples(); } /** - * @return mixed[] + * @return MetricFamilySamples[] */ private function collectGauges(): array { - // Create Redis keys - $registryKey = $this->getMetricRegistryKey(Gauge::TYPE); - $metadataKey = $this->getMetadataKey(Gauge::TYPE); - - // Execute transaction to collect metrics - $result = $this->redis->eval(<<build(); - - // Create or update metric - $metricName = $metadata->getName(); - $builder = $metrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata); - $builder->withSample($gauge['samples'], $metadata->getLabelValues()); - $metrics[$metricName] = $builder; - } - - // Format metrics and hand them off to the calling collector - $gauges = []; - foreach ($metrics as $_ => $metric) { - $gauges[] = $metric->build()->toArray(); - } - return $gauges; + $collector = new GaugeCollecter($this->redis); + return $collector->getMetricFamilySamples(); } /** - * @return mixed[] + * @return MetricFamilySamples[] */ private function collectCounters(): array { - // Create Redis keys - $registryKey = $this->getMetricRegistryKey(Counter::TYPE); - $metadataKey = $this->getMetadataKey(Counter::TYPE); - - // Execute transaction to collect metrics - $result = $this->redis->eval(<<build(); - - // Create or update metric - $metricName = $metadata->getName(); - $builder = $metrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata); - $builder->withSample($counter['samples'], $metadata->getLabelValues()); - $metrics[$metricName] = $builder; - } - - // Format metrics and hand them off to the calling collector - $counters = []; - foreach ($metrics as $_ => $metric) { - $counters[] = $metric->build()->toArray(); - } - return $counters; - } - - /** - * @param int $cmd - * @return string - */ - private function getRedisCommand(int $cmd): string - { - switch ($cmd) { - case Adapter::COMMAND_INCREMENT_INTEGER: - return 'incrby'; - case Adapter::COMMAND_INCREMENT_FLOAT: - return 'incrbyfloat'; - case Adapter::COMMAND_SET: - return 'set'; - default: - throw new InvalidArgumentException("Unknown command"); - } + $collector = new CounterCollecter($this->redis); + return $collector->getMetricFamilySamples(); } /** @@ -898,68 +410,4 @@ 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 $metricType - * @return string - */ - private function getMetricRegistryKey(string $metricType): string - { - $keyPrefix = self::$prefix . $metricType . self::PROMETHEUS_METRIC_KEYS_SUFFIX; - return implode(':', [$keyPrefix, 'keys']); - } - - /** - * @param string $metricType - * @return string - */ - private function getMetadataKey(string $metricType): string - { - return self::$prefix . $metricType . self::PROMETHEUS_METRIC_META_SUFFIX; - } - - /** - * @param Metadata $metadata - * @return string - */ - private function getMetricKey(Metadata $metadata): string - { - $type = $metadata->getType(); - $name = $metadata->getName(); - $labelValues = $metadata->getLabelValuesEncoded(); - $keyPrefix = self::$prefix . $type . self::PROMETHEUS_METRIC_KEYS_SUFFIX; - return implode(':', [$keyPrefix, $name, $labelValues]); - } } diff --git a/src/Prometheus/Storage/RedisTxn/Collecter/AbstractCollecter.php b/src/Prometheus/Storage/RedisTxn/Collecter/AbstractCollecter.php new file mode 100644 index 00000000..c15049d3 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Collecter/AbstractCollecter.php @@ -0,0 +1,57 @@ +helper = new RedisScriptHelper(); + $this->redis = $redis; + } + + /** + * @inheritDoc + */ + public function getHelper(): RedisScriptHelper + { + return $this->helper; + } + + /** + * @inheritDoc + */ + public function getRedis(): Redis + { + return $this->redis; + } + + /** + * @inheritDoc + */ + public function getMetricFamilySamples(): array + { + $metricFamilySamples = []; + $metrics = $this->getMetrics(); + foreach ($metrics as $metric) { + $metricFamilySamples[] = $metric->toMetricFamilySamples(); + } + return $metricFamilySamples; + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Collecter/CollecterInterface.php b/src/Prometheus/Storage/RedisTxn/Collecter/CollecterInterface.php new file mode 100644 index 00000000..aa938fb4 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Collecter/CollecterInterface.php @@ -0,0 +1,36 @@ +getHelper()->getRegistryKey(Counter::TYPE); + $metadataKey = $this->getHelper()->getMetadataKey(Counter::TYPE); + $scriptArgs = [ + $registryKey, + $metadataKey, + ]; + + // Create Redis script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys($numKeys) + ->build(); + } + + /** + * @inheritDoc + */ + public function getMetrics(): array + { + // Retrieve metrics from Redis + $results = $this->getRedisScript()->eval($this->getRedis()); + + // Collate metrics by metric name + $phpMetrics = []; + $redisMetrics = json_decode($results, true); + foreach ($redisMetrics as $redisMetric) { + // Get metadata + $phpMetadata = json_decode($redisMetric['metadata'], true); + $metadata = MetadataBuilder::fromArray($phpMetadata)->build(); + + // Create or update metric + $metricName = $metadata->getName(); + $builder = $phpMetrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata); + $builder->withSample($redisMetric['samples'], $metadata->getLabelValues()); + $phpMetrics[$metricName] = $builder; + } + + // Build metrics + $metrics = []; + foreach ($phpMetrics as $_ => $metric) { + $metrics[] = $metric->build(); + } + return $metrics; + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Collecter/GaugeCollecter.php b/src/Prometheus/Storage/RedisTxn/Collecter/GaugeCollecter.php new file mode 100644 index 00000000..3aa036e2 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Collecter/GaugeCollecter.php @@ -0,0 +1,96 @@ +getHelper()->getRegistryKey(Gauge::TYPE); + $metadataKey = $this->getHelper()->getMetadataKey(Gauge::TYPE); + $scriptArgs = [ + $registryKey, + $metadataKey, + ]; + + // Create Redis script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys($numKeys) + ->build(); + } + + /** + * @inheritDoc + */ + public function getMetrics(): array + { + // Retrieve metrics from Redis + $results = $this->getRedisScript()->eval($this->getRedis()); + + // Collate metrics by metric name + $phpMetrics = []; + $redisMetrics = json_decode($results, true); + foreach ($redisMetrics as $redisMetric) { + // Get metadata + $phpMetadata = json_decode($redisMetric['metadata'], true); + $metadata = MetadataBuilder::fromArray($phpMetadata)->build(); + + // Create or update metric + $metricName = $metadata->getName(); + $builder = $phpMetrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata); + $builder->withSample($redisMetric['samples'], $metadata->getLabelValues()); + $phpMetrics[$metricName] = $builder; + } + + // Build metrics + $metrics = []; + foreach ($phpMetrics as $_ => $metric) { + $metrics[] = $metric->build(); + } + return $metrics; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Collecter/SummaryCollecter.php b/src/Prometheus/Storage/RedisTxn/Collecter/SummaryCollecter.php new file mode 100644 index 00000000..82dea178 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Collecter/SummaryCollecter.php @@ -0,0 +1,104 @@ + 0 and summaryTtl < currentTime then + local startScore = currentTime - summaryTtl + redis.call("zremrangebyscore", summaryKey, "-inf", startScore) + end + + -- Retrieve the set of remaining metric samples + local numSamples = redis.call('zcard', summaryKey) + local summaryMetadata = {} + local summarySamples = {} + if numSamples > 0 then + -- Configure results + summaryMetadata = redis.call("hget", metadataKey, summaryKey) + summarySamples = redis.call("zrange", summaryKey, startScore, "+inf", "byscore") + else + -- Remove the metric's associated metadata if there are no associated samples remaining + redis.call('srem', summaryRegistryKey, summaryKey) + redis.call('hdel', metadataKey, summaryKey) + redis.call('hdel', metadataKey, ttlFieldName) + end + + -- Add the processed metric to the set of results + result[summaryKey] = {} + result[summaryKey]["metadata"] = summaryMetadata + result[summaryKey]["samples"] = summarySamples +end + +-- Return the set of summary metrics +return cjson.encode(result) +LUA; + + /** + * @inheritDoc + */ + public function getRedisScript(): RedisScript + { + // Create Redis script args + $numKeys = 2; + $registryKey = $this->getHelper()->getRegistryKey(Summary::TYPE); + $metadataKey = $this->getHelper()->getMetadataKey(Summary::TYPE); + $currentTime = time(); + $scriptArgs = [ + $registryKey, + $metadataKey, + $currentTime + ]; + + // Create Redis script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys($numKeys) + ->build(); + } + + /** + * @inheritDoc + */ + public function getMetrics(): array + { + // Retrieve metrics from Redis + $results = $this->getRedisScript()->eval($this->getRedis()); + + // Format metrics as MetricFamilySamples + $metrics = []; + $redisMetrics = json_decode($results, true); + foreach ($redisMetrics as $redisMetric) { + $metrics[] = Metric::newSummaryMetricBuilder() + ->withMetadata($redisMetric['metadata']) + ->withSamples($redisMetric['samples']) + ->build(); + } + return $metrics; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Metadata.php b/src/Prometheus/Storage/RedisTxn/Metric/Metadata.php similarity index 98% rename from src/Prometheus/Storage/RedisTxn/Metadata.php rename to src/Prometheus/Storage/RedisTxn/Metric/Metadata.php index c082eadd..77830663 100644 --- a/src/Prometheus/Storage/RedisTxn/Metadata.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/Metadata.php @@ -1,6 +1,6 @@ $this->metadata->getName(), 'help' => $this->metadata->getHelp(), 'type' => $this->metadata->getType(), @@ -72,6 +70,6 @@ public function toArray(): array 'maxAgeSeconds' => $this->metadata->getMaxAgeSeconds(), 'quantiles' => $this->metadata->getQuantiles() ?? [], 'samples' => $this->samples, - ]; + ]); } } diff --git a/src/Prometheus/Storage/RedisTxn/Sample.php b/src/Prometheus/Storage/RedisTxn/Metric/Sample.php similarity index 97% rename from src/Prometheus/Storage/RedisTxn/Sample.php rename to src/Prometheus/Storage/RedisTxn/Metric/Sample.php index 51b18d54..a5a04acb 100644 --- a/src/Prometheus/Storage/RedisTxn/Sample.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/Sample.php @@ -1,6 +1,6 @@ script = $script; + $this->args = $args; + $this->numKeys = $numKeys; + } + + /** + * @return string + */ + public function getScript(): string + { + return $this->script; + } + + /** + * @return array + */ + public function getArgs(): array + { + return $this->args; + } + + /** + * @return int + */ + public function getNumKeys(): int + { + return $this->numKeys; + } + + /** + * @param Redis $redis + * @return mixed + */ + public function eval(Redis $redis) + { + return $redis->eval( + $this->getScript(), + $this->getArgs(), + $this->getNumKeys() + ); + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptBuilder.php b/src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptBuilder.php new file mode 100644 index 00000000..a37b58f8 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptBuilder.php @@ -0,0 +1,77 @@ +script = $script; + return $this; + } + + /** + * @param array $args + * @return RedisScriptBuilder + */ + public function withArgs(array $args): RedisScriptBuilder + { + $this->args = $args; + return $this; + } + + /** + * @param int $numKeys + * @return RedisScriptBuilder + */ + public function withNumKeys(int $numKeys): RedisScriptBuilder + { + $this->numKeys = $numKeys; + return $this; + } + + /** + * @return RedisScript + */ + public function build(): RedisScript + { + $this->validate(); + return new RedisScript( + $this->script, + $this->args ?? [], + $this->numKeys ?? 0 + ); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->script === null) { + throw new InvalidArgumentException('A Redis script is required.'); + } + } +} diff --git a/src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptHelper.php b/src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptHelper.php new file mode 100644 index 00000000..c697ec5f --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/RedisScript/RedisScriptHelper.php @@ -0,0 +1,79 @@ +getType(); + $name = $metadata->getName(); + $labelValues = $metadata->getLabelValuesEncoded(); + $keyPrefix = self::PREFIX . $type . self::PROMETHEUS_METRIC_KEYS_SUFFIX; + return implode(':', [$keyPrefix, $name, $labelValues]); + } + + /** + * @param int $cmd + * @return string + */ + public function getRedisCommand(int $cmd): string + { + switch ($cmd) { + case Adapter::COMMAND_INCREMENT_INTEGER: + return 'incrby'; + case Adapter::COMMAND_INCREMENT_FLOAT: + return 'incrbyfloat'; + case Adapter::COMMAND_SET: + return 'set'; + default: + throw new InvalidArgumentException("Unknown command"); + } + } + + /** + * @return int + */ + public function getDefautlTtl(): int + { + return self::DEFAULT_TTL_SECONDS; + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php new file mode 100644 index 00000000..d50384dc --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php @@ -0,0 +1,126 @@ + 0 then + redis.call('expire', metricKey, ttl) + else + redis.call('persist', metricKey) + end + + -- Register metric value + redis.call('sadd', registryKey, metricKey) + + -- Register metric metadata + redis.call('hset', metadataKey, metricKey, metadata) +end + +-- Report script result +return didUpdate +LUA; + + /** + * @var RedisScriptHelper + */ + private $helper; + + /** + * @var Redis + */ + private $redis; + + /** + * @param Redis $redis + */ + public function __construct(Redis $redis) + { + $this->helper = new RedisScriptHelper(); + $this->redis = $redis; + } + + /** + * @inheritDoc + */ + public function getHelper(): RedisScriptHelper + { + return $this->helper; + } + + /** + * @inheritDoc + */ + public function getRedis(): Redis + { + return $this->redis; + } + + /** + * @inheritDoc + */ + public function getRedisScript(array $data): RedisScript + { + // Prepare metadata + $metadata = MetadataBuilder::fromArray($data)->build(); + + // Create Redis keys + $metricKey = $this->getHelper()->getMetricKey($metadata); + $registryKey = $this->getHelper()->getRegistryKey($metadata->getType()); + $metadataKey = $this->getHelper()->getMetadataKey($metadata->getType()); + + // Prepare script input + $command = $this->getHelper()->getRedisCommand($metadata->getCommand()); + $value = $data['value']; + $ttl = $metadata->getMaxAgeSeconds() ?? $this->getHelper()->getDefautlTtl(); + $scriptArgs = [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $command, + $value, + $ttl + ]; + + // Return script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys(3) + ->build(); + } + + /** + * @inheritDoc + */ + public function update(array $data) + { + return $this->getRedisScript($data)->eval($this->redis); + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php new file mode 100644 index 00000000..69b4d19b --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php @@ -0,0 +1,133 @@ + 0 then + redis.call('expire', metricKey, ttl) + else + redis.call('persist', metricKey) + end + + -- Register metric value + redis.call('sadd', registryKey, metricKey) + + -- Register metric metadata + redis.call('hset', metadataKey, metricKey, metadata) +end + +-- Report script result +return didUpdate +LUA; + + /** + * @var RedisScriptHelper + */ + private $helper; + + /** + * @var Redis + */ + private $redis; + + /** + * @param Redis $redis + */ + public function __construct(Redis $redis) + { + $this->helper = new RedisScriptHelper(); + $this->redis = $redis; + } + + /** + * @inheritDoc + */ + public function getHelper(): RedisScriptHelper + { + $this->helper = $this->helper ?? new RedisScriptHelper(); + return $this->helper; + } + + /** + * @inheritDoc + */ + public function getRedis(): Redis + { + return $this->redis; + } + + /** + * @inheritDoc + */ + public function getRedisScript(array $data): RedisScript + { + // Prepare metadata + $metadata = MetadataBuilder::fromArray($data)->build(); + + // Create Redis keys + $registryKey = $this->getHelper()->getRegistryKey($metadata->getType()); + $metadataKey = $this->getHelper()->getMetadataKey($metadata->getType()); + $metricKey = $this->getHelper()->getMetricKey($metadata); + + // Prepare script input + $command = $this->getHelper()->getRedisCommand($metadata->getCommand()); + $value = $data['value']; + $ttl = $metadata->getMaxAgeSeconds() ?? $this->getHelper()->getDefautlTtl(); + $scriptArgs = [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $command, + $value, + $ttl + ]; + + // Return script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys(3) + ->build(); + } + + /** + * @inheritDoc + */ + public function update(array $data) + { + return $this->getRedisScript($data)->eval($this->redis); + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php new file mode 100644 index 00000000..1389ee9d --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php @@ -0,0 +1,123 @@ +helper = new RedisScriptHelper(); + $this->redis = $redis; + } + + public function getHelper(): RedisScriptHelper + { + $this->helper = $this->helper ?? new RedisScriptHelper(); + return $this->helper; + } + + /** + * @inheritDoc + */ + public function getRedis(): Redis + { + return $this->redis; + } + + /** + * @inheritDoc + */ + public function getRedisScript(array $data): RedisScript + { + // Prepare metadata + $metadata = MetadataBuilder::fromArray($data)->build(); + + // Create Redis keys + $registryKey = $this->getHelper()->getRegistryKey($metadata->getType()); + $metadataKey = $this->getHelper()->getMetadataKey($metadata->getType()); + $metricKey = $this->getHelper()->getMetricKey($metadata); + + // Get summary sample + // + // NOTE: When we persist a summary metric sample into Redis, we write it into a Redis sorted set. + // We append the current time in microseconds as a suffix on the observed value to make each observed value + // durable and unique in the sorted set in accordance with best-practice guidelines described in the article, + // "Redis Best Practices: Sorted Set Time Series" [1]. + // + // See MetricBuilder::processSamples() for the complementary part of this operation. + // + // [1] https://redis.com/redis-best-practices/time-series/sorted-set-time-series/ + $value = implode(':', [$data['value'], microtime(true)]); + + // Prepare script input + $currentTime = time(); + $ttl = $metadata->getMaxAgeSeconds(); + $scriptArgs = [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $value, + $currentTime, + $ttl, + ]; + + // Return script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys(3) + ->build(); + } + + /** + * @inheritDoc + */ + public function update(array $data) + { + return $this->getRedisScript($data)->eval($this->redis); + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Updater/UpdaterInterface.php b/src/Prometheus/Storage/RedisTxn/Updater/UpdaterInterface.php new file mode 100644 index 00000000..9d7050e5 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Updater/UpdaterInterface.php @@ -0,0 +1,32 @@ + Date: Sun, 22 Jan 2023 15:44:41 -0500 Subject: [PATCH 10/14] Add todo note. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php index 76caa024..5f32bcf0 100644 --- a/src/Prometheus/Storage/RedisTxn.php +++ b/src/Prometheus/Storage/RedisTxn.php @@ -30,6 +30,7 @@ * "collect" operations of each metric type within a single Redis transaction. * * @todo Only summary metrics have been refactored so far. Complete refactor for counter, gauge, and histogram metrics. + * @todo Reimplement wipeStorage() to account for reorganized keys in Redis. * @todo Reimplement all Redis scripts with redis.pcall() to trap runtime errors that are ignored by redis.call(). */ class RedisTxn implements Adapter From 488710052c6e196e5aa1c3e0845e7addcdccdc5b Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 15:57:35 -0500 Subject: [PATCH 11/14] Implement AbstractUpdater. Signed-off-by: Arris Ray --- .../RedisTxn/Updater/AbstractUpdater.php | 52 +++++++++++++++++++ .../RedisTxn/Updater/CounterUpdater.php | 47 +---------------- .../Storage/RedisTxn/Updater/GaugeUpdater.php | 48 +---------------- .../RedisTxn/Updater/SummaryUpdater.php | 45 +--------------- 4 files changed, 55 insertions(+), 137 deletions(-) create mode 100644 src/Prometheus/Storage/RedisTxn/Updater/AbstractUpdater.php diff --git a/src/Prometheus/Storage/RedisTxn/Updater/AbstractUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/AbstractUpdater.php new file mode 100644 index 00000000..62444787 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Updater/AbstractUpdater.php @@ -0,0 +1,52 @@ +helper = new RedisScriptHelper(); + $this->redis = $redis; + } + + /** + * @inheritDoc + */ + public function getHelper(): RedisScriptHelper + { + return $this->helper; + } + + /** + * @inheritDoc + */ + public function getRedis(): Redis + { + return $this->redis; + } + + /** + * @inheritDoc + */ + public function update(array $data) + { + return $this->getRedisScript($data)->eval($this->getRedis()); + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php index d50384dc..a5ad1c8d 100644 --- a/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php +++ b/src/Prometheus/Storage/RedisTxn/Updater/CounterUpdater.php @@ -4,10 +4,8 @@ use Prometheus\Storage\RedisTxn\Metric\MetadataBuilder; use Prometheus\Storage\RedisTxn\RedisScript\RedisScript; -use Prometheus\Storage\RedisTxn\RedisScript\RedisScriptHelper; -use Redis; -class CounterUpdater implements UpdaterInterface +class CounterUpdater extends AbstractUpdater { /** * @var string @@ -46,41 +44,6 @@ class CounterUpdater implements UpdaterInterface return didUpdate LUA; - /** - * @var RedisScriptHelper - */ - private $helper; - - /** - * @var Redis - */ - private $redis; - - /** - * @param Redis $redis - */ - public function __construct(Redis $redis) - { - $this->helper = new RedisScriptHelper(); - $this->redis = $redis; - } - - /** - * @inheritDoc - */ - public function getHelper(): RedisScriptHelper - { - return $this->helper; - } - - /** - * @inheritDoc - */ - public function getRedis(): Redis - { - return $this->redis; - } - /** * @inheritDoc */ @@ -115,12 +78,4 @@ public function getRedisScript(array $data): RedisScript ->withNumKeys(3) ->build(); } - - /** - * @inheritDoc - */ - public function update(array $data) - { - return $this->getRedisScript($data)->eval($this->redis); - } } \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php index 69b4d19b..c7eaf5d2 100644 --- a/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php +++ b/src/Prometheus/Storage/RedisTxn/Updater/GaugeUpdater.php @@ -4,10 +4,8 @@ use Prometheus\Storage\RedisTxn\Metric\MetadataBuilder; use Prometheus\Storage\RedisTxn\RedisScript\RedisScript; -use Prometheus\Storage\RedisTxn\RedisScript\RedisScriptHelper; -use Redis; -class GaugeUpdater implements UpdaterInterface +class GaugeUpdater extends AbstractUpdater { /** * @var string @@ -52,42 +50,6 @@ class GaugeUpdater implements UpdaterInterface return didUpdate LUA; - /** - * @var RedisScriptHelper - */ - private $helper; - - /** - * @var Redis - */ - private $redis; - - /** - * @param Redis $redis - */ - public function __construct(Redis $redis) - { - $this->helper = new RedisScriptHelper(); - $this->redis = $redis; - } - - /** - * @inheritDoc - */ - public function getHelper(): RedisScriptHelper - { - $this->helper = $this->helper ?? new RedisScriptHelper(); - return $this->helper; - } - - /** - * @inheritDoc - */ - public function getRedis(): Redis - { - return $this->redis; - } - /** * @inheritDoc */ @@ -122,12 +84,4 @@ public function getRedisScript(array $data): RedisScript ->withNumKeys(3) ->build(); } - - /** - * @inheritDoc - */ - public function update(array $data) - { - return $this->getRedisScript($data)->eval($this->redis); - } } diff --git a/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php index 1389ee9d..255f604b 100644 --- a/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php +++ b/src/Prometheus/Storage/RedisTxn/Updater/SummaryUpdater.php @@ -4,10 +4,8 @@ use Prometheus\Storage\RedisTxn\Metric\MetadataBuilder; use Prometheus\Storage\RedisTxn\RedisScript\RedisScript; -use Prometheus\Storage\RedisTxn\RedisScript\RedisScriptHelper; -use Redis; -class SummaryUpdater implements UpdaterInterface +class SummaryUpdater extends AbstractUpdater { /** * @var string @@ -34,39 +32,6 @@ class SummaryUpdater implements UpdaterInterface redis.call('hset', metaHashKey, ttlFieldName, ttlFieldValue) LUA; - /** - * @var RedisScriptHelper - */ - private $helper; - - /** - * @var Redis - */ - private $redis; - - /** - * @param Redis $redis - */ - public function __construct(Redis $redis) - { - $this->helper = new RedisScriptHelper(); - $this->redis = $redis; - } - - public function getHelper(): RedisScriptHelper - { - $this->helper = $this->helper ?? new RedisScriptHelper(); - return $this->helper; - } - - /** - * @inheritDoc - */ - public function getRedis(): Redis - { - return $this->redis; - } - /** * @inheritDoc */ @@ -112,12 +77,4 @@ public function getRedisScript(array $data): RedisScript ->withNumKeys(3) ->build(); } - - /** - * @inheritDoc - */ - public function update(array $data) - { - return $this->getRedisScript($data)->eval($this->redis); - } } From e2c2c8e120e4d5e28dc1120801b41ff161474a91 Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 20:41:16 -0500 Subject: [PATCH 12/14] Optimize histogram metric collection. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 137 ++---------------- .../RedisTxn/Collecter/HistogramCollecter.php | 99 +++++++++++++ .../Metric/HistogramMetricBuilder.php | 126 ++++++++++++++++ .../Storage/RedisTxn/Metric/Metadata.php | 17 +++ .../RedisTxn/Metric/MetadataBuilder.php | 29 ++++ .../Storage/RedisTxn/Metric/Metric.php | 14 +- .../Storage/RedisTxn/Metric/Sample.php | 32 ++++ .../RedisTxn/Metric/SummaryMetricBuilder.php | 4 +- .../RedisTxn/Updater/HistogramUpdater.php | 99 +++++++++++++ 9 files changed, 427 insertions(+), 130 deletions(-) create mode 100644 src/Prometheus/Storage/RedisTxn/Collecter/HistogramCollecter.php create mode 100644 src/Prometheus/Storage/RedisTxn/Metric/HistogramMetricBuilder.php create mode 100644 src/Prometheus/Storage/RedisTxn/Updater/HistogramUpdater.php diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php index 5f32bcf0..35cf8bc5 100644 --- a/src/Prometheus/Storage/RedisTxn.php +++ b/src/Prometheus/Storage/RedisTxn.php @@ -9,9 +9,11 @@ use Prometheus\MetricFamilySamples; use Prometheus\Storage\RedisTxn\Collecter\CounterCollecter; use Prometheus\Storage\RedisTxn\Collecter\GaugeCollecter; +use Prometheus\Storage\RedisTxn\Collecter\HistogramCollecter; use Prometheus\Storage\RedisTxn\Collecter\SummaryCollecter; use Prometheus\Storage\RedisTxn\Updater\CounterUpdater; use Prometheus\Storage\RedisTxn\Updater\GaugeUpdater; +use Prometheus\Storage\RedisTxn\Updater\HistogramUpdater; use Prometheus\Storage\RedisTxn\Updater\SummaryUpdater; use function \sort; @@ -35,10 +37,6 @@ */ class RedisTxn implements Adapter { - const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; - - const PROMETHEUS_METRIC_META_SUFFIX = '_METRIC_META'; - /** * @var mixed[] */ @@ -152,21 +150,14 @@ public function collect(): array // Ensure Redis connection $this->ensureOpenConnection(); - $metrics = $this->collectHistograms(); - $metricFamilySamples = array_map( - function (array $metric): MetricFamilySamples { - return new MetricFamilySamples($metric); - }, - $metrics - ); - // Collect all metrics $counters = $this->collectCounters(); + $histograms = $this->collectHistograms(); $gauges = $this->collectGauges(); $summaries = $this->collectSummaries(); return array_merge( - $metricFamilySamples, $counters, + $histograms, $gauges, $summaries ); @@ -221,43 +212,16 @@ private function connectToServer(): void } /** - * @param mixed[] $data - * @throws StorageException + * @inheritDoc */ public function updateHistogram(array $data): void { + // Ensure Redis connection $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 - ); + // Update metric + $updater = new HistogramUpdater($this->redis); + $updater->update($data); } /** @@ -300,80 +264,12 @@ public function updateCounter(array $data): void } /** - * @return mixed[] + * @return MetricFamilySamples[] */ 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(str_replace($this->redis->_prefix(''), '', $key)); - $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']; - } - - // 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; + $collector = new HistogramCollecter($this->redis); + return $collector->getMetricFamilySamples(); } /** @@ -402,13 +298,4 @@ private function collectCounters(): array $collector = new CounterCollecter($this->redis); return $collector->getMetricFamilySamples(); } - - /** - * @param mixed[] $data - * @return string - */ - private function toMetricKey(array $data): string - { - return implode(':', [self::$prefix, $data['type'], $data['name']]); - } } diff --git a/src/Prometheus/Storage/RedisTxn/Collecter/HistogramCollecter.php b/src/Prometheus/Storage/RedisTxn/Collecter/HistogramCollecter.php new file mode 100644 index 00000000..5473ddc8 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Collecter/HistogramCollecter.php @@ -0,0 +1,99 @@ +getHelper()->getRegistryKey(Histogram::TYPE); + $metadataKey = $this->getHelper()->getMetadataKey(Histogram::TYPE); + $scriptArgs = [ + $registryKey, + $metadataKey, + ]; + + // Create Redis script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys($numKeys) + ->build(); + } + + /** + * @inheritDoc + */ + public function getMetrics(): array + { + // Retrieve metrics from Redis + $results = $this->getRedisScript()->eval($this->getRedis()); + + // Collate histogram observations by metric name + $builders = []; + $redisMetrics = json_decode($results, true); + foreach ($redisMetrics as $redisMetric) { + $phpMetadata = json_decode($redisMetric['metadata'], true); + $metadata = MetadataBuilder::fromArray($phpMetadata)->build(); + $builder = $builders[$metadata->getName()] ?? Metric::newHistogramMetricBuilder()->withMetadata($metadata); + $builder->withSamples($redisMetric['samples'], $metadata->getLabelValues()); + $builders[$metadata->getName()] = $builder; + } + + // Build collated histograms into Metric structures + $metrics = []; + foreach ($builders as $builder) { + $metrics[] = $builder->build(); + } + return $metrics; + } +} diff --git a/src/Prometheus/Storage/RedisTxn/Metric/HistogramMetricBuilder.php b/src/Prometheus/Storage/RedisTxn/Metric/HistogramMetricBuilder.php new file mode 100644 index 00000000..769596c2 --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Metric/HistogramMetricBuilder.php @@ -0,0 +1,126 @@ +metadata = $metadata; + return $this; + } + + /** + * @param array $samples + * @param array $labelValues + * @return HistogramMetricBuilder + */ + public function withSamples(array $samples, array $labelValues): HistogramMetricBuilder + { + $jsonLabelValues = json_encode($labelValues); + $this->samples[$jsonLabelValues] = $this->processSamples($samples, $labelValues); + return $this; + } + + /** + * @return Metric + */ + public function build(): Metric + { + // Validate + $this->validate(); + + // Natural sort samples by label values + ksort($this->samples, SORT_NATURAL); + + // Flatten observation samples into a single collection + $samples = []; + foreach ($this->samples as $observation) { + foreach ($observation as $observationSample) { + $samples[] = $observationSample->toArray(); + } + } + + // Return metric + return new Metric($this->metadata, $samples); + } + + /** + * @return void + * @throws InvalidArgumentException + */ + private function validate(): void + { + if ($this->metadata === null) { + throw new InvalidArgumentException('Summary metadata field is required.'); + } + + if ($this->samples === null) { + throw new InvalidArgumentException('Summary samples field is required.'); + } + } + + /** + * @param array $sourceSamples + * @param array $labelValues + * @return Sample[] + */ + private function processSamples(array $sourceSamples, array $labelValues): array + { + // Return value + $samples = []; + + // Calculate bucket samples + $bucketSamples = 0.0; + foreach ($this->metadata->getBuckets() as $bucket) { + $bucketSamples += floatval($sourceSamples[$bucket] ?? 0.0); + $name = $this->metadata->getName() . "_bucket"; + $samples[] = Sample::newBuilder() + ->withName($name) + ->withLabelNames(["le"]) + ->withLabelValues(array_merge($labelValues, [$bucket])) + ->withValue($bucketSamples) + ->build(); + } + + // Calculate bucket count + $name = $this->metadata->getName() . "_count"; + $samples[] = Sample::newBuilder() + ->withName($name) + ->withLabelNames([]) + ->withLabelValues($labelValues) + ->withValue($sourceSamples['count'] ?? 0) + ->build(); + + // Calculate bucket sum + $name = $this->metadata->getName() . "_sum"; + $samples[] = Sample::newBuilder() + ->withName($name) + ->withLabelNames([]) + ->withLabelValues($labelValues) + ->withValue($sourceSamples['sum'] ?? 0) + ->build(); + + // Return processed samples + return $samples; + } +} \ No newline at end of file diff --git a/src/Prometheus/Storage/RedisTxn/Metric/Metadata.php b/src/Prometheus/Storage/RedisTxn/Metric/Metadata.php index 77830663..beb5ae2e 100644 --- a/src/Prometheus/Storage/RedisTxn/Metric/Metadata.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/Metadata.php @@ -39,6 +39,11 @@ class Metadata */ private $maxAgeSeconds; + /** + * @var float[] + */ + private $buckets; + /** * @var float[] */ @@ -64,6 +69,7 @@ public static function newBuilder(): MetadataBuilder * @param array $labelNames * @param array $labelValues * @param int $maxAgeSeconds + * @param array $buckets * @param array $quantiles * @param int $command */ @@ -74,6 +80,7 @@ public function __construct( array $labelNames, array $labelValues, int $maxAgeSeconds, + array $buckets, array $quantiles, int $command ) @@ -84,6 +91,7 @@ public function __construct( $this->labelNames = $labelNames; $this->labelValues = $labelValues; $this->maxAgeSeconds = $maxAgeSeconds; + $this->buckets = $buckets; $this->quantiles = $quantiles; $this->command = $command; } @@ -170,6 +178,14 @@ public function getMaxAgeSeconds(): int return $this->maxAgeSeconds; } + /** + * @return float[] + */ + public function getBuckets(): array + { + return $this->buckets; + } + /** * Prometheus metric metadata that describes the set of quantiles to report for a summary-type metric. * @@ -202,6 +218,7 @@ public function toJson(): string 'labelNames' => $this->getLabelNames(), 'labelValues' => $this->getLabelValuesEncoded(), 'maxAgeSeconds' => $this->getMaxAgeSeconds(), + 'buckets' => $this->getBuckets(), 'quantiles' => $this->getQuantiles(), 'command' => $this->getCommand(), ]); diff --git a/src/Prometheus/Storage/RedisTxn/Metric/MetadataBuilder.php b/src/Prometheus/Storage/RedisTxn/Metric/MetadataBuilder.php index 631c166b..4216f569 100644 --- a/src/Prometheus/Storage/RedisTxn/Metric/MetadataBuilder.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/MetadataBuilder.php @@ -40,6 +40,11 @@ class MetadataBuilder */ private $maxAgeSeconds; + /** + * @var float[]|null + */ + private $buckets; + /** * @var float[]|null */ @@ -63,6 +68,7 @@ public static function fromArray(array $metadata): MetadataBuilder ->withLabelNames($metadata['labelNames'] ?? null) ->withLabelValues($metadata['labelValues'] ?? null) ->withMaxAgeSeconds($metadata['maxAgeSeconds'] ?? null) + ->withBuckets($metadata['buckets'] ?? null) ->withQuantiles($metadata['quantiles'] ?? null) ->withCommand($metadata['command'] ?? null); } @@ -132,6 +138,28 @@ public function withMaxAgeSeconds(?int $maxAgeSeconds): MetadataBuilder return $this; } + /** + * @param float[]|null $buckets + * @return MetadataBuilder + */ + public function withBuckets(?array $buckets): MetadataBuilder + { + $this->buckets = $buckets; + if ($buckets !== null) { + // Stringify bucket keys + // NOTE: We do this because PHP implicitly truncates floats to int values when used as associative array keys. + $this->buckets = array_map(function ($key) { + return strval($key); + }, $this->buckets); + + // Add +Inf bucket + if (!in_array('+Inf', $this->buckets)) { + $this->buckets[] = '+Inf'; + } + } + return $this; + } + /** * @param float[]|null $quantiles * @return MetadataBuilder @@ -165,6 +193,7 @@ public function build(): Metadata $this->labelNames ?? [], $this->labelValues ?? [], $this->maxAgeSeconds ?? 0, + $this->buckets ?? [], $this->quantiles ?? [], $this->command ?? Adapter::COMMAND_INCREMENT_FLOAT ); diff --git a/src/Prometheus/Storage/RedisTxn/Metric/Metric.php b/src/Prometheus/Storage/RedisTxn/Metric/Metric.php index 9947b27e..4d422e3a 100644 --- a/src/Prometheus/Storage/RedisTxn/Metric/Metric.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/Metric.php @@ -24,11 +24,11 @@ class Metric private $samples; /** - * @return SummaryMetricBuilder + * @return HistogramMetricBuilder */ - public static function newSummaryMetricBuilder(): SummaryMetricBuilder + public static function newHistogramMetricBuilder(): HistogramMetricBuilder { - return new SummaryMetricBuilder(); + return new HistogramMetricBuilder(); } /** @@ -39,6 +39,14 @@ public static function newScalarMetricBuilder(): ScalarMetricBuilder return new ScalarMetricBuilder(); } + /** + * @return SummaryMetricBuilder + */ + public static function newSummaryMetricBuilder(): SummaryMetricBuilder + { + return new SummaryMetricBuilder(); + } + /** * @param Metadata $metadata * @param array|int|float $samples diff --git a/src/Prometheus/Storage/RedisTxn/Metric/Sample.php b/src/Prometheus/Storage/RedisTxn/Metric/Sample.php index a5a04acb..d8c93467 100644 --- a/src/Prometheus/Storage/RedisTxn/Metric/Sample.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/Sample.php @@ -61,6 +61,38 @@ public function __construct( $this->value = $value; } + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return array|string[] + */ + public function getLabelNames(): array + { + return $this->labelNames; + } + + /** + * @return array + */ + public function getLabelValues(): array + { + return $this->labelValues; + } + + /** + * @return float|int + */ + public function getValue() + { + return $this->value; + } + /** * Represents this structure as a PHP associative array. * diff --git a/src/Prometheus/Storage/RedisTxn/Metric/SummaryMetricBuilder.php b/src/Prometheus/Storage/RedisTxn/Metric/SummaryMetricBuilder.php index dc779f0f..d6d8203e 100644 --- a/src/Prometheus/Storage/RedisTxn/Metric/SummaryMetricBuilder.php +++ b/src/Prometheus/Storage/RedisTxn/Metric/SummaryMetricBuilder.php @@ -39,7 +39,7 @@ public function withMetadata(string $jsonMetadata): SummaryMetricBuilder */ public function withSamples(array $samples): SummaryMetricBuilder { - $this->samples = $this->processSummarySamples($samples); + $this->samples = $this->processSamples($samples); return $this; } @@ -73,7 +73,7 @@ private function validate(): void * @param array $sourceSamples * @return array */ - private function processSummarySamples(array $sourceSamples): array + private function processSamples(array $sourceSamples): array { // Return value $samples = []; diff --git a/src/Prometheus/Storage/RedisTxn/Updater/HistogramUpdater.php b/src/Prometheus/Storage/RedisTxn/Updater/HistogramUpdater.php new file mode 100644 index 00000000..eedec96c --- /dev/null +++ b/src/Prometheus/Storage/RedisTxn/Updater/HistogramUpdater.php @@ -0,0 +1,99 @@ + value +-- if didUpdate ~= true then +-- return false +-- end + +-- Update metric count +local result = redis.call("hincrby", metricKey, 'count', 1) + +-- Update bucket count +result = redis.call("hincrby", metricKey, bucket, 1) +didUpdate = result >= 1 +-- if didUpdate ~= true then +-- return false +-- end + +-- Set metric TTL +-- if ttl > 0 then +-- redis.call('expire', metricKey, ttl) +-- else +-- redis.call('persist', metricKey) +-- end + +-- Register metric key +redis.call('sadd', registryKey, metricKey) + +-- Register metric metadata +redis.call('hset', metadataKey, metricKey, metadata) + +-- Report script result +return true +LUA; + + /** + * @inheritDoc + */ + public function getRedisScript(array $data): RedisScript + { + // Prepare metadata + $metadata = MetadataBuilder::fromArray($data)->build(); + + // Create Redis keys + $metricKey = $this->getHelper()->getMetricKey($metadata); + $registryKey = $this->getHelper()->getRegistryKey($metadata->getType()); + $metadataKey = $this->getHelper()->getMetadataKey($metadata->getType()); + + // Determine minimum eligible bucket + $value = floatval($data['value']); + $targetBucket = '+Inf'; + foreach ($metadata->getBuckets() as $bucket) { + if ($value <= $bucket) { + $targetBucket = $bucket; + break; + } + } + + // Prepare script input + $ttl = $metadata->getMaxAgeSeconds() ?? $this->getHelper()->getDefautlTtl(); + $scriptArgs = [ + $registryKey, + $metadataKey, + $metricKey, + $metadata->toJson(), + $targetBucket, + $value, + $ttl + ]; + + // Return script + return RedisScript::newBuilder() + ->withScript(self::SCRIPT) + ->withArgs($scriptArgs) + ->withNumKeys(3) + ->build(); + } +} From 45f0e8f2216cca75d032fe2d1e27b58365e90fde Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 21:33:21 -0500 Subject: [PATCH 13/14] Minor cleanup in benchmark test. Signed-off-by: Arris Ray --- tests/Test/Benchmark/BenchmarkTest.php | 58 +++++++++++++------------- tests/Test/Benchmark/MetricType.php | 2 +- tests/Test/Benchmark/TestCase.php | 2 +- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/Test/Benchmark/BenchmarkTest.php b/tests/Test/Benchmark/BenchmarkTest.php index 9fea97b0..a8768bb2 100644 --- a/tests/Test/Benchmark/BenchmarkTest.php +++ b/tests/Test/Benchmark/BenchmarkTest.php @@ -21,7 +21,7 @@ public static function setUpBeforeClass(): void { file_put_contents( self::RESULT_FILENAME, - implode(',', TestCaseResult::getCsvHeaders()) + implode(',', TestCaseResult::getCsvHeaders()) . PHP_EOL ); parent::setUpBeforeClass(); } @@ -32,34 +32,34 @@ public static function setUpBeforeClass(): void public function benchmarkProvider(): array { return [ - [AdapterType::REDISNG, MetricType::COUNTER, 1000, 10], - [AdapterType::REDISNG, MetricType::COUNTER, 2000, 10], - [AdapterType::REDISNG, MetricType::COUNTER, 5000, 10], - [AdapterType::REDISNG, MetricType::COUNTER, 10000, 10], - [AdapterType::REDISNG, MetricType::GAUGE, 1000, 10], - [AdapterType::REDISNG, MetricType::GAUGE, 2000, 10], - [AdapterType::REDISNG, MetricType::GAUGE, 5000, 10], - [AdapterType::REDISNG, MetricType::GAUGE, 10000, 10], - [AdapterType::REDISNG, MetricType::HISTOGRAM, 1000, 10], - [AdapterType::REDISNG, MetricType::HISTOGRAM, 2000, 10], - [AdapterType::REDISNG, MetricType::HISTOGRAM, 5000, 10], - [AdapterType::REDISNG, MetricType::HISTOGRAM, 10000, 10], - [AdapterType::REDISNG, MetricType::SUMMARY, 1000, 10], - [AdapterType::REDISNG, MetricType::SUMMARY, 2000, 10], - [AdapterType::REDISNG, MetricType::SUMMARY, 5000, 10], - [AdapterType::REDISNG, MetricType::SUMMARY, 10000, 10], - [AdapterType::REDISTXN, MetricType::COUNTER, 1000, 10], - [AdapterType::REDISTXN, MetricType::COUNTER, 2000, 10], - [AdapterType::REDISTXN, MetricType::COUNTER, 5000, 10], - [AdapterType::REDISTXN, MetricType::COUNTER, 10000, 10], - [AdapterType::REDISTXN, MetricType::GAUGE, 1000, 10], - [AdapterType::REDISTXN, MetricType::GAUGE, 2000, 10], - [AdapterType::REDISTXN, MetricType::GAUGE, 5000, 10], - [AdapterType::REDISTXN, MetricType::GAUGE, 10000, 10], - [AdapterType::REDISTXN, MetricType::HISTOGRAM, 1000, 10], - [AdapterType::REDISTXN, MetricType::HISTOGRAM, 2000, 10], - [AdapterType::REDISTXN, MetricType::HISTOGRAM, 5000, 10], - [AdapterType::REDISTXN, MetricType::HISTOGRAM, 10000, 10], +// [AdapterType::REDISNG, MetricType::COUNTER, 1000, 10], +// [AdapterType::REDISNG, MetricType::COUNTER, 2000, 10], +// [AdapterType::REDISNG, MetricType::COUNTER, 5000, 10], +// [AdapterType::REDISNG, MetricType::COUNTER, 10000, 10], +// [AdapterType::REDISNG, MetricType::GAUGE, 1000, 10], +// [AdapterType::REDISNG, MetricType::GAUGE, 2000, 10], +// [AdapterType::REDISNG, MetricType::GAUGE, 5000, 10], +// [AdapterType::REDISNG, MetricType::GAUGE, 10000, 10], +// [AdapterType::REDISNG, MetricType::HISTOGRAM, 1000, 10], +// [AdapterType::REDISNG, MetricType::HISTOGRAM, 2000, 10], +// [AdapterType::REDISNG, MetricType::HISTOGRAM, 5000, 10], +// [AdapterType::REDISNG, MetricType::HISTOGRAM, 10000, 10], +// [AdapterType::REDISNG, MetricType::SUMMARY, 1000, 10], +// [AdapterType::REDISNG, MetricType::SUMMARY, 2000, 10], +// [AdapterType::REDISNG, MetricType::SUMMARY, 5000, 10], +// [AdapterType::REDISNG, MetricType::SUMMARY, 10000, 10], +// [AdapterType::REDISTXN, MetricType::COUNTER, 1000, 10], +// [AdapterType::REDISTXN, MetricType::COUNTER, 2000, 10], +// [AdapterType::REDISTXN, MetricType::COUNTER, 5000, 10], +// [AdapterType::REDISTXN, MetricType::COUNTER, 10000, 10], +// [AdapterType::REDISTXN, MetricType::GAUGE, 1000, 10], +// [AdapterType::REDISTXN, MetricType::GAUGE, 2000, 10], +// [AdapterType::REDISTXN, MetricType::GAUGE, 5000, 10], +// [AdapterType::REDISTXN, MetricType::GAUGE, 10000, 10], +// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 1000, 10], +// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 2000, 10], +// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 5000, 10], +// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 10000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 1000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 2000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 5000, 10], diff --git a/tests/Test/Benchmark/MetricType.php b/tests/Test/Benchmark/MetricType.php index b8a05160..e4009497 100644 --- a/tests/Test/Benchmark/MetricType.php +++ b/tests/Test/Benchmark/MetricType.php @@ -25,7 +25,7 @@ public static function toString(int $type): string case MetricType::HISTOGRAM: return 'histogram'; case MetricType::SUMMARY: - return 'timer'; + return 'summary'; } throw new InvalidArgumentException("Invalid adapter type: {$type}"); diff --git a/tests/Test/Benchmark/TestCase.php b/tests/Test/Benchmark/TestCase.php index c4902c0c..33afeb67 100644 --- a/tests/Test/Benchmark/TestCase.php +++ b/tests/Test/Benchmark/TestCase.php @@ -257,7 +257,7 @@ private function emitMetric(): string $registry->getOrRegisterHistogram(self::DEFAULT_METRIC_NAMESPACE, $key, self::DEFAULT_METRIC_HELP)->observe($value); break; case MetricType::SUMMARY: - $key = uniqid('timer_', false); + $key = uniqid('summary_', false); $value = random_int(1, PHP_INT_MAX); $registry->getOrRegisterSummary(self::DEFAULT_METRIC_NAMESPACE, $key, self::DEFAULT_METRIC_HELP)->observe($value); break; From 4aa0fc03335cff8d8f772719f4909620e3f8ea03 Mon Sep 17 00:00:00 2001 From: Arris Ray Date: Sun, 22 Jan 2023 21:34:06 -0500 Subject: [PATCH 14/14] Remove completed todo. Signed-off-by: Arris Ray --- src/Prometheus/Storage/RedisTxn.php | 1 - tests/Test/Benchmark/BenchmarkTest.php | 56 +++++++++++++------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Prometheus/Storage/RedisTxn.php b/src/Prometheus/Storage/RedisTxn.php index 35cf8bc5..d8c42660 100644 --- a/src/Prometheus/Storage/RedisTxn.php +++ b/src/Prometheus/Storage/RedisTxn.php @@ -31,7 +31,6 @@ * This adapter refactors the {@see \Prometheus\Storage\RedisNg} adapter to generally try and execute the "update" and * "collect" operations of each metric type within a single Redis transaction. * - * @todo Only summary metrics have been refactored so far. Complete refactor for counter, gauge, and histogram metrics. * @todo Reimplement wipeStorage() to account for reorganized keys in Redis. * @todo Reimplement all Redis scripts with redis.pcall() to trap runtime errors that are ignored by redis.call(). */ diff --git a/tests/Test/Benchmark/BenchmarkTest.php b/tests/Test/Benchmark/BenchmarkTest.php index a8768bb2..b9bba8d9 100644 --- a/tests/Test/Benchmark/BenchmarkTest.php +++ b/tests/Test/Benchmark/BenchmarkTest.php @@ -32,34 +32,34 @@ public static function setUpBeforeClass(): void public function benchmarkProvider(): array { return [ -// [AdapterType::REDISNG, MetricType::COUNTER, 1000, 10], -// [AdapterType::REDISNG, MetricType::COUNTER, 2000, 10], -// [AdapterType::REDISNG, MetricType::COUNTER, 5000, 10], -// [AdapterType::REDISNG, MetricType::COUNTER, 10000, 10], -// [AdapterType::REDISNG, MetricType::GAUGE, 1000, 10], -// [AdapterType::REDISNG, MetricType::GAUGE, 2000, 10], -// [AdapterType::REDISNG, MetricType::GAUGE, 5000, 10], -// [AdapterType::REDISNG, MetricType::GAUGE, 10000, 10], -// [AdapterType::REDISNG, MetricType::HISTOGRAM, 1000, 10], -// [AdapterType::REDISNG, MetricType::HISTOGRAM, 2000, 10], -// [AdapterType::REDISNG, MetricType::HISTOGRAM, 5000, 10], -// [AdapterType::REDISNG, MetricType::HISTOGRAM, 10000, 10], -// [AdapterType::REDISNG, MetricType::SUMMARY, 1000, 10], -// [AdapterType::REDISNG, MetricType::SUMMARY, 2000, 10], -// [AdapterType::REDISNG, MetricType::SUMMARY, 5000, 10], -// [AdapterType::REDISNG, MetricType::SUMMARY, 10000, 10], -// [AdapterType::REDISTXN, MetricType::COUNTER, 1000, 10], -// [AdapterType::REDISTXN, MetricType::COUNTER, 2000, 10], -// [AdapterType::REDISTXN, MetricType::COUNTER, 5000, 10], -// [AdapterType::REDISTXN, MetricType::COUNTER, 10000, 10], -// [AdapterType::REDISTXN, MetricType::GAUGE, 1000, 10], -// [AdapterType::REDISTXN, MetricType::GAUGE, 2000, 10], -// [AdapterType::REDISTXN, MetricType::GAUGE, 5000, 10], -// [AdapterType::REDISTXN, MetricType::GAUGE, 10000, 10], -// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 1000, 10], -// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 2000, 10], -// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 5000, 10], -// [AdapterType::REDISTXN, MetricType::HISTOGRAM, 10000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 1000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 2000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 5000, 10], + [AdapterType::REDISNG, MetricType::COUNTER, 10000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 1000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 2000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 5000, 10], + [AdapterType::REDISNG, MetricType::GAUGE, 10000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 1000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 2000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 5000, 10], + [AdapterType::REDISNG, MetricType::HISTOGRAM, 10000, 10], + [AdapterType::REDISNG, MetricType::SUMMARY, 1000, 10], + [AdapterType::REDISNG, MetricType::SUMMARY, 2000, 10], + [AdapterType::REDISNG, MetricType::SUMMARY, 5000, 10], + [AdapterType::REDISNG, MetricType::SUMMARY, 10000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 1000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 2000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 5000, 10], + [AdapterType::REDISTXN, MetricType::COUNTER, 10000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 1000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 2000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 5000, 10], + [AdapterType::REDISTXN, MetricType::GAUGE, 10000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 1000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 2000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 5000, 10], + [AdapterType::REDISTXN, MetricType::HISTOGRAM, 10000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 1000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 2000, 10], [AdapterType::REDISTXN, MetricType::SUMMARY, 5000, 10],