Skip to content

Commit 8e18a1b

Browse files
committed
Optimize histogram metric collection.
1 parent 0ac0595 commit 8e18a1b

File tree

9 files changed

+427
-130
lines changed

9 files changed

+427
-130
lines changed

src/Prometheus/Storage/RedisTxn.php

+12-125
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
use Prometheus\MetricFamilySamples;
1010
use Prometheus\Storage\RedisTxn\Collecter\CounterCollecter;
1111
use Prometheus\Storage\RedisTxn\Collecter\GaugeCollecter;
12+
use Prometheus\Storage\RedisTxn\Collecter\HistogramCollecter;
1213
use Prometheus\Storage\RedisTxn\Collecter\SummaryCollecter;
1314
use Prometheus\Storage\RedisTxn\Updater\CounterUpdater;
1415
use Prometheus\Storage\RedisTxn\Updater\GaugeUpdater;
16+
use Prometheus\Storage\RedisTxn\Updater\HistogramUpdater;
1517
use Prometheus\Storage\RedisTxn\Updater\SummaryUpdater;
1618
use function \sort;
1719

@@ -35,10 +37,6 @@
3537
*/
3638
class RedisTxn implements Adapter
3739
{
38-
const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS';
39-
40-
const PROMETHEUS_METRIC_META_SUFFIX = '_METRIC_META';
41-
4240
/**
4341
* @var mixed[]
4442
*/
@@ -152,21 +150,14 @@ public function collect(): array
152150
// Ensure Redis connection
153151
$this->ensureOpenConnection();
154152

155-
$metrics = $this->collectHistograms();
156-
$metricFamilySamples = array_map(
157-
function (array $metric): MetricFamilySamples {
158-
return new MetricFamilySamples($metric);
159-
},
160-
$metrics
161-
);
162-
163153
// Collect all metrics
164154
$counters = $this->collectCounters();
155+
$histograms = $this->collectHistograms();
165156
$gauges = $this->collectGauges();
166157
$summaries = $this->collectSummaries();
167158
return array_merge(
168-
$metricFamilySamples,
169159
$counters,
160+
$histograms,
170161
$gauges,
171162
$summaries
172163
);
@@ -221,43 +212,16 @@ private function connectToServer(): void
221212
}
222213

223214
/**
224-
* @param mixed[] $data
225-
* @throws StorageException
215+
* @inheritDoc
226216
*/
227217
public function updateHistogram(array $data): void
228218
{
219+
// Ensure Redis connection
229220
$this->ensureOpenConnection();
230-
$bucketToIncrease = '+Inf';
231-
foreach ($data['buckets'] as $bucket) {
232-
if ($data['value'] <= $bucket) {
233-
$bucketToIncrease = $bucket;
234-
break;
235-
}
236-
}
237-
$metaData = $data;
238-
unset($metaData['value'], $metaData['labelValues']);
239221

240-
$this->redis->eval(
241-
<<<LUA
242-
local result = redis.call('hIncrByFloat', KEYS[1], ARGV[1], ARGV[3])
243-
redis.call('hIncrBy', KEYS[1], ARGV[2], 1)
244-
if tonumber(result) >= tonumber(ARGV[3]) then
245-
redis.call('hSet', KEYS[1], '__meta', ARGV[4])
246-
redis.call('sAdd', KEYS[2], KEYS[1])
247-
end
248-
return result
249-
LUA
250-
,
251-
[
252-
$this->toMetricKey($data),
253-
self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
254-
json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]),
255-
json_encode(['b' => $bucketToIncrease, 'labelValues' => $data['labelValues']]),
256-
$data['value'],
257-
json_encode($metaData),
258-
],
259-
2
260-
);
222+
// Update metric
223+
$updater = new HistogramUpdater($this->redis);
224+
$updater->update($data);
261225
}
262226

263227
/**
@@ -300,80 +264,12 @@ public function updateCounter(array $data): void
300264
}
301265

302266
/**
303-
* @return mixed[]
267+
* @return MetricFamilySamples[]
304268
*/
305269
private function collectHistograms(): array
306270
{
307-
$keys = $this->redis->sMembers(self::$prefix . Histogram::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX);
308-
sort($keys);
309-
$histograms = [];
310-
foreach ($keys as $key) {
311-
$raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key));
312-
$histogram = json_decode($raw['__meta'], true);
313-
unset($raw['__meta']);
314-
$histogram['samples'] = [];
315-
316-
// Add the Inf bucket so we can compute it later on
317-
$histogram['buckets'][] = '+Inf';
318-
319-
$allLabelValues = [];
320-
foreach (array_keys($raw) as $k) {
321-
$d = json_decode($k, true);
322-
if ($d['b'] == 'sum') {
323-
continue;
324-
}
325-
$allLabelValues[] = $d['labelValues'];
326-
}
327-
328-
// We need set semantics.
329-
// This is the equivalent of array_unique but for arrays of arrays.
330-
$allLabelValues = array_map("unserialize", array_unique(array_map("serialize", $allLabelValues)));
331-
sort($allLabelValues);
332-
333-
foreach ($allLabelValues as $labelValues) {
334-
// Fill up all buckets.
335-
// If the bucket doesn't exist fill in values from
336-
// the previous one.
337-
$acc = 0;
338-
foreach ($histogram['buckets'] as $bucket) {
339-
$bucketKey = json_encode(['b' => $bucket, 'labelValues' => $labelValues]);
340-
if (!isset($raw[$bucketKey])) {
341-
$histogram['samples'][] = [
342-
'name' => $histogram['name'] . '_bucket',
343-
'labelNames' => ['le'],
344-
'labelValues' => array_merge($labelValues, [$bucket]),
345-
'value' => $acc,
346-
];
347-
} else {
348-
$acc += $raw[$bucketKey];
349-
$histogram['samples'][] = [
350-
'name' => $histogram['name'] . '_bucket',
351-
'labelNames' => ['le'],
352-
'labelValues' => array_merge($labelValues, [$bucket]),
353-
'value' => $acc,
354-
];
355-
}
356-
}
357-
358-
// Add the count
359-
$histogram['samples'][] = [
360-
'name' => $histogram['name'] . '_count',
361-
'labelNames' => [],
362-
'labelValues' => $labelValues,
363-
'value' => $acc,
364-
];
365-
366-
// Add the sum
367-
$histogram['samples'][] = [
368-
'name' => $histogram['name'] . '_sum',
369-
'labelNames' => [],
370-
'labelValues' => $labelValues,
371-
'value' => $raw[json_encode(['b' => 'sum', 'labelValues' => $labelValues])],
372-
];
373-
}
374-
$histograms[] = $histogram;
375-
}
376-
return $histograms;
271+
$collector = new HistogramCollecter($this->redis);
272+
return $collector->getMetricFamilySamples();
377273
}
378274

379275
/**
@@ -402,13 +298,4 @@ private function collectCounters(): array
402298
$collector = new CounterCollecter($this->redis);
403299
return $collector->getMetricFamilySamples();
404300
}
405-
406-
/**
407-
* @param mixed[] $data
408-
* @return string
409-
*/
410-
private function toMetricKey(array $data): string
411-
{
412-
return implode(':', [self::$prefix, $data['type'], $data['name']]);
413-
}
414301
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace Prometheus\Storage\RedisTxn\Collecter;
4+
5+
use Prometheus\Storage\RedisTxn\Metric\MetadataBuilder;
6+
use Prometheus\Storage\RedisTxn\Metric\Metric;
7+
use Prometheus\Storage\RedisTxn\RedisScript\RedisScript;
8+
use Prometheus\Histogram;
9+
10+
class HistogramCollecter extends AbstractCollecter
11+
{
12+
const SCRIPT = <<<LUA
13+
local function hgetall(hash_key)
14+
local flat_map = redis.call('HGETALL', hash_key)
15+
local result = {}
16+
for i = 1, #flat_map, 2 do
17+
result[flat_map[i]] = flat_map[i + 1]
18+
end
19+
return result
20+
end
21+
22+
-- Parse script input
23+
local summaryRegistryKey = KEYS[1]
24+
local metadataKey = KEYS[2]
25+
26+
-- Process each registered metric
27+
local result = {}
28+
local metricKeys = redis.call('smembers', summaryRegistryKey)
29+
for i, metricKey in ipairs(metricKeys) do
30+
-- Determine if the registered metric key still exists
31+
local doesExist = redis.call('exists', metricKey)
32+
if doesExist then
33+
local metadata = redis.call('hget', metadataKey, metricKey)
34+
local samples = hgetall(metricKey)
35+
36+
-- Add the processed metric to the set of results
37+
result[metricKey] = {}
38+
result[metricKey]["metadata"] = metadata
39+
result[metricKey]["samples"] = samples
40+
else
41+
-- Remove metadata for expired key
42+
redis.call('srem', registryKey, metricKey)
43+
redis.call('hdel', metadataKey, metricKey)
44+
end
45+
end
46+
47+
-- Return the set of summary metrics
48+
return cjson.encode(result)
49+
LUA;
50+
51+
/**
52+
* @inheritDoc
53+
*/
54+
public function getRedisScript(): RedisScript
55+
{
56+
// Create Redis script args
57+
$numKeys = 2;
58+
$registryKey = $this->getHelper()->getRegistryKey(Histogram::TYPE);
59+
$metadataKey = $this->getHelper()->getMetadataKey(Histogram::TYPE);
60+
$scriptArgs = [
61+
$registryKey,
62+
$metadataKey,
63+
];
64+
65+
// Create Redis script
66+
return RedisScript::newBuilder()
67+
->withScript(self::SCRIPT)
68+
->withArgs($scriptArgs)
69+
->withNumKeys($numKeys)
70+
->build();
71+
}
72+
73+
/**
74+
* @inheritDoc
75+
*/
76+
public function getMetrics(): array
77+
{
78+
// Retrieve metrics from Redis
79+
$results = $this->getRedisScript()->eval($this->getRedis());
80+
81+
// Collate histogram observations by metric name
82+
$builders = [];
83+
$redisMetrics = json_decode($results, true);
84+
foreach ($redisMetrics as $redisMetric) {
85+
$phpMetadata = json_decode($redisMetric['metadata'], true);
86+
$metadata = MetadataBuilder::fromArray($phpMetadata)->build();
87+
$builder = $builders[$metadata->getName()] ?? Metric::newHistogramMetricBuilder()->withMetadata($metadata);
88+
$builder->withSamples($redisMetric['samples'], $metadata->getLabelValues());
89+
$builders[$metadata->getName()] = $builder;
90+
}
91+
92+
// Build collated histograms into Metric structures
93+
$metrics = [];
94+
foreach ($builders as $builder) {
95+
$metrics[] = $builder->build();
96+
}
97+
return $metrics;
98+
}
99+
}

0 commit comments

Comments
 (0)