mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch: Migrate alias to dynamic labels (#48555)
This commit is contained in:
parent
3ee99821bc
commit
27821e0bc1
@ -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+$`)
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user