diff --git a/web/api/v1/translate_ast.go b/web/api/v1/translate_ast.go index 3c2bc09943..dc2b0166f8 100644 --- a/web/api/v1/translate_ast.go +++ b/web/api/v1/translate_ast.go @@ -84,7 +84,9 @@ func translateAST(node parser.Expr) any { "type": "matrixSelector", "name": vs.Name, "range": n.Range.Milliseconds(), + "rangeExpr": translateDurationExpr(n.RangeExpr), "offset": vs.OriginalOffset.Milliseconds(), + "offsetExpr": translateDurationExpr(vs.OriginalOffsetExpr), "matchers": translateMatchers(vs.LabelMatchers), "timestamp": vs.Timestamp, "startOrEnd": getStartOrEnd(vs.StartOrEnd), @@ -96,11 +98,16 @@ func translateAST(node parser.Expr) any { "type": "subquery", "expr": translateAST(n.Expr), "range": n.Range.Milliseconds(), + "rangeExpr": translateDurationExpr(n.RangeExpr), "offset": n.OriginalOffset.Milliseconds(), + "offsetExpr": translateDurationExpr(n.OriginalOffsetExpr), "step": n.Step.Milliseconds(), + "stepExpr": translateDurationExpr(n.StepExpr), "timestamp": n.Timestamp, "startOrEnd": getStartOrEnd(n.StartOrEnd), } + case *parser.DurationExpr: + return translateDurationExpr(n) case *parser.NumberLiteral: return map[string]string{ "type": "numberLiteral", @@ -127,6 +134,7 @@ func translateAST(node parser.Expr) any { "type": "vectorSelector", "name": n.Name, "offset": n.OriginalOffset.Milliseconds(), + "offsetExpr": translateDurationExpr(n.OriginalOffsetExpr), "matchers": translateMatchers(n.LabelMatchers), "timestamp": n.Timestamp, "startOrEnd": getStartOrEnd(n.StartOrEnd), @@ -137,6 +145,39 @@ func translateAST(node parser.Expr) any { panic("unsupported node type") } +func translateDurationExpr(node parser.Expr) any { + if node == nil { + return nil + } + + switch n := node.(type) { + case *parser.DurationExpr: + if n == nil { + return nil + } + + return map[string]any{ + "type": "durationExpr", + "op": n.Op.String(), + "lhs": translateDurationExpr(n.LHS), + "rhs": translateDurationExpr(n.RHS), + "wrapped": n.Wrapped, + } + case *parser.NumberLiteral: + if n == nil { + return nil + } + + return map[string]any{ + "type": "numberLiteral", + "val": strconv.FormatFloat(n.Val, 'f', -1, 64), + "duration": n.Duration, + } + default: + return translateAST(n) + } +} + func sanitizeList(l []string) []string { if l == nil { return []string{} diff --git a/web/api/v1/translate_ast_test.go b/web/api/v1/translate_ast_test.go new file mode 100644 index 0000000000..50befb1962 --- /dev/null +++ b/web/api/v1/translate_ast_test.go @@ -0,0 +1,199 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/promql/parser" +) + +func TestTranslateASTDurationExpressions(t *testing.T) { + p := parser.NewParser(parser.Options{}) + + type tc struct { + name string + query string + wantType string + wantFields map[string]any + } + + testcases := []tc{ + { + name: "regular matrix selector", + query: `foo[5m]`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "range": int64(5 * 60 * 1000), + "rangeExpr": nil, + "offset": int64(0), + "offsetExpr": nil, + }, + }, + { + name: "regular matrix selector with offset", + query: `foo[5m] offset 1m`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "range": int64(5 * 60 * 1000), + "rangeExpr": nil, + "offset": int64(60 * 1000), + "offsetExpr": nil, + }, + }, + { + name: "matrix selector range expression", + query: `foo[5m+1m]`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "range": int64(0), + "rangeExpr": durationExpr("+", durationNumber("300", true), durationNumber("60", true), false), + }, + }, + { + name: "matrix selector range expression with offset expression", + query: `foo[5m+1m] offset (10m/2)`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "rangeExpr": durationExpr("+", durationNumber("300", true), durationNumber("60", true), false), + "offset": int64(0), + "offsetExpr": durationExpr("/", durationNumber("600", true), durationNumber("2", false), true), + }, + }, + { + name: "complex matrix selector range expression", + query: `foo[max(step(),5m+3m) ]`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "range": int64(0), + "rangeExpr": durationExpr("max", + durationExpr("step", nil, nil, false), + durationExpr("+", durationNumber("300", true), durationNumber("180", true), false), + false, + ), + }, + }, + { + name: "nested min and max matrix selector range expression", + query: `foo[min(max(step(),5m+3m),10m-2m)]`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "range": int64(0), + "rangeExpr": durationExpr("min", + durationExpr("max", + durationExpr("step", nil, nil, false), + durationExpr("+", durationNumber("300", true), durationNumber("180", true), false), + false, + ), + durationExpr("-", durationNumber("600", true), durationNumber("120", true), false), + false, + ), + }, + }, + { + name: "range preprocessor expression", + query: `foo[range()]`, + wantType: "matrixSelector", + wantFields: map[string]any{ + "range": int64(0), + "rangeExpr": durationExpr("range", nil, nil, false), + }, + }, + { + name: "regular subquery selector", + query: `foo[5m:1m]`, + wantType: "subquery", + wantFields: map[string]any{ + "range": int64(5 * 60 * 1000), + "rangeExpr": nil, + "step": int64(60 * 1000), + "stepExpr": nil, + "offset": int64(0), + "offsetExpr": nil, + }, + }, + { + name: "subquery selector duration expressions", + query: `foo[4s+4s:1s*2] offset (5s-8)`, + wantType: "subquery", + wantFields: map[string]any{ + "range": int64(0), + "rangeExpr": durationExpr("+", durationNumber("4", true), durationNumber("4", true), false), + "step": int64(0), + "stepExpr": durationExpr("*", durationNumber("1", true), durationNumber("2", false), false), + "offset": int64(0), + "offsetExpr": durationExpr("-", durationNumber("5", true), durationNumber("8", false), true), + }, + }, + { + name: "regular vector selector offset", + query: `foo offset 5m`, + wantType: "vectorSelector", + wantFields: map[string]any{ + "offset": int64(5 * 60 * 1000), + "offsetExpr": nil, + }, + }, + { + name: "vector selector offset expression", + query: `foo offset -min(5s,step()+8s)`, + wantType: "vectorSelector", + wantFields: map[string]any{ + "offset": int64(0), + "offsetExpr": durationExpr("-", nil, + durationExpr("min", + durationNumber("5", true), + durationExpr("+", durationExpr("step", nil, nil, false), durationNumber("8", true), false), + false, + ), + false, + ), + }, + }, + } + + for _, tcase := range testcases { + t.Run(tcase.name, func(t *testing.T) { + expr, err := p.ParseExpr(tcase.query) + require.NoError(t, err) + + got := translateAST(expr).(map[string]any) + require.Equal(t, tcase.wantType, got["type"]) + for field, want := range tcase.wantFields { + require.Contains(t, got, field) + require.Equal(t, want, got[field], field) + } + }) + } +} + +func durationExpr(op string, lhs, rhs any, wrapped bool) map[string]any { + return map[string]any{ + "type": "durationExpr", + "op": op, + "lhs": lhs, + "rhs": rhs, + "wrapped": wrapped, + } +} + +func durationNumber(val string, duration bool) map[string]any { + return map[string]any{ + "type": "numberLiteral", + "val": val, + "duration": duration, + } +}