-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RedisTxn storage adapter. #107
Open
arris-ray
wants to merge
15
commits into
PromPHP:main
Choose a base branch
from
arris-ray:ar/add-new-redis-adapter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
d2dae04
Vendor Composer dependencies in php-fpm Docker image.
arris-ray 313342d
Add RedisTxn storage adapter.
arris-ray fdb97a5
Add and update test coverage for new storage adapter.
arris-ray fb0e0db
Add benchmark test.
arris-ray f0adc42
Fix typo.
arris-ray b028f36
Improve benchmark test.
arris-ray f1baf89
Optimize counter metric collection.
arris-ray 7f7d050
Optimize gauge metric collection.
arris-ray c54a8ec
Reorganize concepts.
arris-ray 943e317
Add todo note.
arris-ray 4887100
Implement AbstractUpdater.
arris-ray e2c2c8e
Optimize histogram metric collection.
arris-ray 45f0e8f
Minor cleanup in benchmark test.
arris-ray 4aa0fc0
Remove completed todo.
arris-ray 924d842
Merge branch 'main' into ar/add-new-redis-adapter
LKaemmerling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
/vendor/ | ||
*.iml | ||
/.idea/ | ||
benchmark.csv | ||
composer.lock | ||
composer.phar | ||
.phpunit.result.cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ services: | |
- php-fpm | ||
ports: | ||
- 8080:80 | ||
environment: | ||
- REDIS_HOST=redis | ||
|
||
php-fpm: | ||
build: php-fpm/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ | |
<groups> | ||
<exclude> | ||
<group>Performance</group> | ||
<group>Benchmark</group> | ||
</exclude> | ||
</groups> | ||
</phpunit> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Prometheus\Storage; | ||
|
||
use Prometheus\Exception\StorageException; | ||
use Prometheus\Histogram; | ||
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; | ||
|
||
/** | ||
* This is a storage adapter that persists Prometheus metrics in Redis. | ||
* | ||
* This library currently has two alternative Redis adapters: | ||
* - {@see \Prometheus\Storage\Redis}: Initial Redis adapter written for this library. | ||
* - {@see \Prometheus\Storage\RedisNg}: "Next-generation" adapter refactored to avoid use of the KEYS command to improve performance. | ||
* | ||
* While the next-generation adapter was an enormous performance improvement over the first, it still suffers from | ||
* performance degradation that scales significantly as the number of metrics grows. This is largely due to the fact | ||
* that the "collect" phase for metrics generally involves at least one network request per metric of each type. | ||
* | ||
* 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 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 | ||
{ | ||
/** | ||
* @var mixed[] | ||
*/ | ||
private static $defaultOptions = [ | ||
'host' => '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; | ||
} | ||
|
||
/** | ||
* @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( | ||
<<<LUA | ||
local cursor = "0" | ||
repeat | ||
local results = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) | ||
cursor = results[1] | ||
for _, key in ipairs(results[2]) do | ||
redis.call('DEL', key) | ||
end | ||
until cursor == "0" | ||
LUA | ||
, | ||
[$searchPattern], | ||
0 | ||
); | ||
} | ||
|
||
/** | ||
* @return MetricFamilySamples[] | ||
* @throws StorageException | ||
*/ | ||
public function collect(): array | ||
{ | ||
// Ensure Redis connection | ||
$this->ensureOpenConnection(); | ||
|
||
// Collect all metrics | ||
$counters = $this->collectCounters(); | ||
$histograms = $this->collectHistograms(); | ||
$gauges = $this->collectGauges(); | ||
$summaries = $this->collectSummaries(); | ||
return array_merge( | ||
$counters, | ||
$histograms, | ||
$gauges, | ||
$summaries | ||
); | ||
} | ||
|
||
/** | ||
* @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); | ||
} | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function updateHistogram(array $data): void | ||
{ | ||
// Ensure Redis connection | ||
$this->ensureOpenConnection(); | ||
|
||
// Update metric | ||
$updater = new HistogramUpdater($this->redis); | ||
$updater->update($data); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function updateSummary(array $data): void | ||
{ | ||
// Ensure Redis connection | ||
$this->ensureOpenConnection(); | ||
|
||
// Update metric | ||
$updater = new SummaryUpdater($this->redis); | ||
$updater->update($data); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function updateGauge(array $data): void | ||
{ | ||
// Ensure Redis connection | ||
$this->ensureOpenConnection(); | ||
|
||
// Update metric | ||
$updater = new GaugeUpdater($this->redis); | ||
$updater->update($data); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function updateCounter(array $data): void | ||
{ | ||
// Ensure Redis connection | ||
$this->ensureOpenConnection(); | ||
|
||
// Update metric | ||
$updater = new CounterUpdater($this->redis); | ||
$updater->update($data); | ||
} | ||
|
||
/** | ||
* @return MetricFamilySamples[] | ||
*/ | ||
private function collectHistograms(): array | ||
{ | ||
$collector = new HistogramCollecter($this->redis); | ||
return $collector->getMetricFamilySamples(); | ||
} | ||
|
||
/** | ||
* @return MetricFamilySamples[] | ||
*/ | ||
private function collectSummaries(): array | ||
{ | ||
$collector = new SummaryCollecter($this->redis); | ||
return $collector->getMetricFamilySamples(); | ||
} | ||
|
||
/** | ||
* @return MetricFamilySamples[] | ||
*/ | ||
private function collectGauges(): array | ||
{ | ||
$collector = new GaugeCollecter($this->redis); | ||
return $collector->getMetricFamilySamples(); | ||
} | ||
|
||
/** | ||
* @return MetricFamilySamples[] | ||
*/ | ||
private function collectCounters(): array | ||
{ | ||
$collector = new CounterCollecter($this->redis); | ||
return $collector->getMetricFamilySamples(); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adapter is based on
RedisNg
and currently, the only changes are in the following methods:updateSummary()
collectSummaries()
If there is interest from the repo maintainers, I'd like to apply the same style of refactoring for the remaining metric types:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All metric types have been optimized. Based on benchmark timing, there may be further opportunity to optimize histograms but that will probably be left as an exercise for a future contributor.