From 0e35325d66497d4d82ca43603ed5c9d3c5b1d06a Mon Sep 17 00:00:00 2001 From: Stanislav Petrov Date: Wed, 27 Sep 2017 16:18:26 +0100 Subject: [PATCH 1/3] added failing test --- tests/Test/Prometheus/AbstractCounterTest.php | 47 +++++++++++++++++++ tests/Test/Prometheus/AbstractGaugeTest.php | 46 ++++++++++++++++++ .../Test/Prometheus/AbstractHistogramTest.php | 46 ++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/tests/Test/Prometheus/AbstractCounterTest.php b/tests/Test/Prometheus/AbstractCounterTest.php index 92501f5..188310c 100644 --- a/tests/Test/Prometheus/AbstractCounterTest.php +++ b/tests/Test/Prometheus/AbstractCounterTest.php @@ -6,6 +6,8 @@ use PHPUnit_Framework_TestCase; use Prometheus\Counter; use Prometheus\MetricFamilySamples; +use Prometheus\Sample; +use Prometheus\Storage\Adapter; /** * See https://prometheus.io/docs/instrumenting/exposition_formats/ @@ -139,5 +141,50 @@ public function itShouldRejectInvalidLabelNames() new Counter($this->adapter, 'test', 'some_metric', 'help', array('invalid label')); } + /** + * @test + * @dataProvider labelValuesDataProvider + * + * @param mixed $value The label value + */ + public function isShouldAcceptAnySequenceOfBasicLatinCharactersForLabelValues($value) + { + $label = 'foo'; + $histogram = new Counter($this->adapter, 'test', 'some_metric', 'help', array($label)); + $histogram->inc(array($value)); + + $metrics = $this->adapter->collect(); + self::assertInternalType('array', $metrics); + self::assertCount(1, $metrics); + self::assertContainsOnlyInstancesOf(MetricFamilySamples::class, $metrics); + + $metric = reset($metrics); + $samples = $metric->getSamples(); + self::assertContainsOnlyInstancesOf(Sample::class, $samples); + + foreach ($samples as $sample) { + $labels = array_combine( + array_merge($metric->getLabelNames(), $sample->getLabelNames()), + $sample->getLabelValues() + ); + self::assertEquals($value, $labels[$label]); + } + } + + /** + * @see isShouldAcceptArbitraryLabelValues + * @return array + */ + public function labelValuesDataProvider() + { + $cases = []; + // Basic Latin + // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Basic_Latin + for ($i = 32; $i <= 121; $i++) { + $cases['ASCII code ' . $i] = array(chr($i)); + } + return $cases; + } + public abstract function configureAdapter(); } diff --git a/tests/Test/Prometheus/AbstractGaugeTest.php b/tests/Test/Prometheus/AbstractGaugeTest.php index 175f0b5..33ad02c 100644 --- a/tests/Test/Prometheus/AbstractGaugeTest.php +++ b/tests/Test/Prometheus/AbstractGaugeTest.php @@ -6,6 +6,7 @@ use PHPUnit_Framework_TestCase; use Prometheus\Gauge; use Prometheus\MetricFamilySamples; +use Prometheus\Sample; use Prometheus\Storage\Adapter; /** @@ -308,5 +309,50 @@ public function itShouldRejectInvalidLabelNames() new Gauge($this->adapter, 'test', 'some_metric', 'help', array('invalid label')); } + /** + * @test + * @dataProvider labelValuesDataProvider + * + * @param mixed $value The label value + */ + public function isShouldAcceptAnySequenceOfBasicLatinCharactersForLabelValues($value) + { + $label = 'foo'; + $histogram = new Gauge($this->adapter, 'test', 'some_metric', 'help', array($label)); + $histogram->inc(array($value)); + + $metrics = $this->adapter->collect(); + self::assertInternalType('array', $metrics); + self::assertCount(1, $metrics); + self::assertContainsOnlyInstancesOf(MetricFamilySamples::class, $metrics); + + $metric = reset($metrics); + $samples = $metric->getSamples(); + self::assertContainsOnlyInstancesOf(Sample::class, $samples); + + foreach ($samples as $sample) { + $labels = array_combine( + array_merge($metric->getLabelNames(), $sample->getLabelNames()), + $sample->getLabelValues() + ); + self::assertEquals($value, $labels[$label]); + } + } + + /** + * @see isShouldAcceptArbitraryLabelValues + * @return array + */ + public function labelValuesDataProvider() + { + $cases = []; + // Basic Latin + // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Basic_Latin + for ($i = 32; $i <= 121; $i++) { + $cases['ASCII code ' . $i] = array(chr($i)); + } + return $cases; + } + public abstract function configureAdapter(); } diff --git a/tests/Test/Prometheus/AbstractHistogramTest.php b/tests/Test/Prometheus/AbstractHistogramTest.php index 76d3f5b..358fcad 100644 --- a/tests/Test/Prometheus/AbstractHistogramTest.php +++ b/tests/Test/Prometheus/AbstractHistogramTest.php @@ -6,6 +6,7 @@ use PHPUnit_Framework_TestCase; use Prometheus\Histogram; use Prometheus\MetricFamilySamples; +use Prometheus\Sample; use Prometheus\Storage\Adapter; @@ -421,5 +422,50 @@ public function itShouldRejectInvalidLabelNames() new Histogram($this->adapter, 'test', 'some_metric', 'help', array('invalid label'), array(1)); } + /** + * @test + * @dataProvider labelValuesDataProvider + * + * @param mixed $value The label value + */ + public function isShouldAcceptAnySequenceOfBasicLatinCharactersForLabelValues($value) + { + $label = 'foo'; + $histogram = new Histogram($this->adapter, 'test', 'some_metric', 'help', array($label), array(1)); + $histogram->observe(1, array($value)); + + $metrics = $this->adapter->collect(); + self::assertInternalType('array', $metrics); + self::assertCount(1, $metrics); + self::assertContainsOnlyInstancesOf(MetricFamilySamples::class, $metrics); + + $metric = reset($metrics); + $samples = $metric->getSamples(); + self::assertContainsOnlyInstancesOf(Sample::class, $samples); + + foreach ($samples as $sample) { + $labels = array_combine( + array_merge($metric->getLabelNames(), $sample->getLabelNames()), + $sample->getLabelValues() + ); + self::assertEquals($value, $labels[$label]); + } + } + + /** + * @see isShouldAcceptArbitraryLabelValues + * @return array + */ + public function labelValuesDataProvider() + { + $cases = []; + // Basic Latin + // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Basic_Latin + for ($i = 32; $i <= 121; $i++) { + $cases['ASCII code ' . $i] = array(chr($i)); + } + return $cases; + } + public abstract function configureAdapter(); } From 8750e18d19db928c2790b05e9e058ca9f8c8c6cd Mon Sep 17 00:00:00 2001 From: Stanislav Petrov Date: Thu, 28 Sep 2017 13:59:38 +0100 Subject: [PATCH 2/3] added label value encoding for APC --- src/Prometheus/Storage/APC.php | 60 ++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/src/Prometheus/Storage/APC.php b/src/Prometheus/Storage/APC.php index b5a9c4b..a2a1367 100644 --- a/src/Prometheus/Storage/APC.php +++ b/src/Prometheus/Storage/APC.php @@ -5,6 +5,7 @@ use Prometheus\MetricFamilySamples; +use RuntimeException; class APC implements Adapter { @@ -103,7 +104,13 @@ private function metaKey(array $data) */ private function valueKey(array $data) { - return implode(':', array(self::PROMETHEUS_PREFIX, $data['type'], $data['name'], json_encode($data['labelValues']), 'value')); + return implode(':', array( + self::PROMETHEUS_PREFIX, + $data['type'], + $data['name'], + $this->encodeLabelValues($data['labelValues']), + 'value' + )); } /** @@ -112,7 +119,14 @@ private function valueKey(array $data) */ private function histogramBucketValueKey(array $data, $bucket) { - return implode(':', array(self::PROMETHEUS_PREFIX, $data['type'], $data['name'], json_encode($data['labelValues']), $bucket, 'value')); + return implode(':', array( + self::PROMETHEUS_PREFIX, + $data['type'], + $data['name'], + $this->encodeLabelValues($data['labelValues']), + $bucket, + 'value' + )); } /** @@ -148,7 +162,7 @@ private function collectCounters() $data['samples'][] = array( 'name' => $metaData['name'], 'labelNames' => array(), - 'labelValues' => json_decode($labelValues), + 'labelValues' => $this->decodeLabelValues($labelValues), 'value' => $value['value'] ); } @@ -159,8 +173,8 @@ private function collectCounters() } /** - * @return array - */ + * @return array + */ private function collectGauges() { $gauges = array(); @@ -178,7 +192,7 @@ private function collectGauges() $data['samples'][] = array( 'name' => $metaData['name'], 'labelNames' => array(), - 'labelValues' => json_decode($labelValues), + 'labelValues' => $this->decodeLabelValues($labelValues), 'value' => $this->fromInteger($value['value']) ); } @@ -222,7 +236,7 @@ private function collectHistograms() sort($labels); foreach ($labels as $labelValues) { $acc = 0; - $decodedLabelValues = json_decode($labelValues); + $decodedLabelValues = $this->decodeLabelValues($labelValues); foreach ($data['buckets'] as $bucket) { $bucket = (string) $bucket; if (!isset($histogramBuckets[$labelValues][$bucket])) { @@ -289,4 +303,36 @@ private function sortSamples(array &$samples) return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); }); } + + /** + * @param array $values + * @return string + * @throws RuntimeException + */ + private function encodeLabelValues(array $values) + { + $json = json_encode($values); + if (false === $json) { + throw new RuntimeException(json_last_error_msg()); + } + return base64_encode($json); + } + + /** + * @param string $values + * @return array + * @throws RuntimeException + */ + private function decodeLabelValues($values) + { + $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; + } } From 7eb30b3a5d581981bc83aace32febf468165c8ba Mon Sep 17 00:00:00 2001 From: Stanislav Petrov Date: Thu, 28 Sep 2017 14:48:42 +0100 Subject: [PATCH 3/3] added label value encoding for in-memory storage --- src/Prometheus/Storage/InMemory.php | 41 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Prometheus/Storage/InMemory.php b/src/Prometheus/Storage/InMemory.php index 37c24b1..313dbf4 100644 --- a/src/Prometheus/Storage/InMemory.php +++ b/src/Prometheus/Storage/InMemory.php @@ -4,6 +4,7 @@ use Prometheus\MetricFamilySamples; +use RuntimeException; class InMemory implements Adapter { @@ -60,7 +61,7 @@ private function collectHistograms() sort($labels); foreach ($labels as $labelValues) { $acc = 0; - $decodedLabelValues = json_decode($labelValues); + $decodedLabelValues = $this->decodeLabelValues($labelValues); foreach ($data['buckets'] as $bucket) { $bucket = (string)$bucket; if (!isset($histogramBuckets[$labelValues][$bucket])) { @@ -120,7 +121,7 @@ private function internalCollect(array $metrics) $data['samples'][] = [ 'name' => $metaData['name'], 'labelNames' => [], - 'labelValues' => json_decode($labelValues), + 'labelValues' => $this->decodeLabelValues($labelValues), 'value' => $value ]; } @@ -215,7 +216,7 @@ private function histogramBucketValueKey(array $data, $bucket) return implode(':', [ $data['type'], $data['name'], - json_encode($data['labelValues']), + $this->encodeLabelValues($data['labelValues']), $bucket ]); } @@ -238,7 +239,7 @@ private function metaKey(array $data) private function valueKey(array $data) { return implode(':', - [$data['type'], $data['name'], json_encode($data['labelValues']), 'value']); + [$data['type'], $data['name'], $this->encodeLabelValues($data['labelValues']), 'value']); } /** @@ -261,4 +262,36 @@ private function sortSamples(array &$samples) return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues'])); }); } + + /** + * @param array $values + * @return string + * @throws RuntimeException + */ + private function encodeLabelValues(array $values) + { + $json = json_encode($values); + if (false === $json) { + throw new RuntimeException(json_last_error_msg()); + } + return base64_encode($json); + } + + /** + * @param string $values + * @return array + * @throws RuntimeException + */ + private function decodeLabelValues($values) + { + $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; + } }