From ecaa1dcbfd86d33b816edef2f17d48775fe75ef7 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 20 Jun 2022 11:33:13 +0100 Subject: [PATCH] AzureMonitor: Migrate to backend checkHealth API (#50448) * Add check health functions for each datasource and generic checkHealth function * Log backend errors * Update testDatasource function - Remove unused testDatasource functions from pseudo datasources * Switch datasource to extend DataSourceWithBackend * Improve errors and responses from health endpoint * Fix backend lint issues * Remove unneeded frontend tests * Remove unused/unnecessary datasource methods * Update types * Improve message construction * Stubbing out checkHealth tests * Update tests - Remove comments - Simplify structure * Update log analytics health check to query data rather than retrieve workspace metadata * Fix lint issue * Fix frontend lint issues * Update pkg/tsdb/azuremonitor/azuremonitor.go Co-authored-by: Andres Martinez Gotor * Updates based on PR comments - Don't use deprecated default workspace field - Handle situation if no workspace is found by notifying user - Correctly handle health responses * Remove debug line * Make use of defined api versions * Remove field validation functions * Expose errors in frontend * Update errors and tests * Remove instanceSettings * Update error handling * Improve error handling and presentation * Update tests and correctly check error type * Refactor AzureHealthCheckError and update tests * Fix lint errors Co-authored-by: Andres Martinez Gotor --- pkg/tsdb/azuremonitor/azuremonitor.go | 203 +++++++++++++ pkg/tsdb/azuremonitor/azuremonitor_test.go | 279 ++++++++++++++++++ .../metrics/azuremonitor-datasource.go | 4 +- .../azure-resource-graph-datasource.go | 4 +- pkg/tsdb/azuremonitor/types/types.go | 26 ++ .../azure_log_analytics_datasource.test.ts | 41 +-- .../azure_log_analytics_datasource.ts | 59 ---- .../azure_monitor_datasource.test.ts | 47 +-- .../azure_monitor/azure_monitor_datasource.ts | 42 +-- .../datasource.ts | 53 +--- 10 files changed, 525 insertions(+), 233 deletions(-) diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 509ae8d6433..151d9122ba1 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -1,10 +1,14 @@ package azuremonitor import ( + "bytes" "context" "encoding/json" + "errors" "fmt" + "io" "net/http" + "strconv" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" @@ -161,3 +165,202 @@ func (s *Service) newQueryMux() *datasource.QueryTypeMux { } return mux } + +func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (types.DatasourceInfo, error) { + i, err := s.im.Get(pluginCtx) + if err != nil { + return types.DatasourceInfo{}, err + } + + instance, ok := i.(types.DatasourceInfo) + if !ok { + return types.DatasourceInfo{}, fmt.Errorf("failed to cast datsource info") + } + + return instance, nil +} + +func checkAzureMonitorMetricsHealth(dsInfo types.DatasourceInfo) (*http.Response, error) { + url := fmt.Sprintf("%v/subscriptions?api-version=%v", dsInfo.Routes["Azure Monitor"].URL, metrics.AzureMonitorAPIVersion) + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + res, err := dsInfo.Services["Azure Monitor"].HTTPClient.Do(request) + if err != nil { + return nil, fmt.Errorf("%w: %v", types.ErrorAzureHealthCheck, err) + } + + return res, nil +} + +func checkAzureLogAnalyticsHealth(dsInfo types.DatasourceInfo) (*http.Response, error) { + workspacesUrl := fmt.Sprintf("%v/subscriptions/%v/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview", dsInfo.Routes["Azure Monitor"].URL, dsInfo.Settings.SubscriptionId) + workspacesReq, err := http.NewRequest(http.MethodGet, workspacesUrl, nil) + if err != nil { + return nil, err + } + res, err := dsInfo.Services["Azure Monitor"].HTTPClient.Do(workspacesReq) + if err != nil { + return nil, fmt.Errorf("%w: %v", types.ErrorAzureHealthCheck, err) + } + var target struct { + Value []types.LogAnalyticsWorkspaceResponse + } + err = json.NewDecoder(res.Body).Decode(&target) + if err != nil { + return nil, err + } + + if len(target.Value) == 0 { + return nil, errors.New("no default workspace found") + } + defaultWorkspaceId := target.Value[0].Properties.CustomerId + + body, err := json.Marshal(map[string]interface{}{ + "query": "AzureActivity | limit 1", + }) + if err != nil { + return nil, err + } + + workspaceUrl := fmt.Sprintf("%v/v1/workspaces/%v/query", dsInfo.Routes["Azure Log Analytics"].URL, defaultWorkspaceId) + workspaceReq, err := http.NewRequest(http.MethodPost, workspaceUrl, bytes.NewBuffer(body)) + workspaceReq.Header.Set("Content-Type", "application/json") + if err != nil { + return nil, err + } + + res, err = dsInfo.Services["Azure Log Analytics"].HTTPClient.Do(workspaceReq) + if err != nil { + return nil, fmt.Errorf("%w: %v", types.ErrorAzureHealthCheck, err) + } + + return res, nil +} + +func checkAzureMonitorResourceGraphHealth(dsInfo types.DatasourceInfo) (*http.Response, error) { + body, err := json.Marshal(map[string]interface{}{ + "query": "Resources | project id | limit 1", + "subscriptions": []string{dsInfo.Settings.SubscriptionId}, + }) + if err != nil { + return nil, err + } + url := fmt.Sprintf("%v/providers/Microsoft.ResourceGraph/resources?api-version=%v", dsInfo.Routes["Azure Resource Graph"].URL, resourcegraph.ArgAPIVersion) + request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) + request.Header.Set("Content-Type", "application/json") + if err != nil { + return nil, err + } + + res, err := dsInfo.Services["Azure Resource Graph"].HTTPClient.Do(request) + if err != nil { + return nil, fmt.Errorf("%w: %v", types.ErrorAzureHealthCheck, err) + } + + return res, nil +} + +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + dsInfo, err := s.getDSInfo(req.PluginContext) + if err != nil { + return nil, err + } + + status := backend.HealthStatusOk + metricsLog := "Successfully connected to Azure Monitor endpoint." + logAnalyticsLog := "Successfully connected to Azure Log Analytics endpoint." + graphLog := "Successfully connected to Azure Resource Graph endpoint." + + metricsRes, err := checkAzureMonitorMetricsHealth(dsInfo) + if err != nil || metricsRes.StatusCode != 200 { + status = backend.HealthStatusError + if err != nil { + if ok := errors.Is(err, types.ErrorAzureHealthCheck); ok { + metricsLog = fmt.Sprintf("Error connecting to Azure Monitor endpoint: %s", err.Error()) + } else { + return nil, err + } + } else { + body, err := io.ReadAll(metricsRes.Body) + if err != nil { + return nil, err + } + metricsLog = fmt.Sprintf("Error connecting to Azure Monitor endpoint: %s", string(body)) + } + } + + logsRes, err := checkAzureLogAnalyticsHealth(dsInfo) + if err != nil || logsRes.StatusCode != 200 { + status = backend.HealthStatusError + if err != nil { + if err.Error() == "no default workspace found" { + status = backend.HealthStatusUnknown + logAnalyticsLog = "No Log Analytics workspaces found." + } else if ok := errors.Is(err, types.ErrorAzureHealthCheck); ok { + logAnalyticsLog = fmt.Sprintf("Error connecting to Azure Log Analytics endpoint: %s", err.Error()) + } else { + return nil, err + } + } else { + body, err := io.ReadAll(logsRes.Body) + if err != nil { + return nil, err + } + logAnalyticsLog = fmt.Sprintf("Error connecting to Azure Log Analytics endpoint: %s", string(body)) + } + } + + resourceGraphRes, err := checkAzureMonitorResourceGraphHealth(dsInfo) + if err != nil || resourceGraphRes.StatusCode != 200 { + status = backend.HealthStatusError + if err != nil { + if ok := errors.Is(err, types.ErrorAzureHealthCheck); ok { + graphLog = fmt.Sprintf("Error connecting to Azure Resource Graph endpoint: %s", err.Error()) + } else { + return nil, err + } + } else { + body, err := io.ReadAll(resourceGraphRes.Body) + if err != nil { + return nil, err + } + graphLog = fmt.Sprintf("Error connecting to Azure Resource Graph endpoint: %s", string(body)) + } + } + + defer func() { + if metricsRes != nil { + if err := metricsRes.Body.Close(); err != nil { + backend.Logger.Error("Failed to close response body", "err", err) + } + } + if logsRes != nil { + if err := logsRes.Body.Close(); logsRes != nil && err != nil { + backend.Logger.Error("Failed to close response body", "err", err) + } + } + if resourceGraphRes != nil { + if err := resourceGraphRes.Body.Close(); resourceGraphRes != nil && err != nil { + backend.Logger.Error("Failed to close response body", "err", err) + } + } + }() + + if status == backend.HealthStatusOk { + return &backend.CheckHealthResult{ + Status: status, + Message: "Successfully connected to all Azure Monitor endpoints.", + }, nil + } + + return &backend.CheckHealthResult{ + Status: status, + Message: "One or more health checks failed. See details below.", + JSONDetails: []byte( + fmt.Sprintf(`{"verboseMessage": %s }`, strconv.Quote(fmt.Sprintf("1. %s\n2. %s\n3. %s", metricsLog, logAnalyticsLog, graphLog))), + ), + }, nil +} diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index bad21ca99fc..d4555dfa706 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -1,8 +1,13 @@ package azuremonitor import ( + "bytes" "context" + "encoding/json" + "errors" + "io/ioutil" "net/http" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -16,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -66,14 +72,18 @@ func TestNewInstanceSettings(t *testing.T) { } type fakeInstance struct { + cloud string routes map[string]types.AzRoute services map[string]types.DatasourceService + settings types.AzureMonitorSettings } func (f *fakeInstance) Get(pluginContext backend.PluginContext) (instancemgmt.Instance, error) { return types.DatasourceInfo{ + Cloud: f.cloud, Routes: f.routes, Services: f.services, + Settings: f.settings, }, nil } @@ -158,3 +168,272 @@ func Test_newMux(t *testing.T) { }) } } + +type RoundTripFunc func(req *http.Request) (*http.Response, error) + +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} +func NewTestClient(fn RoundTripFunc) *http.Client { + return &http.Client{ + Transport: fn, + } +} + +func TestCheckHealth(t *testing.T) { + logAnalyticsResponse := func(empty bool) (*http.Response, error) { + if !empty { + body := struct { + Value []types.LogAnalyticsWorkspaceResponse + }{Value: []types.LogAnalyticsWorkspaceResponse{{ + Id: "abcd-1234", + Location: "location", + Name: "test-workspace", + Properties: types.LogAnalyticsWorkspaceProperties{ + CreatedDate: "", + CustomerId: "abcd-1234", + Features: types.LogAnalyticsWorkspaceFeatures{}, + }, + ProvisioningState: "provisioned", + PublicNetworkAccessForIngestion: "enabled", + PublicNetworkAccessForQuery: "disabled", + RetentionInDays: 0}, + }} + bodyMarshal, err := json.Marshal(body) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBuffer(bodyMarshal)), + Header: make(http.Header), + }, nil + } else { + body := struct { + Value []types.LogAnalyticsWorkspaceResponse + }{Value: []types.LogAnalyticsWorkspaceResponse{}} + bodyMarshal, err := json.Marshal(body) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBuffer(bodyMarshal)), + Header: make(http.Header), + }, nil + } + } + azureMonitorClient := func(logAnalyticsEmpty bool, fail bool) *http.Client { + return NewTestClient(func(req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.String(), "workspaces") { + return logAnalyticsResponse(logAnalyticsEmpty) + } else { + if !fail { + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString("OK")), + Header: make(http.Header), + }, nil + } else { + return &http.Response{ + StatusCode: 404, + Body: ioutil.NopCloser(bytes.NewBufferString("not found")), + Header: make(http.Header), + }, nil + } + } + }) + } + okClient := NewTestClient(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString("OK")), + Header: make(http.Header), + }, nil + }) + failClient := func(azureHealthCheckError bool) *http.Client { + return NewTestClient(func(req *http.Request) (*http.Response, error) { + if azureHealthCheckError { + return nil, errors.New("not found") + } + return &http.Response{ + StatusCode: 404, + Body: ioutil.NopCloser(bytes.NewBufferString("not found")), + Header: make(http.Header), + }, nil + }) + } + + cloud := "AzureCloud" + tests := []struct { + name string + errorExpected bool + expectedResult *backend.CheckHealthResult + customServices map[string]types.DatasourceService + }{ + { + name: "Successfully queries all endpoints", + errorExpected: false, + expectedResult: &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Successfully connected to all Azure Monitor endpoints.", + }, + customServices: map[string]types.DatasourceService{ + azureMonitor: { + URL: routes[cloud]["Azure Monitor"].URL, + HTTPClient: azureMonitorClient(false, false), + }, + azureLogAnalytics: { + URL: routes[cloud]["Azure Log Analytics"].URL, + HTTPClient: okClient, + }, + azureResourceGraph: { + URL: routes[cloud]["Azure Resource Graph"].URL, + HTTPClient: okClient, + }}, + }, + { + name: "Successfully queries all endpoints except metrics", + errorExpected: false, + expectedResult: &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "One or more health checks failed. See details below.", + JSONDetails: []byte( + `{"verboseMessage": "1. Error connecting to Azure Monitor endpoint: not found\n2. Successfully connected to Azure Log Analytics endpoint.\n3. Successfully connected to Azure Resource Graph endpoint." }`), + }, + customServices: map[string]types.DatasourceService{ + azureMonitor: { + URL: routes[cloud]["Azure Monitor"].URL, + HTTPClient: azureMonitorClient(false, true), + }, + azureLogAnalytics: { + URL: routes[cloud]["Azure Log Analytics"].URL, + HTTPClient: okClient, + }, + azureResourceGraph: { + URL: routes[cloud]["Azure Resource Graph"].URL, + HTTPClient: okClient, + }}, + }, + { + name: "Successfully queries all endpoints except log analytics", + errorExpected: false, + expectedResult: &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "One or more health checks failed. See details below.", + JSONDetails: []byte( + `{"verboseMessage": "1. Successfully connected to Azure Monitor endpoint.\n2. Error connecting to Azure Log Analytics endpoint: not found\n3. Successfully connected to Azure Resource Graph endpoint." }`), + }, + customServices: map[string]types.DatasourceService{ + azureMonitor: { + URL: routes[cloud]["Azure Monitor"].URL, + HTTPClient: azureMonitorClient(false, false), + }, + azureLogAnalytics: { + URL: routes[cloud]["Azure Log Analytics"].URL, + HTTPClient: failClient(false), + }, + azureResourceGraph: { + URL: routes[cloud]["Azure Resource Graph"].URL, + HTTPClient: okClient, + }}, + }, + { + name: "Successfully queries all endpoints except resource graph", + errorExpected: false, + expectedResult: &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "One or more health checks failed. See details below.", + JSONDetails: []byte( + `{"verboseMessage": "1. Successfully connected to Azure Monitor endpoint.\n2. Successfully connected to Azure Log Analytics endpoint.\n3. Error connecting to Azure Resource Graph endpoint: not found" }`), + }, + customServices: map[string]types.DatasourceService{ + azureMonitor: { + URL: routes[cloud]["Azure Monitor"].URL, + HTTPClient: azureMonitorClient(false, false), + }, + azureLogAnalytics: { + URL: routes[cloud]["Azure Log Analytics"].URL, + HTTPClient: okClient, + }, + azureResourceGraph: { + URL: routes[cloud]["Azure Resource Graph"].URL, + HTTPClient: failClient(false), + }}, + }, + { + name: "Successfully returns UNKNOWN status if no log analytics workspace is found", + errorExpected: false, + expectedResult: &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "One or more health checks failed. See details below.", + JSONDetails: []byte( + `{"verboseMessage": "1. Successfully connected to Azure Monitor endpoint.\n2. No Log Analytics workspaces found.\n3. Successfully connected to Azure Resource Graph endpoint." }`), + }, + customServices: map[string]types.DatasourceService{ + azureMonitor: { + URL: routes[cloud]["Azure Monitor"].URL, + HTTPClient: azureMonitorClient(true, false), + }, + azureLogAnalytics: { + URL: routes[cloud]["Azure Log Analytics"].URL, + HTTPClient: okClient, + }, + azureResourceGraph: { + URL: routes[cloud]["Azure Resource Graph"].URL, + HTTPClient: okClient, + }}, + }, + { + name: "Successfully returns Azure health check errors", + errorExpected: false, + expectedResult: &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "One or more health checks failed. See details below.", + JSONDetails: []byte( + `{"verboseMessage": "1. Error connecting to Azure Monitor endpoint: health check failed: Get \"https://management.azure.com/subscriptions?api-version=2018-01-01\": not found\n2. Error connecting to Azure Log Analytics endpoint: health check failed: Get \"https://management.azure.com/subscriptions//providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview\": not found\n3. Error connecting to Azure Resource Graph endpoint: health check failed: Post \"https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-06-01-preview\": not found" }`), + }, + customServices: map[string]types.DatasourceService{ + azureMonitor: { + URL: routes[cloud]["Azure Monitor"].URL, + HTTPClient: failClient(true), + }, + azureLogAnalytics: { + URL: routes[cloud]["Azure Log Analytics"].URL, + HTTPClient: failClient(true), + }, + azureResourceGraph: { + URL: routes[cloud]["Azure Resource Graph"].URL, + HTTPClient: failClient(true), + }}, + }, + } + + instance := &fakeInstance{ + cloud: cloud, + routes: routes[cloud], + services: map[string]types.DatasourceService{}, + settings: types.AzureMonitorSettings{ + LogAnalyticsDefaultWorkspace: "workspace-id", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + instance.services = tt.customServices + s := &Service{ + im: instance, + } + res, err := s.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }}) + if tt.errorExpected { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expectedResult, res) + }) + } +} diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index 316c915c36d..f7c73a8ef18 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -36,7 +36,7 @@ var ( resourceNameLandmark = regexp.MustCompile(`(?i)(/(?P[\w-\.]+)/providers/Microsoft\.Insights/metrics)`) ) -const azureMonitorAPIVersion = "2018-01-01" +const AzureMonitorAPIVersion = "2018-01-01" func (e *AzureMonitorDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { e.Proxy.Do(rw, req, cli) @@ -114,7 +114,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf } params := url.Values{} - params.Add("api-version", azureMonitorAPIVersion) + params.Add("api-version", AzureMonitorAPIVersion) params.Add("timespan", fmt.Sprintf("%v/%v", query.TimeRange.From.UTC().Format(time.RFC3339), query.TimeRange.To.UTC().Format(time.RFC3339))) params.Add("interval", timeGrain) params.Add("aggregation", azJSONModel.Aggregation) diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index e736cb58c26..b23d8da2439 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -46,7 +46,7 @@ type AzureResourceGraphQuery struct { TimeRange backend.TimeRange } -const argAPIVersion = "2021-06-01-preview" +const ArgAPIVersion = "2021-06-01-preview" const argQueryProviderName = "/providers/Microsoft.ResourceGraph/resources" func (e *AzureResourceGraphDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { @@ -123,7 +123,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query * dataResponse := backend.DataResponse{} params := url.Values{} - params.Add("api-version", argAPIVersion) + params.Add("api-version", ArgAPIVersion) dataResponseErrorWithExecuted := func(err error) backend.DataResponse { dataResponse = backend.DataResponse{Error: err} diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index 5213bf5399c..bb5aaf22dca 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "net/http" "net/url" @@ -190,3 +191,28 @@ type MetricVisualization struct { type ServiceProxy interface { Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter } + +type LogAnalyticsWorkspaceFeatures struct { + EnableLogAccessUsingOnlyResourcePermissions bool `json:"enableLogAccessUsingOnlyResourcePermissions"` + Legacy int `json:"legacy"` + SearchVersion int `json:"searchVersion"` +} + +type LogAnalyticsWorkspaceProperties struct { + CreatedDate string `json:"createdDate"` + CustomerId string `json:"customerId"` + Features LogAnalyticsWorkspaceFeatures `json:"features"` +} + +type LogAnalyticsWorkspaceResponse struct { + Id string `json:"id"` + Location string `json:"location"` + Name string `json:"name"` + Properties LogAnalyticsWorkspaceProperties `json:"properties"` + ProvisioningState string `json:"provisioningState"` + PublicNetworkAccessForIngestion string `json:"publicNetworkAccessForIngestion"` + PublicNetworkAccessForQuery string `json:"publicNetworkAccessForQuery"` + RetentionInDays int `json:"retentionInDays"` +} + +var ErrorAzureHealthCheck = errors.New("health check failed") diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 56144881df2..3d56a7be3b5 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -7,7 +7,7 @@ import createMockQuery from '../__mocks__/query'; import { createTemplateVariables } from '../__mocks__/utils'; import { singleVariable } from '../__mocks__/variables'; import AzureMonitorDatasource from '../datasource'; -import { AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types'; +import { AzureMonitorQuery, AzureQueryType } from '../types'; import FakeSchemaData from './__mocks__/schema'; import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; @@ -42,45 +42,6 @@ describe('AzureLogAnalyticsDatasource', () => { ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings); }); - describe('When performing testDatasource', () => { - beforeEach(() => { - ctx.instanceSettings.jsonData.azureAuthType = 'msi'; - }); - - describe('and an error is returned', () => { - const error = { - data: { - error: { - code: 'InvalidApiVersionParameter', - message: `An error message.`, - }, - }, - status: 400, - statusText: 'Bad Request', - }; - - beforeEach(() => { - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockRejectedValue(error); - }); - - it('should return error status and a detailed error message', () => { - return ctx.ds.azureLogAnalyticsDatasource.testDatasource().then((result: DatasourceValidationResult) => { - expect(result.status).toEqual('error'); - expect(result.message).toEqual( - 'Azure Log Analytics requires access to Azure Monitor but had the following error: Bad Request: InvalidApiVersionParameter. An error message.' - ); - }); - }); - }); - - it('should not include double slashes when getting the resource', async () => { - ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = '/foo/bar'; - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockResolvedValue(true); - await ctx.ds.azureLogAnalyticsDatasource.testDatasource(); - expect(ctx.ds.azureLogAnalyticsDatasource.getResource).toHaveBeenCalledWith('loganalytics/v1/foo/bar/metadata'); - }); - }); - describe('When performing getSchema', () => { beforeEach(() => { ctx.mockGetResource = jest.fn().mockImplementation((path: string) => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 4b5bb2b7095..5e5cdbe4c01 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -320,65 +320,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< }); } - async testDatasource(): Promise { - const validationError = this.validateDatasource(); - if (validationError) { - return validationError; - } - - let resourceOrWorkspace: string; - try { - const result = await this.getFirstWorkspace(); - if (!result) { - return { - status: 'error', - message: 'Workspace not found.', - }; - } - resourceOrWorkspace = result; - } catch (e) { - let message = 'Azure Log Analytics requires access to Azure Monitor but had the following error: '; - return { - status: 'error', - message: this.getErrorMessage(message, e), - }; - } - - try { - const path = isGUIDish(resourceOrWorkspace) - ? `${this.resourcePath}/v1/workspaces/${resourceOrWorkspace}/metadata` - : `${this.resourcePath}/v1${resourceOrWorkspace}/metadata`; - - return await this.getResource(path).then((response: any) => { - return { - status: 'success', - message: 'Successfully queried the Azure Log Analytics service.', - title: 'Success', - }; - }); - } catch (e) { - let message = 'Azure Log Analytics: '; - return { - status: 'error', - message: this.getErrorMessage(message, e), - }; - } - } - - private getErrorMessage(message: string, error: any) { - message += error.statusText ? error.statusText + ': ' : ''; - if (error.data && error.data.error && error.data.error.code) { - message += error.data.error.code + '. ' + error.data.error.message; - } else if (error.data && error.data.error) { - message += error.data.error; - } else if (error.data) { - message += error.data; - } else { - message += 'Cannot connect to Azure Log Analytics REST API.'; - } - return message; - } - private validateDatasource(): DatasourceValidationResult | undefined { const authType = getAuthType(this.instanceSettings); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 5e490151cfc..979e3112d79 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -7,7 +7,7 @@ import createMockQuery from '../__mocks__/query'; import { createTemplateVariables } from '../__mocks__/utils'; import { singleVariable, subscriptionsVariable } from '../__mocks__/variables'; import AzureMonitorDatasource from '../datasource'; -import { AzureDataSourceJsonData, AzureQueryType, DatasourceValidationResult } from '../types'; +import { AzureDataSourceJsonData, AzureQueryType } from '../types'; const templateSrv = new TemplateSrv(); @@ -34,51 +34,6 @@ describe('AzureMonitorDatasource', () => { ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings); }); - describe('When performing testDatasource', () => { - describe('and an error is returned', () => { - const error = { - data: { - error: { - code: 'InvalidApiVersionParameter', - message: `An error message.`, - }, - }, - status: 400, - statusText: 'Bad Request', - }; - - beforeEach(() => { - ctx.instanceSettings.jsonData.azureAuthType = 'msi'; - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockRejectedValue(error); - }); - - it('should return error status and a detailed error message', () => { - return ctx.ds.azureMonitorDatasource.testDatasource().then((result: DatasourceValidationResult) => { - expect(result.status).toEqual('error'); - expect(result.message).toEqual('Azure Monitor: Bad Request: InvalidApiVersionParameter. An error message.'); - }); - }); - }); - - describe('and a list of resource groups is returned', () => { - const response = { - value: [{ name: 'grp1' }, { name: 'grp2' }], - }; - - beforeEach(() => { - ctx.instanceSettings.jsonData.tenantId = 'xxx'; - ctx.instanceSettings.jsonData.clientId = 'xxx'; - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue({ data: response, status: 200 }); - }); - - it('should return success status', () => { - return ctx.ds.azureMonitorDatasource.testDatasource().then((result: DatasourceValidationResult) => { - expect(result.status).toEqual('success'); - }); - }); - }); - }); - describe('When performing getMetricNamespaces', () => { const response = { value: [ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index a55e9288b65..0f7273eda6d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -1,7 +1,7 @@ import { find, startsWith } from 'lodash'; import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; -import { DataSourceWithBackend, getTemplateSrv, isFetchError } from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { resourceTypeDisplayNames, supportedMetricNamespaces } from '../azureMetadata'; @@ -292,46 +292,6 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - const validationError = this.validateDatasource(); - if (validationError) { - return Promise.resolve(validationError); - } - - try { - const url = `${this.resourcePath}/subscriptions?api-version=2019-03-01`; - - return await this.getResource(url).then((response: any) => { - return { - status: 'success', - message: 'Successfully queried the Azure Monitor service.', - title: 'Success', - }; - }); - } catch (e) { - let message = 'Azure Monitor: '; - if (isFetchError(e)) { - message += e.statusText ? e.statusText + ': ' : ''; - - if (e.data && e.data.error && e.data.error.code) { - message += e.data.error.code + '. ' + e.data.error.message; - } else if (e.data && e.data.error) { - message += e.data.error; - } else if (e.data) { - message += e.data; - } else { - message += 'Cannot connect to Azure Monitor REST API.'; - } - } else { - message += 'Cannot connect to Azure Monitor REST API.'; - } - return { - status: 'error', - message: message, - }; - } - } - private validateDatasource(): DatasourceValidationResult | undefined { const authType = getAuthType(this.instanceSettings); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index f5adb0410cd..6110e9d2ae3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -1,4 +1,4 @@ -import { cloneDeep, upperFirst } from 'lodash'; +import { cloneDeep } from 'lodash'; import { forkJoin, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -6,23 +6,23 @@ import { DataFrame, DataQueryRequest, DataQueryResponse, - DataSourceApi, DataSourceInstanceSettings, LoadingState, ScopedVars, } from '@grafana/data'; +import { DataSourceWithBackend } from '@grafana/runtime'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource'; import ResourcePickerData from './resourcePicker/resourcePickerData'; -import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from './types'; +import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from './types'; import migrateAnnotation from './utils/migrateAnnotation'; import { datasourceMigrations } from './utils/migrateQuery'; import { VariableSupport } from './variables'; -export default class Datasource extends DataSourceApi { +export default class Datasource extends DataSourceWithBackend { annotations = { prepareAnnotation: migrateAnnotation, }; @@ -142,48 +142,23 @@ export default class Datasource extends DataSourceApi { - const promises: Array> = []; - - promises.push(this.azureMonitorDatasource.testDatasource()); - promises.push(this.azureLogAnalyticsDatasource.testDatasource()); - - return await Promise.all(promises).then((results) => { - let status: 'success' | 'error' = 'success'; - let message = ''; - - for (let i = 0; i < results.length; i++) { - if (results[i].status !== 'success') { - status = results[i].status; - } - message += `${i + 1}. ${results[i].message} `; - } - - return { - status: status, - message: message, - title: upperFirst(status), - }; - }); - } - /* Azure Monitor REST API methods */ getResourceGroups(subscriptionId: string) { - return this.azureMonitorDatasource.getResourceGroups(this.replaceTemplateVariable(subscriptionId)); + return this.azureMonitorDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId)); } getMetricDefinitions(subscriptionId: string, resourceGroup: string) { return this.azureMonitorDatasource.getMetricDefinitions( - this.replaceTemplateVariable(subscriptionId), - this.replaceTemplateVariable(resourceGroup) + this.templateSrv.replace(subscriptionId), + this.templateSrv.replace(resourceGroup) ); } getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) { return this.azureMonitorDatasource.getResourceNames( - this.replaceTemplateVariable(subscriptionId), - this.replaceTemplateVariable(resourceGroup), - this.replaceTemplateVariable(metricDefinition) + this.templateSrv.replace(subscriptionId), + this.templateSrv.replace(resourceGroup), + this.templateSrv.replace(metricDefinition) ); } @@ -212,17 +187,9 @@ export default class Datasource extends DataSourceApi `$${v.name}`); } - - isTemplateVariable(value: string) { - return this.getVariables().includes(value); - } } function hasQueryForType(query: AzureMonitorQuery): boolean {