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:
Kevin Yu
2025-11-06 12:19:39 -08:00
committed by GitHub
parent 2ac4d0a13e
commit 69060f5437
6 changed files with 240 additions and 32 deletions
+32 -3
View File
@@ -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,
+41 -10
View File
@@ -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
}
}
+90 -10
View File
@@ -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)
})