[v9.3.x] CloudWatch: Cross-account querying support (#59389)
* add cross-account querying support * fix tests
This commit is contained in:
@@ -1,21 +1,55 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/oam"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
)
|
||||
|
||||
type RequestContextFactoryFunc func(pluginCtx backend.PluginContext, region string) (reqCtx RequestContext, err error)
|
||||
|
||||
type RouteHandlerFunc func(pluginCtx backend.PluginContext, reqContextFactory RequestContextFactoryFunc, parameters url.Values) ([]byte, *HttpError)
|
||||
|
||||
type RequestContext struct {
|
||||
MetricsClientProvider MetricsClientProvider
|
||||
LogsAPIProvider CloudWatchLogsAPIProvider
|
||||
OAMClientProvider OAMClientProvider
|
||||
Settings CloudWatchSettings
|
||||
Features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
type ListMetricsProvider interface {
|
||||
GetDimensionKeysByDimensionFilter(resources.DimensionKeysRequest) ([]string, error)
|
||||
GetDimensionKeysByNamespace(string) ([]string, error)
|
||||
GetDimensionValuesByDimensionFilter(resources.DimensionValuesRequest) ([]string, error)
|
||||
GetMetricsByNamespace(namespace string) ([]resources.Metric, error)
|
||||
GetDimensionKeysByDimensionFilter(resources.DimensionKeysRequest) ([]resources.ResourceResponse[string], error)
|
||||
GetDimensionValuesByDimensionFilter(resources.DimensionValuesRequest) ([]resources.ResourceResponse[string], error)
|
||||
GetMetricsByNamespace(r resources.MetricsRequest) ([]resources.ResourceResponse[resources.Metric], error)
|
||||
}
|
||||
|
||||
type MetricsClientProvider interface {
|
||||
ListMetricsWithPageLimit(params *cloudwatch.ListMetricsInput) ([]*cloudwatch.Metric, error)
|
||||
ListMetricsWithPageLimit(params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error)
|
||||
}
|
||||
|
||||
type CloudWatchMetricsAPIProvider interface {
|
||||
ListMetricsPages(*cloudwatch.ListMetricsInput, func(*cloudwatch.ListMetricsOutput, bool) bool) error
|
||||
}
|
||||
|
||||
type CloudWatchLogsAPIProvider interface {
|
||||
DescribeLogGroups(*cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error)
|
||||
}
|
||||
|
||||
type OAMClientProvider interface {
|
||||
ListSinks(*oam.ListSinksInput) (*oam.ListSinksOutput, error)
|
||||
ListAttachedLinks(*oam.ListAttachedLinksInput) (*oam.ListAttachedLinksOutput, error)
|
||||
}
|
||||
|
||||
type LogGroupsProvider interface {
|
||||
GetLogGroups(request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error)
|
||||
}
|
||||
|
||||
type AccountsProvider interface {
|
||||
GetAccountsForCurrentUserOrRole() ([]resources.ResourceResponse[resources.Account], error)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ type CloudWatchQuery struct {
|
||||
TimezoneUTCOffset string
|
||||
MetricQueryType MetricQueryType
|
||||
MetricEditorMode MetricEditorMode
|
||||
AccountId *string
|
||||
}
|
||||
|
||||
func (q *CloudWatchQuery) GetGMDAPIMode(logger log.Logger) GMDApiMode {
|
||||
@@ -95,6 +96,10 @@ func (q *CloudWatchQuery) IsInferredSearchExpression() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if q.AccountId != nil && *q.AccountId == "all" {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(q.Dimensions) == 0 {
|
||||
return !q.MatchExact
|
||||
}
|
||||
@@ -167,6 +172,9 @@ func (q *CloudWatchQuery) BuildDeepLink(startTime time.Time, endTime time.Time,
|
||||
if dynamicLabelEnabled {
|
||||
metricStatMeta.Label = q.Label
|
||||
}
|
||||
if q.AccountId != nil {
|
||||
metricStatMeta.AccountId = *q.AccountId
|
||||
}
|
||||
metricStat = append(metricStat, metricStatMeta)
|
||||
link.Metrics = []interface{}{metricStat}
|
||||
}
|
||||
@@ -214,11 +222,13 @@ type metricsDataQuery struct {
|
||||
QueryType string `json:"type"`
|
||||
Hide *bool `json:"hide"`
|
||||
Alias string `json:"alias"`
|
||||
AccountId *string `json:"accountId"`
|
||||
}
|
||||
|
||||
// ParseMetricDataQueries decodes the metric data queries json, validates, sets default values and returns an array of CloudWatchQueries.
|
||||
// The CloudWatchQuery has a 1 to 1 mapping to a query editor row
|
||||
func ParseMetricDataQueries(dataQueries []backend.DataQuery, startTime time.Time, endTime time.Time, dynamicLabelsEnabled bool) ([]*CloudWatchQuery, error) {
|
||||
func ParseMetricDataQueries(dataQueries []backend.DataQuery, startTime time.Time, endTime time.Time, dynamicLabelsEnabled,
|
||||
crossAccountQueryingEnabled bool) ([]*CloudWatchQuery, error) {
|
||||
var metricDataQueries = make(map[string]metricsDataQuery)
|
||||
for _, query := range dataQueries {
|
||||
var metricsDataQuery metricsDataQuery
|
||||
@@ -250,7 +260,7 @@ func ParseMetricDataQueries(dataQueries []backend.DataQuery, startTime time.Time
|
||||
Expression: mdq.Expression,
|
||||
}
|
||||
|
||||
if err := cwQuery.validateAndSetDefaults(refId, mdq, startTime, endTime); err != nil {
|
||||
if err := cwQuery.validateAndSetDefaults(refId, mdq, startTime, endTime, crossAccountQueryingEnabled); err != nil {
|
||||
return nil, &QueryError{Err: err, RefID: refId}
|
||||
}
|
||||
|
||||
@@ -267,7 +277,8 @@ func (q *CloudWatchQuery) migrateLegacyQuery(query metricsDataQuery, dynamicLabe
|
||||
q.Label = getLabel(query, dynamicLabelsEnabled)
|
||||
}
|
||||
|
||||
func (q *CloudWatchQuery) validateAndSetDefaults(refId string, metricsDataQuery metricsDataQuery, startTime, endTime time.Time) error {
|
||||
func (q *CloudWatchQuery) validateAndSetDefaults(refId string, metricsDataQuery metricsDataQuery, startTime, endTime time.Time,
|
||||
crossAccountQueryingEnabled bool) error {
|
||||
if metricsDataQuery.Statistic == nil && metricsDataQuery.Statistics == nil {
|
||||
return fmt.Errorf("query must have either statistic or statistics field")
|
||||
}
|
||||
@@ -283,6 +294,10 @@ func (q *CloudWatchQuery) validateAndSetDefaults(refId string, metricsDataQuery
|
||||
return fmt.Errorf("failed to parse dimensions: %v", err)
|
||||
}
|
||||
|
||||
if crossAccountQueryingEnabled {
|
||||
q.AccountId = metricsDataQuery.AccountId
|
||||
}
|
||||
|
||||
if metricsDataQuery.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,
|
||||
|
||||
@@ -107,6 +107,53 @@ func TestCloudWatchQuery(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, deepLink, "label")
|
||||
})
|
||||
|
||||
t.Run("includes account id in case its a metric stat query and an account id is set", func(t *testing.T) {
|
||||
startTime := time.Now()
|
||||
endTime := startTime.Add(2 * time.Hour)
|
||||
query := &CloudWatchQuery{
|
||||
RefId: "A",
|
||||
Region: "us-east-1",
|
||||
Expression: "",
|
||||
Statistic: "Average",
|
||||
Period: 300,
|
||||
Id: "id1",
|
||||
MatchExact: true,
|
||||
AccountId: pointer("123456789"),
|
||||
Label: "${PROP('Namespace')}",
|
||||
Dimensions: map[string][]string{
|
||||
"InstanceId": {"i-12345678"},
|
||||
},
|
||||
MetricQueryType: MetricQueryTypeSearch,
|
||||
MetricEditorMode: MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
deepLink, err := query.BuildDeepLink(startTime, endTime, false)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, deepLink, "accountId%22%3A%22123456789")
|
||||
})
|
||||
|
||||
t.Run("does not include account id in case its not a metric stat query", func(t *testing.T) {
|
||||
startTime := time.Now()
|
||||
endTime := startTime.Add(2 * time.Hour)
|
||||
query := &CloudWatchQuery{
|
||||
RefId: "A",
|
||||
Region: "us-east-1",
|
||||
Statistic: "Average",
|
||||
Expression: "SEARCH(someexpression)",
|
||||
AccountId: pointer("123456789"),
|
||||
Period: 300,
|
||||
Id: "id1",
|
||||
MatchExact: true,
|
||||
Label: "${PROP('Namespace')}",
|
||||
MetricQueryType: MetricQueryTypeSearch,
|
||||
MetricEditorMode: MetricEditorModeRaw,
|
||||
}
|
||||
|
||||
deepLink, err := query.BuildDeepLink(startTime, endTime, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, deepLink, "accountId%22%3A%22123456789")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("SEARCH(someexpression) was specified in the query editor", func(t *testing.T) {
|
||||
@@ -269,7 +316,7 @@ func TestRequestParser(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
migratedQueries, err := ParseMetricDataQueries(oldQuery, time.Now(), time.Now(), false)
|
||||
migratedQueries, err := ParseMetricDataQueries(oldQuery, time.Now(), time.Now(), false, false)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, migratedQueries, 1)
|
||||
require.NotNil(t, migratedQueries[0])
|
||||
@@ -300,7 +347,7 @@ func TestRequestParser(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
results, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
results, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
res := results[0]
|
||||
@@ -343,7 +390,7 @@ func TestRequestParser(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
results, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
results, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
res := results[0]
|
||||
@@ -376,7 +423,7 @@ func TestRequestParser(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
_, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, `error parsing query "", failed to parse dimensions: unknown type as dimension value`, err.Error())
|
||||
@@ -405,7 +452,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -437,7 +484,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.Local().Add(time.Minute * time.Duration(5))
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 60, res[0].Period)
|
||||
@@ -447,7 +494,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, 0, -1)
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 60, res[0].Period)
|
||||
@@ -456,7 +503,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
t.Run("Time range is 2 days", func(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, 0, -2)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 300, res[0].Period)
|
||||
@@ -466,7 +513,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, 0, -7)
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 900, res[0].Period)
|
||||
@@ -476,7 +523,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, 0, -30)
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 3600, res[0].Period)
|
||||
@@ -486,7 +533,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, 0, -90)
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 21600, res[0].Period)
|
||||
@@ -496,7 +543,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(-1, 0, 0)
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 21600, res[0].Period)
|
||||
@@ -506,7 +553,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
to := time.Now()
|
||||
from := to.AddDate(-2, 0, 0)
|
||||
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 86400, res[0].Period)
|
||||
@@ -515,7 +562,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
t.Run("Time range is 2 days, but 16 days ago", func(t *testing.T) {
|
||||
to := time.Now().AddDate(0, 0, -14)
|
||||
from := to.AddDate(0, 0, -2)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 300, res[0].Period)
|
||||
@@ -524,7 +571,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
t.Run("Time range is 2 days, but 90 days ago", func(t *testing.T) {
|
||||
to := time.Now().AddDate(0, 0, -88)
|
||||
from := to.AddDate(0, 0, -2)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 3600, res[0].Period)
|
||||
@@ -533,7 +580,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
t.Run("Time range is 2 days, but 456 days ago", func(t *testing.T) {
|
||||
to := time.Now().AddDate(0, 0, -454)
|
||||
from := to.AddDate(0, 0, -2)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false)
|
||||
res, err := ParseMetricDataQueries(query, from, to, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
assert.Equal(t, 21600, res[0].Period)
|
||||
@@ -548,7 +595,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
_, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
_, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, `error parsing query "", failed to parse period as duration: time: invalid duration "invalid"`, err.Error())
|
||||
})
|
||||
@@ -563,7 +610,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, res, 1)
|
||||
@@ -620,6 +667,18 @@ func Test_ParseMetricDataQueries_query_type_and_metric_editor_mode_and_GMD_query
|
||||
expectedMetricEditorMode: dummyTestEditorMode,
|
||||
expectedGMDApiMode: GMDApiModeMetricStat,
|
||||
},
|
||||
"no dimensions, matchExact is false": {
|
||||
extraDataQueryJson: `"matchExact":false,`,
|
||||
expectedMetricQueryType: MetricQueryTypeSearch,
|
||||
expectedMetricEditorMode: MetricEditorModeBuilder,
|
||||
expectedGMDApiMode: GMDApiModeInferredSearchExpression,
|
||||
},
|
||||
"query metricQueryType": {
|
||||
extraDataQueryJson: `"metricQueryType":1,`,
|
||||
expectedMetricQueryType: MetricQueryTypeQuery,
|
||||
expectedMetricEditorMode: MetricEditorModeBuilder,
|
||||
expectedGMDApiMode: GMDApiModeSQLExpression,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
@@ -638,7 +697,7 @@ func Test_ParseMetricDataQueries_query_type_and_metric_editor_mode_and_GMD_query
|
||||
),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -664,7 +723,7 @@ func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -685,7 +744,7 @@ func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -706,7 +765,7 @@ func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -725,7 +784,7 @@ func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -746,7 +805,7 @@ func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -767,7 +826,7 @@ func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -790,7 +849,7 @@ func Test_ParseMetricDataQueries_ID(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -811,7 +870,7 @@ func Test_ParseMetricDataQueries_ID(t *testing.T) {
|
||||
}`),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false)
|
||||
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -838,7 +897,7 @@ func Test_ParseMetricDataQueries_sets_label_when_label_is_present_in_json_query(
|
||||
},
|
||||
}
|
||||
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), true)
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), true, false)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.NotNil(t, res[0])
|
||||
@@ -902,7 +961,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), true)
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), true, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, res, 1)
|
||||
@@ -949,7 +1008,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), true)
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), true, false)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, res, 2)
|
||||
|
||||
@@ -1019,7 +1078,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
|
||||
}`, tc.labelJson)),
|
||||
},
|
||||
}
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), tc.dynamicLabelsFeatureToggleEnabled)
|
||||
res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), tc.dynamicLabelsFeatureToggleEnabled, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, res, 1)
|
||||
@@ -1046,7 +1105,7 @@ func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchE
|
||||
{
|
||||
JSON: []byte("{}"),
|
||||
},
|
||||
}, time.Now(), time.Now(), false)
|
||||
}, time.Now(), time.Now(), false, false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, `error parsing query "", query must have either statistic or statistics field`, err.Error())
|
||||
|
||||
@@ -1059,7 +1118,7 @@ func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchE
|
||||
{
|
||||
JSON: []byte(`{"type":"some other type", "statistic":"Average", "matchExact":false}`),
|
||||
},
|
||||
}, time.Now(), time.Now(), false)
|
||||
}, time.Now(), time.Now(), false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Empty(t, actual)
|
||||
@@ -1071,7 +1130,7 @@ func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchE
|
||||
{
|
||||
JSON: []byte(`{"statistic":"Average"}`),
|
||||
},
|
||||
}, time.Now(), time.Now(), false)
|
||||
}, time.Now(), time.Now(), false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, actual)
|
||||
@@ -1083,7 +1142,7 @@ func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchE
|
||||
{
|
||||
JSON: []byte(`{"statistic":"Average"}`),
|
||||
},
|
||||
}, time.Now(), time.Now(), false)
|
||||
}, time.Now(), time.Now(), false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, actual, 1)
|
||||
@@ -1097,7 +1156,7 @@ func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchE
|
||||
{
|
||||
JSON: []byte(`{"statistic":"Average","matchExact":false}`),
|
||||
},
|
||||
}, time.Now(), time.Now(), false)
|
||||
}, time.Now(), time.Now(), false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, actual, 1)
|
||||
@@ -1105,3 +1164,36 @@ func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchE
|
||||
assert.False(t, actual[0].MatchExact)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ParseMetricDataQueries_account_Id(t *testing.T) {
|
||||
t.Run("account is set when cross account querying enabled", func(t *testing.T) {
|
||||
actual, err := ParseMetricDataQueries(
|
||||
[]backend.DataQuery{
|
||||
{
|
||||
JSON: []byte(`{"accountId":"some account id", "statistic":"Average"}`),
|
||||
},
|
||||
}, time.Now(), time.Now(), false, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, actual, 1)
|
||||
require.NotNil(t, actual[0])
|
||||
require.NotNil(t, actual[0].AccountId)
|
||||
assert.Equal(t, "some account id", *actual[0].AccountId)
|
||||
})
|
||||
|
||||
t.Run("account is not set when cross account querying disabled", func(t *testing.T) {
|
||||
actual, err := ParseMetricDataQueries(
|
||||
[]backend.DataQuery{
|
||||
{
|
||||
JSON: []byte(`{"accountId":"some account id", "statistic":"Average"}`),
|
||||
},
|
||||
}, time.Now(), time.Now(), false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, actual, 1)
|
||||
require.NotNil(t, actual[0])
|
||||
assert.Nil(t, actual[0].AccountId)
|
||||
})
|
||||
}
|
||||
|
||||
func pointer[T any](arg T) *T { return &arg }
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const defaultLogGroupLimit = int64(50)
|
||||
|
||||
type LogGroupsRequest struct {
|
||||
ResourceRequest
|
||||
Limit int64
|
||||
LogGroupNamePrefix, LogGroupNamePattern *string
|
||||
}
|
||||
|
||||
func (r LogGroupsRequest) IsTargetingAllAccounts() bool {
|
||||
return *r.AccountId == "all"
|
||||
}
|
||||
|
||||
func ParseLogGroupsRequest(parameters url.Values) (LogGroupsRequest, error) {
|
||||
logGroupNamePrefix := setIfNotEmptyString(parameters.Get("logGroupNamePrefix"))
|
||||
logGroupPattern := setIfNotEmptyString(parameters.Get("logGroupPattern"))
|
||||
if logGroupNamePrefix != nil && logGroupPattern != nil {
|
||||
return LogGroupsRequest{}, fmt.Errorf("cannot set both log group name prefix and pattern")
|
||||
}
|
||||
|
||||
return LogGroupsRequest{
|
||||
Limit: getLimit(parameters.Get("limit")),
|
||||
ResourceRequest: ResourceRequest{
|
||||
Region: parameters.Get("region"),
|
||||
AccountId: setIfNotEmptyString(parameters.Get("accountId")),
|
||||
},
|
||||
LogGroupNamePrefix: logGroupNamePrefix,
|
||||
LogGroupNamePattern: logGroupPattern,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setIfNotEmptyString(paramValue string) *string {
|
||||
if paramValue == "" {
|
||||
return nil
|
||||
}
|
||||
return ¶mValue
|
||||
}
|
||||
|
||||
func getLimit(limit string) int64 {
|
||||
logGroupLimit := defaultLogGroupLimit
|
||||
intLimit, err := strconv.ParseInt(limit, 10, 64)
|
||||
if err == nil && intLimit > 0 {
|
||||
logGroupLimit = intLimit
|
||||
}
|
||||
return logGroupLimit
|
||||
}
|
||||
@@ -17,13 +17,13 @@ type MetricsRequest struct {
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func GetMetricsRequest(parameters url.Values) (*MetricsRequest, error) {
|
||||
func GetMetricsRequest(parameters url.Values) (MetricsRequest, error) {
|
||||
resourceRequest, err := getResourceRequest(parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return MetricsRequest{}, err
|
||||
}
|
||||
|
||||
return &MetricsRequest{
|
||||
return MetricsRequest{
|
||||
ResourceRequest: resourceRequest,
|
||||
Namespace: parameters.Get("namespace"),
|
||||
}, nil
|
||||
|
||||
@@ -5,8 +5,15 @@ import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const useLinkedAccountsId = "all"
|
||||
|
||||
type ResourceRequest struct {
|
||||
Region string
|
||||
Region string
|
||||
AccountId *string
|
||||
}
|
||||
|
||||
func (r *ResourceRequest) ShouldTargetAllAccounts() bool {
|
||||
return r.AccountId != nil && *r.AccountId == useLinkedAccountsId
|
||||
}
|
||||
|
||||
func getResourceRequest(parameters url.Values) (*ResourceRequest, error) {
|
||||
@@ -14,9 +21,24 @@ func getResourceRequest(parameters url.Values) (*ResourceRequest, error) {
|
||||
Region: parameters.Get("region"),
|
||||
}
|
||||
|
||||
accountId := parameters.Get("accountId")
|
||||
if accountId != "" {
|
||||
request.AccountId = &accountId
|
||||
}
|
||||
|
||||
if request.Region == "" {
|
||||
return nil, fmt.Errorf("region is required")
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
type LogsRequest struct {
|
||||
Limit int64
|
||||
AccountId, LogGroupNamePrefix, LogGroupNamePattern *string
|
||||
IsCrossAccountQueryingEnabled bool
|
||||
}
|
||||
|
||||
func (r LogsRequest) IsTargetingAllAccounts() bool {
|
||||
return *r.AccountId == useLinkedAccountsId
|
||||
}
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
package resources
|
||||
|
||||
import "github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
|
||||
type Dimension struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type ResourceResponse[T any] struct {
|
||||
AccountId *string `json:"accountId,omitempty"`
|
||||
Value T `json:"value"`
|
||||
}
|
||||
|
||||
type MetricResponse struct {
|
||||
*cloudwatch.Metric
|
||||
AccountId *string `json:"accountId,omitempty"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Id string `json:"id"`
|
||||
Arn string `json:"arn"`
|
||||
Label string `json:"label"`
|
||||
IsMonitoringAccount bool `json:"isMonitoringAccount"`
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type LogGroup struct {
|
||||
Arn string `json:"arn"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
)
|
||||
|
||||
type RequestContext struct {
|
||||
MetricsClientProvider MetricsClientProvider
|
||||
Settings CloudWatchSettings
|
||||
}
|
||||
|
||||
type RequestContextFactoryFunc func(pluginCtx backend.PluginContext, region string) (reqCtx RequestContext, err error)
|
||||
|
||||
type RouteHandlerFunc func(pluginCtx backend.PluginContext, reqContextFactory RequestContextFactoryFunc, parameters url.Values) ([]byte, *HttpError)
|
||||
|
||||
type cloudWatchLink struct {
|
||||
View string `json:"view"`
|
||||
Stacked bool `json:"stacked"`
|
||||
@@ -31,7 +16,8 @@ type metricExpression struct {
|
||||
}
|
||||
|
||||
type metricStatMeta struct {
|
||||
Stat string `json:"stat"`
|
||||
Period int `json:"period"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Stat string `json:"stat"`
|
||||
Period int `json:"period"`
|
||||
Label string `json:"label,omitempty"`
|
||||
AccountId string `json:"accountId,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user