CloudWatch: Migrate alias to dynamic labels (#48555)

This commit is contained in:
Shirley 2022-05-03 14:41:51 +02:00 committed by GitHub
parent 3ee99821bc
commit 27821e0bc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 288 additions and 20 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
@ -20,7 +21,8 @@ var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
// 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 (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time) (map[string][]*cloudWatchQuery, error) {
requestQueries := make(map[string][]*cloudWatchQuery)
migratedQueries, err := migrateLegacyQuery(queries, startTime, endTime)
migratedQueries, err := migrateLegacyQuery(queries, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels), startTime, endTime)
if err != nil {
return nil, err
}
@ -51,32 +53,28 @@ func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime
return requestQueries, nil
}
// migrateLegacyQuery migrates queries that has a `statistics` field to use the `statistic` field instead.
// This migration is also done in the frontend, so this should only ever be needed for alerting queries
// 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 migrateLegacyQuery(queries []backend.DataQuery, startTime time.Time, endTime time.Time) ([]*backend.DataQuery, error) {
// migrateLegacyQuery is also done in the frontend, so this should only ever be needed for alerting queries
func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool, startTime time.Time, endTime time.Time) ([]*backend.DataQuery, error) {
migratedQueries := []*backend.DataQuery{}
for _, q := range queries {
query := q
model, err := simplejson.NewJson(query.JSON)
queryJson, err := simplejson.NewJson(query.JSON)
if err != nil {
return nil, err
}
_, err = model.Get("statistic").String()
// 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 err := migrateStatisticsToStatistic(queryJson); err != nil {
return nil, err
}
_, labelExists := queryJson.CheckGet("label")
if !labelExists && dynamicLabelsEnabled {
migrateAliasToDynamicLabel(queryJson)
}
query.JSON, err = queryJson.MarshalJSON()
if err != nil {
stats, err := model.Get("statistics").StringArray()
if err != nil {
return nil, fmt.Errorf("query must have either statistic or statistics field")
}
model.Del("statistics")
model.Set("statistic", stats[0])
query.JSON, err = model.MarshalJSON()
if err != nil {
return nil, err
}
return nil, err
}
migratedQueries = append(migratedQueries, &query)
@ -85,6 +83,53 @@ func migrateLegacyQuery(queries []backend.DataQuery, startTime time.Time, endTim
return migratedQueries, nil
}
// 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 *simplejson.Json) error {
_, err := queryJson.Get("statistic").String()
// 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 err != nil {
stats, err := queryJson.Get("statistics").StringArray()
if err != nil {
return fmt.Errorf("query must have either statistic or statistics field")
}
queryJson.Del("statistics")
queryJson.Set("statistic", stats[0])
}
return nil
}
var aliasPatterns = map[string]string{
"metric": `${PROP('MetricName')}`,
"namespace": `${PROP('Namespace')}`,
"period": `${PROP('Period')}`,
"region": `${PROP('Region')}`,
"stat": `${PROP('Stat')}`,
"label": `${LABEL}`,
}
var legacyAliasRegexp = regexp.MustCompile(`{{\s*(.+?)\s*}}`)
func migrateAliasToDynamicLabel(queryJson *simplejson.Json) {
fullAliasField := queryJson.Get("alias").MustString()
if fullAliasField != "" {
matches := legacyAliasRegexp.FindAllStringSubmatch(fullAliasField, -1)
for _, groups := range matches {
fullMatch := groups[0]
subgroup := groups[1]
if dynamicLabel, ok := aliasPatterns[subgroup]; ok {
fullAliasField = strings.ReplaceAll(fullAliasField, fullMatch, dynamicLabel)
} else {
fullAliasField = strings.ReplaceAll(fullAliasField, fullMatch, fmt.Sprintf(`${PROP('Dim.%s')}`, subgroup))
}
}
}
queryJson.Set("label", fullAliasField)
}
func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) {
plog.Debug("Parsing request query", "query", model)
reNumber := regexp.MustCompile(`^\d+$`)

View File

@ -1,6 +1,7 @@
package cloudwatch
import (
"fmt"
"testing"
"time"
@ -32,7 +33,7 @@ func TestRequestParser(t *testing.T) {
"period": "600",
"hide": false
}`)
migratedQueries, err := migrateLegacyQuery([]backend.DataQuery{*oldQuery}, startTime, endTime)
migratedQueries, err := migrateLegacyQuery([]backend.DataQuery{*oldQuery}, false, startTime, endTime)
require.NoError(t, err)
assert.Equal(t, 1, len(migratedQueries))
@ -322,3 +323,225 @@ func getBaseJsonQuery() *simplejson.Json {
"period": "900",
})
}
func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_creates_new_label(t *testing.T) {
testCases := map[string]struct {
inputAlias string
expectedLabel string
}{
"one known alias pattern: metric": {inputAlias: "{{metric}}", expectedLabel: "${PROP('MetricName')}"},
"one known alias pattern: namespace": {inputAlias: "{{namespace}}", expectedLabel: "${PROP('Namespace')}"},
"one known alias pattern: period": {inputAlias: "{{period}}", expectedLabel: "${PROP('Period')}"},
"one known alias pattern: region": {inputAlias: "{{region}}", expectedLabel: "${PROP('Region')}"},
"one known alias pattern: stat": {inputAlias: "{{stat}}", expectedLabel: "${PROP('Stat')}"},
"one known alias pattern: label": {inputAlias: "{{label}}", expectedLabel: "${LABEL}"},
"one unknown alias pattern becomes dimension": {inputAlias: "{{any_other_word}}", expectedLabel: "${PROP('Dim.any_other_word')}"},
"one known alias pattern with spaces": {inputAlias: "{{ metric }}", expectedLabel: "${PROP('MetricName')}"},
"multiple alias patterns": {inputAlias: "some {{combination }}{{ label}} and {{metric}}", expectedLabel: "some ${PROP('Dim.combination')}${LABEL} and ${PROP('MetricName')}"},
"empty alias still migrates to empty label": {inputAlias: "", expectedLabel: ""},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
queryJson, err := simplejson.NewJson([]byte(fmt.Sprintf(`{
"region": "us-east-1",
"namespace": "ec2",
"metricName": "CPUUtilization",
"alias": "%s",
"dimensions": {
"InstanceId": ["test"]
},
"statistic": "Average",
"period": "600",
"hide": false
}`, tc.inputAlias)))
require.NoError(t, err)
migrateAliasToDynamicLabel(queryJson)
assert.Equal(t, simplejson.NewFromAny(
map[string]interface{}{
"alias": tc.inputAlias,
"dimensions": map[string]interface{}{"InstanceId": []interface{}{"test"}},
"hide": false,
"label": tc.expectedLabel,
"metricName": "CPUUtilization",
"namespace": "ec2",
"period": "600",
"region": "us-east-1",
"statistic": "Average"}), queryJson)
})
}
}
func Test_Test_migrateLegacyQuery(t *testing.T) {
t.Run("migrates alias to label when label does not already exist and feature toggle enabled", func(t *testing.T) {
migratedQueries, err := migrateLegacyQuery(
[]backend.DataQuery{
{
RefID: "A",
QueryType: "timeSeriesQuery",
JSON: []byte(`{
"region": "us-east-1",
"namespace": "ec2",
"metricName": "CPUUtilization",
"alias": "{{period}} {{any_other_word}}",
"dimensions": {
"InstanceId": ["test"]
},
"statistic": "Average",
"period": "600",
"hide": false
}`)},
}, true, time.Now(), time.Now())
require.NoError(t, err)
require.Equal(t, 1, len(migratedQueries))
assert.JSONEq(t, `{
"alias":"{{period}} {{any_other_word}}",
"label":"${PROP('Period')} ${PROP('Dim.any_other_word')}",
"dimensions":{
"InstanceId":[
"test"
]
},
"hide":false,
"metricName":"CPUUtilization",
"namespace":"ec2",
"period":"600",
"region":"us-east-1",
"statistic":"Average"
}`,
string(migratedQueries[0].JSON))
})
t.Run("successfully migrates alias to dynamic label for multiple queries", func(t *testing.T) {
migratedQueries, err := migrateLegacyQuery(
[]backend.DataQuery{
{
RefID: "A",
QueryType: "timeSeriesQuery",
JSON: []byte(`{
"region": "us-east-1",
"namespace": "ec2",
"metricName": "CPUUtilization",
"alias": "{{period}} {{any_other_word}}",
"dimensions": {
"InstanceId": ["test"]
},
"statistic": "Average",
"period": "600",
"hide": false
}`),
},
{
RefID: "B",
QueryType: "timeSeriesQuery",
JSON: []byte(`{
"region": "us-east-1",
"namespace": "ec2",
"metricName": "CPUUtilization",
"alias": "{{ label }}",
"dimensions": {
"InstanceId": ["test"]
},
"statistic": "Average",
"period": "600",
"hide": false
}`),
},
}, true, time.Now(), time.Now())
require.NoError(t, err)
require.Equal(t, 2, len(migratedQueries))
assert.JSONEq(t,
`{
"alias": "{{period}} {{any_other_word}}",
"label":"${PROP('Period')} ${PROP('Dim.any_other_word')}",
"dimensions":{
"InstanceId":[
"test"
]
},
"hide":false,
"metricName":"CPUUtilization",
"namespace":"ec2",
"period":"600",
"region":"us-east-1",
"statistic":"Average"
}`,
string(migratedQueries[0].JSON))
assert.JSONEq(t,
`{
"alias": "{{ label }}",
"label":"${LABEL}",
"dimensions":{
"InstanceId":[
"test"
]
},
"hide":false,
"metricName":"CPUUtilization",
"namespace":"ec2",
"period":"600",
"region":"us-east-1",
"statistic":"Average"
}`,
string(migratedQueries[1].JSON))
})
t.Run("does not migrate alias to label", func(t *testing.T) {
testCases := map[string]struct {
labelJson string
dynamicLabelsFeatureToggleEnabled bool
}{
"when label already exists, feature toggle enabled": {labelJson: `"label":"some label",`, dynamicLabelsFeatureToggleEnabled: true},
"when label does not exist, feature toggle is disabled": {dynamicLabelsFeatureToggleEnabled: false},
"when label already exists, feature toggle is disabled": {labelJson: `"label":"some label",`, dynamicLabelsFeatureToggleEnabled: false},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
migratedQueries, err := migrateLegacyQuery(
[]backend.DataQuery{
{
RefID: "A",
QueryType: "timeSeriesQuery",
JSON: []byte(fmt.Sprintf(`{
"region": "us-east-1",
"namespace": "ec2",
"metricName": "CPUUtilization",
"alias": "{{period}} {{any_other_word}}",
%s
"dimensions": {
"InstanceId": ["test"]
},
"statistic": "Average",
"period": "600",
"hide": false
}`, tc.labelJson))},
}, tc.dynamicLabelsFeatureToggleEnabled, time.Now(), time.Now())
require.NoError(t, err)
require.Equal(t, 1, len(migratedQueries))
assert.JSONEq(t,
fmt.Sprintf(`{
"alias":"{{period}} {{any_other_word}}",
%s
"dimensions":{
"InstanceId":[
"test"
]
},
"hide":false,
"metricName":"CPUUtilization",
"namespace":"ec2",
"period":"600",
"region":"us-east-1",
"statistic":"Average"
}`, tc.labelJson),
string(migratedQueries[0].JSON))
})
}
})
}