grafana/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go
Erik Sundell 254577ba56
CloudWatch: Cross-account querying support (#59362)
* Lattice: Point to private prerelease of aws-sdk-go (#515)

* point to private prerelease of aws-sdk-go

* fix build issue

* Lattice: Adding a feature toggle (#549)

* Adding a feature toggle for lattice

* Change name of feature toggle

* Lattice: List accounts (#543)

* Separate layers

* Introduce testify/mock library

Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>

* point to version that includes metric api changes (#574)

* add accounts component (#575)

* Test refactor: remove unneeded clientFactoryMock (#581)

* Lattice: Add monitoring badge (#576)

* add monitoring badge

* fix tests

* solve conflict

* Lattice: Add dynamic label for account display name (#579)

* Build: Automatically sync lattice-main with OSS

* Lattice: Point to private prerelease of aws-sdk-go (#515)

* point to private prerelease of aws-sdk-go

* fix build issue

* Lattice: Adding a feature toggle (#549)

* Adding a feature toggle for lattice

* Change name of feature toggle

* Lattice: List accounts (#543)

* Separate layers

* Introduce testify/mock library

Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>

* point to version that includes metric api changes (#574)

* add accounts component (#575)

* Test refactor: remove unneeded clientFactoryMock (#581)

* Lattice: Add monitoring badge (#576)

* add monitoring badge

* fix tests

* solve conflict

* add account label

Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>
Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* fix import

* solve merge related problem

* add account info (#608)

* add back namespaces handler

* Lattice: Parse account id and return it to frontend (#609)

* parse account id and return to frontend

* fix route test

* only show badge when feature toggle is enabled (#615)

* Lattice: Refactor resource response type and return account (#613)

* refactor resource response type

* remove not used file.

* go lint

* fix tests

* remove commented code

* Lattice: Use account as input when listing metric names and dimensions (#611)

* use account in resource requests

* add account to response

* revert accountInfo to accountId

* PR feedback

* unit test account in list metrics response

* remove not used asserts

* don't assert on response that is not relevant to the test

* removed dupe test

* pr feedback

* rename request package (#626)

* Lattice: Move account component and add tooltip (#630)

* move accounts component to the top of metric stat editor

* add tooltip

* CloudWatch: add account to GetMetricData queries (#627)

* Add AccountId to metric stat query

* Lattice: Account variable support  (#625)

* add variable support in accounts component

* add account variable query type

* update variables

* interpolate variable before its sent to backend

* handle variable change in hooks

* remove not used import

* Update public/app/plugins/datasource/cloudwatch/components/Account.tsx

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* Update public/app/plugins/datasource/cloudwatch/hooks.ts

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* add one more unit test

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* cleanup (#629)

* Set account Id according to crossAccountQuerying feature flag in backend (#632)

* CloudWatch: Change spelling of feature-toggle (#634)

* Lattice Logs (#631)

* Lattice Logs

* Fixes after CR

* Lattice: Bug: fix dimension keys request (#644)

* fix dimension keys

* fix lint

* more lint

* CloudWatch: Add tests for QueryData with AccountId (#637)

* Update from breaking change (#645)

* Update from breaking change

* Remove extra interface and methods

Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>

* CloudWatch: Add business logic layer for getting log groups (#642)



Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* Lattice: Fix - unset account id in region change handler (#646)

* move reset of account to region change handler

* fix broken test

* Lattice: Add account id to metric stat query deep link (#656)

add account id to metric stat link

* CloudWatch: Add new log groups handler for cross-account querying (#643)

* Lattice: Add feature tracking (#660)

* add tracking for account id prescense in metrics query

* also check feature toggle

* fix broken test

* CloudWatch: Add route for DescribeLogGroups for cross-account querying (#647)

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>

* Lattice: Handle account id default value (#662)

* make sure right type is returned

* set right default values

* Suggestions to lattice changes (#663)

* Change ListMetricsWithPageLimit response to slice of non-pointers

* Change GetAccountsForCurrentUserOrRole response to be not pointer

* Clean test Cleanup calls in test

* Remove CloudWatchAPI as part of mock

* Resolve conflicts

* Add Latest SDK (#672)

* add tooltip (#674)

* Docs: Add documentation for CloudWatch cross account querying (#676)

* wip docs

* change wordings

* add sections about metrics and logs

* change from monitoring to observability

* Update docs/sources/datasources/aws-cloudwatch/_index.md

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md

Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com>

* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md

Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com>

* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>

* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md

Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com>

* apply pr feedback

* fix file name

* more pr feedback

* pr feedback

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>
Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com>

* use latest version of the aws-sdk-go

* Fix tests' mock response type

* Remove change in Azure Monitor

Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com>
Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>
Co-authored-by: Fiona Artiaga <89225282+GrafanaWriter@users.noreply.github.com>
2022-11-28 12:39:12 +01:00

429 lines
15 KiB
Go

package cloudwatch
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"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"
)
func TestMetricDataQueryBuilder(t *testing.T) {
t.Run("buildMetricDataQuery", func(t *testing.T) {
t.Run("should use metric stat", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
require.Empty(t, mdq.Expression)
assert.Equal(t, query.MetricName, *mdq.MetricStat.Metric.MetricName)
assert.Equal(t, query.Namespace, *mdq.MetricStat.Metric.Namespace)
})
t.Run("should pass AccountId in metric stat query", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
query.AccountId = aws.String("some account id")
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
assert.Equal(t, "some account id", *mdq.AccountId)
})
t.Run("should leave AccountId in metric stat query", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
assert.Nil(t, mdq.AccountId)
})
t.Run("should use custom built expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"="lb1"', '', 300))`, *mdq.Expression)
})
t.Run("should use sql expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeQuery
query.SqlExpression = `SELECT SUM(CPUUTilization) FROM "AWS/EC2"`
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, query.SqlExpression, *mdq.Expression)
})
t.Run("should use user defined math expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeSearch
query.Expression = `SUM(x+y)`
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, query.Expression, *mdq.Expression)
})
t.Run("should set period in user defined expression", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
query.Expression = `SUM([a,b])`
mdq, err := executor.buildMetricDataQuery(logger, query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, int64(300), *mdq.Period)
assert.Equal(t, `SUM([a,b])`, *mdq.Expression)
})
t.Run("should set label when dynamic labels feature toggle is enabled", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels))
query := getBaseQuery()
query.Label = "some label"
mdq, err := executor.buildMetricDataQuery(logger, query)
assert.NoError(t, err)
require.NotNil(t, mdq.Label)
assert.Equal(t, "some label", *mdq.Label)
})
testCases := map[string]struct {
feature *featuremgmt.FeatureManager
label string
}{
"should not set label when dynamic labels feature toggle is disabled": {
feature: featuremgmt.WithFeatures(),
label: "some label",
},
"should not set label for empty string query label": {
feature: featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels),
label: "",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, tc.feature)
query := getBaseQuery()
query.Label = tc.label
mdq, err := executor.buildMetricDataQuery(logger, query)
assert.NoError(t, err)
assert.Nil(t, mdq.Label)
})
}
t.Run(`should not specify accountId when it is "all"`, func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels))
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Statistic: "Average",
Period: 60,
MatchExact: false,
AccountId: aws.String("all"),
}
mdq, err := executor.buildMetricDataQuery(logger, query)
assert.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization"', 'Average', 60))`, *mdq.Expression)
})
t.Run("should set accountId when it is specified", func(t *testing.T) {
executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels))
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Statistic: "Average",
Period: 60,
MatchExact: false,
AccountId: aws.String("12345"),
}
mdq, err := executor.buildMetricDataQuery(logger, query)
assert.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" :aws.AccountId="12345"', 'Average', 60))`, *mdq.Expression)
})
})
t.Run("Query should be matched exact", func(t *testing.T) {
const matchExact = true
t.Run("Query has three dimension values for a given dimension key", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, res)
})
t.Run("Query has three dimension values for two given dimension keys", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "i-456", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "InstanceId"=("i-123" OR "i-456" OR "i-789") "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, res)
})
t.Run("No OR operator was added if a star was used for dimension value", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"*"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.NotContains(t, res, "OR")
})
t.Run("Query has one dimension key with a * value", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"*"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","LoadBalancer"} MetricName="CPUUtilization"', 'Average', 300))`, res)
})
t.Run("Query has three dimension values for two given dimension keys, and one value is a star", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "*", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, res)
})
t.Run("Query has multiple dimensions and an account Id", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "*", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
AccountId: aws.String("some account id"),
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") :aws.AccountId="some account id"', 'Average', 300))`, res)
})
t.Run("Query has a dimension key with a space", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/Kafka",
MetricName: "CpuUser",
Dimensions: map[string][]string{
"Cluster Name": {"dev-cluster"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/Kafka","Cluster Name"} MetricName="CpuUser" "Cluster Name"="dev-cluster"', 'Average', 300))`, res)
})
t.Run("Query has a custom namespace contains spaces", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "Test-API Cache by Minute",
MetricName: "CpuUser",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "*", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"Test-API Cache by Minute","InstanceId","LoadBalancer"} MetricName="CpuUser" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, res)
})
})
t.Run("Query should not be matched exact", func(t *testing.T) {
const matchExact = false
t.Run("Query has three dimension values for a given dimension key", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, res)
})
t.Run("Query has three dimension values for two given dimension keys", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "i-456", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "InstanceId"=("i-123" OR "i-456" OR "i-789") "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, res)
})
t.Run("Query has one dimension key with a * value", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"*"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"', 'Average', 300))`, res)
})
t.Run("query has three dimension values for two given dimension keys, and one value is a star", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "*", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") "InstanceId"', 'Average', 300))`, res)
})
t.Run("query has multiple dimensions and an account Id", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1", "lb2", "lb3"},
"InstanceId": {"i-123", "*", "i-789"},
},
Period: 300,
Expression: "",
MatchExact: matchExact,
AccountId: aws.String("some account id"),
}
res := buildSearchExpression(query, "Average")
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") "InstanceId" :aws.AccountId="some account id"', 'Average', 300))`, res)
})
})
t.Run("Query has invalid characters in dimension values", func(t *testing.T) {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"lb4": {`lb4""`},
},
Period: 300,
Expression: "",
MatchExact: true,
}
res := buildSearchExpression(query, "Average")
assert.Contains(t, res, `lb4\"\"`, "Expected escape double quotes")
})
}
func getBaseQuery() *models.CloudWatchQuery {
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
Dimensions: map[string][]string{
"LoadBalancer": {"lb1"},
},
Period: 300,
Expression: "",
MatchExact: true,
}
return query
}