mirror of
https://github.com/grafana/grafana.git
synced 2024-12-29 10:21:41 -06:00
Cloudwatch: Refactor log group model (#60873)
* refactor log group query model * update deprecated comment * refactor test
This commit is contained in:
parent
b88b8bc291
commit
bd09e88e50
@ -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)
|
||||
|
@ -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*"}]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
28
pkg/tsdb/cloudwatch/models/logs_query.go
Normal file
28
pkg/tsdb/cloudwatch/models/logs_query.go
Normal file
@ -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
|
||||
}
|
@ -69,18 +69,14 @@ export class CloudWatchAPI extends CloudWatchRequest {
|
||||
});
|
||||
}
|
||||
|
||||
async describeCrossAccountLogGroups(params: DescribeLogGroupsRequest): Promise<SelectableResourceValue[]> {
|
||||
async describeCrossAccountLogGroups(
|
||||
params: DescribeLogGroupsRequest
|
||||
): Promise<Array<ResourceResponse<LogGroupResponse>>> {
|
||||
return this.memoizedGetRequest<Array<ResourceResponse<LogGroupResponse>>>('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) {
|
||||
|
@ -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' },
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<ResourceResponse<LogGroupResponse>>),
|
||||
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(<CrossAccountLogsQueryField {...defaultProps} fetchLogGroups={fetchLogGroups} />);
|
||||
|
@ -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<SelectableValue<string>>;
|
||||
fetchLogGroups: (params: Partial<DescribeLogGroupsRequest>) => Promise<SelectableResourceValue[]>;
|
||||
onChange: (selectedLogGroups: SelectableResourceValue[]) => void;
|
||||
fetchLogGroups: (params: Partial<DescribeLogGroupsRequest>) => Promise<Array<ResourceResponse<LogGroupResponse>>>;
|
||||
onChange: (selectedLogGroups: LogGroup[]) => void;
|
||||
};
|
||||
|
||||
export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectableLogGroups, setSelectableLogGroups] = useState<SelectableResourceValue[]>([]);
|
||||
const [selectableLogGroups, setSelectableLogGroups] = useState<LogGroup[]>([]);
|
||||
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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
props.accountOptions.forEach((a) => {
|
||||
if (a.value && a.label) {
|
||||
idsToNames[a.value] = a.label;
|
||||
}
|
||||
});
|
||||
return idsToNames;
|
||||
}, [props.accountOptions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal className={styles.modal} title="Select Log Groups" isOpen={isModalOpen} onDismiss={toggleModal}>
|
||||
@ -139,22 +145,22 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
|
||||
)}
|
||||
{!isLoading &&
|
||||
selectableLogGroups.map((row) => (
|
||||
<tr className={styles.row} key={`${row.value}`}>
|
||||
<tr className={styles.row} key={`${row.arn}`}>
|
||||
<td className={styles.cell}>
|
||||
<div className={styles.nestedEntry}>
|
||||
<Checkbox
|
||||
id={row.value}
|
||||
id={row.arn}
|
||||
onChange={(ev) => 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))}
|
||||
/>
|
||||
<Space layout="inline" h={2} />
|
||||
<label className={styles.logGroupSearchResults} htmlFor={row.value}>
|
||||
{row.label}
|
||||
<label className={styles.logGroupSearchResults} htmlFor={row.arn}>
|
||||
{row.name}
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td className={styles.cell}>{accountNameById[row.text]}</td>
|
||||
<td className={styles.cell}>{row.text}</td>
|
||||
<td className={styles.cell}>{row.accountLabel}</td>
|
||||
<td className={styles.cell}>{row.accountId}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -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<DescribeLogGroupsRequest>) =>
|
||||
datasource.api.describeCrossAccountLogGroups({ region: query.region, ...params })
|
||||
}
|
||||
onChange={(selectedLogGroups: SelectableResourceValue[]) => {
|
||||
onChange={(selectedLogGroups: LogGroup[]) => {
|
||||
onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] });
|
||||
}}
|
||||
accountOptions={accountState.value}
|
||||
|
@ -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(<QueryEditor {...props} query={query} />);
|
||||
});
|
||||
expect(screen.getByText('Choose Log Groups')).toBeInTheDocument();
|
||||
expect(screen.getByText('Metric name')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
|
||||
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(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
|
||||
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(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
|
||||
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(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
|
||||
await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument());
|
||||
|
@ -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
|
||||
<div className={styles.selectedLogGroupsContainer}>
|
||||
{visibleSelectecLogGroups.map((lg) => (
|
||||
<Button
|
||||
key={lg.value}
|
||||
key={lg.arn}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="times"
|
||||
className={styles.removeButton}
|
||||
onClick={() => {
|
||||
onChange(selectedLogGroups.filter((slg) => slg.value !== lg.value));
|
||||
onChange(selectedLogGroups.filter((slg) => slg.arn !== lg.arn));
|
||||
}}
|
||||
>
|
||||
{lg.label}
|
||||
{`${lg.name}`}
|
||||
</Button>
|
||||
))}
|
||||
{visibleSelectecLogGroups.length !== selectedLogGroups.length && (
|
||||
|
@ -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<T> {
|
||||
accountId?: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface LogGroup {
|
||||
arn: string;
|
||||
name: string;
|
||||
accountId?: string;
|
||||
accountLabel?: string;
|
||||
}
|
||||
|
@ -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,
|
||||
],
|
||||
|
@ -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) : '';
|
||||
|
Loading…
Reference in New Issue
Block a user