From 735ab8e118ed45e81a0a2b524ffe477572dc446f Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 19 Sep 2023 17:49:11 +0530 Subject: [PATCH] fix: add missed variable substitution for promql querires (#3584) --- .../app/metrics/query_builder.go | 9 +- pkg/query-service/app/parser.go | 24 ++++ pkg/query-service/app/parser_test.go | 105 ++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/pkg/query-service/app/metrics/query_builder.go b/pkg/query-service/app/metrics/query_builder.go index 00f9fb3788..02e0022a0e 100644 --- a/pkg/query-service/app/metrics/query_builder.go +++ b/pkg/query-service/app/metrics/query_builder.go @@ -497,7 +497,7 @@ func PromFormattedValue(v interface{}) string { case float32, float64: return fmt.Sprintf("%f", x) case string: - return fmt.Sprintf("%s", x) + return x case bool: return fmt.Sprintf("%v", x) case []interface{}: @@ -506,7 +506,12 @@ func PromFormattedValue(v interface{}) string { } switch x[0].(type) { case string, int, float32, float64, bool: - return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(x)), "|"), "[]") + // list of values joined by | for promql - a value can contain whitespace + var str []string + for _, sVal := range x { + str = append(str, fmt.Sprintf("%v", sVal)) + } + return strings.Join(str, "|") default: zap.L().Error("invalid type for prom formatted value", zap.Any("type", reflect.TypeOf(x[0]))) return "" diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 8e8467458f..a957ef85e7 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -1044,5 +1044,29 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE } } + // replace go template variables in prometheus query + if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL { + for _, promQuery := range queryRangeParams.CompositeQuery.PromQueries { + if promQuery.Disabled { + continue + } + tmpl := template.New("prometheus-query") + tmpl, err := tmpl.Parse(promQuery.Query) + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err} + } + var query bytes.Buffer + + // replace go template variables + querytemplate.AssignReservedVarsV3(queryRangeParams) + + err = tmpl.Execute(&query, queryRangeParams.Variables) + if err != nil { + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err} + } + promQuery.Query = query.String() + } + } + return queryRangeParams, nil } diff --git a/pkg/query-service/app/parser_test.go b/pkg/query-service/app/parser_test.go index dd668aef2a..65ea226909 100644 --- a/pkg/query-service/app/parser_test.go +++ b/pkg/query-service/app/parser_test.go @@ -757,3 +757,108 @@ func TestParseQueryRangeParamsDashboardVarsSubstitution(t *testing.T) { }) } } + +func TestParseQueryRangeParamsPromQLVars(t *testing.T) { + reqCases := []struct { + desc string + compositeQuery v3.CompositeQuery + variables map[string]interface{} + expectErr bool + errMsg string + expectedQuery string + }{ + { + desc: "valid prom query with dashboard variables", + compositeQuery: v3.CompositeQuery{ + PanelType: v3.PanelTypeGraph, + QueryType: v3.QueryTypePromQL, + PromQueries: map[string]*v3.PromQuery{ + "A": { + Query: "http_calls_total{service_name=\"{{.service_name}}\", operation_name=~\"{{.operation_name}}\"}", + Disabled: false, + }, + }, + }, + variables: map[string]interface{}{ + "service_name": "route", + "operation_name": []interface{}{ + "GET /route", + "POST /route", + }, + }, + expectErr: false, + expectedQuery: "http_calls_total{service_name=\"route\", operation_name=~\"GET /route|POST /route\"}", + }, + { + desc: "valid prom query with dashboard variables", + compositeQuery: v3.CompositeQuery{ + PanelType: v3.PanelTypeGraph, + QueryType: v3.QueryTypePromQL, + PromQueries: map[string]*v3.PromQuery{ + "A": { + Query: "http_calls_total{service_name=\"{{.service_name}}\", status_code=~\"{{.status_code}}\"}", + Disabled: false, + }, + }, + }, + variables: map[string]interface{}{ + "service_name": "route", + "status_code": []interface{}{ + 200, + 505, + }, + }, + expectErr: false, + expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}", + }, + { + desc: "valid prom query with dashboard variables", + compositeQuery: v3.CompositeQuery{ + PanelType: v3.PanelTypeGraph, + QueryType: v3.QueryTypePromQL, + PromQueries: map[string]*v3.PromQuery{ + "A": { + Query: "http_calls_total{service_name=\"{{.service_name}}\", quantity=~\"{{.quantity}}\"}", + Disabled: false, + }, + }, + }, + variables: map[string]interface{}{ + "service_name": "route", + "quantity": []interface{}{ + 4.5, + 4.6, + }, + }, + expectErr: false, + expectedQuery: "http_calls_total{service_name=\"route\", quantity=~\"4.5|4.6\"}", + }, + } + + for _, tc := range reqCases { + t.Run(tc.desc, func(t *testing.T) { + + queryRangeParams := &v3.QueryRangeParamsV3{ + Start: time.Now().Add(-time.Hour).UnixMilli(), + End: time.Now().UnixMilli(), + Step: time.Minute.Microseconds(), + CompositeQuery: &tc.compositeQuery, + Variables: tc.variables, + } + + body := &bytes.Buffer{} + err := json.NewEncoder(body).Encode(queryRangeParams) + require.NoError(t, err) + req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body) + + parsedQueryRangeParams, apiErr := ParseQueryRangeParams(req) + if tc.expectErr { + require.Error(t, apiErr) + require.Contains(t, apiErr.Error(), tc.errMsg) + } else { + require.Nil(t, apiErr) + require.Equal(t, parsedQueryRangeParams.CompositeQuery.PromQueries["A"].Query, tc.expectedQuery) + } + }) + } +}