diff --git a/src/Controllers/Metrics/MetricsEngine.php b/src/Controllers/Metrics/MetricsEngine.php index 375bb096..bcf0d8ca 100644 --- a/src/Controllers/Metrics/MetricsEngine.php +++ b/src/Controllers/Metrics/MetricsEngine.php @@ -6,11 +6,15 @@ use Engelsystem\Renderer\EngineInterface; class MetricsEngine implements EngineInterface { + /** @var string */ + protected $prefix = 'engelsystem_'; + /** * Render metrics * * @param string $path * @param mixed[] $data + * * @return string * * @example $data = ['foo' => [['labels' => ['foo'=>'bar'], 'value'=>42]], 'bar'=>123] @@ -25,21 +29,29 @@ class MetricsEngine implements EngineInterface } $list = is_array($list) ? $list : [$list]; - $name = 'engelsystem_' . $name; + $name = $this->prefix . $name; if (isset($list['help'])) { $return[] = sprintf('# HELP %s %s', $name, $this->escape($list['help'])); unset($list['help']); } + $type = null; if (isset($list['type'])) { + $type = $list['type']; $return[] = sprintf('# TYPE %s %s', $name, $list['type']); unset($list['type']); } $list = (!isset($list['value']) || !isset($list['labels'])) ? $list : [$list]; foreach ($list as $row) { - $row = is_array($row) ? $row : [$row]; + $row = $this->expandData($row); + + if ($type == 'histogram') { + $return = array_merge($return, $this->formatHistogram($row, $name)); + + continue; + } $return[] = $this->formatData($name, $row); } @@ -49,17 +61,72 @@ class MetricsEngine implements EngineInterface } /** - * @param string $path - * @return bool + * @param array $row + * @param string $name + * + * @return array[] */ - public function canRender(string $path): bool + protected function formatHistogram(array $row, string $name): array { - return $path == '/metrics'; + $return = []; + $data = ['labels' => $row['labels']]; + + if (!isset($row['value']['+Inf'])) { + $row['value']['+Inf'] = !empty($row['value']) ? max($row['value']) : 'NaN'; + } + asort($row['value']); + + foreach ($row['value'] as $le => $value) { + $return[] = $this->formatData( + $name . '_bucket', + array_merge_recursive($data, ['value' => $value, 'labels' => ['le' => $le]]) + ); + } + + $sum = isset($row['sum']) ? $row['sum'] : 'NaN'; + $count = $row['value']['+Inf']; + $return[] = $this->formatData($name . '_sum', $data + ['value' => $sum]); + $return[] = $this->formatData($name . '_count', $data + ['value' => $count]); + + return $return; + } + + /** + * Expand the value to be an array + * + * @param $data + * + * @return array + */ + protected function expandData($data): array + { + $data = is_array($data) ? $data : [$data]; + $return = ['labels' => [], 'value' => null]; + + if (isset($data['labels'])) { + $return['labels'] = $data['labels']; + unset($data['labels']); + } + + if (isset($data['sum'])) { + $return['sum'] = $data['sum']; + unset($data['sum']); + } + + if (isset($data['value'])) { + $return['value'] = $data['value']; + unset($data['value']); + } else { + $return['value'] = $data; + } + + return $return; } /** * @param string $name * @param array|mixed $row + * * @return string * @see https://prometheus.io/docs/instrumenting/exposition_formats/ */ @@ -68,23 +135,23 @@ class MetricsEngine implements EngineInterface return sprintf( '%s%s %s', $name, - $this->renderLabels($row), - $this->renderValue($row) + $this->renderLabels($row['labels']), + $this->renderValue($row['value']) ); } /** - * @param array|mixed $row + * @param array $labels + * * @return mixed */ - protected function renderLabels($row): string + protected function renderLabels(array $labels): string { - $labels = []; - if (!is_array($row) || empty($row['labels'])) { + if (empty($labels)) { return ''; } - foreach ($row['labels'] as $type => $value) { + foreach ($labels as $type => $value) { $labels[$type] = $type . '="' . $this->formatValue($value) . '"'; } @@ -93,19 +160,21 @@ class MetricsEngine implements EngineInterface /** * @param array|mixed $row + * * @return mixed */ protected function renderValue($row) { - if (isset($row['value'])) { - return $this->formatValue($row['value']); + if (is_array($row)) { + $row = array_pop($row); } - return $this->formatValue(array_pop($row)); + return $this->formatValue($row); } /** * @param mixed $value + * * @return mixed */ protected function formatValue($value) @@ -119,6 +188,7 @@ class MetricsEngine implements EngineInterface /** * @param mixed $value + * * @return mixed */ protected function escape($value) @@ -136,6 +206,16 @@ class MetricsEngine implements EngineInterface ); } + /** + * @param string $path + * + * @return bool + */ + public function canRender(string $path): bool + { + return $path == '/metrics'; + } + /** * Does nothing as shared data will only result in unexpected behaviour * diff --git a/tests/Unit/Controllers/Metrics/MetricsEngineTest.php b/tests/Unit/Controllers/Metrics/MetricsEngineTest.php index 87a7dc88..62699314 100644 --- a/tests/Unit/Controllers/Metrics/MetricsEngineTest.php +++ b/tests/Unit/Controllers/Metrics/MetricsEngineTest.php @@ -55,6 +55,85 @@ class MetricsEngineTest extends TestCase ); } + /** + * @covers \Engelsystem\Controllers\Metrics\MetricsEngine::expandData + * @covers \Engelsystem\Controllers\Metrics\MetricsEngine::formatHistogram + * @covers \Engelsystem\Controllers\Metrics\MetricsEngine::get + */ + public function testGetHistogram() + { + $engine = new MetricsEngine(); + + $this->assertEquals( + <<<'EOD' +# TYPE engelsystem_test_minimum_histogram histogram +engelsystem_test_minimum_histogram_bucket{le="3"} 4 +engelsystem_test_minimum_histogram_bucket{le="+Inf"} 4 +engelsystem_test_minimum_histogram_sum 1.337 +engelsystem_test_minimum_histogram_count 4 +EOD, + $engine->get('/metrics', [ + 'test_minimum_histogram' => [ + 'type' => 'histogram', [3 => 4, 'sum' => 1.337] + ], + ]) + ); + + $this->assertEquals( + <<<'EOD' +# TYPE engelsystem_test_short_histogram histogram +engelsystem_test_short_histogram_bucket{le="0"} 0 +engelsystem_test_short_histogram_bucket{le="60"} 10 +engelsystem_test_short_histogram_bucket{le="120"} 19 +engelsystem_test_short_histogram_bucket{le="+Inf"} 300 +engelsystem_test_short_histogram_sum 123.456 +engelsystem_test_short_histogram_count 300 +EOD, + $engine->get('/metrics', [ + 'test_short_histogram' => [ + 'type' => 'histogram', + 'value' => [120 => 19, '+Inf' => 300, 60 => 10, 0 => 0, 'sum' => 123.456] + ], + ]) + ); + + $this->assertEquals( + <<<'EOD' +# TYPE engelsystem_test_multiple_histogram histogram +engelsystem_test_multiple_histogram_bucket{handler="foo",le="0.1"} 32 +engelsystem_test_multiple_histogram_bucket{handler="foo",le="3"} 99 +engelsystem_test_multiple_histogram_bucket{handler="foo",le="+Inf"} 99 +engelsystem_test_multiple_histogram_sum{handler="foo"} 42 +engelsystem_test_multiple_histogram_count{handler="foo"} 99 +engelsystem_test_multiple_histogram_bucket{handler="bar",le="0.2"} 0 +engelsystem_test_multiple_histogram_bucket{handler="bar",le="+Inf"} 3 +engelsystem_test_multiple_histogram_sum{handler="bar"} 3 +engelsystem_test_multiple_histogram_count{handler="bar"} 3 +EOD, + $engine->get('/metrics', [ + 'test_multiple_histogram' => [ + 'type' => 'histogram', + ['labels' => ['handler' => 'foo'], 'sum' => '42', 'value' => ['0.1' => 32, 3 => 99]], + ['labels' => ['handler' => 'bar'], 'sum' => '3', 'value' => ['0.2' => 0, '+Inf' => 3]], + ], + ]) + ); + + $this->assertEquals( + <<<'EOD' +# TYPE engelsystem_test_minimum_histogram histogram +engelsystem_test_minimum_histogram_bucket{le="+Inf"} NaN +engelsystem_test_minimum_histogram_sum NaN +engelsystem_test_minimum_histogram_count NaN +EOD, + $engine->get('/metrics', [ + 'test_minimum_histogram' => [ + 'type' => 'histogram', [] + ], + ]) + ); + } + /** * @covers \Engelsystem\Controllers\Metrics\MetricsEngine::canRender */