Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
run: echo "::set-output name=dir::$(composer config cache-files-dir)"

- name: Cache Composer packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }}
Expand Down
9 changes: 9 additions & 0 deletions src/Adapters/Redis/AbstractRedisStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ final public function setMetricValue(string $name, float $value, array $tags = [
$this->redisConnection->setMetrics([new MetricDto($name, $value, $tags)]);
}

/**
* @param HistogramMetricDto $metricDto
* @return void
*/
final public function adjustHistogramMetric(HistogramMetricDto $metricDto): void
{
$this->redisConnection->adjustHistogramMetric($metricDto);
}

/** {@inheritdoc} */
final public function adjustMetricValue(string $name, float $value, array $tags = []): float
{
Expand Down
49 changes: 49 additions & 0 deletions src/Adapters/Redis/HistogramMetricDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Lamoda\Metric\Adapters\Redis;

class HistogramMetricDto
{
/** @var string */
public $name;
/** @var float */
public $value;
/** @var array<string, string> */
public $tags;
/** @var float[] */
public $buckets;
/** @var string */
public $le;

/**
* @param array<string, string> $tags
* @param float[]|int[] $buckets
*/
public function __construct(string $name, float $value, array $buckets, array $tags)
{
$this->name = $name;
$this->value = $value;
$this->tags = $tags;
sort($buckets);
$this->buckets = $buckets;
$this->le = $this->calculateLe($buckets, $value);
}

/**
* @param float[] $buckets
* @param float $value
* @return string
*/
private function calculateLe(array $buckets, float $value): string
{
$bucketToIncrease = '+Inf';
foreach ($buckets as $bucket) {
if ($value <= $bucket) {
$bucketToIncrease = $bucket;
break;
}
}

return (string) $bucketToIncrease;
}
}
6 changes: 6 additions & 0 deletions src/Adapters/Redis/MutatorRedisConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ public function adjustMetric(string $key, float $delta, array $tags): float;
* @param MetricDto[] $metricsData
*/
public function setMetrics(array $metricsData): void;

/**
* @param HistogramMetricDto $metricDto
* @return float
*/
public function adjustHistogramMetric(HistogramMetricDto $metricDto): float;
}
53 changes: 53 additions & 0 deletions src/Adapters/Redis/RedisConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ public function setMetrics(array $metricsData): void
$this->client->hmset($this->metricsKey, $fields);
}

/** {@inheritdoc} */
public function adjustHistogramMetric(HistogramMetricDto $metricDto): float
{
$this->client->multi();
$this->client->hincrbyfloat($this->metricsKey, $this->buildHistogramFieldForSum($metricDto), $metricDto->value);
$this->client->hincrby($this->metricsKey, $this->buildHistogramFieldForValue($metricDto), 1);
$result = $this->client->exec();

return (float) ($result[0] ?? null);
}

/** {@inheritdoc} */
public function getMetricValue(string $key, array $tags): ?float
{
Expand All @@ -77,6 +88,48 @@ private function buildField(string $name, array $tags)
]);
}

/**
* @param HistogramMetricDto $histogramMetricDto
* @return string
*/
private function buildHistogramFieldForValue(HistogramMetricDto $histogramMetricDto): string
{
return json_encode([
'name' => $histogramMetricDto->name,
'tags' => $this->convertTagsForStorage(array_merge(
$histogramMetricDto->tags,
[
'le' => $histogramMetricDto->le,
'_meta' => [
'type' => 'histogram',
'buckets' => $histogramMetricDto->buckets
]
]
))
]);
}

/**
* @param HistogramMetricDto $histogramMetricDto
* @return string
*/
private function buildHistogramFieldForSum(HistogramMetricDto $histogramMetricDto): string
{
return json_encode([
'name' => $histogramMetricDto->name,
'tags' => $this->convertTagsForStorage(array_merge(
$histogramMetricDto->tags,
[
'_meta' => [
'type' => 'histogram',
'buckets' => $histogramMetricDto->buckets,
'is_sum' => true
]
]
))
]);
}

private function convertTagsForStorage(array $tags): string
{
return json_encode($this->normalizeTags($tags));
Expand Down
114 changes: 111 additions & 3 deletions src/Responder/ResponseFactory/PrometheusResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lamoda\Metric\Responder\ResponseFactory;

use GuzzleHttp\Psr7\Response;
use Lamoda\Metric\Common\MetricInterface;
use Lamoda\Metric\Common\MetricSourceInterface;
use Lamoda\Metric\Responder\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -29,21 +30,128 @@ final class PrometheusResponseFactory implements ResponseFactoryInterface
public function create(MetricSourceInterface $source, array $options = []): ResponseInterface
{
$data = [];
$histogramMetricsData = [];
$prefix = $options['prefix'] ?? '';
foreach ($source->getMetrics() as $metric) {
$tags = $metric->getTags();

if (isset($tags['_meta']) && ($tags['_meta']['type'] ?? '') === 'histogram') {
$histogramMetricsData = $this->prepareHistogramMetric($metric, $histogramMetricsData);
continue;
}

$data[] = [
'name' => ($options['prefix'] ?? '') . $metric->getName(),
'name' => $prefix . $metric->getName(),
'value' => $metric->resolve(),
'tags' => $metric->getTags(),
'tags' => $tags,
];
}

$histogramData = $this->calculateHistogramMetric($histogramMetricsData, $prefix);

return new Response(
200,
['Content-Type' => self::CONTENT_TYPE],
$this->getContent($data)
$this->getContent(array_merge($data, $histogramData))
);
}

private function buildHistogramMetricHash(MetricInterface $metric): string
{
return md5($metric->getName() . implode('', $this->clearTags($metric->getTags())));
}

/**
* @param array<string, string> $tags
* @return array<string, string>
*/
private function clearTags(array $tags): array
{
if (isset($tags['_meta'])) {
unset($tags['_meta']);
}

if (isset($tags['le'])) {
unset($tags['le']);
}

return $tags;
}

/**
* @param array<string, array<string, mixed>> $preparedHistogramMetricsData
* @return array<string, array<string, mixed>>
*/
private function prepareHistogramMetric(MetricInterface $metric, array $preparedHistogramMetricsData): array
{
$tags = $metric->getTags();
$metaTags = $tags['_meta'];
$le = $tags['le'] ?? null;
$tags = $this->clearTags($tags);
$keyMetric = $this->buildHistogramMetricHash($metric);

if (!isset($preparedHistogramMetricsData[$keyMetric])) {
$preparedHistogramMetricsData[$keyMetric] = [
'name' => $metric->getName(),
'buckets' => $metaTags['buckets'],
'tags' => $tags,
'data' => [],
'sum' => 0,
];
}

if (isset($metaTags['is_sum'])) {
$preparedHistogramMetricsData[$keyMetric]['sum'] = $metric->resolve();
}

if ($le !== null) {
$preparedHistogramMetricsData[$keyMetric]['data'][(string) $le] = $metric->resolve();
}

return $preparedHistogramMetricsData;
}

/**
* @return array<string, array<int, mixed>> $histogramMetricsData
* @return array<int, array<string, string>>
*/
private function calculateHistogramMetric(array $histogramMetricsData, string $prefix = ''): array
{
$data = [];
foreach ($histogramMetricsData as $histogramMetricData) {
$total = 0;
$buckets = $histogramMetricData['buckets'];
if (!in_array('+Inf', $buckets)) {
$buckets[] = '+Inf';
}

foreach ($buckets as $bucket) {
$value = $histogramMetricData['data'][(string)$bucket] ?? 0;
$total += $value;

$data[] = [
'name' => $prefix . $histogramMetricData['name'] . '_bucket',
'value' => $total,
'tags' => array_merge($histogramMetricData['tags'], ['le' => (string) $bucket])
];
}

$data[] = [
'name' => $prefix . $histogramMetricData['name'] . '_sum',
'value' => $histogramMetricData['sum'],
'tags' => $histogramMetricData['tags'],
];

$data[] = [
'name' => $prefix . $histogramMetricData['name'] . '_count',
'value' => $total,
'tags' => $histogramMetricData['tags'],
];
}

return $data;
}

/**
* Get response content.
*
Expand Down
31 changes: 31 additions & 0 deletions tests/Adapters/Redis/RedisConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Adapters\Redis;

use Lamoda\Metric\Adapters\Redis\HistogramMetricDto;
use Lamoda\Metric\Adapters\Redis\MetricDto;
use Lamoda\Metric\Adapters\Redis\RedisConnection;
use PHPUnit\Framework\MockObject\MockObject;
Expand Down Expand Up @@ -57,6 +58,36 @@ public function testAdjustMetric(): void
self::assertEquals(17, $actual);
}

public function testAdjustHistogramMetric(): void
{
$value = 1.5;
$this->redis
->expects($this->once())
->method('hincrbyfloat')
->with(
self::METRICS_KEY,
'{"name":"test","tags":"{\"_meta\":{\"type\":\"histogram\",\"buckets\":[1,2,3],\"is_sum\":true},\"severity\":\"high\"}"}',
$value
);

$this->redis
->expects($this->once())
->method('hincrby')
->with(
self::METRICS_KEY,
'{"name":"test","tags":"{\"_meta\":{\"type\":\"histogram\",\"buckets\":[1,2,3]},\"le\":\"2\",\"severity\":\"high\"}"}',
1
);

$this->redis
->expects($this->once())
->method('exec')
->willReturn([$value, 1]);

$actual = $this->redisConnection->adjustHistogramMetric(new HistogramMetricDto('test', $value, [1, 2, 3], ['severity' => 'high']));
self::assertEquals($value, $actual);
}

public function testSetMetrics(): void
{
$fields = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public function testResponseFormat(): void
new Metric('metrics_orders', 200.0, ['country' => 'ru']),
new Metric('metrics_errors', 0.0, ['country' => 'ru']),
new Metric('untagged_metric', 5.0),
new Metric('histogram_metric', 1.0, ['_meta' => ['type' => 'histogram', 'buckets' => [0.1,0.5,0.9]], 'country' => 'ru', 'le' =>'0.5']),
new Metric('histogram_metric', 0.5, ['_meta' => ['type' => 'histogram', 'buckets' => [0.1,0.5,0.9], 'is_sum' => true], 'country' => 'ru'])
]
);

Expand All @@ -30,6 +32,12 @@ public function testResponseFormat(): void
metrics_orders{country="ru"} 200
metrics_errors{country="ru"} 0
untagged_metric 5
histogram_metric_bucket{country="ru",le="0.1"} 0
histogram_metric_bucket{country="ru",le="0.5"} 1
histogram_metric_bucket{country="ru",le="0.9"} 1
histogram_metric_bucket{country="ru",le="+Inf"} 1
histogram_metric_sum{country="ru"} 0.5
histogram_metric_count{country="ru"} 1

PROMETHEUS
,
Expand Down
Loading