Skip to content
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
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions .gitignore
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
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ services:
- php-fpm
ports:
- 8080:80
environment:
- REDIS_HOST=redis

php-fpm:
build: php-fpm/
Expand Down
2 changes: 2 additions & 0 deletions examples/flush_adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
2 changes: 2 additions & 0 deletions examples/metrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions examples/some_counter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions examples/some_gauge.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions examples/some_histogram.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions examples/some_summary.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions php-fpm/Dockerfile
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/
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<groups>
<exclude>
<group>Performance</group>
<group>Benchmark</group>
</exclude>
</groups>
</phpunit>
300 changes: 300 additions & 0 deletions src/Prometheus/Storage/RedisTxn.php
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
Copy link
Author

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:

  • counters
  • gauges
  • histograms

Copy link
Author

@arris-ray arris-ray Jan 24, 2023

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.

{
/**
* @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();
}
}
Loading