grafana/pkg/tsdb/cloudwatch/query_transformer.go
Will Browne d7862c50b8
Plugins: Migrate CloudWatch to backend plugin SDK (#31149)
* first pass

* add instance manager

* fix tests

* remove dead code

* unexport fields

* cleanup

* remove ds instance from executor

* cleanup

* inline im

* remove old func

* get error working

* unexport field

* let fe do its magic

* fix channel name

* revert some tsdb changes

* fix annotations

* cleanup
2021-03-23 16:32:12 +01:00

237 lines
7.0 KiB
Go

package cloudwatch
import (
"encoding/json"
"fmt"
"net/url"
"sort"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// returns a map of queries with query id as key. In the case a q request query
// has more than one statistic defined, one cloudwatchQuery will be created for each statistic.
// If the query doesn't have an Id defined by the user, we'll give it an with format `query[RefId]`. In the case
// the incoming query had more than one stat, it will ge an id like `query[RefId]_[StatName]`, eg queryC_Average
func (e *cloudWatchExecutor) transformRequestQueriesToCloudWatchQueries(requestQueries []*requestQuery) (
map[string]*cloudWatchQuery, error) {
plog.Debug("Transforming CloudWatch request queries")
cloudwatchQueries := make(map[string]*cloudWatchQuery)
for _, requestQuery := range requestQueries {
for _, stat := range requestQuery.Statistics {
id := requestQuery.Id
if id == "" {
id = fmt.Sprintf("query%s", requestQuery.RefId)
}
if len(requestQuery.Statistics) > 1 {
id = fmt.Sprintf("%s_%v", id, strings.ReplaceAll(*stat, ".", "_"))
}
if _, ok := cloudwatchQueries[id]; ok {
return nil, fmt.Errorf("error in query %q - query ID %q is not unique", requestQuery.RefId, id)
}
query := &cloudWatchQuery{
Id: id,
RefId: requestQuery.RefId,
Region: requestQuery.Region,
Namespace: requestQuery.Namespace,
MetricName: requestQuery.MetricName,
Dimensions: requestQuery.Dimensions,
Stats: *stat,
Period: requestQuery.Period,
Alias: requestQuery.Alias,
Expression: requestQuery.Expression,
ReturnData: requestQuery.ReturnData,
MatchExact: requestQuery.MatchExact,
}
cloudwatchQueries[id] = query
}
}
return cloudwatchQueries, nil
}
func (e *cloudWatchExecutor) transformQueryResponsesToQueryResult(cloudwatchResponses []*cloudwatchResponse, requestQueries []*requestQuery, startTime time.Time, endTime time.Time) (map[string]*backend.DataResponse, error) {
responsesByRefID := make(map[string][]*cloudwatchResponse)
refIDs := sort.StringSlice{}
for _, res := range cloudwatchResponses {
refIDs = append(refIDs, res.RefId)
responsesByRefID[res.RefId] = append(responsesByRefID[res.RefId], res)
}
// Ensure stable results
refIDs.Sort()
results := make(map[string]*backend.DataResponse)
for _, refID := range refIDs {
responses := responsesByRefID[refID]
queryResult := backend.DataResponse{}
frames := make(data.Frames, 0, len(responses))
requestExceededMaxLimit := false
partialData := false
var executedQueries []executedQuery
for _, response := range responses {
frames = append(frames, response.DataFrames...)
requestExceededMaxLimit = requestExceededMaxLimit || response.RequestExceededMaxLimit
partialData = partialData || response.PartialData
if requestExceededMaxLimit {
frames[0].AppendNotices(data.Notice{
Severity: data.NoticeSeverityWarning,
Text: "cloudwatch GetMetricData error: Maximum number of allowed metrics exceeded. Your search may have been limited",
})
}
if partialData {
frames[0].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",
})
}
executedQueries = append(executedQueries, executedQuery{
Expression: response.Expression,
ID: response.Id,
Period: response.Period,
})
}
sort.Slice(frames, func(i, j int) bool {
return frames[i].Name < frames[j].Name
})
eq, err := json.Marshal(executedQueries)
if err != nil {
return nil, fmt.Errorf("could not marshal executedString struct: %w", err)
}
link, err := buildDeepLink(refID, requestQueries, executedQueries, startTime, endTime)
if err != nil {
return nil, fmt.Errorf("could not build deep link: %w", err)
}
createDataLinks := func(link string) []data.DataLink {
return []data.DataLink{{
Title: "View in CloudWatch console",
TargetBlank: true,
URL: link,
}}
}
for _, frame := range frames {
if frame.Meta != nil {
frame.Meta.ExecutedQueryString = string(eq)
} else {
frame.Meta = &data.FrameMeta{
ExecutedQueryString: string(eq),
}
}
if link == "" || len(frame.Fields) < 2 {
continue
}
if frame.Fields[1].Config == nil {
frame.Fields[1].Config = &data.FieldConfig{}
}
frame.Fields[1].Config.Links = createDataLinks(link)
}
queryResult.Frames = frames
results[refID] = &queryResult
}
return results, nil
}
// buildDeepLink generates a deep link from Grafana to the CloudWatch console. The link params are based on
// metric(s) for a given query row in the Query Editor.
func buildDeepLink(refID string, requestQueries []*requestQuery, executedQueries []executedQuery, startTime time.Time,
endTime time.Time) (string, error) {
if isMathExpression(executedQueries) {
return "", nil
}
requestQuery := &requestQuery{}
for _, rq := range requestQueries {
if rq.RefId == refID {
requestQuery = rq
break
}
}
metricItems := []interface{}{}
cloudWatchLinkProps := &cloudWatchLink{
Title: refID,
View: "timeSeries",
Stacked: false,
Region: requestQuery.Region,
Start: startTime.UTC().Format(time.RFC3339),
End: endTime.UTC().Format(time.RFC3339),
}
expressions := []interface{}{}
for _, meta := range executedQueries {
if strings.Contains(meta.Expression, "SEARCH(") {
expressions = append(expressions, &metricExpression{Expression: meta.Expression})
}
}
if len(expressions) != 0 {
cloudWatchLinkProps.Metrics = expressions
} else {
for _, stat := range requestQuery.Statistics {
metricStat := []interface{}{requestQuery.Namespace, requestQuery.MetricName}
for dimensionKey, dimensionValues := range requestQuery.Dimensions {
metricStat = append(metricStat, dimensionKey, dimensionValues[0])
}
metricStat = append(metricStat, &metricStatMeta{
Stat: *stat,
Period: requestQuery.Period,
})
metricItems = append(metricItems, metricStat)
}
cloudWatchLinkProps.Metrics = metricItems
}
linkProps, err := json.Marshal(cloudWatchLinkProps)
if err != nil {
return "", fmt.Errorf("could not marshal link: %w", err)
}
url, err := url.Parse(fmt.Sprintf(`https://%s.console.aws.amazon.com/cloudwatch/deeplink.js`, requestQuery.Region))
if err != nil {
return "", fmt.Errorf("unable to parse CloudWatch console deep link")
}
fragment := url.Query()
fragment.Set("", string(linkProps))
q := url.Query()
q.Set("region", requestQuery.Region)
url.RawQuery = q.Encode()
link := fmt.Sprintf(`%s#metricsV2:graph%s`, url.String(), fragment.Encode())
return link, nil
}
func isMathExpression(executedQueries []executedQuery) bool {
isMathExpression := false
for _, query := range executedQueries {
if strings.Contains(query.Expression, "SEARCH(") {
return false
} else if query.Expression != "" {
isMathExpression = true
}
}
return isMathExpression
}