Skip to content

Commit 21673b9

Browse files
committed
Optimize counter metric collection.
1 parent 87997f8 commit 21673b9

File tree

7 files changed

+775
-471
lines changed

7 files changed

+775
-471
lines changed

src/Prometheus/Storage/RedisTxn.php

+180-63
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Prometheus\Histogram;
1212
use Prometheus\MetricFamilySamples;
1313
use Prometheus\Storage\RedisTxn\Metadata;
14+
use Prometheus\Storage\RedisTxn\MetadataBuilder;
1415
use Prometheus\Storage\RedisTxn\Metric;
1516
use Prometheus\Summary;
1617
use RedisException;
@@ -39,6 +40,21 @@ class RedisTxn implements Adapter
3940

4041
const PROMETHEUS_METRIC_META_SUFFIX = '_METRIC_META';
4142

43+
const TTL_FIELD = 'ttl';
44+
45+
const DEFAULT_TTL_SECONDS = 600;
46+
47+
const HGETALL_FUNCTION = <<<LUA
48+
local function hgetall(key)
49+
local flat_map = redis.call('hgetall', key)
50+
local result = {}
51+
for i = 1, #flat_map, 2 do
52+
result[flat_map[i]] = flat_map[i + 1]
53+
end
54+
return result
55+
end
56+
LUA;
57+
4258
/**
4359
* @var mixed[]
4460
*/
@@ -303,15 +319,14 @@ public function updateSummary(array $data): void
303319
{
304320
$this->ensureOpenConnection();
305321

306-
// Prepare summary metadata
307-
$metaHashKey = self::$prefix . self::PROMETHEUS_METRIC_META_SUFFIX;
308-
$summaryMetadata = $this->toMetadata($data);
309-
$ttl = $summaryMetadata->getMaxAgeSeconds();
322+
// Prepare metadata
323+
$metadata = $this->toMetadata($data);
324+
$ttl = $metadata->getMaxAgeSeconds();
310325

311-
// Create summary key
312-
$keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX;
313-
$summaryKey = implode(':', [$keyPrefix, $data['name'], $summaryMetadata->getLabelValuesEncoded()]);
314-
$summaryRegistryKey = implode(':', [$keyPrefix, 'keys']);
326+
// Create Redis keys
327+
$metricKey = $this->getMetricKey($metadata);
328+
$registryKey = $this->getMetricRegistryKey($metadata->getType());
329+
$metadataKey = $this->getMetadataKey($metadata->getType());
315330

316331
// Get summary sample
317332
//
@@ -350,10 +365,10 @@ public function updateSummary(array $data): void
350365
LUA
351366
,
352367
[
353-
$summaryRegistryKey,
354-
$metaHashKey,
355-
$summaryKey,
356-
$summaryMetadata->toJson(),
368+
$registryKey,
369+
$metadataKey,
370+
$metricKey,
371+
$metadata->toJson(),
357372
$value,
358373
$currentTime,
359374
$ttl,
@@ -407,27 +422,50 @@ public function updateGauge(array $data): void
407422
public function updateCounter(array $data): void
408423
{
409424
$this->ensureOpenConnection();
410-
$metaData = $data;
411-
unset($metaData['value'], $metaData['labelValues'], $metaData['command']);
412-
$this->redis->eval(
413-
<<<LUA
414-
local result = redis.call(ARGV[1], KEYS[1], ARGV[3], ARGV[2])
415-
local added = redis.call('sAdd', KEYS[2], KEYS[1])
416-
if added == 1 then
417-
redis.call('hMSet', KEYS[1], '__meta', ARGV[4])
418-
end
419-
return result
425+
426+
// Prepare metadata
427+
$metadata = $this->toMetadata($data);
428+
429+
// Create Redis keys
430+
$metricKey = $this->getMetricKey($metadata);
431+
$registryKey = $this->getMetricRegistryKey($metadata->getType());
432+
$metadataKey = $this->getMetadataKey($metadata->getType());
433+
434+
// Prepare script input
435+
$command = $metadata->getCommand() === Adapter::COMMAND_INCREMENT_INTEGER ? 'incrby' : 'incrbyfloat';
436+
$value = $data['value'];
437+
$ttl = time() + ($metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS);
438+
439+
$this->redis->eval(<<<LUA
440+
-- Parse script input
441+
local registryKey = KEYS[1]
442+
local metadataKey = KEYS[2]
443+
local metricKey = KEYS[3]
444+
local metadata = ARGV[1]
445+
local observeCommand = ARGV[2]
446+
local value = ARGV[3]
447+
local ttl = ARGV[4]
448+
449+
-- Register metric value
450+
redis.call('sadd', registryKey, metricKey)
451+
452+
-- Register metric metadata
453+
redis.call('hset', metadataKey, metricKey, metadata)
454+
455+
-- Update metric value
456+
redis.call(observeCommand, metricKey, value)
457+
redis.call('expire', metricKey, ttl)
420458
LUA
421-
,
422-
[
423-
$this->toMetricKey($data),
424-
self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
425-
$this->getRedisCommand($data['command']),
426-
$data['value'],
427-
json_encode($data['labelValues']),
428-
json_encode($metaData),
429-
],
430-
2
459+
, [
460+
$registryKey,
461+
$metadataKey,
462+
$metricKey,
463+
$metadata->toJson(),
464+
$command,
465+
$value,
466+
$ttl
467+
],
468+
3
431469
);
432470
}
433471

@@ -440,11 +478,13 @@ private function toMetadata(array $data): Metadata
440478
{
441479
return Metadata::newBuilder()
442480
->withName($data['name'])
481+
->withType($data['type'])
443482
->withHelp($data['help'])
444483
->withLabelNames($data['labelNames'])
445484
->withLabelValues($data['labelValues'])
446-
->withQuantiles($data['quantiles'])
447-
->withMaxAgeSeconds($data['maxAgeSeconds'])
485+
->withQuantiles($data['quantiles'] ?? null)
486+
->withMaxAgeSeconds($data['maxAgeSeconds'] ?? null)
487+
->withCommand($data['command'] ?? null)
448488
->build();
449489
}
450490

@@ -548,23 +588,23 @@ private function collectSummaries(): array
548588
// Register summary key
549589
$keyPrefix = self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX;
550590
$summaryRegistryKey = implode(':', [$keyPrefix, 'keys']);
551-
$metaHashKey = self::$prefix . self::PROMETHEUS_METRIC_META_SUFFIX;
591+
$metadataKey = $this->getMetadataKey(Summary::TYPE);
552592
$currentTime = time();
553593

554594
$result = $this->redis->eval(<<<LUA
555595
-- Parse script input
556596
local summaryRegistryKey = KEYS[1]
557-
local metaHashKey = KEYS[2]
597+
local metadataKey = KEYS[2]
558598
local currentTime = tonumber(ARGV[1])
559-
local result = {}
560599
561600
-- Process each registered summary metric
601+
local result = {}
562602
local summaryKeys = redis.call('smembers', summaryRegistryKey)
563603
for i, summaryKey in ipairs(summaryKeys) do
564604
-- Get metric sample TTL
565605
local ttlFieldName = summaryKey .. ":ttl"
566606
redis.call('set', 'foo', ttlFieldName)
567-
local summaryTtl = redis.call("hget", metaHashKey, ttlFieldName)
607+
local summaryTtl = redis.call("hget", metadataKey, ttlFieldName)
568608
if summaryTtl ~= nil then
569609
summaryTtl = tonumber(summaryTtl)
570610
end
@@ -582,13 +622,13 @@ private function collectSummaries(): array
582622
local summarySamples = {}
583623
if numSamples > 0 then
584624
-- Configure results
585-
summaryMetadata = redis.call("hget", metaHashKey, summaryKey)
625+
summaryMetadata = redis.call("hget", metadataKey, summaryKey)
586626
summarySamples = redis.call("zrange", summaryKey, startScore, "+inf", "byscore")
587627
else
588628
-- Remove the metric's associated metadata if there are no associated samples remaining
589629
redis.call('srem', summaryRegistryKey, summaryKey)
590-
redis.call('hdel', metaHashKey, summaryKey)
591-
redis.call('hdel', metaHashKey, ttlFieldName)
630+
redis.call('hdel', metadataKey, summaryKey)
631+
redis.call('hdel', metadataKey, ttlFieldName)
592632
end
593633
594634
-- Add the processed metric to the set of results
@@ -603,17 +643,17 @@ private function collectSummaries(): array
603643
,
604644
[
605645
$summaryRegistryKey,
606-
$metaHashKey,
646+
$metadataKey,
607647
$currentTime,
608648
],
609649
2
610650
);
611651

612-
// Format summary metrics and hand them off to the calling collector
652+
// Format metrics and hand them off to the calling collector
613653
$summaries = [];
614654
$redisSummaries = json_decode($result, true);
615655
foreach ($redisSummaries as $summary) {
616-
$serializedSummary = Metric::newBuilder()
656+
$serializedSummary = Metric::newSummaryMetricBuilder()
617657
->withMetadata($summary['metadata'])
618658
->withSamples($summary['samples'])
619659
->build()
@@ -657,26 +697,71 @@ private function collectGauges(): array
657697
*/
658698
private function collectCounters(): array
659699
{
660-
$keys = $this->redis->sMembers(self::$prefix . Counter::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX);
661-
sort($keys);
700+
// Create Redis keys
701+
$registryKey = $this->getMetricRegistryKey(Counter::TYPE);
702+
$metadataKey = $this->getMetadataKey(Counter::TYPE);
703+
704+
// Execute transaction to collect metrics
705+
$hgetallFunction = self::HGETALL_FUNCTION;
706+
$result = $this->redis->eval(<<<LUA
707+
$hgetallFunction
708+
709+
-- Parse script input
710+
local registryKey = KEYS[1]
711+
local metadataKey = KEYS[2]
712+
713+
-- Process each registered counter metric
714+
local result = {}
715+
local counterKeys = redis.call('smembers', registryKey)
716+
for i, counterKey in ipairs(counterKeys) do
717+
local doesExist = redis.call('exists', counterKey)
718+
if doesExist then
719+
-- Get counter metadata
720+
local metadata = redis.call('hget', metadataKey, counterKey)
721+
722+
-- Get counter sample
723+
local sample = redis.call('get', counterKey)
724+
725+
-- Add the processed metric to the set of results
726+
result[counterKey] = {}
727+
result[counterKey]["metadata"] = metadata
728+
result[counterKey]["samples"] = sample
729+
else
730+
-- Remove metadata for expired key
731+
redis.call('srem', registryKey, counterKey)
732+
redis.call('hdel', metadataKey, counterKey)
733+
end
734+
end
735+
736+
-- Return the set of collected metrics
737+
return cjson.encode(result)
738+
LUA
739+
, [
740+
$registryKey,
741+
$metadataKey,
742+
],
743+
2
744+
);
745+
746+
// Collate counter metrics by metric name
747+
$metrics = [];
748+
$redisCounters = json_decode($result, true);
749+
foreach ($redisCounters as $counter) {
750+
// Get metadata
751+
$phpMetadata = json_decode($counter['metadata'], true);
752+
$metadata = MetadataBuilder::fromArray($phpMetadata)->build();
753+
754+
// Create or update metric
755+
$metricName = $metadata->getName();
756+
$builder = $metrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata);
757+
$builder->withSample($counter['samples'], $metadata->getLabelValues());
758+
$metrics[$metricName] = $builder;
759+
}
760+
761+
// Format metrics and hand them off to the calling collector
662762
$counters = [];
663-
foreach ($keys as $key) {
664-
$raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key));
665-
$counter = json_decode($raw['__meta'], true);
666-
unset($raw['__meta']);
667-
$counter['samples'] = [];
668-
foreach ($raw as $k => $value) {
669-
$counter['samples'][] = [
670-
'name' => $counter['name'],
671-
'labelNames' => [],
672-
'labelValues' => json_decode($k, true),
673-
'value' => $value,
674-
];
675-
}
676-
usort($counter['samples'], function ($a, $b): int {
677-
return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues']));
678-
});
679-
$counters[] = $counter;
763+
foreach ($metrics as $_ => $metric) {
764+
$counters[] = $metric->build()->toArray();
680765
}
681766
return $counters;
682767
}
@@ -739,4 +824,36 @@ private function decodeLabelValues(string $values): array
739824
}
740825
return $decodedValues;
741826
}
827+
828+
/**
829+
* @param string $metricType
830+
* @return string
831+
*/
832+
private function getMetricRegistryKey(string $metricType): string
833+
{
834+
$keyPrefix = self::$prefix . $metricType . self::PROMETHEUS_METRIC_KEYS_SUFFIX;
835+
return implode(':', [$keyPrefix, 'keys']);
836+
}
837+
838+
/**
839+
* @param string $metricType
840+
* @return string
841+
*/
842+
private function getMetadataKey(string $metricType): string
843+
{
844+
return self::$prefix . $metricType . self::PROMETHEUS_METRIC_META_SUFFIX;
845+
}
846+
847+
/**
848+
* @param Metadata $metadata
849+
* @return string
850+
*/
851+
private function getMetricKey(Metadata $metadata): string
852+
{
853+
$type = $metadata->getType();
854+
$name = $metadata->getName();
855+
$labelValues = $metadata->getLabelValuesEncoded();
856+
$keyPrefix = self::$prefix . $type . self::PROMETHEUS_METRIC_KEYS_SUFFIX;
857+
return implode(':', [$keyPrefix, $name, $labelValues]);
858+
}
742859
}

0 commit comments

Comments
 (0)