From 763fb3bc323abd5c8c68f6d082bf76a5ce917ef5 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 10 Mar 2020 21:14:58 +0100 Subject: [PATCH] CloudWatch: Expand alias variables when query yields no result (#22695) * Return empty time series with expanded aliases in case query yielded no results * PR feedback --- pkg/tsdb/cloudwatch/cloudwatch_query.go | 16 ++++ pkg/tsdb/cloudwatch/cloudwatch_query_test.go | 23 ++++++ pkg/tsdb/cloudwatch/response_parser.go | 86 +++++++++++++------- pkg/tsdb/cloudwatch/response_parser_test.go | 76 +++++++++++++++++ 4 files changed, 172 insertions(+), 29 deletions(-) diff --git a/pkg/tsdb/cloudwatch/cloudwatch_query.go b/pkg/tsdb/cloudwatch/cloudwatch_query.go index 1386626e5b4..0830f00bfd4 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_query.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_query.go @@ -55,6 +55,22 @@ func (q *cloudWatchQuery) isInferredSearchExpression() bool { return false } +func (q *cloudWatchQuery) isMultiValuedDimensionExpression() bool { + for _, values := range q.Dimensions { + for _, v := range values { + if v == "*" { + return false + } + } + + if len(values) > 1 { + return true + } + } + + return false +} + func (q *cloudWatchQuery) isMetricStat() bool { return !q.isSearchExpression() && !q.isMathExpression() } diff --git a/pkg/tsdb/cloudwatch/cloudwatch_query_test.go b/pkg/tsdb/cloudwatch/cloudwatch_query_test.go index 792c0bfd6c8..e1f6105d873 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_query_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_query_test.go @@ -95,6 +95,29 @@ func TestCloudWatchQuery(t *testing.T) { }) }) + Convey("and query has a multi-valued dimension", func() { + query := &cloudWatchQuery{ + RefId: "A", + Region: "us-east-1", + Expression: "", + Stats: "Average", + Period: 300, + Id: "id1", + Dimensions: map[string][]string{ + "InstanceId": {"i-12345678", "i-12345679"}, + "InstanceType": {"abc"}, + }, + } + + Convey("it is a search expression", func() { + So(query.isSearchExpression(), ShouldBeTrue) + }) + + Convey("it is a multi-valued dimension expression", func() { + So(query.isMultiValuedDimensionExpression(), ShouldBeTrue) + }) + }) + Convey("and no dimensions were added", func() { query := &cloudWatchQuery{ RefId: "A", diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index 7d99f45d4d0..9ea4fd27d25 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -83,44 +83,72 @@ func parseGetMetricDataTimeSeries(metricDataResults map[string]*cloudwatch.Metri } } - series := tsdb.TimeSeries{ - Tags: make(map[string]string), - Points: make([]tsdb.TimePoint, 0), - } + // In case a multi-valued dimension is used and the cloudwatch query yields no values, create one empty time series for each dimension value. + // Use that dimension value to expand the alias field + if len(metricDataResult.Values) == 0 && query.isMultiValuedDimensionExpression() { + series := 0 + multiValuedDimension := "" + for key, values := range query.Dimensions { + if len(values) > series { + series = len(values) + multiValuedDimension = key + } + } - keys := make([]string, 0) - for k := range query.Dimensions { - keys = append(keys, k) - } - sort.Strings(keys) + for _, value := range query.Dimensions[multiValuedDimension] { + emptySeries := tsdb.TimeSeries{ + Tags: map[string]string{multiValuedDimension: value}, + Points: make([]tsdb.TimePoint, 0), + } + for key, values := range query.Dimensions { + if key != multiValuedDimension && len(values) > 0 { + emptySeries.Tags[key] = values[0] + } + } - for _, key := range keys { - values := query.Dimensions[key] - if len(values) == 1 && values[0] != "*" { - series.Tags[key] = values[0] - } else { - for _, value := range values { - if value == label || value == "*" { - series.Tags[key] = label - } else if strings.Contains(label, value) { - series.Tags[key] = value + emptySeries.Name = formatAlias(query, query.Stats, emptySeries.Tags, label) + result = append(result, &emptySeries) + } + } else { + keys := make([]string, 0) + for k := range query.Dimensions { + keys = append(keys, k) + } + sort.Strings(keys) + + series := tsdb.TimeSeries{ + Tags: make(map[string]string), + Points: make([]tsdb.TimePoint, 0), + } + + for _, key := range keys { + values := query.Dimensions[key] + if len(values) == 1 && values[0] != "*" { + series.Tags[key] = values[0] + } else { + for _, value := range values { + if value == label || value == "*" { + series.Tags[key] = label + } else if strings.Contains(label, value) { + series.Tags[key] = value + } } } } - } - series.Name = formatAlias(query, query.Stats, series.Tags, label) + series.Name = formatAlias(query, query.Stats, series.Tags, label) - for j, t := range metricDataResult.Timestamps { - if j > 0 { - expectedTimestamp := metricDataResult.Timestamps[j-1].Add(time.Duration(query.Period) * time.Second) - if expectedTimestamp.Before(*t) { - series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000))) + for j, t := range metricDataResult.Timestamps { + if j > 0 { + expectedTimestamp := metricDataResult.Timestamps[j-1].Add(time.Duration(query.Period) * time.Second) + if expectedTimestamp.Before(*t) { + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000))) + } } + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*metricDataResult.Values[j]), float64((*t).Unix())*1000)) } - series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*metricDataResult.Values[j]), float64((*t).Unix())*1000)) + result = append(result, &series) } - result = append(result, &series) } return &result, partialData, nil } @@ -142,7 +170,7 @@ func formatAlias(query *cloudWatchQuery, stat string, dimensions map[string]stri return query.Id } - if len(query.Alias) == 0 && query.isInferredSearchExpression() { + if len(query.Alias) == 0 && query.isInferredSearchExpression() && !query.isMultiValuedDimensionExpression() { return label } diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index 045ac8508f4..f6e42aaabca 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -189,6 +189,82 @@ func TestCloudWatchResponseParser(t *testing.T) { So((*series)[1].Name, ShouldEqual, "lb4 Expanded") }) + Convey("can expand dimension value when no values are returned and a multi-valued template variabel is used", func() { + timestamp := time.Unix(0, 0) + resp := map[string]*cloudwatch.MetricDataResult{ + "lb3": { + Id: aws.String("lb3"), + Label: aws.String("lb3"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + aws.Time(timestamp.Add(60 * time.Second)), + aws.Time(timestamp.Add(180 * time.Second)), + }, + Values: []*float64{}, + StatusCode: aws.String("Complete"), + }, + } + + query := &cloudWatchQuery{ + RefId: "refId1", + Region: "us-east-1", + Namespace: "AWS/ApplicationELB", + MetricName: "TargetResponseTime", + Dimensions: map[string][]string{ + "LoadBalancer": {"lb1", "lb2"}, + }, + Stats: "Average", + Period: 60, + Alias: "{{LoadBalancer}} Expanded", + } + series, partialData, err := parseGetMetricDataTimeSeries(resp, query) + + So(err, ShouldBeNil) + So(partialData, ShouldBeFalse) + So(len(*series), ShouldEqual, 2) + So((*series)[0].Name, ShouldEqual, "lb1 Expanded") + So((*series)[1].Name, ShouldEqual, "lb2 Expanded") + }) + + Convey("can expand dimension value when no values are returned and a multi-valued template variable and two single-valued dimensions are used", func() { + timestamp := time.Unix(0, 0) + resp := map[string]*cloudwatch.MetricDataResult{ + "lb3": { + Id: aws.String("lb3"), + Label: aws.String("lb3"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + aws.Time(timestamp.Add(60 * time.Second)), + aws.Time(timestamp.Add(180 * time.Second)), + }, + Values: []*float64{}, + StatusCode: aws.String("Complete"), + }, + } + + query := &cloudWatchQuery{ + RefId: "refId1", + Region: "us-east-1", + Namespace: "AWS/ApplicationELB", + MetricName: "TargetResponseTime", + Dimensions: map[string][]string{ + "LoadBalancer": {"lb1", "lb2"}, + "InstanceType": {"micro"}, + "Resource": {"res"}, + }, + Stats: "Average", + Period: 60, + Alias: "{{LoadBalancer}} Expanded {{InstanceType}} - {{Resource}}", + } + series, partialData, err := parseGetMetricDataTimeSeries(resp, query) + + So(err, ShouldBeNil) + So(partialData, ShouldBeFalse) + So(len(*series), ShouldEqual, 2) + So((*series)[0].Name, ShouldEqual, "lb1 Expanded micro - res") + So((*series)[1].Name, ShouldEqual, "lb2 Expanded micro - res") + }) + Convey("can parse cloudwatch response", func() { timestamp := time.Unix(0, 0) resp := map[string]*cloudwatch.MetricDataResult{