Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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