diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index a2d65a23bde..bbd68f269e7 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -169,6 +169,49 @@ func (e *cloudWatchExecutor) CallResource(ctx context.Context, req *backend.Call return e.resourceHandler.CallResource(ctx, req, sender) } +func (e *cloudWatchExecutor) checkHealthMetrics(pluginCtx backend.PluginContext) error { + namespace := "AWS/Billing" + metric := "EstimatedCharges" + params := &cloudwatch.ListMetricsInput{ + Namespace: &namespace, + MetricName: &metric, + } + _, err := e.listMetrics(pluginCtx, defaultRegion, params) + return err +} + +func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx backend.PluginContext) error { + logsClient, err := e.getCWLogsClient(pluginCtx, defaultRegion) + if err != nil { + return err + } + _, err = e.handleDescribeLogGroups(ctx, logsClient, simplejson.NewFromAny(map[string]interface{}{"limit": "1"})) + return err +} + +func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + status := backend.HealthStatusOk + metricsTest := "Successfully queried the CloudWatch metrics API." + logsTest := "Successfully queried the CloudWatch logs API." + + err := e.checkHealthMetrics(req.PluginContext) + if err != nil { + status = backend.HealthStatusError + metricsTest = fmt.Sprintf("CloudWatch metrics query failed: %s", err.Error()) + } + + err = e.checkHealthLogs(ctx, req.PluginContext) + if err != nil { + status = backend.HealthStatusError + logsTest = fmt.Sprintf("CloudWatch logs query failed: %s", err.Error()) + } + + return &backend.CheckHealthResult{ + Status: status, + Message: fmt.Sprintf("1. %s\n2. %s", metricsTest, logsTest), + }, nil +} + func (e *cloudWatchExecutor) newSession(pluginCtx backend.PluginContext, region string) (*session.Session, error) { dsInfo, err := e.getDSInfo(pluginCtx) if err != nil { diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index 928667d61f9..b0d6a187da3 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -1,12 +1,24 @@ package cloudwatch import ( + "context" + "fmt" "testing" + "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" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,3 +84,101 @@ func TestNewInstanceSettings(t *testing.T) { }) } } + +func Test_CheckHealth(t *testing.T) { + origNewCWClient := NewCWClient + origNewCWLogsClient := NewCWLogsClient + t.Cleanup(func() { + NewCWClient = origNewCWClient + NewCWLogsClient = origNewCWLogsClient + }) + + var client fakeCheckHealthClient + NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { + return client + } + NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI { + return client + } + + t.Run("successfully query metrics and logs", func(t *testing.T) { + client = fakeCheckHealthClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "1. Successfully queried the CloudWatch metrics API.\n2. Successfully queried the CloudWatch logs API.", + }, resp) + }) + + 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) { + return nil, fmt.Errorf("some logs query error") + }} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "1. Successfully queried the CloudWatch metrics API.\n2. CloudWatch logs query failed: some logs query error", + }, resp) + }) + + t.Run("successfully queries logs, fails during metrics query", func(t *testing.T) { + client = fakeCheckHealthClient{ + listMetricsPages: func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { + return fmt.Errorf("some list metrics error") + }} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "1. CloudWatch metrics query failed: some list metrics error\n2. Successfully queried the CloudWatch logs API.", + }, resp) + }) + + t.Run("fail to get clients", func(t *testing.T) { + client = fakeCheckHealthClient{} + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { + return nil, fmt.Errorf("some sessions error") + }}) + + resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + }) + + assert.NoError(t, err) + assert.Equal(t, &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "1. CloudWatch metrics query failed: some sessions error\n2. CloudWatch logs query failed: some sessions error", + }, resp) + }) +} diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index d8c8ce1f07a..eeb34a5ad5c 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -172,6 +172,29 @@ func (c fakeRGTAClient) GetResourcesPages(in *resourcegroupstaggingapi.GetResour return nil } +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) +} + +func (c fakeCheckHealthClient) ListMetricsPages(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { + if c.listMetricsPages != nil { + return c.listMetricsPages(input, fn) + } + 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...) + } + return nil, nil +} + func chunkSlice(slice []*cloudwatch.Metric, chunkSize int) [][]*cloudwatch.Metric { var chunks [][]*cloudwatch.Metric for { @@ -194,9 +217,13 @@ func newTestConfig() *setting.Cfg { } type fakeSessionCache struct { + getSession func(c awsds.SessionConfig) (*session.Session, error) } func (s fakeSessionCache) GetSession(c awsds.SessionConfig) (*session.Session, error) { + if s.getSession != nil { + return s.getSession(c) + } return &session.Session{ Config: &aws.Config{}, }, nil diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 4e1c038829e..734fa4c93fe 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -17,7 +17,6 @@ import { toLegacyResponseData, } from '@grafana/data'; import { DataSourceWithBackend, FetchError, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; -import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; @@ -870,24 +869,6 @@ export class CloudWatchDatasource ); } - async testDatasource() { - // use billing metrics for test - const region = this.defaultRegion; - const namespace = 'AWS/Billing'; - const metricName = 'EstimatedCharges'; - const dimensions = {}; - - try { - await this.getDimensionValues(region ?? '', namespace, metricName, 'ServiceName', dimensions); - return { - status: 'success', - message: 'Data source is working', - }; - } catch (error) { - return toTestingStatus(error); - } - } - awsRequest(url: string, data: MetricRequest, headers: Record = {}): Observable { const options = { method: 'POST',