2019-11-14 03:59:41 -06:00
|
|
|
package cloudwatch
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-01-17 08:27:03 -06:00
|
|
|
"sort"
|
2019-11-14 03:59:41 -06:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
2021-09-08 09:06:43 -05:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
2020-10-06 06:45:58 -05:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
2021-09-08 09:06:43 -05:00
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
2022-05-05 06:59:23 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
2019-11-14 03:59:41 -06:00
|
|
|
)
|
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
func (e *cloudWatchExecutor) parseResponse(startTime time.Time, endTime time.Time, metricDataOutputs []*cloudwatch.GetMetricDataOutput,
|
|
|
|
queries []*cloudWatchQuery) ([]*responseWrapper, error) {
|
|
|
|
aggregatedResponse := aggregateResponse(metricDataOutputs)
|
|
|
|
queriesById := map[string]*cloudWatchQuery{}
|
|
|
|
for _, query := range queries {
|
|
|
|
queriesById[query.Id] = query
|
|
|
|
}
|
|
|
|
|
|
|
|
results := []*responseWrapper{}
|
|
|
|
for id, response := range aggregatedResponse {
|
|
|
|
queryRow := queriesById[id]
|
|
|
|
dataRes := backend.DataResponse{}
|
|
|
|
|
|
|
|
if response.HasArithmeticError {
|
|
|
|
dataRes.Error = fmt.Errorf("ArithmeticError in query %q: %s", queryRow.RefId, response.ArithmeticErrorMessage)
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2022-05-05 06:59:23 -05:00
|
|
|
dataRes.Frames, err = buildDataFrames(startTime, endTime, response, queryRow, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels))
|
2021-09-08 09:06:43 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
results = append(results, &responseWrapper{
|
|
|
|
DataResponse: &dataRes,
|
|
|
|
RefId: queryRow.RefId,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func aggregateResponse(getMetricDataOutputs []*cloudwatch.GetMetricDataOutput) map[string]queryRowResponse {
|
|
|
|
responseByID := make(map[string]queryRowResponse)
|
2022-04-04 08:44:19 -05:00
|
|
|
errorCodes := map[string]bool{
|
|
|
|
maxMetricsExceeded: false,
|
|
|
|
maxQueryTimeRangeExceeded: false,
|
|
|
|
maxQueryResultsExceeded: false,
|
|
|
|
maxMatchingResultsExceeded: false,
|
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
for _, gmdo := range getMetricDataOutputs {
|
|
|
|
for _, message := range gmdo.Messages {
|
2022-04-04 08:44:19 -05:00
|
|
|
if _, exists := errorCodes[*message.Code]; exists {
|
|
|
|
errorCodes[*message.Code] = true
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
for _, r := range gmdo.MetricDataResults {
|
2020-08-07 04:48:40 -05:00
|
|
|
id := *r.Id
|
|
|
|
label := *r.Label
|
2021-09-08 09:06:43 -05:00
|
|
|
|
2022-05-08 02:27:03 -05:00
|
|
|
response := newQueryRowResponse()
|
2021-09-08 09:06:43 -05:00
|
|
|
if _, exists := responseByID[id]; exists {
|
|
|
|
response = responseByID[id]
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, message := range r.Messages {
|
|
|
|
if *message.Code == "ArithmeticError" {
|
|
|
|
response.addArithmeticError(message.Value)
|
2019-11-19 06:36:32 -06:00
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
|
|
|
|
if _, exists := response.Metrics[label]; !exists {
|
|
|
|
response.addMetricDataResult(r)
|
|
|
|
} else {
|
|
|
|
response.appendTimeSeries(r)
|
|
|
|
}
|
|
|
|
|
2022-04-04 08:44:19 -05:00
|
|
|
for code := range errorCodes {
|
|
|
|
if _, exists := response.ErrorCodes[code]; exists {
|
|
|
|
response.ErrorCodes[code] = errorCodes[code]
|
|
|
|
}
|
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
responseByID[id] = response
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
return responseByID
|
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
func getLabels(cloudwatchLabel string, query *cloudWatchQuery) data.Labels {
|
|
|
|
dims := make([]string, 0, len(query.Dimensions))
|
|
|
|
for k := range query.Dimensions {
|
|
|
|
dims = append(dims, k)
|
|
|
|
}
|
|
|
|
sort.Strings(dims)
|
|
|
|
labels := data.Labels{}
|
|
|
|
for _, dim := range dims {
|
|
|
|
values := query.Dimensions[dim]
|
|
|
|
if len(values) == 1 && values[0] != "*" {
|
|
|
|
labels[dim] = values[0]
|
|
|
|
} else {
|
|
|
|
for _, value := range values {
|
|
|
|
if value == cloudwatchLabel || value == "*" {
|
|
|
|
labels[dim] = cloudwatchLabel
|
|
|
|
} else if strings.Contains(cloudwatchLabel, value) {
|
|
|
|
labels[dim] = value
|
|
|
|
}
|
|
|
|
}
|
2020-05-18 05:25:58 -05:00
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
return labels
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse queryRowResponse,
|
2022-05-05 06:59:23 -05:00
|
|
|
query *cloudWatchQuery, dynamicLabelEnabled bool) (data.Frames, error) {
|
2020-10-06 06:45:58 -05:00
|
|
|
frames := data.Frames{}
|
2021-09-08 09:06:43 -05:00
|
|
|
for _, label := range aggregatedResponse.Labels {
|
|
|
|
metric := aggregatedResponse.Metrics[label]
|
2019-11-14 03:59:41 -06:00
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
deepLink, err := query.buildDeepLink(startTime, endTime)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
|
|
|
|
2020-10-06 06:45:58 -05:00
|
|
|
// 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
|
2021-09-08 09:06:43 -05:00
|
|
|
if len(metric.Values) == 0 && query.isMultiValuedDimensionExpression() {
|
2020-03-10 15:14:58 -05:00
|
|
|
series := 0
|
|
|
|
multiValuedDimension := ""
|
|
|
|
for key, values := range query.Dimensions {
|
|
|
|
if len(values) > series {
|
|
|
|
series = len(values)
|
|
|
|
multiValuedDimension = key
|
|
|
|
}
|
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
|
2020-03-10 15:14:58 -05:00
|
|
|
for _, value := range query.Dimensions[multiValuedDimension] {
|
2021-09-08 09:06:43 -05:00
|
|
|
labels := map[string]string{multiValuedDimension: value}
|
2020-03-10 15:14:58 -05:00
|
|
|
for key, values := range query.Dimensions {
|
|
|
|
if key != multiValuedDimension && len(values) > 0 {
|
2021-09-08 09:06:43 -05:00
|
|
|
labels[key] = values[0]
|
2020-03-10 15:14:58 -05:00
|
|
|
}
|
|
|
|
}
|
2020-01-17 08:27:03 -06:00
|
|
|
|
2020-11-03 04:24:26 -06:00
|
|
|
timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, []*time.Time{})
|
2021-09-08 09:06:43 -05:00
|
|
|
valueField := data.NewField(data.TimeSeriesValueFieldName, labels, []*float64{})
|
2020-10-06 06:45:58 -05:00
|
|
|
|
2022-05-05 06:59:23 -05:00
|
|
|
frameName := label
|
|
|
|
if !dynamicLabelEnabled {
|
|
|
|
frameName = formatAlias(query, query.Statistic, labels, label)
|
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: frameName, Links: createDataLinks(deepLink)})
|
2020-10-06 06:45:58 -05:00
|
|
|
|
|
|
|
emptyFrame := data.Frame{
|
|
|
|
Name: frameName,
|
|
|
|
Fields: []*data.Field{
|
|
|
|
timeField,
|
|
|
|
valueField,
|
|
|
|
},
|
|
|
|
RefID: query.RefId,
|
2021-09-08 09:06:43 -05:00
|
|
|
Meta: createMeta(query),
|
2020-10-06 06:45:58 -05:00
|
|
|
}
|
|
|
|
frames = append(frames, &emptyFrame)
|
2020-03-10 15:14:58 -05:00
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
continue
|
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
labels := getLabels(label, query)
|
|
|
|
timestamps := []*time.Time{}
|
|
|
|
points := []*float64{}
|
|
|
|
for j, t := range metric.Timestamps {
|
|
|
|
val := metric.Values[j]
|
|
|
|
timestamps = append(timestamps, t)
|
|
|
|
points = append(points, val)
|
|
|
|
}
|
2020-10-06 06:45:58 -05:00
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timestamps)
|
|
|
|
valueField := data.NewField(data.TimeSeriesValueFieldName, labels, points)
|
2020-10-06 06:45:58 -05:00
|
|
|
|
2022-05-05 06:59:23 -05:00
|
|
|
frameName := label
|
|
|
|
if !dynamicLabelEnabled {
|
|
|
|
frameName = formatAlias(query, query.Statistic, labels, label)
|
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: frameName, Links: createDataLinks(deepLink)})
|
2020-10-06 06:45:58 -05:00
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
frame := data.Frame{
|
|
|
|
Name: frameName,
|
|
|
|
Fields: []*data.Field{
|
|
|
|
timeField,
|
|
|
|
valueField,
|
|
|
|
},
|
|
|
|
RefID: query.RefId,
|
|
|
|
Meta: createMeta(query),
|
|
|
|
}
|
|
|
|
|
2022-04-04 08:44:19 -05:00
|
|
|
warningTextMap := map[string]string{
|
|
|
|
"MaxMetricsExceeded": "Maximum number of allowed metrics exceeded. Your search may have been limited",
|
|
|
|
"MaxQueryTimeRangeExceeded": "Max time window exceeded for query",
|
|
|
|
"MaxQueryResultsExceeded": "Only the first 500 time series can be returned by a query.",
|
|
|
|
"MaxMatchingResultsExceeded": "The query matched more than 10.000 metrics, results might not be accurate.",
|
|
|
|
}
|
|
|
|
for code := range aggregatedResponse.ErrorCodes {
|
|
|
|
if aggregatedResponse.ErrorCodes[code] {
|
|
|
|
frame.AppendNotices(data.Notice{
|
|
|
|
Severity: data.NoticeSeverityWarning,
|
|
|
|
Text: "cloudwatch GetMetricData error: " + warningTextMap[code],
|
|
|
|
})
|
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
|
|
|
|
if aggregatedResponse.StatusCode != "Complete" {
|
|
|
|
frame.AppendNotices(data.Notice{
|
|
|
|
Severity: data.NoticeSeverityWarning,
|
|
|
|
Text: "cloudwatch GetMetricData error: Too many datapoints requested - your search has been limited. Please try to reduce the time range",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
frames = append(frames, &frame)
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
2020-10-06 06:45:58 -05:00
|
|
|
|
2021-09-08 09:06:43 -05:00
|
|
|
return frames, nil
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func formatAlias(query *cloudWatchQuery, stat string, dimensions map[string]string, label string) string {
|
|
|
|
region := query.Region
|
|
|
|
namespace := query.Namespace
|
|
|
|
metricName := query.MetricName
|
|
|
|
period := strconv.Itoa(query.Period)
|
|
|
|
|
|
|
|
if query.isUserDefinedSearchExpression() {
|
|
|
|
pIndex := strings.LastIndex(query.Expression, ",")
|
|
|
|
period = strings.Trim(query.Expression[pIndex+1:], " )")
|
|
|
|
sIndex := strings.LastIndex(query.Expression[:pIndex], ",")
|
|
|
|
stat = strings.Trim(query.Expression[sIndex+1:pIndex], " '")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(query.Alias) == 0 && query.isMathExpression() {
|
|
|
|
return query.Id
|
|
|
|
}
|
2020-03-10 15:14:58 -05:00
|
|
|
if len(query.Alias) == 0 && query.isInferredSearchExpression() && !query.isMultiValuedDimensionExpression() {
|
2019-11-14 03:59:41 -06:00
|
|
|
return label
|
|
|
|
}
|
2021-11-30 03:53:31 -06:00
|
|
|
if len(query.Alias) == 0 && query.MetricQueryType == MetricQueryTypeQuery {
|
|
|
|
return label
|
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
|
2021-11-30 03:53:31 -06:00
|
|
|
// common fields
|
2020-05-18 05:25:58 -05:00
|
|
|
data := map[string]string{
|
2021-11-30 03:53:31 -06:00
|
|
|
"region": region,
|
|
|
|
"period": period,
|
2020-05-18 05:25:58 -05:00
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
if len(label) != 0 {
|
|
|
|
data["label"] = label
|
|
|
|
}
|
2021-11-30 03:53:31 -06:00
|
|
|
|
|
|
|
// since the SQL query string is not (yet) parsed, we don't know what namespace, metric, statistic and labels it's using at this point
|
|
|
|
if query.MetricQueryType != MetricQueryTypeQuery {
|
|
|
|
data["namespace"] = namespace
|
|
|
|
data["metric"] = metricName
|
|
|
|
data["stat"] = stat
|
|
|
|
for k, v := range dimensions {
|
|
|
|
data[k] = v
|
|
|
|
}
|
2019-11-14 03:59:41 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
result := aliasFormat.ReplaceAllFunc([]byte(query.Alias), func(in []byte) []byte {
|
|
|
|
labelName := strings.Replace(string(in), "{{", "", 1)
|
|
|
|
labelName = strings.Replace(labelName, "}}", "", 1)
|
|
|
|
labelName = strings.TrimSpace(labelName)
|
|
|
|
if val, exists := data[labelName]; exists {
|
|
|
|
return []byte(val)
|
|
|
|
}
|
|
|
|
|
|
|
|
return in
|
|
|
|
})
|
|
|
|
|
|
|
|
if string(result) == "" {
|
|
|
|
return metricName + "_" + stat
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(result)
|
|
|
|
}
|
2021-09-08 09:06:43 -05:00
|
|
|
|
|
|
|
func createDataLinks(link string) []data.DataLink {
|
|
|
|
dataLinks := []data.DataLink{}
|
|
|
|
if link != "" {
|
|
|
|
dataLinks = append(dataLinks, data.DataLink{
|
|
|
|
Title: "View in CloudWatch console",
|
|
|
|
TargetBlank: true,
|
|
|
|
URL: link,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return dataLinks
|
|
|
|
}
|
|
|
|
|
|
|
|
func createMeta(query *cloudWatchQuery) *data.FrameMeta {
|
|
|
|
return &data.FrameMeta{
|
|
|
|
ExecutedQueryString: query.UsedExpression,
|
|
|
|
Custom: simplejson.NewFromAny(map[string]interface{}{
|
|
|
|
"period": query.Period,
|
|
|
|
"id": query.Id,
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|