Commit 763fb3bc by Erik Sundell Committed by GitHub

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
parent 73c4bef7
......@@ -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()
}
......@@ -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",
......
......@@ -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
}
......
......@@ -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{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment