diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec index dfb72640736a5..692d8900e2a7c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec @@ -164,3 +164,22 @@ null | three | 2024-05-10T00:05:00.000 null | three | 2024-05-10T00:02:00.000Z null | three | 2024-05-10T00:01:00.000Z ; + + +max_over_time +required_capability: metrics_command +required_capability: max_over_time +TS k8s | STATS cost=sum(max_over_time(network.cost)) BY cluster, time_bucket = bucket(@timestamp,1minute) | SORT cost DESC, time_bucket DESC, cluster | LIMIT 10; + +cost:double | cluster:keyword | time_bucket:datetime +32.75 | qa | 2024-05-10T00:17:00.000Z +32.25 | staging | 2024-05-10T00:09:00.000Z +31.75 | qa | 2024-05-10T00:06:00.000Z +29.0 | prod | 2024-05-10T00:19:00.000Z +28.625 | qa | 2024-05-10T00:09:00.000Z +24.625 | qa | 2024-05-10T00:18:00.000Z +23.25 | qa | 2024-05-10T00:11:00.000Z +23.125 | staging | 2024-05-10T00:08:00.000Z +22.75 | prod | 2024-05-10T00:13:00.000Z +22.75 | qa | 2024-05-10T00:08:00.000Z +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 18fe50678040f..b089b42a41ea2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -959,7 +959,12 @@ public enum Cap { /** * Listing queries and getting information on a specific query. */ - QUERY_MONITORING; + QUERY_MONITORING, + + /** + * Support max_over_time aggregation + */ + MAX_OVER_TIME(Build.current().isSnapshot()); private final boolean enabled; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 41c2b64004b8f..a5558b348b8b8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Count; import org.elasticsearch.xpack.esql.expression.function.aggregate.CountDistinct; import org.elasticsearch.xpack.esql.expression.function.aggregate.Max; +import org.elasticsearch.xpack.esql.expression.function.aggregate.MaxOverTime; import org.elasticsearch.xpack.esql.expression.function.aggregate.Median; import org.elasticsearch.xpack.esql.expression.function.aggregate.MedianAbsoluteDeviation; import org.elasticsearch.xpack.esql.expression.function.aggregate.Min; @@ -432,6 +433,7 @@ private static FunctionDefinition[][] snapshotFunctions() { // This is an experimental function and can be removed without notice. def(Delay.class, Delay::new, "delay"), def(Rate.class, Rate::withUnresolvedTimestamp, "rate"), + def(MaxOverTime.class, uni(MaxOverTime::new), "max_over_time"), def(Term.class, bi(Term::new), "term") } }; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java index db1d2a9e6f254..360ba7d3ca8ad 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java @@ -30,6 +30,7 @@ public static List getNamedWriteables() { Sum.ENTRY, Top.ENTRY, Values.ENTRY, + MaxOverTime.ENTRY, // internal functions ToPartial.ENTRY, FromPartial.ENTRY, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MaxOverTime.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MaxOverTime.java new file mode 100644 index 0000000000000..266fbdf211699 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MaxOverTime.java @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.aggregate; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionType; +import org.elasticsearch.xpack.esql.expression.function.Param; + +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.emptyList; + +/** + * Similar to {@link Max}, but it is used to calculate the maximum value over a time series of values from the given field. + */ +public class MaxOverTime extends TimeSeriesAggregateFunction { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Expression.class, + "MaxOverTime", + MaxOverTime::new + ); + + @FunctionInfo( + returnType = { "boolean", "double", "integer", "long", "date", "date_nanos", "ip", "keyword", "long", "version" }, + description = "The maximum over time value of a field.", + type = FunctionType.AGGREGATE + ) + public MaxOverTime( + Source source, + @Param( + name = "field", + type = { + "aggregate_metric_double", + "boolean", + "double", + "integer", + "long", + "date", + "date_nanos", + "ip", + "keyword", + "text", + "long", + "version" } + ) Expression field + ) { + this(source, field, Literal.TRUE); + } + + public MaxOverTime(Source source, Expression field, Expression filter) { + super(source, field, filter, emptyList()); + } + + private MaxOverTime(StreamInput in) throws IOException { + super(in); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + @Override + public MaxOverTime withFilter(Expression filter) { + return new MaxOverTime(source(), field(), filter); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, MaxOverTime::new, field(), filter()); + } + + @Override + public MaxOverTime replaceChildren(List newChildren) { + return new MaxOverTime(source(), newChildren.get(0), newChildren.get(1)); + } + + @Override + protected TypeResolution resolveType() { + return perTimeSeriesAggregation().resolveType(); + } + + @Override + public DataType dataType() { + return perTimeSeriesAggregation().dataType(); + } + + @Override + public Max perTimeSeriesAggregation() { + return new Max(source(), field(), filter()); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Rate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Rate.java index de7b15a6087c1..d4b7764a30b34 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Rate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Rate.java @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; -import org.elasticsearch.xpack.esql.planner.ToTimeSeriesAggregator; +import org.elasticsearch.xpack.esql.planner.ToAggregator; import java.io.IOException; import java.util.List; @@ -33,7 +33,7 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; -public class Rate extends AggregateFunction implements OptionalArgument, ToTimeSeriesAggregator { +public class Rate extends TimeSeriesAggregateFunction implements OptionalArgument, ToAggregator { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Rate", Rate::new); private final Expression timestamp; @@ -119,6 +119,11 @@ public AggregatorFunctionSupplier supplier() { }; } + @Override + public Rate perTimeSeriesAggregation() { + return this; + } + @Override public String toString() { return "rate(" + field() + ")"; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TimeSeriesAggregateFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TimeSeriesAggregateFunction.java new file mode 100644 index 0000000000000..faa75caf1ca09 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TimeSeriesAggregateFunction.java @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.esql.expression.function.aggregate; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; + +import java.io.IOException; +import java.util.List; + +/** + * Extends {@link AggregateFunction} to support aggregation per time_series, + * such as {@link Rate} or {@link MaxOverTime}. + */ +public abstract class TimeSeriesAggregateFunction extends AggregateFunction { + + protected TimeSeriesAggregateFunction(Source source, Expression field, Expression filter, List parameters) { + super(source, field, filter, parameters); + } + + protected TimeSeriesAggregateFunction(StreamInput in) throws IOException { + super(in); + } + + /** + * Returns the aggregation function to be used in the first aggregation stage, + * which is grouped by `_tsid` (and `time_bucket`). + * + * @see org.elasticsearch.xpack.esql.optimizer.rules.logical.TranslateTimeSeriesAggregate + */ + public abstract AggregateFunction perTimeSeriesAggregation(); +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SubstituteSurrogates.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SubstituteSurrogates.java index 1831bfaf30f33..f667e7c97e4f7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SubstituteSurrogates.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SubstituteSurrogates.java @@ -17,7 +17,7 @@ import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.expression.SurrogateExpression; import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction; -import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate; +import org.elasticsearch.xpack.esql.expression.function.aggregate.TimeSeriesAggregateFunction; import org.elasticsearch.xpack.esql.plan.logical.Aggregate; import org.elasticsearch.xpack.esql.plan.logical.Eval; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; @@ -70,8 +70,7 @@ protected LogicalPlan rule(Aggregate aggregate) { if (s instanceof AggregateFunction == false) { // 1. collect all aggregate functions from the expression var surrogateWithRefs = s.transformUp(AggregateFunction.class, af -> { - // TODO: more generic than this? - if (af instanceof Rate) { + if (af instanceof TimeSeriesAggregateFunction) { return af; } // 2. check if they are already use otherwise add them to the Aggregate with some made-up aliases diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/TranslateTimeSeriesAggregate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/TranslateTimeSeriesAggregate.java index 2f925470eb41d..de8568610e025 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/TranslateTimeSeriesAggregate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/TranslateTimeSeriesAggregate.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.esql.core.util.Holder; import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.FromPartial; -import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate; +import org.elasticsearch.xpack.esql.expression.function.aggregate.TimeSeriesAggregateFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.ToPartial; import org.elasticsearch.xpack.esql.expression.function.aggregate.Values; import org.elasticsearch.xpack.esql.expression.function.grouping.Bucket; @@ -34,7 +34,7 @@ import java.util.Map; /** - * Rate aggregation is special because it must be computed per time series, regardless of the grouping keys. + * Time-series aggregation is special because it must be computed per time series, regardless of the grouping keys. * The keys must be `_tsid` or a pair of `_tsid` and `time_bucket`. To support user-defined grouping keys, * we first execute the rate aggregation using the time-series keys, then perform another aggregation with * the resulting rate using the user-specific keys. @@ -113,6 +113,17 @@ * | STATS rate(request), $p1=to_partial(min(memory_used)), VALUES(pod) BY _tsid, `bucket(@timestamp, 5m)` * | STATS sum(`rate(request)`), `min(memory_used)` = from_partial($p1, min($)) BY pod=`VALUES(pod)`, `bucket(@timestamp, 5m)` * | KEEP `min(memory_used)`, `sum(rate(request))`, pod, `bucket(@timestamp, 5m)` + * + * {agg}_over_time time-series aggregation will be rewritten in the similar way + * + * TS k8s | STATS sum(max_over_time(memory_usage)) BY host, bucket(@timestamp, 1minute) + * + * becomes + * + * TS k8s + * | STATS max_memory_usage = max(memory_usage), host_values=VALUES(host) BY _tsid, time_bucket=bucket(@timestamp, 1minute) + * | STATS sum(max_memory_usage) BY host_values, time_bucket + * * */ public final class TranslateTimeSeriesAggregate extends OptimizerRules.OptimizerRule { @@ -131,20 +142,21 @@ protected LogicalPlan rule(Aggregate aggregate) { } LogicalPlan translate(TimeSeriesAggregate aggregate) { - Map rateAggs = new HashMap<>(); + Map timeSeriesAggs = new HashMap<>(); List firstPassAggs = new ArrayList<>(); List secondPassAggs = new ArrayList<>(); for (NamedExpression agg : aggregate.aggregates()) { if (agg instanceof Alias alias && alias.child() instanceof AggregateFunction af) { Holder changed = new Holder<>(Boolean.FALSE); - Expression outerAgg = af.transformDown(Rate.class, rate -> { + Expression outerAgg = af.transformDown(TimeSeriesAggregateFunction.class, tsAgg -> { changed.set(Boolean.TRUE); - Alias rateAgg = rateAggs.computeIfAbsent(rate, k -> { - Alias newRateAgg = new Alias(rate.source(), agg.name(), rate); - firstPassAggs.add(newRateAgg); - return newRateAgg; + AggregateFunction firstStageFn = tsAgg.perTimeSeriesAggregation(); + Alias newAgg = timeSeriesAggs.computeIfAbsent(firstStageFn, k -> { + Alias firstStageAlias = new Alias(tsAgg.source(), agg.name(), firstStageFn); + firstPassAggs.add(firstStageAlias); + return firstStageAlias; }); - return rateAgg.toAttribute(); + return newAgg.toAttribute(); }); if (changed.get()) { secondPassAggs.add(new Alias(alias.source(), alias.name(), outerAgg, agg.id())); @@ -156,7 +168,7 @@ LogicalPlan translate(TimeSeriesAggregate aggregate) { } } } - if (rateAggs.isEmpty()) { + if (timeSeriesAggs.isEmpty()) { // no time-series aggregations, run a regular aggregation instead. return new Aggregate(aggregate.source(), aggregate.child(), aggregate.groupings(), aggregate.aggregates()); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Aggregate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Aggregate.java index d4677ba112aa4..593a31228fc9a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Aggregate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Aggregate.java @@ -28,6 +28,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.FilteredExpression; import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate; +import org.elasticsearch.xpack.esql.expression.function.aggregate.TimeSeriesAggregateFunction; import org.elasticsearch.xpack.esql.expression.function.grouping.Categorize; import org.elasticsearch.xpack.esql.expression.function.grouping.GroupingFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; @@ -223,8 +224,8 @@ public void postAnalysisVerification(Failures failures) { aggregates.forEach(a -> checkRateAggregates(a, 0, failures)); } else { forEachExpression( - Rate.class, - r -> failures.add(fail(r, "the rate aggregate[{}] can only be used with the TS command", r.sourceText())) + TimeSeriesAggregateFunction.class, + r -> failures.add(fail(r, "time_series aggregate[{}] can only be used with the TS command", r.sourceText())) ); } checkCategorizeGrouping(failures); @@ -370,7 +371,7 @@ else if (c instanceof GroupingFunction gf) { if (e instanceof AggregateFunction af) { af.field().forEachDown(AggregateFunction.class, f -> { // rate aggregate is allowed to be inside another aggregate - if (f instanceof Rate == false) { + if (f instanceof TimeSeriesAggregateFunction == false) { failures.add(fail(f, "nested aggregations [{}] not allowed inside other aggregations [{}]", f, af)); } }); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/ToTimeSeriesAggregator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/ToTimeSeriesAggregator.java deleted file mode 100644 index 869bad8a0f111..0000000000000 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/ToTimeSeriesAggregator.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.planner; - -/** - * An interface indicates that this is a time-series aggregator and it requires time-series source - */ -public interface ToTimeSeriesAggregator extends ToAggregator { - -} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index bd8c57dbe526e..c1416d9f83b55 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1136,11 +1136,15 @@ public void testNotAllowRateOutsideMetrics() { assumeTrue("requires snapshot builds", Build.current().isSnapshot()); assertThat( error("FROM tests | STATS avg(rate(network.bytes_in))", tsdb), - equalTo("1:24: the rate aggregate[rate(network.bytes_in)] can only be used with the TS command") + equalTo("1:24: time_series aggregate[rate(network.bytes_in)] can only be used with the TS command") ); assertThat( error("FROM tests | STATS rate(network.bytes_in)", tsdb), - equalTo("1:20: the rate aggregate[rate(network.bytes_in)] can only be used with the TS command") + equalTo("1:20: time_series aggregate[rate(network.bytes_in)] can only be used with the TS command") + ); + assertThat( + error("FROM tests | STATS max_over_time(network.connections)", tsdb), + equalTo("1:20: time_series aggregate[max_over_time(network.connections)] can only be used with the TS command") ); assertThat( error("FROM tests | EVAL r = rate(network.bytes_in)", tsdb), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index b514d51d5ca02..47ca4ec31ee5e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -7072,6 +7072,34 @@ public void testAdjustMetricsRateBeforeFinalAgg() { assertThat(Expressions.attribute(values.field()).name(), equalTo("cluster")); } + public void testTranslateMaxOverTime() { + assumeTrue("requires snapshot builds", Build.current().isSnapshot()); + var query = "TS k8s | STATS sum(max_over_time(network.bytes_in)) BY bucket(@timestamp, 1h)"; + var plan = logicalOptimizer.optimize(metricsAnalyzer.analyze(parser.createStatement(query))); + Limit limit = as(plan, Limit.class); + Aggregate finalAgg = as(limit.child(), Aggregate.class); + assertThat(finalAgg, not(instanceOf(TimeSeriesAggregate.class))); + assertThat(finalAgg.aggregates(), hasSize(2)); + TimeSeriesAggregate aggsByTsid = as(finalAgg.child(), TimeSeriesAggregate.class); + assertThat(aggsByTsid.aggregates(), hasSize(2)); // _tsid is dropped + assertNotNull(aggsByTsid.timeBucket()); + assertThat(aggsByTsid.timeBucket().buckets().fold(FoldContext.small()), equalTo(Duration.ofHours(1))); + Eval eval = as(aggsByTsid.child(), Eval.class); + assertThat(eval.fields(), hasSize(1)); + as(eval.child(), EsRelation.class); + + Sum sum = as(Alias.unwrap(finalAgg.aggregates().get(0)), Sum.class); + assertThat(Expressions.attribute(sum.field()).id(), equalTo(aggsByTsid.aggregates().get(0).id())); + assertThat(finalAgg.groupings(), hasSize(1)); + assertThat(Expressions.attribute(finalAgg.groupings().get(0)).id(), equalTo(aggsByTsid.aggregates().get(1).id())); + + Max max = as(Alias.unwrap(aggsByTsid.aggregates().get(0)), Max.class); + assertThat(Expressions.attribute(max.field()).name(), equalTo("network.bytes_in")); + assertThat(Expressions.attribute(aggsByTsid.groupings().get(1)).id(), equalTo(eval.fields().get(0).id())); + Bucket bucket = as(Alias.unwrap(eval.fields().get(0)), Bucket.class); + assertThat(Expressions.attribute(bucket.field()).name(), equalTo("@timestamp")); + } + public void testMetricsWithoutRate() { assumeTrue("requires snapshot builds", Build.current().isSnapshot()); List queries = List.of(""" diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml index 3b657f03b149f..2e23ba53c7a12 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml @@ -33,7 +33,7 @@ setup: path: /_query parameters: [] # A snapshot function was removed in match_function_options, it can't work on mixed cluster tests otherwise. - capabilities: [ snapshot_test_for_telemetry, fn_byte_length, match_function_options] + capabilities: [ snapshot_test_for_telemetry, fn_byte_length, match_function_options, max_over_time] reason: "Test that should only be executed on snapshot versions" - do: {xpack.usage: {}} @@ -101,7 +101,7 @@ setup: - match: {esql.functions.coalesce: $functions_coalesce} - gt: {esql.functions.categorize: $functions_categorize} # Testing for the entire function set isn't feasbile, so we just check that we return the correct count as an approximation. - - length: {esql.functions: 134} # check the "sister" test below for a likely update to the same esql.functions length check + - length: {esql.functions: 135} # check the "sister" test below for a likely update to the same esql.functions length check --- "Basic ESQL usage output (telemetry) non-snapshot version":