diff --git a/README.md b/README.md index 63c2ceb..439bed5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ PROMETHEUS_STORAGE_ADAPTER=memory REDIS_HOST=localhost REDIS_PORT=6379 PROMETHEUS_REDIS_PREFIX=PROMETHEUS_ +PROMETHEUS_REDIS_DATABASE=0 ``` To customize the configuration file, publish the package configuration using Artisan. diff --git a/config/prometheus.php b/config/prometheus.php index 8eb5c0f..4aa9e6f 100644 --- a/config/prometheus.php +++ b/config/prometheus.php @@ -99,6 +99,7 @@ return [ 'read_timeout' => 10, 'persistent_connections' => false, 'prefix' => env('PROMETHEUS_REDIS_PREFIX', 'PROMETHEUS_'), + 'database' => env('PROMETHEUS_REDIS_DATABASE', 0) ], ], diff --git a/src/Storage/Redis.php b/src/Storage/Redis.php new file mode 100644 index 0000000..ebf7396 --- /dev/null +++ b/src/Storage/Redis.php @@ -0,0 +1,355 @@ +options = array_merge(self::$defaultOptions, $options); + $this->redis = new \Redis(); + } + + /** + * @param array $options + */ + public static function setDefaultOptions(array $options) + { + self::$defaultOptions = array_merge(self::$defaultOptions, $options); + } + + public static function setPrefix($prefix) + { + self::$prefix = $prefix; + } + + public function flushRedis() + { + $this->openConnection(); + $this->redis->flushAll(); + } + + /** + * @return MetricFamilySamples[] + * @throws StorageException + */ + public function collect() + { + $this->openConnection(); + $metrics = $this->collectHistograms(); + $metrics = array_merge($metrics, $this->collectGauges()); + $metrics = array_merge($metrics, $this->collectCounters()); + return array_map( + function (array $metric) { + return new MetricFamilySamples($metric); + }, + $metrics + ); + } + + /** + * @throws StorageException + */ + private function openConnection() + { + try { + if ($this->options['persistent_connections']) { + @$this->redis->pconnect($this->options['host'], $this->options['port'], $this->options['timeout']); + } else { + @$this->redis->connect($this->options['host'], $this->options['port'], $this->options['timeout']); + } + if ($this->options['password']) { + $this->redis->auth($this->options['password']); + } + $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $this->options['read_timeout']); + if(isset($this->options['database'])) { + $this->redis->select( (int) $this->options['database'] ); + } + } catch (\RedisException $e) { + throw new StorageException("Can't connect to Redis server", 0, $e); + } + } + + public function updateHistogram(array $data) + { + $this->openConnection(); + $bucketToIncrease = '+Inf'; + foreach ($data['buckets'] as $bucket) { + if ($data['value'] <= $bucket) { + $bucketToIncrease = $bucket; + break; + } + } + $metaData = $data; + unset($metaData['value']); + unset($metaData['labelValues']); + $this->redis->eval(<<toMetricKey($data), + json_encode(array('b' => 'sum', 'labelValues' => $data['labelValues'])), + json_encode(array('b' => $bucketToIncrease, 'labelValues' => $data['labelValues'])), + self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + $data['value'], + json_encode($metaData), + ), + 4 + ); + } + + public function updateGauge(array $data) + { + $this->openConnection(); + $metaData = $data; + unset($metaData['value']); + unset($metaData['labelValues']); + unset($metaData['command']); + $this->redis->eval(<<toMetricKey($data), + $this->getRedisCommand($data['command']), + self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + json_encode($data['labelValues']), + $data['value'], + json_encode($metaData), + ), + 4 + ); + } + + public function updateCounter(array $data) + { + $this->openConnection(); + $metaData = $data; + unset($metaData['value']); + unset($metaData['labelValues']); + unset($metaData['command']); + $result = $this->redis->eval(<<toMetricKey($data), + $this->getRedisCommand($data['command']), + self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + json_encode($data['labelValues']), + $data['value'], + json_encode($metaData), + ), + 4 + ); + return $result; + } + + private function collectHistograms() + { + $keys = $this->redis->sMembers(self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $histograms = array(); + foreach ($keys as $key) { + $raw = $this->redis->hGetAll($key); + $histogram = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $histogram['samples'] = array(); + + // Add the Inf bucket so we can compute it later on + $histogram['buckets'][] = '+Inf'; + + $allLabelValues = array(); + 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(array('b' => $bucket, 'labelValues' => $labelValues)); + if (!isset($raw[$bucketKey])) { + $histogram['samples'][] = array( + 'name' => $histogram['name'] . '_bucket', + 'labelNames' => array('le'), + 'labelValues' => array_merge($labelValues, array($bucket)), + 'value' => $acc + ); + } else { + $acc += $raw[$bucketKey]; + $histogram['samples'][] = array( + 'name' => $histogram['name'] . '_bucket', + 'labelNames' => array('le'), + 'labelValues' => array_merge($labelValues, array($bucket)), + 'value' => $acc + ); + } + } + + // Add the count + $histogram['samples'][] = array( + 'name' => $histogram['name'] . '_count', + 'labelNames' => array(), + 'labelValues' => $labelValues, + 'value' => $acc + ); + + // Add the sum + $histogram['samples'][] = array( + 'name' => $histogram['name'] . '_sum', + 'labelNames' => array(), + 'labelValues' => $labelValues, + 'value' => $raw[json_encode(array('b' => 'sum', 'labelValues' => $labelValues))] + ); + } + $histograms[] = $histogram; + } + return $histograms; + } + + private function collectGauges() + { + $keys = $this->redis->sMembers(self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $gauges = array(); + foreach ($keys as $key) { + $raw = $this->redis->hGetAll($key); + $gauge = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $gauge['samples'] = array(); + foreach ($raw as $k => $value) { + $gauge['samples'][] = array( + 'name' => $gauge['name'], + 'labelNames' => array(), + 'labelValues' => json_decode($k, true), + 'value' => $value + ); + } + usort($gauge['samples'], function($a, $b){ + return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); + }); + $gauges[] = $gauge; + } + return $gauges; + } + + private function collectCounters() + { + $keys = $this->redis->sMembers(self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $counters = array(); + foreach ($keys as $key) { + $raw = $this->redis->hGetAll($key); + $counter = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $counter['samples'] = array(); + foreach ($raw as $k => $value) { + $counter['samples'][] = array( + 'name' => $counter['name'], + 'labelNames' => array(), + 'labelValues' => json_decode($k, true), + 'value' => $value + ); + } + usort($counter['samples'], function($a, $b){ + return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); + }); + $counters[] = $counter; + } + return $counters; + } + + private function getRedisCommand($cmd) + { + 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 array $data + * @return string + */ + private function toMetricKey(array $data) + { + return implode(':', array(self::$prefix, $data['type'], $data['name'])); + } +} diff --git a/src/StorageAdapterFactory.php b/src/StorageAdapterFactory.php index c2fac93..b0ec0f0 100644 --- a/src/StorageAdapterFactory.php +++ b/src/StorageAdapterFactory.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use Prometheus\Storage\Adapter; use Prometheus\Storage\APC; use Prometheus\Storage\InMemory; -use Prometheus\Storage\Redis; +use Superbalist\LaravelPrometheusExporter\Storage\Redis; class StorageAdapterFactory { diff --git a/tests/StorageAdapterFactoryTest.php b/tests/StorageAdapterFactoryTest.php index 9061d70..c72d980 100644 --- a/tests/StorageAdapterFactoryTest.php +++ b/tests/StorageAdapterFactoryTest.php @@ -5,7 +5,7 @@ namespace Tests; use PHPUnit\Framework\TestCase; use Prometheus\Storage\APC; use Prometheus\Storage\InMemory; -use Prometheus\Storage\Redis; +use Superbalist\LaravelPrometheusExporter\Storage\Redis; use Superbalist\LaravelPrometheusExporter\StorageAdapterFactory; class StorageAdapterFactoryTest extends TestCase