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:
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
|
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
|
// 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) {
|
func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time) (map[string][]*cloudWatchQuery, error) {
|
||||||
requestQueries := make(map[string][]*cloudWatchQuery)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -51,32 +53,28 @@ func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime
|
|||||||
return requestQueries, nil
|
return requestQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrateLegacyQuery migrates queries that has a `statistics` field to use the `statistic` field instead.
|
// migrateLegacyQuery is also done in the frontend, so this should only ever be needed for alerting queries
|
||||||
// This migration 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) {
|
||||||
// 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) {
|
|
||||||
migratedQueries := []*backend.DataQuery{}
|
migratedQueries := []*backend.DataQuery{}
|
||||||
for _, q := range queries {
|
for _, q := range queries {
|
||||||
query := q
|
query := q
|
||||||
model, err := simplejson.NewJson(query.JSON)
|
queryJson, err := simplejson.NewJson(query.JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = model.Get("statistic").String()
|
if err := migrateStatisticsToStatistic(queryJson); err != nil {
|
||||||
// If there's not a statistic property in the json, we know it's the legacy format and then it has to be migrated
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, labelExists := queryJson.CheckGet("label")
|
||||||
|
if !labelExists && dynamicLabelsEnabled {
|
||||||
|
migrateAliasToDynamicLabel(queryJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.JSON, err = queryJson.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stats, err := model.Get("statistics").StringArray()
|
return nil, err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
migratedQueries = append(migratedQueries, &query)
|
migratedQueries = append(migratedQueries, &query)
|
||||||
@@ -85,6 +83,53 @@ func migrateLegacyQuery(queries []backend.DataQuery, startTime time.Time, endTim
|
|||||||
return migratedQueries, nil
|
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) {
|
func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time, endTime time.Time) (*cloudWatchQuery, error) {
|
||||||
plog.Debug("Parsing request query", "query", model)
|
plog.Debug("Parsing request query", "query", model)
|
||||||
reNumber := regexp.MustCompile(`^\d+$`)
|
reNumber := regexp.MustCompile(`^\d+$`)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cloudwatch
|
package cloudwatch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ func TestRequestParser(t *testing.T) {
|
|||||||
"period": "600",
|
"period": "600",
|
||||||
"hide": false
|
"hide": false
|
||||||
}`)
|
}`)
|
||||||
migratedQueries, err := migrateLegacyQuery([]backend.DataQuery{*oldQuery}, startTime, endTime)
|
migratedQueries, err := migrateLegacyQuery([]backend.DataQuery{*oldQuery}, false, startTime, endTime)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, len(migratedQueries))
|
assert.Equal(t, 1, len(migratedQueries))
|
||||||
|
|
||||||
@@ -322,3 +323,225 @@ func getBaseJsonQuery() *simplejson.Json {
|
|||||||
"period": "900",
|
"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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user