CloudWatch: move QueryData input parsing types to separate package (#57165)

* CloudWatch: move parse request types separate package

* Move metric query constants, unexport metricDataQuery json decoding type

* Unexport isSearchExpression
This commit is contained in:
Shirley 2022-10-20 11:21:13 +02:00 committed by GitHub
parent ee6ff18122
commit cadc6088db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 280 additions and 267 deletions

View File

@ -5,11 +5,13 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func (e *cloudWatchExecutor) buildMetricDataInput(startTime time.Time, endTime time.Time,
queries []*cloudWatchQuery) (*cloudwatch.GetMetricDataInput, error) {
queries []*models.CloudWatchQuery) (*cloudwatch.GetMetricDataInput, error) {
metricDataInput := &cloudwatch.GetMetricDataInput{
StartTime: aws.Time(startTime),
EndTime: aws.Time(endTime),
@ -27,7 +29,7 @@ func (e *cloudWatchExecutor) buildMetricDataInput(startTime time.Time, endTime t
for _, query := range queries {
metricDataQuery, err := e.buildMetricDataQuery(query)
if err != nil {
return nil, &queryError{err, query.RefId}
return nil, &models.QueryError{Err: err, RefID: query.RefId}
}
metricDataInput.MetricDataQueries = append(metricDataInput.MetricDataQueries, metricDataQuery)
}

View File

@ -6,9 +6,11 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func TestMetricDataInputBuilder(t *testing.T) {
@ -34,7 +36,7 @@ func TestMetricDataInputBuilder(t *testing.T) {
from := now.Add(time.Hour * -2)
to := now.Add(time.Hour * -1)
mdi, err := executor.buildMetricDataInput(from, to, []*cloudWatchQuery{query})
mdi, err := executor.buildMetricDataInput(from, to, []*models.CloudWatchQuery{query})
assert.NoError(t, err)
require.NotNil(t, mdi)

View File

@ -9,9 +9,10 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func (e *cloudWatchExecutor) buildMetricDataQuery(query *cloudWatchQuery) (*cloudwatch.MetricDataQuery, error) {
func (e *cloudWatchExecutor) buildMetricDataQuery(query *models.CloudWatchQuery) (*cloudwatch.MetricDataQuery, error) {
mdq := &cloudwatch.MetricDataQuery{
Id: aws.String(query.Id),
ReturnData: aws.Bool(query.ReturnData),
@ -21,16 +22,16 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(query *cloudWatchQuery) (*clou
mdq.Label = &query.Label
}
switch query.getGMDAPIMode() {
case GMDApiModeMathExpression:
switch query.GetGMDAPIMode() {
case models.GMDApiModeMathExpression:
mdq.Period = aws.Int64(int64(query.Period))
mdq.Expression = aws.String(query.Expression)
case GMDApiModeSQLExpression:
case models.GMDApiModeSQLExpression:
mdq.Period = aws.Int64(int64(query.Period))
mdq.Expression = aws.String(query.SqlExpression)
case GMDApiModeInferredSearchExpression:
case models.GMDApiModeInferredSearchExpression:
mdq.Expression = aws.String(buildSearchExpression(query, query.Statistic))
case GMDApiModeMetricStat:
case models.GMDApiModeMetricStat:
mdq.MetricStat = &cloudwatch.MetricStat{
Metric: &cloudwatch.Metric{
Namespace: aws.String(query.Namespace),
@ -58,7 +59,7 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(query *cloudWatchQuery) (*clou
return mdq, nil
}
func buildSearchExpression(query *cloudWatchQuery, stat string) string {
func buildSearchExpression(query *models.CloudWatchQuery, stat string) string {
knownDimensions := make(map[string][]string)
dimensionNames := []string{}
dimensionNamesWithoutKnownValues := []string{}

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -13,8 +14,8 @@ func TestMetricDataQueryBuilder(t *testing.T) {
t.Run("should use metric stat", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = MetricEditorModeBuilder
query.MetricQueryType = MetricQueryTypeSearch
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
mdq, err := executor.buildMetricDataQuery(query)
require.NoError(t, err)
require.Empty(t, mdq.Expression)
@ -25,8 +26,8 @@ func TestMetricDataQueryBuilder(t *testing.T) {
t.Run("should use custom built expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = MetricEditorModeBuilder
query.MetricQueryType = MetricQueryTypeSearch
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
mdq, err := executor.buildMetricDataQuery(query)
require.NoError(t, err)
@ -37,8 +38,8 @@ func TestMetricDataQueryBuilder(t *testing.T) {
t.Run("should use sql expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = MetricEditorModeRaw
query.MetricQueryType = MetricQueryTypeQuery
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeQuery
query.SqlExpression = `SELECT SUM(CPUUTilization) FROM "AWS/EC2"`
mdq, err := executor.buildMetricDataQuery(query)
require.NoError(t, err)
@ -49,8 +50,8 @@ func TestMetricDataQueryBuilder(t *testing.T) {
t.Run("should use user defined math expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = MetricEditorModeRaw
query.MetricQueryType = MetricQueryTypeSearch
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeSearch
query.Expression = `SUM(x+y)`
mdq, err := executor.buildMetricDataQuery(query)
require.NoError(t, err)
@ -61,8 +62,8 @@ func TestMetricDataQueryBuilder(t *testing.T) {
t.Run("should set period in user defined expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = MetricEditorModeRaw
query.MetricQueryType = MetricQueryTypeSearch
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
query.Expression = `SUM([a,b])`
mdq, err := executor.buildMetricDataQuery(query)
@ -116,7 +117,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
const matchExact = true
t.Run("Query has three dimension values for a given dimension key", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -132,7 +133,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has three dimension values for two given dimension keys", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -149,7 +150,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("No OR operator was added if a star was used for dimension value", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -165,7 +166,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has one dimension key with a * value", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -181,7 +182,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has three dimension values for two given dimension keys, and one value is a star", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -198,7 +199,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has a dimension key with a space", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/Kafka",
MetricName: "CpuUser",
Dimensions: map[string][]string{
@ -214,7 +215,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has a custom namespace contains spaces", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "Test-API Cache by Minute",
MetricName: "CpuUser",
Dimensions: map[string][]string{
@ -235,7 +236,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
const matchExact = false
t.Run("Query has three dimension values for a given dimension key", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -251,7 +252,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has three dimension values for two given dimension keys", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -268,7 +269,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has one dimension key with a * value", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -284,7 +285,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("query has three dimension values for two given dimension keys, and one value is a star", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -302,7 +303,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("Query has invalid characters in dimension values", func(t *testing.T) {
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
@ -318,8 +319,8 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
}
func getBaseQuery() *cloudWatchQuery {
query := &cloudWatchQuery{
func getBaseQuery() *models.CloudWatchQuery {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{

View File

@ -1,4 +1,4 @@
package cloudwatch
package models
import (
"encoding/json"
@ -10,7 +10,30 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/cwlog"
)
type cloudWatchQuery struct {
type (
MetricEditorMode uint32
MetricQueryType uint32
GMDApiMode uint32
)
const (
MetricEditorModeBuilder MetricEditorMode = iota
MetricEditorModeRaw
)
const (
MetricQueryTypeSearch MetricQueryType = iota
MetricQueryTypeQuery
)
const (
GMDApiModeMetricStat GMDApiMode = iota
GMDApiModeInferredSearchExpression
GMDApiModeMathExpression
GMDApiModeSQLExpression
)
type CloudWatchQuery struct {
RefId string
Region string
Id string
@ -27,13 +50,13 @@ type cloudWatchQuery struct {
MatchExact bool
UsedExpression string
TimezoneUTCOffset string
MetricQueryType metricQueryType
MetricEditorMode metricEditorMode
MetricQueryType MetricQueryType
MetricEditorMode MetricEditorMode
}
func (q *cloudWatchQuery) getGMDAPIMode() gmdApiMode {
func (q *CloudWatchQuery) GetGMDAPIMode() GMDApiMode {
if q.MetricQueryType == MetricQueryTypeSearch && q.MetricEditorMode == MetricEditorModeBuilder {
if q.isInferredSearchExpression() {
if q.IsInferredSearchExpression() {
return GMDApiModeInferredSearchExpression
}
return GMDApiModeMetricStat
@ -43,23 +66,23 @@ func (q *cloudWatchQuery) getGMDAPIMode() gmdApiMode {
return GMDApiModeSQLExpression
}
cwlog.Warn("Could not resolve CloudWatch metric query type. Falling back to metric stat.", "query", q)
cwlog.Warn("could not resolve CloudWatch metric query type. Falling back to metric stat.", "query", q)
return GMDApiModeMetricStat
}
func (q *cloudWatchQuery) isMathExpression() bool {
return q.MetricQueryType == MetricQueryTypeSearch && q.MetricEditorMode == MetricEditorModeRaw && !q.isUserDefinedSearchExpression()
func (q *CloudWatchQuery) IsMathExpression() bool {
return q.MetricQueryType == MetricQueryTypeSearch && q.MetricEditorMode == MetricEditorModeRaw && !q.IsUserDefinedSearchExpression()
}
func (q *cloudWatchQuery) isSearchExpression() bool {
return q.MetricQueryType == MetricQueryTypeSearch && (q.isUserDefinedSearchExpression() || q.isInferredSearchExpression())
func (q *CloudWatchQuery) isSearchExpression() bool {
return q.MetricQueryType == MetricQueryTypeSearch && (q.IsUserDefinedSearchExpression() || q.IsInferredSearchExpression())
}
func (q *cloudWatchQuery) isUserDefinedSearchExpression() bool {
func (q *CloudWatchQuery) IsUserDefinedSearchExpression() bool {
return q.MetricQueryType == MetricQueryTypeSearch && q.MetricEditorMode == MetricEditorModeRaw && strings.Contains(q.Expression, "SEARCH(")
}
func (q *cloudWatchQuery) isInferredSearchExpression() bool {
func (q *CloudWatchQuery) IsInferredSearchExpression() bool {
if q.MetricQueryType != MetricQueryTypeSearch || q.MetricEditorMode != MetricEditorModeBuilder {
return false
}
@ -84,7 +107,7 @@ func (q *cloudWatchQuery) isInferredSearchExpression() bool {
return false
}
func (q *cloudWatchQuery) isMultiValuedDimensionExpression() bool {
func (q *CloudWatchQuery) IsMultiValuedDimensionExpression() bool {
if q.MetricQueryType != MetricQueryTypeSearch || q.MetricEditorMode != MetricEditorModeBuilder {
return false
}
@ -104,8 +127,8 @@ func (q *cloudWatchQuery) isMultiValuedDimensionExpression() bool {
return false
}
func (q *cloudWatchQuery) buildDeepLink(startTime time.Time, endTime time.Time, dynamicLabelEnabled bool) (string, error) {
if q.isMathExpression() || q.MetricQueryType == MetricQueryTypeQuery {
func (q *CloudWatchQuery) BuildDeepLink(startTime time.Time, endTime time.Time, dynamicLabelEnabled bool) (string, error) {
if q.IsMathExpression() || q.MetricQueryType == MetricQueryTypeQuery {
return "", nil
}

View File

@ -1,4 +1,4 @@
package cloudwatch
package models
import (
"testing"
@ -13,7 +13,7 @@ func TestCloudWatchQuery(t *testing.T) {
t.Run("is not generated for MetricQueryTypeQuery", func(t *testing.T) {
startTime := time.Now()
endTime := startTime.Add(2 * time.Hour)
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -28,7 +28,7 @@ func TestCloudWatchQuery(t *testing.T) {
MetricEditorMode: MetricEditorModeBuilder,
}
deepLink, err := query.buildDeepLink(startTime, endTime, false)
deepLink, err := query.BuildDeepLink(startTime, endTime, false)
require.NoError(t, err)
assert.Empty(t, deepLink)
})
@ -36,7 +36,7 @@ func TestCloudWatchQuery(t *testing.T) {
t.Run("does not include label in case dynamic label is diabled", func(t *testing.T) {
startTime := time.Now()
endTime := startTime.Add(2 * time.Hour)
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -52,7 +52,7 @@ func TestCloudWatchQuery(t *testing.T) {
MetricEditorMode: MetricEditorModeBuilder,
}
deepLink, err := query.buildDeepLink(startTime, endTime, false)
deepLink, err := query.BuildDeepLink(startTime, endTime, false)
require.NoError(t, err)
assert.NotContains(t, deepLink, "label")
})
@ -60,7 +60,7 @@ func TestCloudWatchQuery(t *testing.T) {
t.Run("includes label in case dynamic label is enabled and it's a metric stat query", func(t *testing.T) {
startTime := time.Now()
endTime := startTime.Add(2 * time.Hour)
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -76,7 +76,7 @@ func TestCloudWatchQuery(t *testing.T) {
MetricEditorMode: MetricEditorModeBuilder,
}
deepLink, err := query.buildDeepLink(startTime, endTime, false)
deepLink, err := query.BuildDeepLink(startTime, endTime, false)
require.NoError(t, err)
assert.NotContains(t, deepLink, "label")
})
@ -84,7 +84,7 @@ func TestCloudWatchQuery(t *testing.T) {
t.Run("includes label in case dynamic label is enabled and it's a math expression query", func(t *testing.T) {
startTime := time.Now()
endTime := startTime.Add(2 * time.Hour)
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Statistic: "Average",
@ -97,14 +97,14 @@ func TestCloudWatchQuery(t *testing.T) {
MetricEditorMode: MetricEditorModeRaw,
}
deepLink, err := query.buildDeepLink(startTime, endTime, false)
deepLink, err := query.BuildDeepLink(startTime, endTime, false)
require.NoError(t, err)
assert.NotContains(t, deepLink, "label")
})
})
t.Run("SEARCH(someexpression) was specified in the query editor", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "SEARCH(someexpression)",
@ -114,11 +114,11 @@ func TestCloudWatchQuery(t *testing.T) {
}
assert.True(t, query.isSearchExpression(), "Expected a search expression")
assert.False(t, query.isMathExpression(), "Expected not math expression")
assert.False(t, query.IsMathExpression(), "Expected not math expression")
})
t.Run("No expression, no multi dimension key values and no * was used", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -132,11 +132,11 @@ func TestCloudWatchQuery(t *testing.T) {
}
assert.False(t, query.isSearchExpression(), "Expected not a search expression")
assert.False(t, query.isMathExpression(), "Expected not math expressions")
assert.False(t, query.IsMathExpression(), "Expected not math expressions")
})
t.Run("No expression but multi dimension key values exist", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -149,11 +149,11 @@ func TestCloudWatchQuery(t *testing.T) {
}
assert.True(t, query.isSearchExpression(), "Expected a search expression")
assert.False(t, query.isMathExpression(), "Expected not math expressions")
assert.False(t, query.IsMathExpression(), "Expected not math expressions")
})
t.Run("No expression but dimension values has *", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -167,11 +167,11 @@ func TestCloudWatchQuery(t *testing.T) {
}
assert.True(t, query.isSearchExpression(), "Expected a search expression")
assert.False(t, query.isMathExpression(), "Expected not math expression")
assert.False(t, query.IsMathExpression(), "Expected not math expression")
})
t.Run("Query has a multi-valued dimension", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -185,11 +185,11 @@ func TestCloudWatchQuery(t *testing.T) {
}
assert.True(t, query.isSearchExpression(), "Expected a search expression")
assert.True(t, query.isMultiValuedDimensionExpression(), "Expected a multi-valued dimension expression")
assert.True(t, query.IsMultiValuedDimensionExpression(), "Expected a multi-valued dimension expression")
})
t.Run("No dimensions were added", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -202,18 +202,18 @@ func TestCloudWatchQuery(t *testing.T) {
t.Run("Match exact is false", func(t *testing.T) {
query.MatchExact = false
assert.True(t, query.isSearchExpression(), "Expected a search expression")
assert.False(t, query.isMathExpression(), "Expected not math expression")
assert.False(t, query.IsMathExpression(), "Expected not math expression")
})
t.Run("Match exact is true", func(t *testing.T) {
query.MatchExact = true
assert.False(t, query.isSearchExpression(), "Exxpected not search expression")
assert.False(t, query.isMathExpression(), "Expected not math expression")
assert.False(t, query.IsMathExpression(), "Expected not math expression")
})
})
t.Run("Match exact is", func(t *testing.T) {
query := &cloudWatchQuery{
query := &CloudWatchQuery{
RefId: "A",
Region: "us-east-1",
Expression: "",
@ -227,6 +227,6 @@ func TestCloudWatchQuery(t *testing.T) {
}
assert.True(t, query.isSearchExpression(), "Expected search expression")
assert.False(t, query.isMathExpression(), "Expected not math expression")
assert.False(t, query.IsMathExpression(), "Expected not math expression")
})
}

View File

@ -0,0 +1,12 @@
package models
import "fmt"
type QueryError struct {
Err error
RefID string
}
func (e *QueryError) Error() string {
return fmt.Sprintf("error parsing query %q, %s", e.RefID, e.Err)
}

View File

@ -1,4 +1,4 @@
package cloudwatch
package models
import (
"encoding/json"
@ -13,12 +13,15 @@ import (
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/cwlog"
)
const timeSeriesQuery = "timeSeriesQuery"
var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
type QueryJson struct {
type metricsDataQuery struct {
Datasource map[string]string `json:"datasource,omitempty"`
Dimensions map[string]interface{} `json:"dimensions,omitempty"`
Expression string `json:"expression,omitempty"`
@ -28,7 +31,7 @@ type QueryJson struct {
MaxDataPoints int `json:"maxDataPoints,omitempty"`
MetricEditorMode *int `json:"metricEditorMode,omitempty"`
MetricName string `json:"metricName,omitempty"`
MetricQueryType metricQueryType `json:"metricQueryType,omitempty"`
MetricQueryType MetricQueryType `json:"metricQueryType,omitempty"`
Namespace string `json:"namespace,omitempty"`
Period string `json:"period,omitempty"`
RefId string `json:"refId,omitempty"`
@ -42,44 +45,44 @@ type QueryJson struct {
Alias string `json:"alias,omitempty"`
}
// parseQueries parses the json queries and returns a map of cloudWatchQueries by region. The cloudWatchQuery has a 1 to 1 mapping to a query editor row
func parseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time, dynamicLabelsEnabled bool) (map[string][]*cloudWatchQuery, error) {
requestQueries := make(map[string][]*cloudWatchQuery)
// ParseQueries parses the json queries and returns a map of cloudWatchQueries by region. The cloudWatchQuery has a 1 to 1 mapping to a query editor row
func ParseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time, dynamicLabelsEnabled bool) (map[string][]*CloudWatchQuery, error) {
result := make(map[string][]*CloudWatchQuery)
migratedQueries, err := migrateLegacyQuery(queries, dynamicLabelsEnabled)
if err != nil {
return nil, err
}
for _, query := range migratedQueries {
var model QueryJson
err := json.Unmarshal(query.JSON, &model)
var metricsDataQuery metricsDataQuery
err := json.Unmarshal(query.JSON, &metricsDataQuery)
if err != nil {
return nil, &queryError{err: err, RefID: query.RefID}
return nil, &QueryError{Err: err, RefID: query.RefID}
}
queryType := model.QueryType
queryType := metricsDataQuery.QueryType
if queryType != timeSeriesQuery && queryType != "" {
continue
}
if model.MatchExact == nil {
if metricsDataQuery.MatchExact == nil {
trueBooleanValue := true
model.MatchExact = &trueBooleanValue
metricsDataQuery.MatchExact = &trueBooleanValue
}
refID := query.RefID
query, err := parseRequestQuery(model, refID, startTime, endTime)
query, err := parseRequestQuery(metricsDataQuery, refID, startTime, endTime)
if err != nil {
return nil, &queryError{err: err, RefID: refID}
return nil, &QueryError{Err: err, RefID: refID}
}
if _, exist := requestQueries[query.Region]; !exist {
requestQueries[query.Region] = []*cloudWatchQuery{}
if _, exist := result[query.Region]; !exist {
result[query.Region] = []*CloudWatchQuery{}
}
requestQueries[query.Region] = append(requestQueries[query.Region], query)
result[query.Region] = append(result[query.Region], query)
}
return requestQueries, nil
return result, nil
}
// migrateLegacyQuery is also done in the frontend, so this should only ever be needed for alerting queries
@ -87,7 +90,7 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool)
migratedQueries := []*backend.DataQuery{}
for _, q := range queries {
query := q
var queryJson *QueryJson
var queryJson *metricsDataQuery
err := json.Unmarshal(query.JSON, &queryJson)
if err != nil {
return nil, err
@ -114,7 +117,7 @@ func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool)
// migrateStatisticsToStatistic migrates queries that has a `statistics` field to use the `statistic` field instead.
// In case the query used more than one stat, the first stat in the slice will be used in the statistic field
// Read more here https://github.com/grafana/grafana/issues/30629
func migrateStatisticsToStatistic(queryJson *QueryJson) error {
func migrateStatisticsToStatistic(queryJson *metricsDataQuery) error {
// If there's not a statistic property in the json, we know it's the legacy format and then it has to be migrated
if queryJson.Statistic == nil {
if queryJson.Statistics == nil {
@ -139,7 +142,7 @@ var aliasPatterns = map[string]string{
var legacyAliasRegexp = regexp.MustCompile(`{{\s*(.+?)\s*}}`)
func migrateAliasToDynamicLabel(queryJson *QueryJson) {
func migrateAliasToDynamicLabel(queryJson *metricsDataQuery) {
fullAliasField := queryJson.Alias
if fullAliasField != "" {
@ -158,33 +161,33 @@ func migrateAliasToDynamicLabel(queryJson *QueryJson) {
queryJson.Label = &fullAliasField
}
func parseRequestQuery(model QueryJson, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) {
cwlog.Debug("Parsing request query", "query", model)
cloudWatchQuery := cloudWatchQuery{
Alias: model.Alias,
func parseRequestQuery(dataQuery metricsDataQuery, refId string, startTime time.Time, endTime time.Time) (*CloudWatchQuery, error) {
cwlog.Debug("Parsing request query", "query", dataQuery)
result := CloudWatchQuery{
Alias: dataQuery.Alias,
Label: "",
MatchExact: true,
Statistic: "",
ReturnData: true,
UsedExpression: "",
RefId: refId,
Id: model.Id,
Region: model.Region,
Namespace: model.Namespace,
MetricName: model.MetricName,
MetricQueryType: model.MetricQueryType,
SqlExpression: model.SqlExpression,
TimezoneUTCOffset: model.TimezoneUTCOffset,
Expression: model.Expression,
Id: dataQuery.Id,
Region: dataQuery.Region,
Namespace: dataQuery.Namespace,
MetricName: dataQuery.MetricName,
MetricQueryType: dataQuery.MetricQueryType,
SqlExpression: dataQuery.SqlExpression,
TimezoneUTCOffset: dataQuery.TimezoneUTCOffset,
Expression: dataQuery.Expression,
}
reNumber := regexp.MustCompile(`^\d+$`)
dimensions, err := parseDimensions(model.Dimensions)
dimensions, err := parseDimensions(dataQuery.Dimensions)
if err != nil {
return nil, fmt.Errorf("failed to parse dimensions: %v", err)
}
cloudWatchQuery.Dimensions = dimensions
result.Dimensions = dimensions
p := model.Period
p := dataQuery.Period
var period int
if strings.ToLower(p) == "auto" || p == "" {
deltaInSeconds := endTime.Sub(startTime).Seconds()
@ -211,55 +214,55 @@ func parseRequestQuery(model QueryJson, refId string, startTime time.Time, endTi
period = int(d.Seconds())
}
}
cloudWatchQuery.Period = period
result.Period = period
if model.Id == "" {
if dataQuery.Id == "" {
// Why not just use refId if id is not specified in the frontend? When specifying an id in the editor,
// and alphabetical must be used. The id must be unique, so if an id like for example a, b or c would be used,
// it would likely collide with some ref id. That's why the `query` prefix is used.
suffix := refId
if !validMetricDataID.MatchString(suffix) {
uuid := uuid.NewString()
suffix = strings.Replace(uuid, "-", "", -1)
newUUID := uuid.NewString()
suffix = strings.Replace(newUUID, "-", "", -1)
}
cloudWatchQuery.Id = fmt.Sprintf("query%s", suffix)
result.Id = fmt.Sprintf("query%s", suffix)
}
if model.Hide != nil {
cloudWatchQuery.ReturnData = !*model.Hide
if dataQuery.Hide != nil {
result.ReturnData = !*dataQuery.Hide
}
if model.QueryType == "" {
if dataQuery.QueryType == "" {
// If no type is provided we assume we are called by alerting service, which requires to return data!
// Note, this is sort of a hack, but the official Grafana interfaces do not carry the information
// who (which service) called the TsdbQueryEndpoint.Query(...) function.
cloudWatchQuery.ReturnData = true
result.ReturnData = true
}
if model.MetricEditorMode == nil && len(model.Expression) > 0 {
if dataQuery.MetricEditorMode == nil && len(dataQuery.Expression) > 0 {
// this should only ever happen if this is an alerting query that has not yet been migrated in the frontend
cloudWatchQuery.MetricEditorMode = MetricEditorModeRaw
result.MetricEditorMode = MetricEditorModeRaw
} else {
if model.MetricEditorMode != nil {
cloudWatchQuery.MetricEditorMode = metricEditorMode(*model.MetricEditorMode)
if dataQuery.MetricEditorMode != nil {
result.MetricEditorMode = MetricEditorMode(*dataQuery.MetricEditorMode)
} else {
cloudWatchQuery.MetricEditorMode = metricEditorMode(0)
result.MetricEditorMode = MetricEditorMode(0)
}
}
if model.Statistic != nil {
cloudWatchQuery.Statistic = *model.Statistic
if dataQuery.Statistic != nil {
result.Statistic = *dataQuery.Statistic
}
if model.MatchExact != nil {
cloudWatchQuery.MatchExact = *model.MatchExact
if dataQuery.MatchExact != nil {
result.MatchExact = *dataQuery.MatchExact
}
if model.Label != nil {
cloudWatchQuery.Label = *model.Label
if dataQuery.Label != nil {
result.Label = *dataQuery.Label
}
return &cloudWatchQuery, nil
return &result, nil
}
func getRetainedPeriods(timeSince time.Duration) []int {

View File

@ -1,4 +1,4 @@
package cloudwatch
package models
import (
"encoding/json"
@ -15,7 +15,7 @@ func TestQueryJSON(t *testing.T) {
jsonString := []byte(`{
"type": "timeSeriesQuery"
}`)
var res QueryJson
var res metricsDataQuery
err := json.Unmarshal(jsonString, &res)
require.NoError(t, err)
assert.Equal(t, "timeSeriesQuery", res.QueryType)
@ -49,7 +49,7 @@ func TestRequestParser(t *testing.T) {
migratedQuery := migratedQueries[0]
assert.Equal(t, "A", migratedQuery.RefID)
var model QueryJson
var model metricsDataQuery
err = json.Unmarshal(migratedQuery.JSON, &model)
require.NoError(t, err)
assert.Equal(t, "Average", *model.Statistic)
@ -57,7 +57,7 @@ func TestRequestParser(t *testing.T) {
})
t.Run("New dimensions structure", func(t *testing.T) {
query := QueryJson{
query := metricsDataQuery{
RefId: "ref1",
Region: "us-east-1",
Namespace: "ec2",
@ -91,7 +91,7 @@ func TestRequestParser(t *testing.T) {
})
t.Run("Old dimensions structure (backwards compatibility)", func(t *testing.T) {
query := QueryJson{
query := metricsDataQuery{
RefId: "ref1",
Region: "us-east-1",
Namespace: "ec2",
@ -125,7 +125,7 @@ func TestRequestParser(t *testing.T) {
})
t.Run("Period defined in the editor by the user is being used when time range is short", func(t *testing.T) {
query := QueryJson{
query := metricsDataQuery{
RefId: "ref1",
Region: "us-east-1",
Namespace: "ec2",
@ -147,7 +147,7 @@ func TestRequestParser(t *testing.T) {
})
t.Run("Period is parsed correctly if not defined by user", func(t *testing.T) {
query := QueryJson{
query := metricsDataQuery{
RefId: "ref1",
Region: "us-east-1",
Namespace: "ec2",
@ -278,7 +278,7 @@ func TestRequestParser(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
assert.Equal(t, MetricEditorModeBuilder, res.MetricEditorMode)
assert.Equal(t, GMDApiModeMetricStat, res.getGMDAPIMode())
assert.Equal(t, GMDApiModeMetricStat, res.GetGMDAPIMode())
})
t.Run("and an expression is specified it should be metric search builder", func(t *testing.T) {
@ -288,7 +288,7 @@ func TestRequestParser(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
assert.Equal(t, MetricEditorModeRaw, res.MetricEditorMode)
assert.Equal(t, GMDApiModeMathExpression, res.getGMDAPIMode())
assert.Equal(t, GMDApiModeMathExpression, res.GetGMDAPIMode())
})
})
@ -299,7 +299,7 @@ func TestRequestParser(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, MetricQueryTypeSearch, res.MetricQueryType)
assert.Equal(t, MetricEditorModeRaw, res.MetricEditorMode)
assert.Equal(t, GMDApiModeMathExpression, res.getGMDAPIMode())
assert.Equal(t, GMDApiModeMathExpression, res.GetGMDAPIMode())
})
})
@ -363,9 +363,9 @@ func TestRequestParser(t *testing.T) {
})
}
func getBaseJsonQuery() QueryJson {
func getBaseJsonQuery() metricsDataQuery {
average := "Average"
return QueryJson{
return metricsDataQuery{
RefId: "ref1",
Region: "us-east-1",
Namespace: "ec2",
@ -396,7 +396,7 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create
average := "Average"
false := false
queryToMigrate := QueryJson{
queryToMigrate := metricsDataQuery{
Region: "us-east-1",
Namespace: "ec2",
MetricName: "CPUUtilization",
@ -411,7 +411,7 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create
migrateAliasToDynamicLabel(&queryToMigrate)
expected := QueryJson{
expected := metricsDataQuery{
Alias: tc.inputAlias,
Dimensions: map[string]interface{}{
"InstanceId": []interface{}{"test"},

View File

@ -0,0 +1,22 @@
package models
type cloudWatchLink struct {
View string `json:"view"`
Stacked bool `json:"stacked"`
Title string `json:"title"`
Start string `json:"start"`
End string `json:"end"`
Region string `json:"region"`
Metrics []interface{} `json:"metrics"`
}
type metricExpression struct {
Expression string `json:"expression"`
Label string `json:"label,omitempty"`
}
type metricStatMeta struct {
Stat string `json:"stat"`
Period int `json:"period"`
Label string `json:"label,omitempty"`
}

View File

@ -11,12 +11,13 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func (e *cloudWatchExecutor) parseResponse(startTime time.Time, endTime time.Time, metricDataOutputs []*cloudwatch.GetMetricDataOutput,
queries []*cloudWatchQuery) ([]*responseWrapper, error) {
queries []*models.CloudWatchQuery) ([]*responseWrapper, error) {
aggregatedResponse := aggregateResponse(metricDataOutputs)
queriesById := map[string]*cloudWatchQuery{}
queriesById := map[string]*models.CloudWatchQuery{}
for _, query := range queries {
queriesById[query.Id] = query
}
@ -87,7 +88,7 @@ func aggregateResponse(getMetricDataOutputs []*cloudwatch.GetMetricDataOutput) m
return responseByID
}
func getLabels(cloudwatchLabel string, query *cloudWatchQuery) data.Labels {
func getLabels(cloudwatchLabel string, query *models.CloudWatchQuery) data.Labels {
dims := make([]string, 0, len(query.Dimensions))
for k := range query.Dimensions {
dims = append(dims, k)
@ -112,19 +113,19 @@ func getLabels(cloudwatchLabel string, query *cloudWatchQuery) data.Labels {
}
func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse queryRowResponse,
query *cloudWatchQuery, dynamicLabelEnabled bool) (data.Frames, error) {
query *models.CloudWatchQuery, dynamicLabelEnabled bool) (data.Frames, error) {
frames := data.Frames{}
for _, metric := range aggregatedResponse.Metrics {
label := *metric.Label
deepLink, err := query.buildDeepLink(startTime, endTime, dynamicLabelEnabled)
deepLink, err := query.BuildDeepLink(startTime, endTime, dynamicLabelEnabled)
if err != nil {
return nil, err
}
// 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(metric.Values) == 0 && query.isMultiValuedDimensionExpression() {
if len(metric.Values) == 0 && query.IsMultiValuedDimensionExpression() {
series := 0
multiValuedDimension := ""
for key, values := range query.Dimensions {
@ -221,45 +222,45 @@ func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse
return frames, nil
}
func formatAlias(query *cloudWatchQuery, stat string, dimensions map[string]string, label string) string {
func formatAlias(query *models.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() {
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() {
if len(query.Alias) == 0 && query.IsMathExpression() {
return query.Id
}
if len(query.Alias) == 0 && query.isInferredSearchExpression() && !query.isMultiValuedDimensionExpression() {
if len(query.Alias) == 0 && query.IsInferredSearchExpression() && !query.IsMultiValuedDimensionExpression() {
return label
}
if len(query.Alias) == 0 && query.MetricQueryType == MetricQueryTypeQuery {
if len(query.Alias) == 0 && query.MetricQueryType == models.MetricQueryTypeQuery {
return label
}
// common fields
data := map[string]string{
commonFields := map[string]string{
"region": region,
"period": period,
}
if len(label) != 0 {
data["label"] = label
commonFields["label"] = label
}
// 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
if query.MetricQueryType != models.MetricQueryTypeQuery {
commonFields["namespace"] = namespace
commonFields["metric"] = metricName
commonFields["stat"] = stat
for k, v := range dimensions {
data[k] = v
commonFields[k] = v
}
}
@ -267,7 +268,7 @@ func formatAlias(query *cloudWatchQuery, stat string, dimensions map[string]stri
labelName := strings.Replace(string(in), "{{", "", 1)
labelName = strings.Replace(labelName, "}}", "", 1)
labelName = strings.TrimSpace(labelName)
if val, exists := data[labelName]; exists {
if val, exists := commonFields[labelName]; exists {
return []byte(val)
}
@ -293,7 +294,7 @@ func createDataLinks(link string) []data.DataLink {
return dataLinks
}
func createMeta(query *cloudWatchQuery) *data.FrameMeta {
func createMeta(query *models.CloudWatchQuery) *data.FrameMeta {
return &data.FrameMeta{
ExecutedQueryString: query.UsedExpression,
Custom: fmt.Sprintf(`{

View File

@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -148,7 +149,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -160,8 +161,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{LoadBalancer}} Expanded",
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeBuilder,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeBuilder,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -211,7 +212,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -223,8 +224,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{LoadBalancer}} Expanded",
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeBuilder,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeBuilder,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -275,7 +276,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -287,8 +288,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{LoadBalancer}} Expanded",
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeBuilder,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeBuilder,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -314,7 +315,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
},
}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -325,8 +326,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{LoadBalancer}} Expanded",
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeBuilder,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeBuilder,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -354,7 +355,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -367,8 +368,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{LoadBalancer}} Expanded {{InstanceType}} - {{Resource}}",
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeBuilder,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeBuilder,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -394,7 +395,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -407,8 +408,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{LoadBalancer}} {{InstanceType}} {{metric}} {{namespace}} {{stat}} {{region}} {{period}}",
MetricQueryType: MetricQueryTypeQuery,
MetricEditorMode: MetricEditorModeRaw,
MetricQueryType: models.MetricQueryTypeQuery,
MetricEditorMode: models.MetricEditorModeRaw,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -444,7 +445,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}
query := &cloudWatchQuery{
query := &models.CloudWatchQuery{
RefId: "refId1",
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
@ -456,8 +457,8 @@ func TestCloudWatchResponseParser(t *testing.T) {
Statistic: "Average",
Period: 60,
Alias: "{{namespace}}_{{metric}}_{{stat}}",
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeBuilder,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeBuilder,
}
frames, err := buildDataFrames(startTime, endTime, *response, query, false)
require.NoError(t, err)
@ -485,7 +486,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
},
}
frames, err := buildDataFrames(startTime, endTime, *response, &cloudWatchQuery{}, true)
frames, err := buildDataFrames(startTime, endTime, *response, &models.CloudWatchQuery{}, true)
assert.NoError(t, err)
require.Len(t, frames, 1)

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/cwlog"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"golang.org/x/sync/errgroup"
)
@ -30,7 +31,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
return nil, fmt.Errorf("invalid time range: start time must be before end time")
}
requestQueriesByRegion, err := parseQueries(req.Queries, startTime, endTime, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels))
requestQueriesByRegion, err := models.ParseQueries(req.Queries, startTime, endTime, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels))
if err != nil {
return nil, err
}

View File

@ -13,7 +13,9 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -143,16 +145,16 @@ type queryDimensions struct {
}
type queryParameters struct {
MetricQueryType metricQueryType `json:"metricQueryType"`
MetricEditorMode metricEditorMode `json:"metricEditorMode"`
Dimensions queryDimensions `json:"dimensions"`
Expression string `json:"expression"`
Alias string `json:"alias"`
Label *string `json:"label"`
Statistic string `json:"statistic"`
Period string `json:"period"`
MatchExact bool `json:"matchExact"`
MetricName string `json:"metricName"`
MetricQueryType models.MetricQueryType `json:"metricQueryType"`
MetricEditorMode models.MetricEditorMode `json:"metricEditorMode"`
Dimensions queryDimensions `json:"dimensions"`
Expression string `json:"expression"`
Alias string `json:"alias"`
Label *string `json:"label"`
Statistic string `json:"statistic"`
Period string `json:"period"`
MatchExact bool `json:"matchExact"`
MetricName string `json:"metricName"`
}
var queryId = "query id"
@ -161,11 +163,11 @@ func newTestQuery(t testing.TB, p queryParameters) json.RawMessage {
t.Helper()
tsq := struct {
Type string `json:"type"`
MetricQueryType metricQueryType `json:"metricQueryType"`
MetricEditorMode metricEditorMode `json:"metricEditorMode"`
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Type string `json:"type"`
MetricQueryType models.MetricQueryType `json:"metricQueryType"`
MetricEditorMode models.MetricEditorMode `json:"metricEditorMode"`
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions struct {
InstanceID []string `json:"InstanceId,omitempty"`
} `json:"dimensions"`
@ -315,8 +317,8 @@ func Test_QueryData_response_data_frame_names(t *testing.T) {
t.Run("where user defines search expression and alias is defined, then frame name prioritizes period and stat from expression over input", func(t *testing.T) {
query := newTestQuery(t, queryParameters{
MetricQueryType: MetricQueryTypeSearch, // contributes to isUserDefinedSearchExpression = true
MetricEditorMode: MetricEditorModeRaw, // contributes to isUserDefinedSearchExpression = true
MetricQueryType: models.MetricQueryTypeSearch, // contributes to isUserDefinedSearchExpression = true
MetricEditorMode: models.MetricEditorModeRaw, // contributes to isUserDefinedSearchExpression = true
Alias: "{{period}} {{stat}}",
Expression: `SEARCH('{AWS/EC2,InstanceId} MetricName="CPUUtilization"', 'Average', 300)`, // period 300 and stat 'Average' parsed from this expression
Statistic: "Maximum", // stat parsed from expression takes precedence over 'Maximum'
@ -340,8 +342,8 @@ func Test_QueryData_response_data_frame_names(t *testing.T) {
t.Run("where no alias is provided and query is math expression, then frame name is queryId", func(t *testing.T) {
query := newTestQuery(t, queryParameters{
MetricQueryType: MetricQueryTypeSearch,
MetricEditorMode: MetricEditorModeRaw,
MetricQueryType: models.MetricQueryTypeSearch,
MetricEditorMode: models.MetricEditorModeRaw,
})
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
@ -361,7 +363,7 @@ func Test_QueryData_response_data_frame_names(t *testing.T) {
t.Run("where no alias provided and query type is MetricQueryTypeQuery, then frame name is label", func(t *testing.T) {
query := newTestQuery(t, queryParameters{
MetricQueryType: MetricQueryTypeQuery,
MetricQueryType: models.MetricQueryTypeQuery,
})
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{

View File

@ -1,58 +0,0 @@
package cloudwatch
import (
"fmt"
)
type queryError struct {
err error
RefID string
}
func (e *queryError) Error() string {
return fmt.Sprintf("error parsing query %q, %s", e.RefID, e.err)
}
type cloudWatchLink struct {
View string `json:"view"`
Stacked bool `json:"stacked"`
Title string `json:"title"`
Start string `json:"start"`
End string `json:"end"`
Region string `json:"region"`
Metrics []interface{} `json:"metrics"`
}
type metricExpression struct {
Expression string `json:"expression"`
Label string `json:"label,omitempty"`
}
type metricStatMeta struct {
Stat string `json:"stat"`
Period int `json:"period"`
Label string `json:"label,omitempty"`
}
type metricQueryType uint32
const (
MetricQueryTypeSearch metricQueryType = iota
MetricQueryTypeQuery
)
type metricEditorMode uint32
const (
MetricEditorModeBuilder metricEditorMode = iota
MetricEditorModeRaw
)
type gmdApiMode uint32
const (
GMDApiModeMetricStat gmdApiMode = iota
GMDApiModeInferredSearchExpression
GMDApiModeMathExpression
GMDApiModeSQLExpression
)