@@ -1896,7 +1896,7 @@ public function find(array $queries = [], ?string $type = null): array
18961896 if ($ type !== null ) {
18971897 if ($ this ->useDailyRollups && $ type === Usage::TYPE_EVENT ) {
18981898 $ plan = $ this ->extractRoutingPlan ($ queries );
1899- $ route = $ this ->selectAggregateSource ($ plan );
1899+ $ route = $ this ->selectAggregateSource ($ plan, Usage:: TYPE_EVENT );
19001900 $ this ->recordRoute ('find ' , $ plan , $ route );
19011901
19021902 if (str_starts_with ($ route , 'daily_by_ ' )) {
@@ -1918,6 +1918,32 @@ public function find(array $queries = [], ?string $type = null): array
19181918 }
19191919 }
19201920 }
1921+
1922+ if ($ this ->useDailyRollups && $ type === Usage::TYPE_GAUGE ) {
1923+ $ plan = $ this ->extractRoutingPlan ($ queries );
1924+ $ route = $ this ->selectAggregateSource ($ plan , Usage::TYPE_GAUGE );
1925+ $ this ->recordRoute ('find ' , $ plan , $ route );
1926+
1927+ if (str_starts_with ($ route , 'gauges_daily_by_ ' )) {
1928+ $ name = substr ($ route , strlen ('gauges_daily_ ' ));
1929+ $ rollup = $ this ->getGaugeRollupByName ($ name );
1930+ if ($ rollup !== null ) {
1931+ $ result = $ this ->findFromGaugeDailyByDim ($ rollup , $ queries );
1932+ $ this ->maybeDualRead ($ queries , $ type , $ route , $ plan , $ result );
1933+ return $ result ;
1934+ }
1935+ }
1936+ if (str_starts_with ($ route , 'gauges_hybrid_by_ ' )) {
1937+ $ name = substr ($ route , strlen ('gauges_hybrid_ ' ));
1938+ $ rollup = $ this ->getGaugeRollupByName ($ name );
1939+ if ($ rollup !== null ) {
1940+ $ result = $ this ->findHybridFromGaugeDailyByDim ($ rollup , $ queries );
1941+ $ this ->maybeDualRead ($ queries , $ type , $ route , $ plan , $ result );
1942+ return $ result ;
1943+ }
1944+ }
1945+ }
1946+
19211947 return $ this ->findFromTable ($ queries , $ type );
19221948 }
19231949
@@ -2434,22 +2460,25 @@ private function extractRoutingPlan(array $queries): array
24342460 * Pure routing decision: which physical table satisfies this read.
24352461 *
24362462 * Returns one of:
2437- * - 'raw' — scan the events table.
2438- * - 'daily' — read the existing daily MV (no grouping, only
2463+ * - 'raw' — scan the raw events / gauges table.
2464+ * - 'daily' — read the existing events daily MV (no grouping, only
24392465 * daily-MV-compatible filters, window in closed days).
2440- * - 'hybrid' — closed days from daily MV, today's partial from raw.
2466+ * - 'hybrid' — closed days from events daily MV, today's partial from raw.
24412467 * - 'daily_by_path' / 'daily_by_country' / 'daily_by_service' /
2442- * 'daily_by_method_status' — read a per-dim rollup when the
2443- * requested dimensions[] and filter columns match one of the
2444- * four MVs.
2445- * - 'hybrid_by_<dim>' — multi-dim MV closed days + raw today's partial.
2468+ * 'daily_by_method_status' — read an events per-dim rollup.
2469+ * - 'hybrid_by_<dim>' — events multi-dim MV closed days + raw today's
2470+ * partial.
2471+ * - 'gauges_daily_by_service' / 'gauges_daily_by_resource' — read a
2472+ * gauges per-dim AMT rollup.
2473+ * - 'gauges_hybrid_by_<dim>' — gauges multi-dim AMT MV closed days +
2474+ * raw today's partial.
24462475 *
24472476 * Any caller-requested sub-day interval forces 'raw' because the
24482477 * rollups are day-grained.
24492478 *
24502479 * @param array{metric: ?string, start: ?string, end: ?string, filterColumns: array<int, string>, dimensions: array<int, string>, interval: ?string} $plan
24512480 */
2452- private function selectAggregateSource (array $ plan ): string
2481+ private function selectAggregateSource (array $ plan, string $ type = Usage:: TYPE_EVENT ): string
24532482 {
24542483 if ($ plan ['interval ' ] !== null ) {
24552484 return 'raw ' ;
@@ -2467,6 +2496,27 @@ private function selectAggregateSource(array $plan): string
24672496 }
24682497 $ straddlesToday = $ endDt >= $ boundaryDt ;
24692498
2499+ if ($ type === Usage::TYPE_GAUGE ) {
2500+ if (empty ($ plan ['dimensions ' ])) {
2501+ return 'raw ' ;
2502+ }
2503+
2504+ $ dimMatch = $ this ->matchGaugeDimRollup ($ plan ['dimensions ' ]);
2505+ if ($ dimMatch === null ) {
2506+ return 'raw ' ;
2507+ }
2508+
2509+ $ allowedFilters = array_merge (['metric ' , 'time ' , 'tenant ' ], $ dimMatch ['dims ' ]);
2510+ foreach ($ plan ['filterColumns ' ] as $ column ) {
2511+ if (!in_array ($ column , $ allowedFilters , true )) {
2512+ return 'raw ' ;
2513+ }
2514+ }
2515+
2516+ $ base = 'gauges_daily_ ' . $ dimMatch ['name ' ];
2517+ return $ straddlesToday ? 'gauges_hybrid_ ' . $ dimMatch ['name ' ] : $ base ;
2518+ }
2519+
24702520 if (empty ($ plan ['dimensions ' ])) {
24712521 foreach ($ plan ['filterColumns ' ] as $ column ) {
24722522 if (!in_array ($ column , self ::DAILY_COLUMNS , true ) && $ column !== 'tenant ' ) {
@@ -2493,8 +2543,8 @@ private function selectAggregateSource(array $plan): string
24932543 }
24942544
24952545 /**
2496- * Find the per-dim rollup whose dim set matches the request exactly
2497- * (order-insensitive). Returns null when no MV covers this shape.
2546+ * Find the events per-dim rollup whose dim set matches the request
2547+ * exactly (order-insensitive). Returns null when no MV covers this shape.
24982548 *
24992549 * @param array<int, string> $dimensions
25002550 * @return array{name: string, dims: array<int, string>}|null
@@ -2514,6 +2564,41 @@ private function matchDimRollup(array $dimensions): ?array
25142564 return null ;
25152565 }
25162566
2567+ /**
2568+ * Find the gauge per-dim AMT rollup whose dim set matches the request
2569+ * exactly (order-insensitive). Returns null when no MV covers this shape.
2570+ *
2571+ * @param array<int, string> $dimensions
2572+ * @return array{name: string, dims: array<int, string>}|null
2573+ */
2574+ private function matchGaugeDimRollup (array $ dimensions ): ?array
2575+ {
2576+ $ needle = $ dimensions ;
2577+ sort ($ needle );
2578+
2579+ foreach (self ::GAUGE_DIM_ROLLUPS as $ rollup ) {
2580+ $ candidate = $ rollup ['dims ' ];
2581+ sort ($ candidate );
2582+ if ($ candidate === $ needle ) {
2583+ return $ rollup ;
2584+ }
2585+ }
2586+ return null ;
2587+ }
2588+
2589+ /**
2590+ * @return array{name: string, dims: array<int, string>}|null
2591+ */
2592+ private function getGaugeRollupByName (string $ name ): ?array
2593+ {
2594+ foreach (self ::GAUGE_DIM_ROLLUPS as $ rollup ) {
2595+ if ($ rollup ['name ' ] === $ name ) {
2596+ return $ rollup ;
2597+ }
2598+ }
2599+ return null ;
2600+ }
2601+
25172602 private function stringifyTime (mixed $ value ): ?string
25182603 {
25192604 if ($ value instanceof \DateTime) {
@@ -2674,6 +2759,116 @@ private function findHybridFromDailyByDim(array $rollup, array $queries): array
26742759 return $ this ->parseAggregatedResults ($ result , Usage::TYPE_EVENT );
26752760 }
26762761
2762+ /**
2763+ * Day-grained aggregated read from a per-dim gauge AMT rollup table.
2764+ * Same return shape as findFromDailyByDim but emits argMaxMerge(value)
2765+ * instead of sum(value) — gauges store argMaxState partials.
2766+ *
2767+ * @param array{name: string, dims: array<int, string>} $rollup
2768+ * @param array<Query> $queries
2769+ * @return array<Metric>
2770+ */
2771+ private function findFromGaugeDailyByDim (array $ rollup , array $ queries ): array
2772+ {
2773+ $ tableName = $ this ->getGaugeDimRollupTableName ($ rollup ['name ' ]);
2774+ $ fromTable = $ this ->buildTableReference ($ tableName );
2775+
2776+ $ parsed = $ this ->parseQueries ($ queries , Usage::TYPE_GAUGE );
2777+ $ whereData = $ this ->buildWhereClause ($ parsed ['filters ' ], $ parsed ['params ' ]);
2778+
2779+ $ dimSelect = '' ;
2780+ $ dimGroup = '' ;
2781+ if (!empty ($ rollup ['dims ' ])) {
2782+ $ escapedDims = array_map (fn (string $ d ): string => $ this ->escapeIdentifier ($ d ), $ rollup ['dims ' ]);
2783+ $ dimSelect = ', ' . implode (', ' , $ escapedDims );
2784+ $ dimGroup = ', ' . implode (', ' , $ escapedDims );
2785+ }
2786+
2787+ $ orderClause = ' ORDER BY value DESC ' ;
2788+ if (!empty ($ parsed ['orderBy ' ])) {
2789+ $ orderClause = ' ORDER BY ' . implode (', ' , $ parsed ['orderBy ' ]);
2790+ }
2791+ $ limitClause = isset ($ parsed ['limit ' ]) ? ' LIMIT {limit:UInt64} ' : '' ;
2792+ $ offsetClause = isset ($ parsed ['offset ' ]) ? ' OFFSET {offset:UInt64} ' : '' ;
2793+
2794+ $ sql = "
2795+ SELECT metric, argMaxMerge(value) AS value {$ dimSelect }
2796+ FROM {$ fromTable }{$ whereData ['clause ' ]}
2797+ GROUP BY metric {$ dimGroup }{$ orderClause }{$ limitClause }{$ offsetClause }
2798+ FORMAT JSON
2799+ " ;
2800+
2801+ $ result = $ this ->query ($ sql , $ whereData ['params ' ]);
2802+ return $ this ->parseAggregatedResults ($ result , Usage::TYPE_GAUGE );
2803+ }
2804+
2805+ /**
2806+ * Hybrid gauge AMT read: closed-day rollup partials (argMaxState) UNION
2807+ * raw gauges (argMaxState(value, time) over today's partial), with an
2808+ * outer argMaxMerge that combines both sides. Same return shape as
2809+ * findHybridFromDailyByDim but argMax-based.
2810+ *
2811+ * @param array{name: string, dims: array<int, string>} $rollup
2812+ * @param array<Query> $queries
2813+ * @return array<Metric>
2814+ */
2815+ private function findHybridFromGaugeDailyByDim (array $ rollup , array $ queries ): array
2816+ {
2817+ $ startOfToday = (new \DateTime ('today ' , new \DateTimeZone ('UTC ' )))->format ('Y-m-d H:i:s.v ' );
2818+
2819+ $ dailyName = $ this ->getGaugeDimRollupTableName ($ rollup ['name ' ]);
2820+ $ dailyTable = $ this ->buildTableReference ($ dailyName );
2821+ $ gaugesTable = $ this ->buildTableReference ($ this ->getGaugesTableName ());
2822+
2823+ $ parsed = $ this ->parseQueries ($ queries , Usage::TYPE_GAUGE );
2824+ $ rawFilters = $ parsed ['filters ' ];
2825+ $ params = $ parsed ['params ' ];
2826+
2827+ $ dailyFilters = $ rawFilters ;
2828+
2829+ $ params ['hybrid_boundary ' ] = $ startOfToday ;
2830+ $ dailyFilters [] = '`time` < {hybrid_boundary:DateTime64(3)} ' ;
2831+ $ rawFilters [] = '`time` >= {hybrid_boundary:DateTime64(3)} ' ;
2832+
2833+ $ dailyWhere = $ this ->buildWhereClause ($ dailyFilters , $ params );
2834+ $ rawWhere = $ this ->buildWhereClause ($ rawFilters , $ dailyWhere ['params ' ]);
2835+
2836+ $ dimSelect = '' ;
2837+ $ dimGroup = '' ;
2838+ $ dimCols = '' ;
2839+ if (!empty ($ rollup ['dims ' ])) {
2840+ $ escapedDims = array_map (fn (string $ d ): string => $ this ->escapeIdentifier ($ d ), $ rollup ['dims ' ]);
2841+ $ dimList = implode (', ' , $ escapedDims );
2842+ $ dimSelect = ', ' . $ dimList ;
2843+ $ dimGroup = ', ' . $ dimList ;
2844+ $ dimCols = ', ' . $ dimList ;
2845+ }
2846+
2847+ $ orderClause = ' ORDER BY value DESC ' ;
2848+ if (!empty ($ parsed ['orderBy ' ])) {
2849+ $ orderClause = ' ORDER BY ' . implode (', ' , $ parsed ['orderBy ' ]);
2850+ }
2851+ $ limitClause = isset ($ parsed ['limit ' ]) ? ' LIMIT {limit:UInt64} ' : '' ;
2852+ $ offsetClause = isset ($ parsed ['offset ' ]) ? ' OFFSET {offset:UInt64} ' : '' ;
2853+
2854+ $ sql = "
2855+ SELECT metric, argMaxMerge(value) AS value {$ dimSelect }
2856+ FROM (
2857+ SELECT metric {$ dimCols }, value
2858+ FROM {$ dailyTable }{$ dailyWhere ['clause ' ]}
2859+ UNION ALL
2860+ SELECT metric {$ dimCols }, argMaxState(value, time) AS value
2861+ FROM {$ gaugesTable }{$ rawWhere ['clause ' ]}
2862+ GROUP BY metric {$ dimGroup }
2863+ )
2864+ GROUP BY metric {$ dimGroup }{$ orderClause }{$ limitClause }{$ offsetClause }
2865+ FORMAT JSON
2866+ " ;
2867+
2868+ $ result = $ this ->query ($ sql , $ rawWhere ['params ' ]);
2869+ return $ this ->parseAggregatedResults ($ result , Usage::TYPE_GAUGE );
2870+ }
2871+
26772872 /**
26782873 * Dual-read sampler: with probability `$dualReadSampleRate`, re-run
26792874 * the same logical query against raw events and log a warning if the
0 commit comments