Skip to content

Commit 7f7d050

Browse files
committed
Optimize gauge metric collection.
Signed-off-by: Arris Ray <arris.ray+github@gmail.com>
1 parent f1baf89 commit 7f7d050

File tree

1 file changed

+193
-71
lines changed

1 file changed

+193
-71
lines changed

src/Prometheus/Storage/RedisTxn.php

Lines changed: 193 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* "collect" operations of each metric type within a single Redis transaction.
3434
*
3535
* @todo Only summary metrics have been refactored so far. Complete refactor for counter, gauge, and histogram metrics.
36+
* @todo Reimplement all Redis scripts with redis.pcall() to trap runtime errors that are ignored by redis.call().
3637
*/
3738
class RedisTxn implements Adapter
3839
{
@@ -371,34 +372,74 @@ public function updateSummary(array $data): void
371372
public function updateGauge(array $data): void
372373
{
373374
$this->ensureOpenConnection();
374-
$metaData = $data;
375-
unset($metaData['value'], $metaData['labelValues'], $metaData['command']);
376-
$this->redis->eval(
377-
<<<LUA
378-
local result = redis.call(ARGV[1], KEYS[1], ARGV[2], ARGV[3])
379375

380-
if ARGV[1] == 'hSet' then
381-
if result == 1 then
382-
redis.call('hSet', KEYS[1], '__meta', ARGV[4])
383-
redis.call('sAdd', KEYS[2], KEYS[1])
384-
end
385-
else
386-
if result == ARGV[3] then
387-
redis.call('hSet', KEYS[1], '__meta', ARGV[4])
388-
redis.call('sAdd', KEYS[2], KEYS[1])
376+
// Prepare metadata
377+
$metadata = $this->toMetadata($data);
378+
379+
// Create Redis keys
380+
$metricKey = $this->getMetricKey($metadata);
381+
$registryKey = $this->getMetricRegistryKey($metadata->getType());
382+
$metadataKey = $this->getMetadataKey($metadata->getType());
383+
384+
// Prepare script and input
385+
$command = $this->getRedisCommand($metadata->getCommand());
386+
$value = $data['value'];
387+
$ttl = $metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS;
388+
$numKeys = 3;
389+
$scriptArgs = [
390+
$registryKey,
391+
$metadataKey,
392+
$metricKey,
393+
$metadata->toJson(),
394+
$command,
395+
$value,
396+
$ttl
397+
];
398+
$script = <<<LUA
399+
-- Parse script input
400+
local registryKey = KEYS[1]
401+
local metadataKey = KEYS[2]
402+
local metricKey = KEYS[3]
403+
local metadata = ARGV[1]
404+
local command = ARGV[2]
405+
local value = ARGV[3]
406+
local ttl = tonumber(ARGV[4])
407+
408+
-- Update metric value
409+
local didUpdate = false
410+
if command == "set" then
411+
local result = redis.call(command, metricKey, value)
412+
didUpdate = result["ok"] == "OK"
413+
else -- {incrby, incrbyfloat}
414+
local result = redis.call(command, metricKey, value)
415+
didUpdate = tostring(result) == value
416+
end
417+
418+
-- Update metric metadata
419+
if didUpdate == true then
420+
-- Set metric TTL
421+
if ttl > 0 then
422+
redis.call('expire', metricKey, ttl)
423+
else
424+
redis.call('persist', metricKey)
389425
end
426+
427+
-- Register metric value
428+
redis.call('sadd', registryKey, metricKey)
429+
430+
-- Register metric metadata
431+
redis.call('hset', metadataKey, metricKey, metadata)
390432
end
391-
LUA
392-
,
393-
[
394-
$this->toMetricKey($data),
395-
self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX,
396-
$this->getRedisCommand($data['command']),
397-
json_encode($data['labelValues']),
398-
$data['value'],
399-
json_encode($metaData),
400-
],
401-
2
433+
434+
-- Report script result
435+
return didUpdate
436+
LUA;
437+
438+
// Call script
439+
$this->redis->eval(
440+
$script,
441+
$scriptArgs,
442+
$numKeys
402443
);
403444
}
404445

@@ -419,11 +460,54 @@ public function updateCounter(array $data): void
419460
$metadataKey = $this->getMetadataKey($metadata->getType());
420461

421462
// Prepare script input
422-
$command = $metadata->getCommand() === Adapter::COMMAND_INCREMENT_INTEGER ? 'incrby' : 'incrbyfloat';
463+
$command = $this->getRedisCommand($metadata->getCommand());
423464
$value = $data['value'];
424-
$ttl = time() + ($metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS);
465+
$ttl = $metadata->getMaxAgeSeconds() ?? self::DEFAULT_TTL_SECONDS;
466+
$scriptArgs = [
467+
$registryKey,
468+
$metadataKey,
469+
$metricKey,
470+
$metadata->toJson(),
471+
$command,
472+
$value,
473+
$ttl
474+
];
475+
$numKeyArgs = 3;
476+
$script = <<<LUA
477+
-- Parse script input
478+
local registryKey = KEYS[1]
479+
local metadataKey = KEYS[2]
480+
local metricKey = KEYS[3]
481+
local metadata = ARGV[1]
482+
local command = ARGV[2]
483+
local value = ARGV[3]
484+
local ttl = tonumber(ARGV[4])
425485
426-
$this->redis->eval(<<<LUA
486+
-- Update metric value
487+
local result = redis.call(command, metricKey, value)
488+
local didUpdate = tostring(result) == value
489+
490+
-- Update metric metadata
491+
if didUpdate == true then
492+
-- Set metric TTL
493+
if ttl > 0 then
494+
redis.call('expire', metricKey, ttl)
495+
else
496+
redis.call('persist', metricKey)
497+
end
498+
499+
-- Register metric value
500+
redis.call('sadd', registryKey, metricKey)
501+
502+
-- Register metric metadata
503+
redis.call('hset', metadataKey, metricKey, metadata)
504+
end
505+
506+
-- Report script result
507+
return didUpdate
508+
LUA;
509+
510+
$oldScript = <<<LUA
427511
-- Parse script input
428512
local registryKey = KEYS[1]
429513
local metadataKey = KEYS[2]
@@ -442,17 +526,13 @@ public function updateCounter(array $data): void
442526
-- Update metric value
443527
redis.call(observeCommand, metricKey, value)
444528
redis.call('expire', metricKey, ttl)
445-
LUA
446-
, [
447-
$registryKey,
448-
$metadataKey,
449-
$metricKey,
450-
$metadata->toJson(),
451-
$command,
452-
$value,
453-
$ttl
454-
],
455-
3
529+
LUA;
530+
531+
// Call script
532+
$result = $this->redis->eval(
533+
$script,
534+
$scriptArgs,
535+
$numKeyArgs
456536
);
457537
}
458538

@@ -655,26 +735,68 @@ private function collectSummaries(): array
655735
*/
656736
private function collectGauges(): array
657737
{
658-
$keys = $this->redis->sMembers(self::$prefix . Gauge::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX);
659-
sort($keys);
738+
// Create Redis keys
739+
$registryKey = $this->getMetricRegistryKey(Gauge::TYPE);
740+
$metadataKey = $this->getMetadataKey(Gauge::TYPE);
741+
742+
// Execute transaction to collect metrics
743+
$result = $this->redis->eval(<<<LUA
744+
-- Parse script input
745+
local registryKey = KEYS[1]
746+
local metadataKey = KEYS[2]
747+
748+
-- Process each registered metric
749+
local result = {}
750+
local metricKeys = redis.call('smembers', registryKey)
751+
for i, metricKey in ipairs(metricKeys) do
752+
local doesExist = redis.call('exists', metricKey)
753+
if doesExist then
754+
-- Get metric metadata
755+
local metadata = redis.call('hget', metadataKey, metricKey)
756+
757+
-- Get metric sample
758+
local sample = redis.call('get', metricKey)
759+
760+
-- Add the processed metric to the set of results
761+
result[metricKey] = {}
762+
result[metricKey]["metadata"] = metadata
763+
result[metricKey]["samples"] = sample
764+
else
765+
-- Remove metadata for expired key
766+
redis.call('srem', registryKey, metricKey)
767+
redis.call('hdel', metadataKey, metricKey)
768+
end
769+
end
770+
771+
-- Return the set of collected metrics
772+
return cjson.encode(result)
773+
LUA
774+
, [
775+
$registryKey,
776+
$metadataKey,
777+
],
778+
2
779+
);
780+
781+
// Collate metrics by metric name
782+
$metrics = [];
783+
$redisGauges = json_decode($result, true);
784+
foreach ($redisGauges as $gauge) {
785+
// Get metadata
786+
$phpMetadata = json_decode($gauge['metadata'], true);
787+
$metadata = MetadataBuilder::fromArray($phpMetadata)->build();
788+
789+
// Create or update metric
790+
$metricName = $metadata->getName();
791+
$builder = $metrics[$metricName] ?? Metric::newScalarMetricBuilder()->withMetadata($metadata);
792+
$builder->withSample($gauge['samples'], $metadata->getLabelValues());
793+
$metrics[$metricName] = $builder;
794+
}
795+
796+
// Format metrics and hand them off to the calling collector
660797
$gauges = [];
661-
foreach ($keys as $key) {
662-
$raw = $this->redis->hGetAll(str_replace($this->redis->_prefix(''), '', $key));
663-
$gauge = json_decode($raw['__meta'], true);
664-
unset($raw['__meta']);
665-
$gauge['samples'] = [];
666-
foreach ($raw as $k => $value) {
667-
$gauge['samples'][] = [
668-
'name' => $gauge['name'],
669-
'labelNames' => [],
670-
'labelValues' => json_decode($k, true),
671-
'value' => $value,
672-
];
673-
}
674-
usort($gauge['samples'], function ($a, $b): int {
675-
return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues']));
676-
});
677-
$gauges[] = $gauge;
798+
foreach ($metrics as $_ => $metric) {
799+
$gauges[] = $metric->build()->toArray();
678800
}
679801
return $gauges;
680802
}
@@ -696,24 +818,24 @@ private function collectCounters(): array
696818
697819
-- Process each registered counter metric
698820
local result = {}
699-
local counterKeys = redis.call('smembers', registryKey)
700-
for i, counterKey in ipairs(counterKeys) do
701-
local doesExist = redis.call('exists', counterKey)
821+
local metricKeys = redis.call('smembers', registryKey)
822+
for i, metricKey in ipairs(metricKeys) do
823+
local doesExist = redis.call('exists', metricKey)
702824
if doesExist then
703825
-- Get counter metadata
704-
local metadata = redis.call('hget', metadataKey, counterKey)
826+
local metadata = redis.call('hget', metadataKey, metricKey)
705827
706828
-- Get counter sample
707-
local sample = redis.call('get', counterKey)
829+
local sample = redis.call('get', metricKey)
708830
709831
-- Add the processed metric to the set of results
710-
result[counterKey] = {}
711-
result[counterKey]["metadata"] = metadata
712-
result[counterKey]["samples"] = sample
832+
result[metricKey] = {}
833+
result[metricKey]["metadata"] = metadata
834+
result[metricKey]["samples"] = sample
713835
else
714836
-- Remove metadata for expired key
715-
redis.call('srem', registryKey, counterKey)
716-
redis.call('hdel', metadataKey, counterKey)
837+
redis.call('srem', registryKey, metricKey)
838+
redis.call('hdel', metadataKey, metricKey)
717839
end
718840
end
719841
@@ -758,11 +880,11 @@ private function getRedisCommand(int $cmd): string
758880
{
759881
switch ($cmd) {
760882
case Adapter::COMMAND_INCREMENT_INTEGER:
761-
return 'hIncrBy';
883+
return 'incrby';
762884
case Adapter::COMMAND_INCREMENT_FLOAT:
763-
return 'hIncrByFloat';
885+
return 'incrbyfloat';
764886
case Adapter::COMMAND_SET:
765-
return 'hSet';
887+
return 'set';
766888
default:
767889
throw new InvalidArgumentException("Unknown command");
768890
}

0 commit comments

Comments
 (0)