move describe log groups to resource api (#55485)

This commit is contained in:
Erik Sundell 2022-09-21 10:55:54 +02:00 committed by GitHub
parent 79a5e3e802
commit 28ebdf1641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 370 additions and 371 deletions

View File

@ -56,7 +56,7 @@ exports[`no enzyme tests`] = {
"public/app/features/dimensions/editors/ThresholdsEditor/ThresholdsEditor.test.tsx:145048794": [
[0, 17, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:2210656642": [
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:2983010995": [
[1, 19, 13, "RegExp match", "2409514259"]
]
}`
@ -5890,15 +5890,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloudwatch/api.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
"time"
@ -82,7 +83,6 @@ const (
var plog = log.New("tsdb.cloudwatch")
var aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
var baseLimit = int64(1)
func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles) *CloudWatchService {
plog.Debug("initing")
@ -203,17 +203,12 @@ func (e *cloudWatchExecutor) checkHealthMetrics(pluginCtx backend.PluginContext)
return err
}
func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx backend.PluginContext) error {
logsClient, err := e.getCWLogsClient(pluginCtx, defaultRegion)
if err != nil {
return err
func (e *cloudWatchExecutor) checkHealthLogs(pluginCtx backend.PluginContext) error {
parameters := url.Values{
"limit": []string{"1"},
}
parameters := LogQueryJson{
Limit: &baseLimit,
}
_, err = e.handleDescribeLogGroups(ctx, logsClient, parameters)
_, err := e.handleGetLogGroups(pluginCtx, parameters)
return err
}
@ -228,7 +223,7 @@ func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.Check
metricsTest = fmt.Sprintf("CloudWatch metrics query failed: %s", err.Error())
}
err = e.checkHealthLogs(ctx, req.PluginContext)
err = e.checkHealthLogs(req.PluginContext)
if err != nil {
status = backend.HealthStatusError
logsTest = fmt.Sprintf("CloudWatch logs query failed: %s", err.Error())

View File

@ -4,11 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
awsrequest "github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
@ -124,8 +124,7 @@ func Test_CheckHealth(t *testing.T) {
t.Run("successfully queries metrics, fails during logs query", func(t *testing.T) {
client = fakeCheckHealthClient{
describeLogGroupsWithContext: func(ctx aws.Context, input *cloudwatchlogs.DescribeLogGroupsInput,
options ...awsrequest.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
describeLogGroups: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
return nil, fmt.Errorf("some logs query error")
}}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
@ -248,3 +247,140 @@ func Test_executeLogAlertQuery(t *testing.T) {
assert.Equal(t, []string{"instance manager's region"}, sess.calledRegions)
})
}
func TestQuery_ResourceRequest_DescribeLogGroups(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() {
NewCWLogsClient = origNewCWLogsClient
})
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
return &cli
}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return datasourceInfo{}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
sender := &mockedCallResourceResponseSenderForOauth{}
t.Run("Should map log groups to SuggestData response", func(t *testing.T) {
cli = fakeCWLogsClient{
logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{
{LogGroups: []*cloudwatchlogs.LogGroup{
{
LogGroupName: aws.String("group_a"),
},
{
LogGroupName: aws.String("group_b"),
},
{
LogGroupName: aws.String("group_c"),
},
}},
},
}
req := &backend.CallResourceRequest{
Method: "GET",
Path: "/log-groups",
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
ID: 0,
},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusOK, sent.Status)
suggestDataResponse := []suggestData{}
err = json.Unmarshal(sent.Body, &suggestDataResponse)
require.Nil(t, err)
assert.Equal(t, []suggestData{
{Text: "group_a", Value: "group_a", Label: "group_a"}, {Text: "group_b", Value: "group_b", Label: "group_b"}, {Text: "group_c", Value: "group_c", Label: "group_c"}}, suggestDataResponse)
})
t.Run("Should call api with LogGroupNamePrefix if passed in resource call", func(t *testing.T) {
cli = fakeCWLogsClient{
logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{
{LogGroups: []*cloudwatchlogs.LogGroup{}},
},
}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return datasourceInfo{}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
req := &backend.CallResourceRequest{
Method: "GET",
Path: "/log-groups?logGroupNamePrefix=test",
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
ID: 0,
},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusOK, sent.Status)
assert.Equal(t, []*cloudwatchlogs.DescribeLogGroupsInput{
{
Limit: aws.Int64(defaultLogGroupLimit),
LogGroupNamePrefix: aws.String("test"),
},
}, cli.calls.describeLogGroups)
})
t.Run("Should call api without LogGroupNamePrefix if not passed in resource call", func(t *testing.T) {
cli = fakeCWLogsClient{
logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{
{LogGroups: []*cloudwatchlogs.LogGroup{}},
},
}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return datasourceInfo{}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
req := &backend.CallResourceRequest{
Method: "GET",
Path: "/log-groups?limit=100",
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
ID: 0,
},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusOK, sent.Status)
assert.Equal(t, []*cloudwatchlogs.DescribeLogGroupsInput{
{
Limit: aws.Int64(100),
},
}, cli.calls.describeLogGroups)
})
}

View File

@ -126,8 +126,6 @@ func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, model LogQuer
var data *data.Frame = nil
switch model.SubType {
case "DescribeLogGroups":
data, err = e.handleDescribeLogGroups(ctx, logsClient, model)
case "DescribeAllLogGroups":
data, err = e.handleDescribeAllLogGroups(ctx, logsClient, model)
case "GetLogGroupFields":
@ -203,40 +201,6 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient
return data.NewFrame("logEvents", timestampField, messageField), nil
}
func (e *cloudWatchExecutor) handleDescribeLogGroups(ctx context.Context,
logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters LogQueryJson) (*data.Frame, error) {
logGroupLimit := defaultLogGroupLimit
if parameters.Limit != nil && *parameters.Limit != 0 {
logGroupLimit = *parameters.Limit
}
var response *cloudwatchlogs.DescribeLogGroupsOutput = nil
var err error
if len(parameters.LogGroupNamePrefix) == 0 {
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int64(logGroupLimit),
})
} else {
response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int64(logGroupLimit),
LogGroupNamePrefix: aws.String(parameters.LogGroupNamePrefix),
})
}
if err != nil || response == nil {
return nil, err
}
logGroupNames := make([]*string, 0)
for _, logGroup := range response.LogGroups {
logGroupNames = append(logGroupNames, logGroup.LogGroupName)
}
groupNamesField := data.NewField("logGroupName", nil, logGroupNames)
frame := data.NewFrame("logGroups", groupNamesField)
return frame, nil
}
func (e *cloudWatchExecutor) handleDescribeAllLogGroups(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters LogQueryJson) (*data.Frame, error) {
var namePrefix, nextToken *string
if len(parameters.LogGroupNamePrefix) != 0 {

View File

@ -106,133 +106,6 @@ func TestQuery_GetLogEvents(t *testing.T) {
}
}
func TestQuery_DescribeLogGroups(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() {
NewCWLogsClient = origNewCWLogsClient
})
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
return &cli
}
t.Run("Empty log group name prefix", func(t *testing.T) {
cli = fakeCWLogsClient{
logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{
{LogGroups: []*cloudwatchlogs.LogGroup{
{
LogGroupName: aws.String("group_a"),
},
{
LogGroupName: aws.String("group_b"),
},
{
LogGroupName: aws.String("group_c"),
},
}},
},
}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return datasourceInfo{}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
Queries: []backend.DataQuery{
{
JSON: json.RawMessage(`{
"type": "logAction",
"subtype": "DescribeLogGroups",
"limit": 50
}`),
},
},
})
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{
"": backend.DataResponse{
Frames: data.Frames{
&data.Frame{
Name: "logGroups",
Fields: []*data.Field{
data.NewField("logGroupName", nil, []*string{
aws.String("group_a"), aws.String("group_b"), aws.String("group_c"),
}),
},
},
},
},
},
}, resp)
})
t.Run("Non-empty log group name prefix", func(t *testing.T) {
cli = fakeCWLogsClient{
logGroups: []cloudwatchlogs.DescribeLogGroupsOutput{
{LogGroups: []*cloudwatchlogs.LogGroup{
{
LogGroupName: aws.String("group_a"),
},
{
LogGroupName: aws.String("group_b"),
},
{
LogGroupName: aws.String("group_c"),
},
}},
},
}
im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return datasourceInfo{}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
Queries: []backend.DataQuery{
{
JSON: json.RawMessage(`{
"type": "logAction",
"subtype": "DescribeLogGroups",
"limit": 50,
"region": "default",
"logGroupNamePrefix": "g"
}`),
},
},
})
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{
"": backend.DataResponse{
Frames: data.Frames{
&data.Frame{
Name: "logGroups",
Fields: []*data.Field{
data.NewField("logGroupName", nil, []*string{
aws.String("group_a"), aws.String("group_b"), aws.String("group_c"),
}),
},
},
},
},
},
}, resp)
})
}
func TestQuery_DescribeAllLogGroups(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() {

View File

@ -7,6 +7,7 @@ import (
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
@ -14,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@ -649,6 +651,41 @@ func (e *cloudWatchExecutor) getDimensionsForCustomMetrics(region, namespace str
return customMetricsDimensionsMap[dsInfo.profile][dsInfo.region][namespace].Cache, nil
}
func (e *cloudWatchExecutor) handleGetLogGroups(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
limit := parameters.Get("limit")
logGroupNamePrefix := parameters.Get("logGroupNamePrefix")
logsClient, err := e.getCWLogsClient(pluginCtx, region)
if err != nil {
return nil, err
}
logGroupLimit := defaultLogGroupLimit
intLimit, err := strconv.ParseInt(limit, 10, 64)
if err == nil && intLimit > 0 {
logGroupLimit = intLimit
}
var response *cloudwatchlogs.DescribeLogGroupsOutput = nil
input := &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(logGroupLimit)}
if len(logGroupNamePrefix) > 0 {
input.LogGroupNamePrefix = aws.String(logGroupNamePrefix)
}
response, err = logsClient.DescribeLogGroups(input)
if err != nil || response == nil {
return nil, err
}
result := make([]suggestData, 0)
for _, logGroup := range response.LogGroups {
logGroupName := *logGroup.LogGroupName
result = append(result, suggestData{Text: logGroupName, Value: logGroupName, Label: logGroupName})
}
return result, nil
}
func isDuplicate(nameList []string, target string) bool {
for _, name := range nameList {
if name == target {

View File

@ -21,6 +21,7 @@ func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds))
mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute))
mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns))
mux.HandleFunc("/log-groups", handleResourceReq(e.handleGetLogGroups))
return mux
}

View File

@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/setting"
)
@ -33,6 +34,7 @@ type fakeCWLogsClient struct {
type logsQueryCalls struct {
startQueryWithContext []*cloudwatchlogs.StartQueryInput
getEventsWithContext []*cloudwatchlogs.GetLogEventsInput
describeLogGroups []*cloudwatchlogs.DescribeLogGroupsInput
}
func (m *fakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
@ -53,6 +55,13 @@ func (m *fakeCWLogsClient) StopQueryWithContext(ctx context.Context, input *clou
}, nil
}
func (m *fakeCWLogsClient) DescribeLogGroups(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
m.calls.describeLogGroups = append(m.calls.describeLogGroups, input)
output := &m.logGroups[m.logGroupsIndex]
m.logGroupsIndex++
return output, nil
}
func (m *fakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
output := &m.logGroups[m.logGroupsIndex]
m.logGroupsIndex++
@ -192,9 +201,8 @@ type fakeCheckHealthClient struct {
cloudwatchiface.CloudWatchAPI
cloudwatchlogsiface.CloudWatchLogsAPI
listMetricsPages func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error
describeLogGroupsWithContext func(ctx aws.Context, input *cloudwatchlogs.DescribeLogGroupsInput,
options ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error)
listMetricsPages func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error
describeLogGroups func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error)
}
func (c fakeCheckHealthClient) ListMetricsPages(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error {
@ -204,9 +212,9 @@ func (c fakeCheckHealthClient) ListMetricsPages(input *cloudwatch.ListMetricsInp
return nil
}
func (c fakeCheckHealthClient) DescribeLogGroupsWithContext(ctx aws.Context, input *cloudwatchlogs.DescribeLogGroupsInput, options ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
if c.describeLogGroupsWithContext != nil {
return c.describeLogGroupsWithContext(ctx, input, options...)
func (c fakeCheckHealthClient) DescribeLogGroups(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
if c.describeLogGroups != nil {
return c.describeLogGroups(input)
}
return nil, nil
}
@ -247,3 +255,12 @@ func (s *fakeSessionCache) GetSession(c awsds.SessionConfig) (*session.Session,
Config: &aws.Config{},
}, nil
}
type mockedCallResourceResponseSenderForOauth struct {
Response *backend.CallResourceResponse
}
func (s *mockedCallResourceResponseSenderForOauth) Send(resp *backend.CallResourceResponse) error {
s.Response = resp
return nil
}

View File

@ -0,0 +1,29 @@
import { getBackendSrv, setBackendSrv } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariableModel } from 'app/features/variables/types';
import { CloudWatchAPI } from '../api';
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
export function setupMockedAPI({
variables,
response,
}: {
response?: Array<{ text: string; label: string; value: string }>;
variables?: CustomVariableModel[];
mockGetVariableName?: boolean;
} = {}) {
let templateService = variables ? setupMockedTemplateService(variables) : new TemplateSrv();
const timeSrv = getTimeSrv();
const api = new CloudWatchAPI(CloudWatchSettings, templateService);
const resourceRequestMock = jest.fn().mockReturnValue(response);
setBackendSrv({
...getBackendSrv(),
get: resourceRequestMock,
});
return { api, resourceRequestMock, templateService, timeSrv };
}

View File

@ -76,7 +76,7 @@ export function setupMockedDataSource({
const timeSrv = getTimeSrv();
const datasource = new CloudWatchDatasource(CloudWatchSettings, templateService, timeSrv);
datasource.getVariables = () => ['test'];
datasource.api.describeLogGroups = jest.fn().mockResolvedValue([]);
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getRegions = jest.fn().mockResolvedValue([]);
datasource.logsQueryRunner.defaultLogGroups = [];
@ -84,6 +84,7 @@ export function setupMockedDataSource({
setBackendSrv({
...getBackendSrv(),
fetch: fetchMock,
get: jest.fn(),
});
return { datasource, fetchMock, templateService, timeSrv };

View File

@ -0,0 +1,55 @@
import { setupMockedAPI } from './__mocks__/API';
describe('describeLogGroup', () => {
it('replaces region correctly in the query', async () => {
const { api, resourceRequestMock } = setupMockedAPI();
await api.describeLogGroups({ region: 'default' });
expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1');
await api.describeLogGroups({ region: 'eu-east' });
expect(resourceRequestMock.mock.calls[1][1].region).toBe('eu-east');
});
it('should return log groups as an array of options', async () => {
const response = [
{
text: '/aws/containerinsights/dev303-workshop/application',
value: '/aws/containerinsights/dev303-workshop/application',
label: '/aws/containerinsights/dev303-workshop/application',
},
{
text: '/aws/containerinsights/dev303-workshop/flowlogs',
value: '/aws/containerinsights/dev303-workshop/flowlogs',
label: '/aws/containerinsights/dev303-workshop/flowlogs',
},
{
text: '/aws/containerinsights/dev303-workshop/dataplane',
value: '/aws/containerinsights/dev303-workshop/dataplane',
label: '/aws/containerinsights/dev303-workshop/dataplane',
},
];
const { api } = setupMockedAPI({ response });
const expectedLogGroups = [
{
text: '/aws/containerinsights/dev303-workshop/application',
value: '/aws/containerinsights/dev303-workshop/application',
label: '/aws/containerinsights/dev303-workshop/application',
},
{
text: '/aws/containerinsights/dev303-workshop/flowlogs',
value: '/aws/containerinsights/dev303-workshop/flowlogs',
label: '/aws/containerinsights/dev303-workshop/flowlogs',
},
{
text: '/aws/containerinsights/dev303-workshop/dataplane',
value: '/aws/containerinsights/dev303-workshop/dataplane',
label: '/aws/containerinsights/dev303-workshop/dataplane',
},
];
const logGroups = await api.describeLogGroups({ region: 'default' });
expect(logGroups).toEqual(expectedLogGroups);
});
});

View File

@ -1,25 +1,30 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, BackendSrv } from '@grafana/runtime';
import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CloudWatchRequest } from './query-runner/CloudWatchRequest';
import { CloudWatchJsonData, Dimensions } from './types';
import { CloudWatchJsonData, DescribeLogGroupsRequest, Dimensions, MultiFilters } from './types';
export interface SelectableResourceValue extends SelectableValue<string> {
text: string;
}
export class CloudWatchAPI extends CloudWatchRequest {
private backendSrv: BackendSrv;
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
super(instanceSettings, templateSrv);
this.backendSrv = getBackendSrv();
}
resourceRequest(subtype: string, parameters?: any): Promise<Array<{ text: any; label: any; value: any }>> {
return this.backendSrv.get(`/api/datasources/${this.instanceSettings.id}/resources/${subtype}`, parameters);
resourceRequest(
subtype: string,
parameters?: Record<string, string | string[] | number>
): Promise<SelectableResourceValue[]> {
return getBackendSrv().get(`/api/datasources/${this.instanceSettings.id}/resources/${subtype}`, parameters);
}
getRegions(): Promise<Array<{ label: string; value: string; text: string }>> {
return this.resourceRequest('regions').then((regions: any) => [
getRegions() {
return this.resourceRequest('regions').then((regions) => [
{ label: 'default', value: 'default', text: 'default' },
...regions,
...regions.filter((r) => r.value),
]);
}
@ -27,6 +32,13 @@ export class CloudWatchAPI extends CloudWatchRequest {
return this.resourceRequest('namespaces');
}
async describeLogGroups(params: DescribeLogGroupsRequest) {
return this.resourceRequest('log-groups', {
...params,
region: this.templateSrv.replace(this.getActualRegion(params.region)),
});
}
async getMetrics(namespace: string | undefined, region?: string) {
if (!namespace) {
return [];
@ -38,7 +50,7 @@ export class CloudWatchAPI extends CloudWatchRequest {
});
}
async getAllMetrics(region: string): Promise<Array<{ metricName: string; namespace: string }>> {
async getAllMetrics(region: string): Promise<Array<{ metricName?: string; namespace: string }>> {
const values = await this.resourceRequest('all-metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
});
@ -93,7 +105,7 @@ export class CloudWatchAPI extends CloudWatchRequest {
});
}
getEc2InstanceAttribute(region: string, attributeName: string, filters: any) {
getEc2InstanceAttribute(region: string, attributeName: string, filters: MultiFilters) {
return this.resourceRequest('ec2-instance-attribute', {
region: this.templateSrv.replace(this.getActualRegion(region)),
attributeName: this.templateSrv.replace(attributeName),
@ -101,7 +113,7 @@ export class CloudWatchAPI extends CloudWatchRequest {
});
}
getResourceARNs(region: string, resourceType: string, tags: any) {
getResourceARNs(region: string, resourceType: string, tags: MultiFilters) {
return this.resourceRequest('resource-arns', {
region: this.templateSrv.replace(this.getActualRegion(region)),
resourceType: this.templateSrv.replace(resourceType),

View File

@ -116,11 +116,11 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
this.templateSrv.replace(namespaceToken?.value.replace(/\"/g, '')),
this.templateSrv.replace(this.region)
);
metrics.map((m) => addSuggestion(m.value));
metrics.forEach((m) => m.value && addSuggestion(m.value));
} else {
// If no namespace is specified in the query, just list all metrics
const metrics = await this.api.getAllMetrics(this.templateSrv.replace(this.region));
uniq(metrics.map((m) => m.metricName)).map((m) => addSuggestion(m, { insertText: m }));
uniq(metrics.map((m) => m.metricName)).forEach((m) => m && addSuggestion(m, { insertText: m }));
}
}
break;
@ -186,8 +186,8 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
metricNameToken?.value ?? ''
);
keys.map((m) => {
const key = /[\s\.-]/.test(m.value) ? `"${m.value}"` : m.value;
addSuggestion(key);
const key = /[\s\.-]/.test(m.value ?? '') ? `"${m.value}"` : m.value;
key && addSuggestion(key);
});
}
}

View File

@ -4,6 +4,7 @@ import React from 'react';
import selectEvent from 'react-select-event';
import { AwsAuthType } from '@grafana/aws-sdk';
import { toOption } from '@grafana/data';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
@ -13,6 +14,7 @@ jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
loadDatasource: jest.fn().mockResolvedValue({
api: {
describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar'].map(toOption)),
getRegions: jest.fn().mockResolvedValue([
{
label: 'ap-east-1',
@ -22,9 +24,6 @@ jest.mock('app/features/plugins/datasource_srv', () => ({
},
getActualRegion: jest.fn().mockReturnValue('ap-east-1'),
getVariables: jest.fn().mockReturnValue([]),
logsQueryRunner: {
describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar']),
},
}),
}),
}));

View File

@ -16,6 +16,7 @@ import { createWarningNotification } from 'app/core/copy/appNotification';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { store } from 'app/store/store';
import { SelectableResourceValue } from '../api';
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types';
@ -63,7 +64,16 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
{...props}
loadRegions={
datasource &&
(() => datasource.api.getRegions().then((r) => r.filter((r) => r.value !== 'default').map((v) => v.value)))
(async () => {
return datasource.api
.getRegions()
.then((regions) =>
regions.reduce(
(acc: string[], curr: SelectableResourceValue) => (curr.value ? [...acc, curr.value] : acc),
[]
)
);
})
}
>
<InlineField label="Namespaces of Custom Metrics" labelWidth={28} tooltip="Namespaces of Custom Metrics.">

View File

@ -4,6 +4,8 @@ import lodash from 'lodash'; // eslint-disable-line lodash/import-scope
import React from 'react';
import { openMenu, select } from 'react-select-event';
import { toOption } from '@grafana/data';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
import { DescribeLogGroupsRequest } from '../types';
@ -25,15 +27,13 @@ describe('LogGroupSelector', () => {
});
it('updates upstream query log groups on region change', async () => {
ds.datasource.logsQueryRunner.describeLogGroups = jest
.fn()
.mockImplementation(async (params: DescribeLogGroupsRequest) => {
if (params.region === 'region1') {
return Promise.resolve(['log_group_1']);
} else {
return Promise.resolve(['log_group_2']);
}
});
ds.datasource.api.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
if (params.region === 'region1') {
return Promise.resolve(['log_group_1'].map(toOption));
} else {
return Promise.resolve(['log_group_2'].map(toOption));
}
});
const props = {
...defaultProps,
selectedLogGroups: ['log_group_1'],
@ -50,15 +50,13 @@ describe('LogGroupSelector', () => {
});
it('does not update upstream query log groups if saved is false', async () => {
ds.datasource.logsQueryRunner.describeLogGroups = jest
.fn()
.mockImplementation(async (params: DescribeLogGroupsRequest) => {
if (params.region === 'region1') {
return Promise.resolve(['log_group_1']);
} else {
return Promise.resolve(['log_group_2']);
}
});
ds.datasource.api.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
if (params.region === 'region1') {
return Promise.resolve(['log_group_1'].map(toOption));
} else {
return Promise.resolve(['log_group_2'].map(toOption));
}
});
const props = {
...defaultProps,
selectedLogGroups: ['log_group_1'],
@ -98,14 +96,12 @@ describe('LogGroupSelector', () => {
];
const testLimit = 10;
ds.datasource.logsQueryRunner.describeLogGroups = jest
.fn()
.mockImplementation(async (params: DescribeLogGroupsRequest) => {
const theLogGroups = allLogGroups
.filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
.slice(0, Math.max(params.limit ?? testLimit, testLimit));
return Promise.resolve(theLogGroups);
});
ds.datasource.api.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
const theLogGroups = allLogGroups
.filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
.slice(0, Math.max(params.limit ?? testLimit, testLimit));
return Promise.resolve(theLogGroups.map(toOption));
});
const props = {
...defaultProps,
};
@ -129,7 +125,7 @@ describe('LogGroupSelector', () => {
it('should render template variables a selectable option', async () => {
lodash.debounce = jest.fn().mockImplementation((fn) => fn);
ds.datasource.logsQueryRunner.describeLogGroups = jest.fn().mockResolvedValue([]);
ds.datasource.api.describeLogGroups = jest.fn().mockResolvedValue([]);
const onChange = jest.fn();
const props = {
...defaultProps,

View File

@ -52,12 +52,12 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
return [];
}
try {
const logGroups: string[] = await datasource.logsQueryRunner.describeLogGroups({
const logGroups = await datasource.api.describeLogGroups({
refId,
region,
logGroupNamePrefix,
});
return logGroups.map(toOption);
return logGroups;
} catch (err) {
dispatch(notifyApp(createErrorNotification(typeof err === 'string' ? err : JSON.stringify(err))));
return [];
@ -98,7 +98,7 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
}
setLoadingLogGroups(true);
return fetchLogGroupOptions(datasource.getActualRegion(region) ?? '')
return fetchLogGroupOptions(datasource.getActualRegion(region))
.then((logGroups) => {
const newSelectedLogGroups = intersection(
selectedLogGroups,

View File

@ -48,7 +48,6 @@ export class CloudWatchDatasource
private metricsQueryRunner: CloudWatchMetricsQueryRunner;
private annotationQueryRunner: CloudWatchAnnotationQueryRunner;
// this member should be private too, but we need to fix https://github.com/grafana/grafana/issues/55243 to enable that
logsQueryRunner: CloudWatchLogsQueryRunner;
api: CloudWatchAPI;
@ -170,7 +169,7 @@ export class CloudWatchDatasource
getActualRegion(region?: string) {
if (region === 'default' || region === undefined || region === '') {
return this.defaultRegion;
return this.defaultRegion ?? '';
}
return region;
}

View File

@ -1,7 +1,6 @@
import { interval, lastValueFrom, of } from 'rxjs';
import { LogRowModel, MutableDataFrame, FieldType, LogLevel, dataFrameToJSON, DataQueryErrorType } from '@grafana/data';
import { BackendDataSourceResponse } from '@grafana/runtime';
import { genMockFrames, setupMockedLogsQueryRunner } from '../__mocks__/LogsQueryRunner';
import { validLogsQuery } from '../__mocks__/queries';
@ -14,114 +13,6 @@ describe('CloudWatchLogsQueryRunner', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('describeLogGroup', () => {
it('replaces region correctly in the query', async () => {
const { runner, fetchMock } = setupMockedLogsQueryRunner();
await runner.describeLogGroups({ region: 'default' });
expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1');
await runner.describeLogGroups({ region: 'eu-east' });
expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east');
});
it('should return log groups as an array of strings', async () => {
const data: BackendDataSourceResponse = {
results: {
A: {
frames: [
{
schema: {
name: 'logGroups',
refId: 'A',
fields: [{ name: 'logGroupName', type: FieldType.string }],
},
data: {
values: [
[
'/aws/containerinsights/dev303-workshop/application',
'/aws/containerinsights/dev303-workshop/dataplane',
'/aws/containerinsights/dev303-workshop/flowlogs',
'/aws/containerinsights/dev303-workshop/host',
'/aws/containerinsights/dev303-workshop/performance',
'/aws/containerinsights/dev303-workshop/prometheus',
'/aws/containerinsights/ecommerce-sockshop/application',
'/aws/containerinsights/ecommerce-sockshop/dataplane',
'/aws/containerinsights/ecommerce-sockshop/host',
'/aws/containerinsights/ecommerce-sockshop/performance',
'/aws/containerinsights/watchdemo-perf/application',
'/aws/containerinsights/watchdemo-perf/dataplane',
'/aws/containerinsights/watchdemo-perf/host',
'/aws/containerinsights/watchdemo-perf/performance',
'/aws/containerinsights/watchdemo-perf/prometheus',
'/aws/containerinsights/watchdemo-prod-us-east-1/performance',
'/aws/containerinsights/watchdemo-staging/application',
'/aws/containerinsights/watchdemo-staging/dataplane',
'/aws/containerinsights/watchdemo-staging/host',
'/aws/containerinsights/watchdemo-staging/performance',
'/aws/ecs/containerinsights/bugbash-ec2/performance',
'/aws/ecs/containerinsights/ecs-demoworkshop/performance',
'/aws/ecs/containerinsights/ecs-workshop-dev/performance',
'/aws/eks/dev303-workshop/cluster',
'/aws/events/cloudtrail',
'/aws/events/ecs',
'/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
'/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
'/ecs/ecs-cwagent-daemon-service',
'/ecs/ecs-demo-limitTask',
'CloudTrail/DefaultLogGroup',
'container-insights-prometheus-beta',
'container-insights-prometheus-demo',
],
],
},
},
],
},
},
};
const { runner } = setupMockedLogsQueryRunner({ data });
const expectedLogGroups = [
'/aws/containerinsights/dev303-workshop/application',
'/aws/containerinsights/dev303-workshop/dataplane',
'/aws/containerinsights/dev303-workshop/flowlogs',
'/aws/containerinsights/dev303-workshop/host',
'/aws/containerinsights/dev303-workshop/performance',
'/aws/containerinsights/dev303-workshop/prometheus',
'/aws/containerinsights/ecommerce-sockshop/application',
'/aws/containerinsights/ecommerce-sockshop/dataplane',
'/aws/containerinsights/ecommerce-sockshop/host',
'/aws/containerinsights/ecommerce-sockshop/performance',
'/aws/containerinsights/watchdemo-perf/application',
'/aws/containerinsights/watchdemo-perf/dataplane',
'/aws/containerinsights/watchdemo-perf/host',
'/aws/containerinsights/watchdemo-perf/performance',
'/aws/containerinsights/watchdemo-perf/prometheus',
'/aws/containerinsights/watchdemo-prod-us-east-1/performance',
'/aws/containerinsights/watchdemo-staging/application',
'/aws/containerinsights/watchdemo-staging/dataplane',
'/aws/containerinsights/watchdemo-staging/host',
'/aws/containerinsights/watchdemo-staging/performance',
'/aws/ecs/containerinsights/bugbash-ec2/performance',
'/aws/ecs/containerinsights/ecs-demoworkshop/performance',
'/aws/ecs/containerinsights/ecs-workshop-dev/performance',
'/aws/eks/dev303-workshop/cluster',
'/aws/events/cloudtrail',
'/aws/events/ecs',
'/aws/lambda/cwsyn-mycanary-fac97ded-f134-499a-9d71-4c3be1f63182',
'/aws/lambda/cwsyn-watch-linkchecks-ef7ef273-5da2-4663-af54-d2f52d55b060',
'/ecs/ecs-cwagent-daemon-service',
'/ecs/ecs-demo-limitTask',
'CloudTrail/DefaultLogGroup',
'container-insights-prometheus-beta',
'container-insights-prometheus-demo',
];
const logGroups = await runner.describeLogGroups({ region: 'default' });
expect(logGroups).toEqual(expectedLogGroups);
});
});
describe('getLogRowContext', () => {
it('replaces parameters correctly in the query', async () => {

View File

@ -425,13 +425,6 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest {
};
};
async describeLogGroups(params: DescribeLogGroupsRequest): Promise<string[]> {
const dataFrames = await lastValueFrom(this.makeLogActionRequest('DescribeLogGroups', [params]));
const logGroupNames = dataFrames[0]?.fields[0]?.values.toArray() ?? [];
return logGroupNames;
}
async describeAllLogGroups(params: DescribeLogGroupsRequest): Promise<string[]> {
const dataFrames = await lastValueFrom(this.makeLogActionRequest('DescribeAllLogGroups', [params]));

View File

@ -68,7 +68,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
async handleRegionsQuery() {
const regions = await this.api.getRegions();
return regions.map((s: { label: string; value: string }) => ({
return regions.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -77,7 +77,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
async handleNamespacesQuery() {
const namespaces = await this.api.getNamespaces();
return namespaces.map((s: { label: string; value: string }) => ({
return namespaces.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -86,7 +86,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
async handleMetricsQuery({ namespace, region }: VariableQuery) {
const metrics = await this.api.getMetrics(namespace, region);
return metrics.map((s: { label: string; value: string }) => ({
return metrics.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -95,7 +95,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
async handleDimensionKeysQuery({ namespace, region }: VariableQuery) {
const keys = await this.api.getDimensionKeys(namespace, region);
return keys.map((s: { label: string; value: string }) => ({
return keys.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -107,7 +107,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
return [];
}
const keys = await this.api.getDimensionValues(region, namespace, metricName, dimensionKey, dimensionFilters ?? {});
return keys.map((s: { label: string; value: string }) => ({
return keys.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -119,7 +119,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
return [];
}
const ids = await this.api.getEbsVolumeIds(region, instanceID);
return ids.map((s: { label: string; value: string }) => ({
return ids.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -131,7 +131,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
return [];
}
const values = await this.api.getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {});
return values.map((s: { label: string; value: string }) => ({
return values.map((s) => ({
text: s.label,
value: s.value,
expandable: true,
@ -143,7 +143,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
return [];
}
const keys = await this.api.getResourceARNs(region, resourceType, tags ?? {});
return keys.map((s: { label: string; value: string }) => ({
return keys.map((s) => ({
text: s.label,
value: s.value,
expandable: true,