Skip to content

Commit b9ff124

Browse files
pnv4501100-technikolay.pasechny
andauthored
add histogram metric for prometheus (#26)
* add histogram metric for prometheus * fix actions/cache --------- Co-authored-by: nikolay.pasechny <[email protected]>
1 parent 0451d7e commit b9ff124

File tree

8 files changed

+268
-4
lines changed

8 files changed

+268
-4
lines changed

.github/workflows/php.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
4646

4747
- name: Cache Composer packages
48-
uses: actions/cache@v2
48+
uses: actions/cache@v3
4949
with:
5050
path: ${{ steps.composer-cache.outputs.dir }}
5151
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }}

src/Adapters/Redis/AbstractRedisStorage.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ final public function setMetricValue(string $name, float $value, array $tags = [
8484
$this->redisConnection->setMetrics([new MetricDto($name, $value, $tags)]);
8585
}
8686

87+
/**
88+
* @param HistogramMetricDto $metricDto
89+
* @return void
90+
*/
91+
final public function adjustHistogramMetric(HistogramMetricDto $metricDto): void
92+
{
93+
$this->redisConnection->adjustHistogramMetric($metricDto);
94+
}
95+
8796
/** {@inheritdoc} */
8897
final public function adjustMetricValue(string $name, float $value, array $tags = []): float
8998
{
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Lamoda\Metric\Adapters\Redis;
4+
5+
class HistogramMetricDto
6+
{
7+
/** @var string */
8+
public $name;
9+
/** @var float */
10+
public $value;
11+
/** @var array<string, string> */
12+
public $tags;
13+
/** @var float[] */
14+
public $buckets;
15+
/** @var string */
16+
public $le;
17+
18+
/**
19+
* @param array<string, string> $tags
20+
* @param float[]|int[] $buckets
21+
*/
22+
public function __construct(string $name, float $value, array $buckets, array $tags)
23+
{
24+
$this->name = $name;
25+
$this->value = $value;
26+
$this->tags = $tags;
27+
sort($buckets);
28+
$this->buckets = $buckets;
29+
$this->le = $this->calculateLe($buckets, $value);
30+
}
31+
32+
/**
33+
* @param float[] $buckets
34+
* @param float $value
35+
* @return string
36+
*/
37+
private function calculateLe(array $buckets, float $value): string
38+
{
39+
$bucketToIncrease = '+Inf';
40+
foreach ($buckets as $bucket) {
41+
if ($value <= $bucket) {
42+
$bucketToIncrease = $bucket;
43+
break;
44+
}
45+
}
46+
47+
return (string) $bucketToIncrease;
48+
}
49+
}

src/Adapters/Redis/MutatorRedisConnectionInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@ public function adjustMetric(string $key, float $delta, array $tags): float;
1212
* @param MetricDto[] $metricsData
1313
*/
1414
public function setMetrics(array $metricsData): void;
15+
16+
/**
17+
* @param HistogramMetricDto $metricDto
18+
* @return float
19+
*/
20+
public function adjustHistogramMetric(HistogramMetricDto $metricDto): float;
1521
}

src/Adapters/Redis/RedisConnection.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ public function setMetrics(array $metricsData): void
5858
$this->client->hmset($this->metricsKey, $fields);
5959
}
6060

61+
/** {@inheritdoc} */
62+
public function adjustHistogramMetric(HistogramMetricDto $metricDto): float
63+
{
64+
$this->client->multi();
65+
$this->client->hincrbyfloat($this->metricsKey, $this->buildHistogramFieldForSum($metricDto), $metricDto->value);
66+
$this->client->hincrby($this->metricsKey, $this->buildHistogramFieldForValue($metricDto), 1);
67+
$result = $this->client->exec();
68+
69+
return (float) ($result[0] ?? null);
70+
}
71+
6172
/** {@inheritdoc} */
6273
public function getMetricValue(string $key, array $tags): ?float
6374
{
@@ -77,6 +88,48 @@ private function buildField(string $name, array $tags)
7788
]);
7889
}
7990

91+
/**
92+
* @param HistogramMetricDto $histogramMetricDto
93+
* @return string
94+
*/
95+
private function buildHistogramFieldForValue(HistogramMetricDto $histogramMetricDto): string
96+
{
97+
return json_encode([
98+
'name' => $histogramMetricDto->name,
99+
'tags' => $this->convertTagsForStorage(array_merge(
100+
$histogramMetricDto->tags,
101+
[
102+
'le' => $histogramMetricDto->le,
103+
'_meta' => [
104+
'type' => 'histogram',
105+
'buckets' => $histogramMetricDto->buckets
106+
]
107+
]
108+
))
109+
]);
110+
}
111+
112+
/**
113+
* @param HistogramMetricDto $histogramMetricDto
114+
* @return string
115+
*/
116+
private function buildHistogramFieldForSum(HistogramMetricDto $histogramMetricDto): string
117+
{
118+
return json_encode([
119+
'name' => $histogramMetricDto->name,
120+
'tags' => $this->convertTagsForStorage(array_merge(
121+
$histogramMetricDto->tags,
122+
[
123+
'_meta' => [
124+
'type' => 'histogram',
125+
'buckets' => $histogramMetricDto->buckets,
126+
'is_sum' => true
127+
]
128+
]
129+
))
130+
]);
131+
}
132+
80133
private function convertTagsForStorage(array $tags): string
81134
{
82135
return json_encode($this->normalizeTags($tags));

src/Responder/ResponseFactory/PrometheusResponseFactory.php

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Lamoda\Metric\Responder\ResponseFactory;
44

55
use GuzzleHttp\Psr7\Response;
6+
use Lamoda\Metric\Common\MetricInterface;
67
use Lamoda\Metric\Common\MetricSourceInterface;
78
use Lamoda\Metric\Responder\ResponseFactoryInterface;
89
use Psr\Http\Message\ResponseInterface;
@@ -29,21 +30,128 @@ final class PrometheusResponseFactory implements ResponseFactoryInterface
2930
public function create(MetricSourceInterface $source, array $options = []): ResponseInterface
3031
{
3132
$data = [];
33+
$histogramMetricsData = [];
34+
$prefix = $options['prefix'] ?? '';
3235
foreach ($source->getMetrics() as $metric) {
36+
$tags = $metric->getTags();
37+
38+
if (isset($tags['_meta']) && ($tags['_meta']['type'] ?? '') === 'histogram') {
39+
$histogramMetricsData = $this->prepareHistogramMetric($metric, $histogramMetricsData);
40+
continue;
41+
}
42+
3343
$data[] = [
34-
'name' => ($options['prefix'] ?? '') . $metric->getName(),
44+
'name' => $prefix . $metric->getName(),
3545
'value' => $metric->resolve(),
36-
'tags' => $metric->getTags(),
46+
'tags' => $tags,
3747
];
3848
}
49+
50+
$histogramData = $this->calculateHistogramMetric($histogramMetricsData, $prefix);
3951

4052
return new Response(
4153
200,
4254
['Content-Type' => self::CONTENT_TYPE],
43-
$this->getContent($data)
55+
$this->getContent(array_merge($data, $histogramData))
4456
);
4557
}
4658

59+
private function buildHistogramMetricHash(MetricInterface $metric): string
60+
{
61+
return md5($metric->getName() . implode('', $this->clearTags($metric->getTags())));
62+
}
63+
64+
/**
65+
* @param array<string, string> $tags
66+
* @return array<string, string>
67+
*/
68+
private function clearTags(array $tags): array
69+
{
70+
if (isset($tags['_meta'])) {
71+
unset($tags['_meta']);
72+
}
73+
74+
if (isset($tags['le'])) {
75+
unset($tags['le']);
76+
}
77+
78+
return $tags;
79+
}
80+
81+
/**
82+
* @param array<string, array<string, mixed>> $preparedHistogramMetricsData
83+
* @return array<string, array<string, mixed>>
84+
*/
85+
private function prepareHistogramMetric(MetricInterface $metric, array $preparedHistogramMetricsData): array
86+
{
87+
$tags = $metric->getTags();
88+
$metaTags = $tags['_meta'];
89+
$le = $tags['le'] ?? null;
90+
$tags = $this->clearTags($tags);
91+
$keyMetric = $this->buildHistogramMetricHash($metric);
92+
93+
if (!isset($preparedHistogramMetricsData[$keyMetric])) {
94+
$preparedHistogramMetricsData[$keyMetric] = [
95+
'name' => $metric->getName(),
96+
'buckets' => $metaTags['buckets'],
97+
'tags' => $tags,
98+
'data' => [],
99+
'sum' => 0,
100+
];
101+
}
102+
103+
if (isset($metaTags['is_sum'])) {
104+
$preparedHistogramMetricsData[$keyMetric]['sum'] = $metric->resolve();
105+
}
106+
107+
if ($le !== null) {
108+
$preparedHistogramMetricsData[$keyMetric]['data'][(string) $le] = $metric->resolve();
109+
}
110+
111+
return $preparedHistogramMetricsData;
112+
}
113+
114+
/**
115+
* @return array<string, array<int, mixed>> $histogramMetricsData
116+
* @return array<int, array<string, string>>
117+
*/
118+
private function calculateHistogramMetric(array $histogramMetricsData, string $prefix = ''): array
119+
{
120+
$data = [];
121+
foreach ($histogramMetricsData as $histogramMetricData) {
122+
$total = 0;
123+
$buckets = $histogramMetricData['buckets'];
124+
if (!in_array('+Inf', $buckets)) {
125+
$buckets[] = '+Inf';
126+
}
127+
128+
foreach ($buckets as $bucket) {
129+
$value = $histogramMetricData['data'][(string)$bucket] ?? 0;
130+
$total += $value;
131+
132+
$data[] = [
133+
'name' => $prefix . $histogramMetricData['name'] . '_bucket',
134+
'value' => $total,
135+
'tags' => array_merge($histogramMetricData['tags'], ['le' => (string) $bucket])
136+
];
137+
}
138+
139+
$data[] = [
140+
'name' => $prefix . $histogramMetricData['name'] . '_sum',
141+
'value' => $histogramMetricData['sum'],
142+
'tags' => $histogramMetricData['tags'],
143+
];
144+
145+
$data[] = [
146+
'name' => $prefix . $histogramMetricData['name'] . '_count',
147+
'value' => $total,
148+
'tags' => $histogramMetricData['tags'],
149+
];
150+
}
151+
152+
return $data;
153+
}
154+
47155
/**
48156
* Get response content.
49157
*

tests/Adapters/Redis/RedisConnectionTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Adapters\Redis;
66

7+
use Lamoda\Metric\Adapters\Redis\HistogramMetricDto;
78
use Lamoda\Metric\Adapters\Redis\MetricDto;
89
use Lamoda\Metric\Adapters\Redis\RedisConnection;
910
use PHPUnit\Framework\MockObject\MockObject;
@@ -57,6 +58,36 @@ public function testAdjustMetric(): void
5758
self::assertEquals(17, $actual);
5859
}
5960

61+
public function testAdjustHistogramMetric(): void
62+
{
63+
$value = 1.5;
64+
$this->redis
65+
->expects($this->once())
66+
->method('hincrbyfloat')
67+
->with(
68+
self::METRICS_KEY,
69+
'{"name":"test","tags":"{\"_meta\":{\"type\":\"histogram\",\"buckets\":[1,2,3],\"is_sum\":true},\"severity\":\"high\"}"}',
70+
$value
71+
);
72+
73+
$this->redis
74+
->expects($this->once())
75+
->method('hincrby')
76+
->with(
77+
self::METRICS_KEY,
78+
'{"name":"test","tags":"{\"_meta\":{\"type\":\"histogram\",\"buckets\":[1,2,3]},\"le\":\"2\",\"severity\":\"high\"}"}',
79+
1
80+
);
81+
82+
$this->redis
83+
->expects($this->once())
84+
->method('exec')
85+
->willReturn([$value, 1]);
86+
87+
$actual = $this->redisConnection->adjustHistogramMetric(new HistogramMetricDto('test', $value, [1, 2, 3], ['severity' => 'high']));
88+
self::assertEquals($value, $actual);
89+
}
90+
6091
public function testSetMetrics(): void
6192
{
6293
$fields = [

tests/Responder/ResponseFactory/PrometheusResponseFactoryTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public function testResponseFormat(): void
1919
new Metric('metrics_orders', 200.0, ['country' => 'ru']),
2020
new Metric('metrics_errors', 0.0, ['country' => 'ru']),
2121
new Metric('untagged_metric', 5.0),
22+
new Metric('histogram_metric', 1.0, ['_meta' => ['type' => 'histogram', 'buckets' => [0.1,0.5,0.9]], 'country' => 'ru', 'le' =>'0.5']),
23+
new Metric('histogram_metric', 0.5, ['_meta' => ['type' => 'histogram', 'buckets' => [0.1,0.5,0.9], 'is_sum' => true], 'country' => 'ru'])
2224
]
2325
);
2426

@@ -30,6 +32,12 @@ public function testResponseFormat(): void
3032
metrics_orders{country="ru"} 200
3133
metrics_errors{country="ru"} 0
3234
untagged_metric 5
35+
histogram_metric_bucket{country="ru",le="0.1"} 0
36+
histogram_metric_bucket{country="ru",le="0.5"} 1
37+
histogram_metric_bucket{country="ru",le="0.9"} 1
38+
histogram_metric_bucket{country="ru",le="+Inf"} 1
39+
histogram_metric_sum{country="ru"} 0.5
40+
histogram_metric_count{country="ru"} 1
3341
3442
PROMETHEUS
3543
,

0 commit comments

Comments
 (0)