CloudWatch Logs: Limit CloudWatch logs queries to use logGroupIdentifiers only for monitoring accounts (#113137)
* Set the log group name when executing log queries from the frontend * Add helper for a data source instance to check if its a monitoring account * Execute log queries with log group identifiers only for monitoring account queries * fix cloudwatch datasource.ts tests * remove unneeded check
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
@@ -54,9 +55,10 @@ type DataSource struct {
|
||||
ProxyOpts *proxy.Options
|
||||
AWSConfigProvider awsauth.ConfigProvider
|
||||
|
||||
logger log.Logger
|
||||
tagValueCache *cache.Cache
|
||||
resourceHandler backend.CallResourceHandler
|
||||
logger log.Logger
|
||||
tagValueCache *cache.Cache
|
||||
resourceHandler backend.CallResourceHandler
|
||||
monitoringAccountCache sync.Map
|
||||
}
|
||||
|
||||
func (ds *DataSource) newAWSConfig(ctx context.Context, region string) (aws.Config, error) {
|
||||
@@ -273,6 +275,33 @@ func (ds *DataSource) getRGTAClient(ctx context.Context, region string) (resourc
|
||||
return NewRGTAClient(cfg), nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) isMonitoringAccount(ctx context.Context, region string) (bool, error) {
|
||||
if value, ok := ds.monitoringAccountCache.Load(region); ok {
|
||||
cached := value.(bool)
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
client, err := ds.GetAccountsService(ctx, region)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
accounts, err := client.GetAccountsForCurrentUserOrRole(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, account := range accounts {
|
||||
if account.Value.IsMonitoringAccount {
|
||||
ds.monitoringAccountCache.Store(region, true)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
ds.monitoringAccountCache.Store(region, false)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var terminatedStates = []cloudwatchlogstypes.QueryStatus{
|
||||
cloudwatchlogstypes.QueryStatusComplete,
|
||||
cloudwatchlogstypes.QueryStatusCancelled,
|
||||
|
||||
@@ -213,17 +213,48 @@ func (ds *DataSource) executeStartQuery(ctx context.Context, logsClient models.C
|
||||
|
||||
// log group identifiers can be left out if the query is an SQL query
|
||||
if *logsQuery.QueryLanguage != dataquery.LogsQueryLanguageSQL {
|
||||
if len(logsQuery.LogGroups) > 0 && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) {
|
||||
var logGroupIdentifiers []string
|
||||
for _, lg := range logsQuery.LogGroups {
|
||||
arn := lg.Arn
|
||||
// due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error
|
||||
logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*"))
|
||||
useLogGroupIdentifiers := false
|
||||
logGroupsFromQuery := len(logsQuery.LogGroups) > 0
|
||||
if logGroupsFromQuery && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) {
|
||||
region := logsQuery.Region
|
||||
if region == "" || region == defaultRegion {
|
||||
region = ds.Settings.Region
|
||||
}
|
||||
if region != "" {
|
||||
isMonitoringAccount, err := ds.isMonitoringAccount(ctx, region)
|
||||
if err != nil {
|
||||
ds.logger.FromContext(ctx).Debug("failed to determine monitoring account status", "err", err)
|
||||
} else if isMonitoringAccount {
|
||||
// monitoring accounts require querying by log group identifiers because log group names are not unique across accounts.
|
||||
var logGroupIdentifiers []string
|
||||
for _, lg := range logsQuery.LogGroups {
|
||||
// due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error
|
||||
arn := strings.TrimSuffix(lg.Arn, "*")
|
||||
logGroupIdentifiers = append(logGroupIdentifiers, arn)
|
||||
}
|
||||
startQueryInput.LogGroupIdentifiers = logGroupIdentifiers
|
||||
useLogGroupIdentifiers = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !useLogGroupIdentifiers {
|
||||
// even though logsQuery.LogGroupNames is deprecated, we still need to support it for backwards compatibility and alert queries
|
||||
startQueryInput.LogGroupNames = append([]string(nil), logsQuery.LogGroupNames...)
|
||||
if len(startQueryInput.LogGroupNames) == 0 && logGroupsFromQuery {
|
||||
// deduplicate log group names because we only deduplicate log groups by their ARNs instead of their names when the query is created
|
||||
seenLogGroupNames := make(map[string]struct{}, len(logsQuery.LogGroups))
|
||||
for _, lg := range logsQuery.LogGroups {
|
||||
if lg.Name == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seenLogGroupNames[lg.Name]; exists {
|
||||
continue
|
||||
}
|
||||
seenLogGroupNames[lg.Name] = struct{}{}
|
||||
startQueryInput.LogGroupNames = append(startQueryInput.LogGroupNames, lg.Name)
|
||||
}
|
||||
}
|
||||
startQueryInput.LogGroupIdentifiers = logGroupIdentifiers
|
||||
} else {
|
||||
// even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries
|
||||
startQueryInput.LogGroupNames = logsQuery.LogGroupNames
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -445,7 +445,9 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
|
||||
t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
ds := newTestDatasource()
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.monitoringAccountCache.Store("us-east-1", true)
|
||||
})
|
||||
|
||||
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
@@ -459,7 +461,8 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
"limit": 12,
|
||||
"queryLanguage": "CWLI",
|
||||
"queryString":"fields @message",
|
||||
"logGroups":[{"arn": "fakeARN"}]
|
||||
"logGroups":[{"arn": "fakeARN"}],
|
||||
"region": "us-east-1"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
@@ -480,7 +483,9 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
|
||||
t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled and strips out trailing *", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
ds := newTestDatasource()
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.monitoringAccountCache.Store("us-east-1", true)
|
||||
})
|
||||
|
||||
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
@@ -493,7 +498,8 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
"subtype": "StartQuery",
|
||||
"limit": 12,
|
||||
"queryString":"fields @message",
|
||||
"logGroups":[{"arn": "*fake**ARN*"}]
|
||||
"logGroups":[{"arn": "*fake**ARN*"}],
|
||||
"region": "us-east-1"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
@@ -512,6 +518,44 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("queries by LogGroupNames on StartQueryInput when queried region is not a monitoring account region for the data source", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
// note that the query's region is set to us-east-2, but the data source is only a monitoring account in us-east-1 so it should query by LogGroupNames
|
||||
ds.monitoringAccountCache.Store("us-east-1", true)
|
||||
})
|
||||
|
||||
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
|
||||
JSON: json.RawMessage(`{
|
||||
"type": "logAction",
|
||||
"subtype": "StartQuery",
|
||||
"limit": 12,
|
||||
"queryString":"fields @message",
|
||||
"logGroups":[{"arn": "arn:aws:logs:us-east-1:123456789012:log-group:group","name":"/log-group"}],
|
||||
"region": "us-east-2"
|
||||
}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*cloudwatchlogs.StartQueryInput{
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []string{"/log-group"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("uses LogGroupNames if the cross account feature flag is not enabled, and log group names is present", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
ds := newTestDatasource()
|
||||
@@ -545,6 +589,42 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("deduplicates log group names when derived from logGroups", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
ds := newTestDatasource()
|
||||
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)},
|
||||
JSON: json.RawMessage(`{
|
||||
"type": "logAction",
|
||||
"subtype": "StartQuery",
|
||||
"limit": 12,
|
||||
"queryString":"fields @message",
|
||||
"logGroups":[
|
||||
{"arn": "arn:aws:logs:us-east-1:123456789012:log-group:group1","name":"/log-group"},
|
||||
{"arn": "arn:aws:logs:us-east-1:123456789012:log-group:group2","name":"/log-group"}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*cloudwatchlogs.StartQueryInput{
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []string{"/log-group"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("ignores logGroups if feature flag is disabled even if logGroupNames is not present", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
ds := newTestDatasource()
|
||||
@@ -600,12 +680,12 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*cloudwatchlogs.StartQueryInput{
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupIdentifiers: []string{"*fake**ARN"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []string{"/log-group"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user