mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
ee6ff18122
commit
cadc6088db
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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{}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
12
pkg/tsdb/cloudwatch/models/query_error.go
Normal file
12
pkg/tsdb/cloudwatch/models/query_error.go
Normal 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)
|
||||
}
|
@ -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 {
|
@ -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"},
|
22
pkg/tsdb/cloudwatch/models/types.go
Normal file
22
pkg/tsdb/cloudwatch/models/types.go
Normal 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"`
|
||||
}
|
@ -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(`{
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
)
|
Loading…
Reference in New Issue
Block a user