From 0c98cc37b2981c4463e64cd7b7415f1bfb8040c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Virse=CC=81n?= Date: Mon, 21 Oct 2019 15:31:04 +0200 Subject: [PATCH 1/5] Added summary collector (courtesy of buffcode) --- src/Prometheus/CollectorRegistry.php | 65 ++++++++++++++++ src/Prometheus/Storage/APC.php | 8 ++ src/Prometheus/Storage/Adapter.php | 6 ++ src/Prometheus/Storage/InMemory.php | 9 +++ src/Prometheus/Storage/Redis.php | 102 +++++++++++++++++++++++++ src/Prometheus/Summary.php | 107 +++++++++++++++++++++++++++ 6 files changed, 297 insertions(+) create mode 100644 src/Prometheus/Summary.php diff --git a/src/Prometheus/CollectorRegistry.php b/src/Prometheus/CollectorRegistry.php index 541e1d8..7140a1c 100644 --- a/src/Prometheus/CollectorRegistry.php +++ b/src/Prometheus/CollectorRegistry.php @@ -36,6 +36,11 @@ class CollectorRegistry */ private $histograms = []; + /** + * @var Summary[] + */ + private $summaries = []; + /** * CollectorRegistry constructor. * @param Adapter $redisAdapter @@ -238,6 +243,66 @@ public function getOrRegisterHistogram($namespace, $name, $help, $labels = [], $ return $histogram; } + /** + * @param string $namespace e.g. cms + * @param string $name e.g. duration_seconds + * @param string $help e.g. A duration in seconds. + * @param array $labels e.g. ['controller', 'action'] + * @param array $quantiles e.g. [0.1, 0.5, 0.9] + * @return Summary + * @throws MetricsRegistrationException + */ + public function registerSummary($namespace, $name, $help, $labels = [], $quantiles = null): Summary + { + $metricIdentifier = self::metricIdentifier($namespace, $name); + if (isset($this->summaries[$metricIdentifier])) { + throw new MetricsRegistrationException("Metric already registered"); + } + $this->summaries[$metricIdentifier] = new Summary( + $this->storageAdapter, + $namespace, + $name, + $help, + $labels, + $quantiles + ); + return $this->summaries[$metricIdentifier]; + } + + /** + * @param string $namespace + * @param string $name + * @return Summary + * @throws MetricNotFoundException + */ + public function getSummary($namespace, $name): Summary + { + $metricIdentifier = self::metricIdentifier($namespace, $name); + if (!isset($this->summaries[$metricIdentifier])) { + throw new MetricNotFoundException("Metric not found:" . $metricIdentifier); + } + return $this->summaries[self::metricIdentifier($namespace, $name)]; + } + + /** + * @param string $namespace e.g. cms + * @param string $name e.g. duration_seconds + * @param string $help e.g. A duration in seconds. + * @param array $labels e.g. ['controller', 'action'] + * @param array $quantiles e.g. [0.1, 0.5, 0.9] + * @return Summary + * @throws MetricsRegistrationException + */ + public function getOrRegisterSummary($namespace, $name, $help, $labels = [], $quantiles = null): Summary + { + try { + $summary = $this->getSummary($namespace, $name); + } catch (MetricNotFoundException $e) { + $summary = $this->registerSummary($namespace, $name, $help, $labels, $quantiles); + } + return $summary; + } + /** * @param $namespace * @param $name diff --git a/src/Prometheus/Storage/APC.php b/src/Prometheus/Storage/APC.php index 2fe659d..4598a6d 100644 --- a/src/Prometheus/Storage/APC.php +++ b/src/Prometheus/Storage/APC.php @@ -59,6 +59,14 @@ public function updateHistogram(array $data): void apcu_inc($this->histogramBucketValueKey($data, $bucketToIncrease)); } + /** + * @param array $data + */ + public function updateSummary(array $data) + { + // TODO: Implement updateSummary() method. + } + /** * @param array $data */ diff --git a/src/Prometheus/Storage/Adapter.php b/src/Prometheus/Storage/Adapter.php index 068e1d9..ceb4230 100644 --- a/src/Prometheus/Storage/Adapter.php +++ b/src/Prometheus/Storage/Adapter.php @@ -23,6 +23,12 @@ public function collect(); */ public function updateHistogram(array $data): void; + /** + * @param array $data + * @return void + */ + public function updateSummary(array $data): void; + /** * @param array $data * @return void diff --git a/src/Prometheus/Storage/InMemory.php b/src/Prometheus/Storage/InMemory.php index bfd4c70..a64e4b2 100644 --- a/src/Prometheus/Storage/InMemory.php +++ b/src/Prometheus/Storage/InMemory.php @@ -175,6 +175,15 @@ public function updateHistogram(array $data): void $this->histograms[$metaKey]['samples'][$bucketKey] += 1; } + /** + * @param array $data + * @return void + */ + public function updateSummary(array $data): void + { + // TODO: Implement updateSummary() method. + } + /** * @param array $data */ diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 85e3000..6187cdf 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -10,6 +10,7 @@ use Prometheus\Gauge; use Prometheus\Histogram; use Prometheus\MetricFamilySamples; +use Prometheus\Summary; class Redis implements Adapter { @@ -105,6 +106,7 @@ public function collect(): array $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) { return new MetricFamilySamples($metric); @@ -198,6 +200,46 @@ public function updateHistogram(array $data): void ); } + /** + * @param array $data + * @throws StorageException + */ + public function updateSummary(array $data): void + { + $this->openConnection(); + $metaData = $data; + unset($metaData['value']); + unset($metaData['labelValues']); + $this->redis->eval(<<toMetricKey($data), + json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]), + json_encode(['b' => 'count', 'labelValues' => $data['labelValues']]), + json_encode(['b' => 'values', 'labelValues' => $data['labelValues']]), + self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + $data['value'], + json_encode($metaData), + ], + 5 + ); + } + + /** * @param array $data * @throws StorageException @@ -348,6 +390,66 @@ private function collectHistograms(): array return $histograms; } + /** + * @return array + */ + private function collectSummaries(): array + { + $keys = $this->redis->sMembers(self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $summaries = []; + + foreach ($keys as $key) { + $raw = $this->redis->hGetAll($key); + $summary = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $allLabelValues = []; + + foreach (array_keys($raw) as $k) { + $d = json_decode($k, true); + $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) { + $valuesKey = json_encode(['b' => 'values', 'labelValues' => $labelValues]); + $values = !empty($raw[$valuesKey]) ? explode(',', $raw[$valuesKey]) : []; + foreach($summary['quantiles'] as $quantile) { + $summary['samples'][] = [ + 'name' => $summary['name'], + 'labelNames' => ['quantile'], + 'labelValues' => array_merge($labelValues, ['quantile' => $quantile]), + 'value' => Summary::getQuantile($quantile, $values) + ]; + } + + // Add the count + $countKey = json_encode(['b' => 'count', 'labelValues' => $labelValues]); + $summary['samples'][] = [ + 'name' => $summary['name'] . '_count', + 'labelNames' => [], + 'labelValues' => $labelValues, + 'value' => !empty($raw[$countKey]) ? (int)$raw[$countKey] : 0, + ]; + + // Add the sum + $sumKey = json_encode(['b' => 'sum', 'labelValues' => $labelValues]); + $summary['samples'][] = [ + 'name' => $summary['name'] . '_sum', + 'labelNames' => [], + 'labelValues' => $labelValues, + 'value' => !empty($raw[$sumKey]) ? $raw[$sumKey] : 0, + ]; + } + $summaries[] = $summary; + } + return $summaries; + } + /** * @return array */ diff --git a/src/Prometheus/Summary.php b/src/Prometheus/Summary.php new file mode 100644 index 0000000..6883e92 --- /dev/null +++ b/src/Prometheus/Summary.php @@ -0,0 +1,107 @@ += $quantiles[$i + 1]) { + throw new InvalidArgumentException( + 'Summary quantiles must be in increasing order: ' . + $quantiles[$i] . ' >= ' . $quantiles[$i + 1] + ); + } + } + foreach ($labels as $label) { + if ($label === 'quantile') { + throw new InvalidArgumentException('Summary cannot have a label named "quantile".'); + } + } + $this->quantiles = $quantiles; + } + + /** + * List of default quantiles + * @return array + */ + public static function getDefaultQuantiles(): array + { + return [ + 0.01, 0.05, 0.5, 0.9, 0.99, + ]; + } + + /** + * @param double $value e.g. 123 + * @param array $labels e.g. ['status', 'opcode'] + */ + public function observe($value, $labels = []): void + { + $this->assertLabelsAreDefinedCorrectly($labels); + $this->storageAdapter->updateSummary( + [ + 'value' => $value, + 'name' => $this->getName(), + 'help' => $this->getHelp(), + 'type' => $this->getType(), + 'labelNames' => $this->getLabelNames(), + 'labelValues' => $labels, + 'quantiles' => $this->quantiles, + ] + ); + } + + /** + * @param float $percentile + * @param array $values + * @return float + */ + public static function getQuantile($percentile, $values): float + { + sort($values); + $index = (int)($percentile * count($values)); + return (floor($index) === $index) + ? ($values[$index - 1] + $values[$index]) / 2 + : $result = $values[(int)floor($index)]; + } + + /** + * @return string + */ + public function getType(): string + { + return self::TYPE; + } +} \ No newline at end of file From 2c826b9ecf76db3a7b60e86f8f430619c03e8baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Virse=CC=81n?= Date: Mon, 21 Oct 2019 15:34:22 +0200 Subject: [PATCH 2/5] removed return type --- src/Prometheus/Summary.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Prometheus/Summary.php b/src/Prometheus/Summary.php index 6883e92..551af00 100644 --- a/src/Prometheus/Summary.php +++ b/src/Prometheus/Summary.php @@ -88,13 +88,13 @@ public function observe($value, $labels = []): void * @param array $values * @return float */ - public static function getQuantile($percentile, $values): float + public static function getQuantile($percentile, $values) { sort($values); $index = (int)($percentile * count($values)); return (floor($index) === $index) ? ($values[$index - 1] + $values[$index]) / 2 - : $result = $values[(int)floor($index)]; + : $values[(int)floor($index)]; } /** @@ -104,4 +104,4 @@ public function getType(): string { return self::TYPE; } -} \ No newline at end of file +} From 4d2e10fa87b48d382caf5b208daf89bad98b842b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Virse=CC=81n?= Date: Wed, 4 Dec 2019 12:44:11 +0100 Subject: [PATCH 3/5] added type hints --- src/Prometheus/Summary.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Prometheus/Summary.php b/src/Prometheus/Summary.php index 551af00..6f96445 100644 --- a/src/Prometheus/Summary.php +++ b/src/Prometheus/Summary.php @@ -1,4 +1,5 @@ assertLabelsAreDefinedCorrectly($labels); $this->storageAdapter->updateSummary( @@ -88,7 +89,7 @@ public function observe($value, $labels = []): void * @param array $values * @return float */ - public static function getQuantile($percentile, $values) + public static function getQuantile(float $percentile, array $values) { sort($values); $index = (int)($percentile * count($values)); From 91e4c1fd6a2e769c30837538906b62dac104edc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Virse=CC=81n?= Date: Wed, 4 Dec 2019 12:44:31 +0100 Subject: [PATCH 4/5] phpcs formatting fixes --- src/Prometheus/Storage/Redis.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 6187cdf..0b49aea 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -210,7 +210,8 @@ public function updateSummary(array $data): void $metaData = $data; unset($metaData['value']); unset($metaData['labelValues']); - $this->redis->eval(<<redis->eval( + << 'values', 'labelValues' => $labelValues]); $values = !empty($raw[$valuesKey]) ? explode(',', $raw[$valuesKey]) : []; - foreach($summary['quantiles'] as $quantile) { + foreach ($summary['quantiles'] as $quantile) { $summary['samples'][] = [ 'name' => $summary['name'], 'labelNames' => ['quantile'], From 0e8cd5c4c689af5bd681f81b825c6ccf246e9ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Virse=CC=81n?= Date: Wed, 4 Dec 2019 16:12:38 +0100 Subject: [PATCH 5/5] added more type hints --- src/Prometheus/CollectorRegistry.php | 6 +++--- src/Prometheus/Summary.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Prometheus/CollectorRegistry.php b/src/Prometheus/CollectorRegistry.php index 7140a1c..2742ac8 100644 --- a/src/Prometheus/CollectorRegistry.php +++ b/src/Prometheus/CollectorRegistry.php @@ -252,7 +252,7 @@ public function getOrRegisterHistogram($namespace, $name, $help, $labels = [], $ * @return Summary * @throws MetricsRegistrationException */ - public function registerSummary($namespace, $name, $help, $labels = [], $quantiles = null): Summary + public function registerSummary(string $namespace, string $name, string $help, array $labels = [], ?array $quantiles = null): Summary { $metricIdentifier = self::metricIdentifier($namespace, $name); if (isset($this->summaries[$metricIdentifier])) { @@ -275,7 +275,7 @@ public function registerSummary($namespace, $name, $help, $labels = [], $quantil * @return Summary * @throws MetricNotFoundException */ - public function getSummary($namespace, $name): Summary + public function getSummary(string $namespace, string $name): Summary { $metricIdentifier = self::metricIdentifier($namespace, $name); if (!isset($this->summaries[$metricIdentifier])) { @@ -293,7 +293,7 @@ public function getSummary($namespace, $name): Summary * @return Summary * @throws MetricsRegistrationException */ - public function getOrRegisterSummary($namespace, $name, $help, $labels = [], $quantiles = null): Summary + public function getOrRegisterSummary(string $namespace, string $name, string $help, array $labels = [], ?array $quantiles = null): Summary { try { $summary = $this->getSummary($namespace, $name); diff --git a/src/Prometheus/Summary.php b/src/Prometheus/Summary.php index 6f96445..7a2166c 100644 --- a/src/Prometheus/Summary.php +++ b/src/Prometheus/Summary.php @@ -25,7 +25,7 @@ class Summary extends Collector * @param array $quantiles * @throws InvalidArgumentException */ - public function __construct(Adapter $adapter, $namespace, $name, $help, $labels = [], $quantiles = null) + public function __construct(Adapter $adapter, string $namespace, string $name, string $help, array $labels = [], ?array $quantiles = null) { parent::__construct($adapter, $namespace, $name, $help, $labels); @@ -89,13 +89,13 @@ public function observe(float $value, array $labels = []): void * @param array $values * @return float */ - public static function getQuantile(float $percentile, array $values) + public static function getQuantile(float $percentile, array $values): float { sort($values); $index = (int)($percentile * count($values)); return (floor($index) === $index) ? ($values[$index - 1] + $values[$index]) / 2 - : $values[(int)floor($index)]; + : (float) $values[(int)floor($index)]; } /**