diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go index 59f1640f842..a8e8d6eebee 100644 --- a/pkg/tsdb/cloudwatch/log_actions.go +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" ) const ( @@ -35,27 +36,6 @@ type AWSError struct { Payload map[string]string } -type LogQueryJson struct { - LogType string `json:"type"` - SubType string - Limit *int64 - Time int64 - StartTime *int64 - EndTime *int64 - LogGroupName string - LogGroupNames []string - LogGroups []suggestData - LogGroupNamePrefix string - LogStreamName string - StartFromHead bool - Region string - QueryString string - QueryId string - StatsGroups []string - Subtype string - Expression string -} - func (e *AWSError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } @@ -67,15 +47,15 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, logger log.L eg, ectx := errgroup.WithContext(ctx) for _, query := range req.Queries { - var model LogQueryJson - err := json.Unmarshal(query.JSON, &model) + var logsQuery models.LogsQuery + err := json.Unmarshal(query.JSON, &logsQuery) if err != nil { return nil, err } query := query eg.Go(func() error { - dataframe, err := e.executeLogAction(ectx, logger, model, query, req.PluginContext) + dataframe, err := e.executeLogAction(ectx, logger, logsQuery, query, req.PluginContext) if err != nil { var AWSError *AWSError if errors.As(err, &AWSError) { @@ -87,7 +67,7 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, logger log.L return err } - groupedFrames, err := groupResponseFrame(dataframe, model.StatsGroups) + groupedFrames, err := groupResponseFrame(dataframe, logsQuery.StatsGroups) if err != nil { return err } @@ -115,15 +95,15 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, logger log.L return resp, nil } -func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, logger log.Logger, model LogQueryJson, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) { +func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, logger log.Logger, logsQuery models.LogsQuery, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) { instance, err := e.getInstance(pluginCtx) if err != nil { return nil, err } region := instance.Settings.Region - if model.Region != "" { - region = model.Region + if logsQuery.Region != "" { + region = logsQuery.Region } logsClient, err := e.getCWLogsClient(pluginCtx, region) @@ -132,53 +112,53 @@ func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, logger log.Lo } var data *data.Frame = nil - switch model.SubType { + switch logsQuery.SubType { case "GetLogGroupFields": - data, err = e.handleGetLogGroupFields(ctx, logsClient, model, query.RefID) + data, err = e.handleGetLogGroupFields(ctx, logsClient, logsQuery, query.RefID) case "StartQuery": - data, err = e.handleStartQuery(ctx, logger, logsClient, model, query.TimeRange, query.RefID) + data, err = e.handleStartQuery(ctx, logger, logsClient, logsQuery, query.TimeRange, query.RefID) case "StopQuery": - data, err = e.handleStopQuery(ctx, logsClient, model) + data, err = e.handleStopQuery(ctx, logsClient, logsQuery) case "GetQueryResults": - data, err = e.handleGetQueryResults(ctx, logsClient, model, query.RefID) + data, err = e.handleGetQueryResults(ctx, logsClient, logsQuery, query.RefID) case "GetLogEvents": - data, err = e.handleGetLogEvents(ctx, logsClient, model) + data, err = e.handleGetLogEvents(ctx, logsClient, logsQuery) } if err != nil { - return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", model.SubType, err) + return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", logsQuery.SubType, err) } return data, nil } func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson) (*data.Frame, error) { + logsQuery models.LogsQuery) (*data.Frame, error) { limit := defaultEventLimit - if parameters.Limit != nil && *parameters.Limit > 0 { - limit = *parameters.Limit + if logsQuery.Limit != nil && *logsQuery.Limit > 0 { + limit = *logsQuery.Limit } queryRequest := &cloudwatchlogs.GetLogEventsInput{ Limit: aws.Int64(limit), - StartFromHead: aws.Bool(parameters.StartFromHead), + StartFromHead: aws.Bool(logsQuery.StartFromHead), } - if parameters.LogGroupName == "" { + if logsQuery.LogGroupName == "" { return nil, fmt.Errorf("Error: Parameter 'logGroupName' is required") } - queryRequest.SetLogGroupName(parameters.LogGroupName) + queryRequest.SetLogGroupName(logsQuery.LogGroupName) - if parameters.LogStreamName == "" { + if logsQuery.LogStreamName == "" { return nil, fmt.Errorf("Error: Parameter 'logStreamName' is required") } - queryRequest.SetLogStreamName(parameters.LogStreamName) + queryRequest.SetLogStreamName(logsQuery.LogStreamName) - if parameters.StartTime != nil && *parameters.StartTime != 0 { - queryRequest.SetStartTime(*parameters.StartTime) + if logsQuery.StartTime != nil && *logsQuery.StartTime != 0 { + queryRequest.SetStartTime(*logsQuery.StartTime) } - if parameters.EndTime != nil && *parameters.EndTime != 0 { - queryRequest.SetEndTime(*parameters.EndTime) + if logsQuery.EndTime != nil && *logsQuery.EndTime != 0 { + queryRequest.SetEndTime(*logsQuery.EndTime) } logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest) @@ -207,7 +187,7 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient } func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) { + logsQuery models.LogsQuery, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) { startTime := timeRange.From endTime := timeRange.To @@ -220,7 +200,7 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c // The usage of ltrim around the @log/@logStream fields is a necessary workaround, as without it, // CloudWatch wouldn't consider a query using a non-alised @log/@logStream valid. modifiedQueryString := "fields @timestamp,ltrim(@log) as " + logIdentifierInternal + ",ltrim(@logStream) as " + - logStreamIdentifierInternal + "|" + parameters.QueryString + logStreamIdentifierInternal + "|" + logsQuery.QueryString startQueryInput := &cloudwatchlogs.StartQueryInput{ StartTime: aws.Int64(startTime.Unix()), @@ -234,10 +214,10 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c } if e.features.IsEnabled(featuremgmt.FlagCloudWatchCrossAccountQuerying) { - if parameters.LogGroups != nil && len(parameters.LogGroups) > 0 { + if logsQuery.LogGroups != nil && len(logsQuery.LogGroups) > 0 { var logGroupIdentifiers []string - for _, lg := range parameters.LogGroups { - arn := lg.Value + 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, "*")) } @@ -246,11 +226,11 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c } if startQueryInput.LogGroupIdentifiers == nil { - startQueryInput.LogGroupNames = aws.StringSlice(parameters.LogGroupNames) + startQueryInput.LogGroupNames = aws.StringSlice(logsQuery.LogGroupNames) } - if parameters.Limit != nil { - startQueryInput.Limit = aws.Int64(*parameters.Limit) + if logsQuery.Limit != nil { + startQueryInput.Limit = aws.Int64(*logsQuery.Limit) } logger.Debug("calling startquery with context with input", "input", startQueryInput) @@ -258,8 +238,8 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c } func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logger log.Logger, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - model LogQueryJson, timeRange backend.TimeRange, refID string) (*data.Frame, error) { - startQueryResponse, err := e.executeStartQuery(ctx, logsClient, model, timeRange) + logsQuery models.LogsQuery, timeRange backend.TimeRange, refID string) (*data.Frame, error) { + startQueryResponse, err := e.executeStartQuery(ctx, logsClient, logsQuery, timeRange) if err != nil { var awsErr awserr.Error if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" { @@ -273,8 +253,8 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logger log.Lo dataFrame.RefID = refID region := "default" - if model.Region != "" { - region = model.Region + if logsQuery.Region != "" { + region = logsQuery.Region } dataFrame.Meta = &data.FrameMeta{ @@ -287,9 +267,9 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logger log.Lo } func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson) (*cloudwatchlogs.StopQueryOutput, error) { + logsQuery models.LogsQuery) (*cloudwatchlogs.StopQueryOutput, error) { queryInput := &cloudwatchlogs.StopQueryInput{ - QueryId: aws.String(parameters.QueryId), + QueryId: aws.String(logsQuery.QueryId), } response, err := logsClient.StopQueryWithContext(ctx, queryInput) @@ -308,8 +288,8 @@ func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cl } func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson) (*data.Frame, error) { - response, err := e.executeStopQuery(ctx, logsClient, parameters) + logsQuery models.LogsQuery) (*data.Frame, error) { + response, err := e.executeStopQuery(ctx, logsClient, logsQuery) if err != nil { return nil, err } @@ -319,17 +299,17 @@ func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient clo } func (e *cloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson) (*cloudwatchlogs.GetQueryResultsOutput, error) { + logsQuery models.LogsQuery) (*cloudwatchlogs.GetQueryResultsOutput, error) { queryInput := &cloudwatchlogs.GetQueryResultsInput{ - QueryId: aws.String(parameters.QueryId), + QueryId: aws.String(logsQuery.QueryId), } return logsClient.GetQueryResultsWithContext(ctx, queryInput) } func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson, refID string) (*data.Frame, error) { - getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, parameters) + logsQuery models.LogsQuery, refID string) (*data.Frame, error) { + getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, logsQuery) if err != nil { return nil, err } @@ -346,10 +326,10 @@ func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClie } func (e *cloudWatchExecutor) handleGetLogGroupFields(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - parameters LogQueryJson, refID string) (*data.Frame, error) { + logsQuery models.LogsQuery, refID string) (*data.Frame, error) { queryInput := &cloudwatchlogs.GetLogGroupFieldsInput{ - LogGroupName: aws.String(parameters.LogGroupName), - Time: aws.Int64(parameters.Time), + LogGroupName: aws.String(logsQuery.LogGroupName), + Time: aws.Int64(logsQuery.Time), } getLogGroupFieldsOutput, err := logsClient.GetLogGroupFieldsWithContext(ctx, queryInput) diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index 97eadf80651..900e088ef52 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -410,7 +410,7 @@ func Test_executeStartQuery(t *testing.T) { "subtype": "StartQuery", "limit": 12, "queryString":"fields @message", - "logGroups":[{"value": "fakeARN"}] + "logGroups":[{"arn": "fakeARN"}] }`), }, }, @@ -446,7 +446,7 @@ func Test_executeStartQuery(t *testing.T) { "subtype": "StartQuery", "limit": 12, "queryString":"fields @message", - "logGroups":[{"value": "*fake**ARN*"}] + "logGroups":[{"arn": "*fake**ARN*"}] }`), }, }, diff --git a/pkg/tsdb/cloudwatch/log_alert.go b/pkg/tsdb/cloudwatch/log_alert.go index 4275c587983..3b8ddc8cf39 100644 --- a/pkg/tsdb/cloudwatch/log_alert.go +++ b/pkg/tsdb/cloudwatch/log_alert.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" ) const ( @@ -21,22 +22,22 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back resp := backend.NewQueryDataResponse() for _, q := range req.Queries { - var model LogQueryJson - err := json.Unmarshal(q.JSON, &model) + var logsQuery models.LogsQuery + err := json.Unmarshal(q.JSON, &logsQuery) if err != nil { continue } - model.Subtype = "StartQuery" - model.QueryString = model.Expression + logsQuery.Subtype = "StartQuery" + logsQuery.QueryString = logsQuery.Expression - region := model.Region - if model.Region == "" || region == defaultRegion { + region := logsQuery.Region + if logsQuery.Region == "" || region == defaultRegion { instance, err := e.getInstance(req.PluginContext) if err != nil { return nil, err } - model.Region = instance.Settings.Region + logsQuery.Region = instance.Settings.Region } logsClient, err := e.getCWLogsClient(req.PluginContext, region) @@ -44,7 +45,7 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back return nil, err } - getQueryResultsOutput, err := e.alertQuery(ctx, logsClient, q, model) + getQueryResultsOutput, err := e.alertQuery(ctx, logsClient, q, logsQuery) if err != nil { return nil, err } @@ -55,8 +56,8 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back } var frames []*data.Frame - if len(model.StatsGroups) > 0 && len(dataframe.Fields) > 0 { - frames, err = groupResults(dataframe, model.StatsGroups) + if len(logsQuery.StatsGroups) > 0 && len(dataframe.Fields) > 0 { + frames, err = groupResults(dataframe, logsQuery.StatsGroups) if err != nil { return nil, err } @@ -73,14 +74,14 @@ func (e *cloudWatchExecutor) executeLogAlertQuery(ctx context.Context, req *back } func (e *cloudWatchExecutor) alertQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, - queryContext backend.DataQuery, model LogQueryJson) (*cloudwatchlogs.GetQueryResultsOutput, error) { - startQueryOutput, err := e.executeStartQuery(ctx, logsClient, model, queryContext.TimeRange) + queryContext backend.DataQuery, logsQuery models.LogsQuery) (*cloudwatchlogs.GetQueryResultsOutput, error) { + startQueryOutput, err := e.executeStartQuery(ctx, logsClient, logsQuery, queryContext.TimeRange) if err != nil { return nil, err } - requestParams := LogQueryJson{ - Region: model.Region, + requestParams := models.LogsQuery{ + Region: logsQuery.Region, QueryId: *startQueryOutput.QueryId, } diff --git a/pkg/tsdb/cloudwatch/models/logs_query.go b/pkg/tsdb/cloudwatch/models/logs_query.go new file mode 100644 index 00000000000..14406e9baeb --- /dev/null +++ b/pkg/tsdb/cloudwatch/models/logs_query.go @@ -0,0 +1,28 @@ +package models + +type LogGroup struct { + ARN string `json:"arn"` + Name string `json:"name"` + AccountID string `json:"accountId"` +} + +type LogsQuery struct { + LogType string `json:"type"` + SubType string + Limit *int64 + Time int64 + StartTime *int64 + EndTime *int64 + LogGroupName string + LogGroupNames []string + LogGroups []LogGroup `json:"logGroups"` + LogGroupNamePrefix string + LogStreamName string + StartFromHead bool + Region string + QueryString string + QueryId string + StatsGroups []string + Subtype string + Expression string +} diff --git a/public/app/plugins/datasource/cloudwatch/api.ts b/public/app/plugins/datasource/cloudwatch/api.ts index 1edf5b49bad..6e1049b49aa 100644 --- a/public/app/plugins/datasource/cloudwatch/api.ts +++ b/public/app/plugins/datasource/cloudwatch/api.ts @@ -69,18 +69,14 @@ export class CloudWatchAPI extends CloudWatchRequest { }); } - async describeCrossAccountLogGroups(params: DescribeLogGroupsRequest): Promise { + async describeCrossAccountLogGroups( + params: DescribeLogGroupsRequest + ): Promise>> { return this.memoizedGetRequest>>('describe-log-groups', { ...params, region: this.templateSrv.replace(this.getActualRegion(params.region)), accountId: this.templateSrv.replace(params.accountId), - }).then((resourceResponse) => - resourceResponse.map((resource) => ({ - label: resource.value.name, - value: resource.value.arn, - text: resource.accountId || '', - })) - ); + }); } async describeAllLogGroups(params: DescribeLogGroupsRequest) { diff --git a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.test.tsx b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.test.tsx index 4ea04cf0ea7..27266cd26df 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.test.tsx @@ -51,8 +51,8 @@ describe('CloudWatchLink', () => { ...validLogsQuery, logGroupNames: undefined, logGroups: [ - { value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test1', text: '/aws/lambda/test1' }, - { value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test2', text: '/aws/lambda/test2' }, + { arn: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test1', name: '/aws/lambda/test1' }, + { arn: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test2', name: '/aws/lambda/test2' }, ], }; diff --git a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx index e8e4e5c8929..e535aa91d38 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx @@ -21,8 +21,8 @@ export function CloudWatchLink({ panelData, query, datasource }: Props) { useEffect(() => { if (prevPanelData !== panelData && panelData?.request?.range) { const arns = (query.logGroups ?? []) - .filter((group) => group?.value) - .map((group) => (group.value ?? '').replace(/:\*$/, '')); // remove `:*` from end of arn + .filter((group) => group?.arn) + .map((group) => (group.arn ?? '').replace(/:\*$/, '')); // remove `:*` from end of arn const logGroupNames = query.logGroupNames; let sources = arns?.length ? arns : logGroupNames; diff --git a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx index f3eda22db87..bb393f64682 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.test.tsx @@ -5,6 +5,8 @@ import lodash from 'lodash'; import React from 'react'; import selectEvent from 'react-select-event'; +import { ResourceResponse, LogGroupResponse } from '../types'; + import { CrossAccountLogsQueryField } from './CrossAccountLogsQueryField'; const defaultProps = { @@ -24,16 +26,20 @@ const defaultProps = { fetchLogGroups: () => Promise.resolve([ { - label: 'logGroup1', - text: 'logGroup1', - value: 'arn:partition:service:region:account-id123:loggroup:someloggroup', + accountId: '123', + value: { + name: 'logGroup1', + arn: 'arn:partition:service:region:account-id123:loggroup:someloggroup', + }, }, { - label: 'logGroup2', - text: 'logGroup2', - value: 'arn:partition:service:region:account-id456:loggroup:someotherloggroup', + accountId: '456', + value: { + name: 'logGroup2', + arn: 'arn:partition:service:region:account-id456:loggroup:someotherloggroup', + }, }, - ]), + ] as Array>), onChange: jest.fn(), }; @@ -155,9 +161,10 @@ describe('CrossAccountLogsQueryField', () => { await userEvent.click(screen.getByText('Add log groups')); expect(onChange).toHaveBeenCalledWith([ { - label: 'logGroup2', - text: 'logGroup2', - value: 'arn:partition:service:region:account-id456:loggroup:someotherloggroup', + name: 'logGroup2', + arn: 'arn:partition:service:region:account-id456:loggroup:someotherloggroup', + accountId: '456', + accountLabel: undefined, }, ]); }); @@ -171,9 +178,10 @@ describe('CrossAccountLogsQueryField', () => { await userEvent.click(screen.getByText('Cancel')); expect(onChange).not.toHaveBeenCalledWith([ { - label: 'logGroup2', - text: 'logGroup2', - value: 'arn:partition:service:region:account-id456:loggroup:someotherloggroup', + name: 'logGroup2', + arn: 'arn:partition:service:region:account-id456:loggroup:someotherloggroup', + accountId: '456', + accountLabel: undefined, }, ]); }); @@ -198,9 +206,10 @@ describe('CrossAccountLogsQueryField', () => { const fetchLogGroups = jest.fn(async () => { await Promise.all([defer.promise]); return Array(50).map((i) => ({ - value: `logGroup${i}`, - text: `logGroup${i}`, - label: `logGroup${i}`, + value: { + arn: `logGroup${i}`, + name: `logGroup${i}`, + }, })); }); render(); diff --git a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx index 59e156b3a01..83ef392e84b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CrossAccountLogsQueryField.tsx @@ -5,23 +5,22 @@ import { EditorField, Space } from '@grafana/experimental'; import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui'; import Search from '../Search'; -import { SelectableResourceValue } from '../api'; -import { DescribeLogGroupsRequest } from '../types'; +import { DescribeLogGroupsRequest, LogGroup, LogGroupResponse, ResourceResponse } from '../types'; import { Account, ALL_ACCOUNTS_OPTION } from './Account'; import { SelectedLogsGroups } from './SelectedLogsGroups'; import getStyles from './styles'; type CrossAccountLogsQueryProps = { - selectedLogGroups: SelectableResourceValue[]; + selectedLogGroups: LogGroup[]; accountOptions: Array>; - fetchLogGroups: (params: Partial) => Promise; - onChange: (selectedLogGroups: SelectableResourceValue[]) => void; + fetchLogGroups: (params: Partial) => Promise>>; + onChange: (selectedLogGroups: LogGroup[]) => void; }; export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => { const [isModalOpen, setIsModalOpen] = useState(false); - const [selectableLogGroups, setSelectableLogGroups] = useState([]); + const [selectableLogGroups, setSelectableLogGroups] = useState([]); const [selectedLogGroups, setSelectedLogGroups] = useState(props.selectedLogGroups); const [searchPhrase, setSearchPhrase] = useState(''); const [searchAccountId, setSearchAccountId] = useState(ALL_ACCOUNTS_OPTION.value); @@ -36,6 +35,16 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => } }; + const accountNameById = useMemo(() => { + const idsToNames: Record = {}; + props.accountOptions.forEach((a) => { + if (a.value && a.label) { + idsToNames[a.value] = a.label; + } + }); + return idsToNames; + }, [props.accountOptions]); + const searchFn = async (searchTerm?: string, accountId?: string) => { setIsLoading(true); try { @@ -43,18 +52,25 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => logGroupPattern: searchTerm, accountId: accountId, }); - setSelectableLogGroups(possibleLogGroups); + setSelectableLogGroups( + possibleLogGroups.map((lg) => ({ + arn: lg.value.arn, + name: lg.value.name, + accountId: lg.accountId, + accountLabel: lg.accountId ? accountNameById[lg.accountId] : undefined, + })) + ); } catch (err) { setSelectableLogGroups([]); } setIsLoading(false); }; - const handleSelectCheckbox = (row: SelectableResourceValue, isChecked: boolean) => { + const handleSelectCheckbox = (row: LogGroup, isChecked: boolean) => { if (isChecked) { setSelectedLogGroups([...selectedLogGroups, row]); } else { - setSelectedLogGroups(selectedLogGroups.filter((lg) => lg.value !== row.value)); + setSelectedLogGroups(selectedLogGroups.filter((lg) => lg.arn !== row.arn)); } }; @@ -68,16 +84,6 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => toggleModal(); }; - const accountNameById = useMemo(() => { - const idsToNames: Record = {}; - props.accountOptions.forEach((a) => { - if (a.value && a.label) { - idsToNames[a.value] = a.label; - } - }); - return idsToNames; - }, [props.accountOptions]); - return ( <> @@ -139,22 +145,22 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => )} {!isLoading && selectableLogGroups.map((row) => ( - +
handleSelectCheckbox(row, ev.currentTarget.checked)} - value={!!(row.value && selectedLogGroups.some((lg) => lg.value === row.value))} + value={!!(row.arn && selectedLogGroups.some((lg) => lg.arn === row.arn))} /> -
- {accountNameById[row.text]} - {row.text} + {row.accountLabel} + {row.accountId} ))} diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx index bc0e633c112..ec22e62d809 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroupSelection.tsx @@ -4,10 +4,9 @@ import React from 'react'; import { config } from '@grafana/runtime'; import { LegacyForms } from '@grafana/ui'; -import { SelectableResourceValue } from '../api'; import { CloudWatchDatasource } from '../datasource'; import { useAccountOptions } from '../hooks'; -import { CloudWatchLogsQuery, CloudWatchQuery, DescribeLogGroupsRequest } from '../types'; +import { CloudWatchLogsQuery, CloudWatchQuery, DescribeLogGroupsRequest, LogGroup } from '../types'; import { CrossAccountLogsQueryField } from './CrossAccountLogsQueryField'; import { LogGroupSelector } from './LogGroupSelector'; @@ -32,7 +31,7 @@ export const LogGroupSelection = ({ datasource, query, onChange }: Props) => { fetchLogGroups={(params: Partial) => datasource.api.describeCrossAccountLogGroups({ region: query.region, ...params }) } - onChange={(selectedLogGroups: SelectableResourceValue[]) => { + onChange={(selectedLogGroups: LogGroup[]) => { onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] }); }} accountOptions={accountState.value} diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx index c0d12af2044..e08727e6258 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx @@ -76,7 +76,7 @@ describe('QueryEditor should render right editor', () => { const query = { ...migratedFields, alias: '', - apiMode: 'Logs', + apiMode: 'Metrics', dimensions: { InstanceId: 'i-123', }, @@ -87,7 +87,7 @@ describe('QueryEditor should render right editor', () => { metricName: 'CPUUtilization', namespace: 'AWS/EC2', period: '', - queryMode: 'Logs', + queryMode: 'Metrics', refId: 'A', region: 'ap-northeast-2', statistics: 'Average', @@ -95,7 +95,7 @@ describe('QueryEditor should render right editor', () => { await act(async () => { render(); }); - expect(screen.getByText('Choose Log Groups')).toBeInTheDocument(); + expect(screen.getByText('Metric name')).toBeInTheDocument(); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx index b514d386903..d414fc52d2e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.test.tsx @@ -2,21 +2,22 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { LogGroup } from '../types'; + import { SelectedLogsGroups } from './SelectedLogsGroups'; +const selectedLogGroups: LogGroup[] = [ + { + arn: 'aws/lambda/lambda-name1', + name: 'aws/lambda/lambda-name1', + }, + { + arn: 'aws/lambda/lambda-name2', + name: 'aws/lambda/lambda-name2', + }, +]; const defaultProps = { - selectedLogGroups: [ - { - value: 'aws/lambda/lambda-name1', - label: 'aws/lambda/lambda-name1', - text: 'aws/lambda/lambda-name1', - }, - { - value: 'aws/lambda/lambda-name2', - label: 'aws/lambda/lambda-name2', - text: 'aws/lambda/lambda-name2', - }, - ], + selectedLogGroups, onChange: jest.fn(), }; @@ -35,9 +36,8 @@ describe('SelectedLogsGroups', () => { }); it('should be displayed in case more than 10 log groups have been selected', async () => { const selectedLogGroups = Array(12).map((i) => ({ - value: `logGroup${i}`, - text: `logGroup${i}`, - label: `logGroup${i}`, + arn: `logGroup${i}`, + name: `logGroup${i}`, })); render(); await waitFor(() => expect(screen.getByText('Show all')).toBeInTheDocument()); @@ -51,9 +51,8 @@ describe('SelectedLogsGroups', () => { }); it('should be displayed in case at least one log group have been selected', async () => { const selectedLogGroups = Array(11).map((i) => ({ - value: `logGroup${i}`, - text: `logGroup${i}`, - label: `logGroup${i}`, + arn: `logGroup${i}`, + name: `logGroup${i}`, })); render(); await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument()); @@ -61,9 +60,8 @@ describe('SelectedLogsGroups', () => { it('should display confirm dialog before clearing all selections', async () => { const selectedLogGroups = Array(11).map((i) => ({ - value: `logGroup${i}`, - text: `logGroup${i}`, - label: `logGroup${i}`, + arn: `logGroup${i}`, + name: `logGroup${i}`, })); render(); await waitFor(() => userEvent.click(screen.getByText('Clear selection'))); @@ -82,9 +80,8 @@ describe('SelectedLogsGroups', () => { }); it('should be displayed in case at least one log group have been selected', async () => { const selectedLogGroups = Array(11).map((i) => ({ - value: `logGroup${i}`, - text: `logGroup${i}`, - label: `logGroup${i}`, + arn: `logGroup${i}`, + name: `logGroup${i}`, })); render(); await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument()); diff --git a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx b/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx index 0733da6aabd..e4ab695865b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SelectedLogsGroups.tsx @@ -2,13 +2,13 @@ import React, { useEffect, useState } from 'react'; import { Button, ConfirmModal, useStyles2 } from '@grafana/ui'; -import { SelectableResourceValue } from '../api'; +import { LogGroup } from '../types'; import getStyles from './styles'; type CrossAccountLogsQueryProps = { - selectedLogGroups: SelectableResourceValue[]; - onChange: (selectedLogGroups: SelectableResourceValue[]) => void; + selectedLogGroups: LogGroup[]; + onChange: (selectedLogGroups: LogGroup[]) => void; }; const MAX_VISIBLE_LOG_GROUPS = 6; @@ -29,16 +29,16 @@ export const SelectedLogsGroups = ({ selectedLogGroups, onChange }: CrossAccount
{visibleSelectecLogGroups.map((lg) => ( ))} {visibleSelectecLogGroups.length !== selectedLogGroups.length && ( diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index 3320bde8586..b4aa822a928 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -1,7 +1,6 @@ import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from '@grafana/aws-sdk'; import { DataFrame, DataQuery, DataSourceRef, SelectableValue } from '@grafana/data'; -import { SelectableResourceValue } from './api'; import { QueryEditorArrayExpression, QueryEditorFunctionExpression, @@ -101,8 +100,8 @@ export interface CloudWatchLogsQuery extends DataQuery { region: string; expression?: string; statsGroups?: string[]; - logGroups?: SelectableResourceValue[]; - /* not quite deprecated yet, but will be soon */ + logGroups?: LogGroup[]; + /* deprecated, use logGroups instead */ logGroupNames?: string[]; } // We want to allow setting defaults for both Logs and Metrics queries @@ -263,42 +262,6 @@ export interface TSDBTimeSeries { } export type TSDBTimePoint = [number, number]; -export interface LogGroup { - /** - * The name of the log group. - */ - logGroupName?: string; - /** - * The creation time of the log group, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. - */ - creationTime?: number; - retentionInDays?: number; - /** - * The number of metric filters. - */ - metricFilterCount?: number; - /** - * The Amazon Resource Name (ARN) of the log group. - */ - arn?: string; - /** - * The number of bytes stored. - */ - storedBytes?: number; - /** - * The Amazon Resource Name (ARN) of the CMK to use when encrypting log data. - */ - kmsKeyId?: string; -} - -export interface DescribeLogGroupsResponse { - /** - * The log groups. - */ - logGroups?: LogGroup[]; - nextToken?: string; -} - export interface GetLogGroupFieldsRequest { /** * The name of the log group to search. @@ -338,7 +301,7 @@ export interface StartQueryRequest { * The list of log groups to be queried. You can include up to 20 log groups. A StartQuery operation must include a logGroupNames or a logGroupName parameter, but not both. */ logGroupNames?: string[] /* not quite deprecated yet, but will be soon */; - logGroups?: SelectableResourceValue[]; + logGroups?: LogGroup[]; /** * The query string to use. For more information, see CloudWatch Logs Insights Query Syntax. */ @@ -501,3 +464,10 @@ export interface ResourceResponse { accountId?: string; value: T; } + +export interface LogGroup { + arn: string; + name: string; + accountId?: string; + accountLabel?: string; +} diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts index 9f146fd90a7..5ebaa007c29 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts @@ -118,8 +118,8 @@ describe('addDataLinksToLogsResponse', () => { expression: 'stats count(@message) by bin(1h)', logGroupNames: [''], logGroups: [ - { value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test:*' }, - { value: 'arn:aws:logs:us-east-2:222222222222:log-group:/ecs/prometheus:*' }, + { arn: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test:*' }, + { arn: 'arn:aws:logs:us-east-2:222222222222:log-group:/ecs/prometheus:*' }, ], region: 'us-east-1', } as CloudWatchQuery, @@ -177,7 +177,7 @@ describe('addDataLinksToLogsResponse', () => { refId: 'A', expression: 'stats count(@message) by bin(1h)', logGroupNames: [''], - logGroups: [{ value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test' }], + logGroups: [{ arn: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test' }], region: 'us-east-1', } as CloudWatchQuery, ], diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts index 8d29e0338a6..56d7c0abd3f 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts @@ -71,8 +71,8 @@ function createAwsConsoleLink( getVariableValue: (value: string) => string[] ) { const arns = (target.logGroups ?? []) - .filter((group) => group?.value) - .map((group) => (group.value ?? '').replace(/:\*$/, '')); // remove `:*` from end of arn + .filter((group) => group?.arn) + .map((group) => (group.arn ?? '').replace(/:\*$/, '')); // remove `:*` from end of arn const logGroupNames = target.logGroupNames ?? []; const sources = arns?.length ? arns : logGroupNames; const interpolatedExpression = target.expression ? replace(target.expression) : '';