1111use Prometheus \Histogram ;
1212use Prometheus \MetricFamilySamples ;
1313use Prometheus \Storage \RedisTxn \Metadata ;
14+ use Prometheus \Storage \RedisTxn \MetadataBuilder ;
1415use Prometheus \Storage \RedisTxn \Metric ;
1516use Prometheus \Summary ;
1617use 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
350365LUA
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)
420458LUA
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
556596local summaryRegistryKey = KEYS[1]
557- local metaHashKey = KEYS[2]
597+ local metadataKey = KEYS[2]
558598local currentTime = tonumber(ARGV[1])
559- local result = {}
560599
561600-- Process each registered summary metric
601+ local result = {}
562602local summaryKeys = redis.call('smembers', summaryRegistryKey)
563603for 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