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 */
3738class 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)
390432end
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
428512local registryKey = KEYS[1]
429513local metadataKey = KEYS[2]
@@ -442,17 +526,13 @@ public function updateCounter(array $data): void
442526-- Update metric value
443527redis.call(observeCommand, metricKey, value)
444528redis.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
698820local 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
718840end
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