mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Azure Monitor: Implement CallResourceHandler in the backend (#35581)
This commit is contained in:
parent
a6872deeb9
commit
96efbbaed1
@ -20,7 +20,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ApplicationInsightsDatasource calls the application insights query API.
|
// ApplicationInsightsDatasource calls the application insights query API.
|
||||||
type ApplicationInsightsDatasource struct{}
|
type ApplicationInsightsDatasource struct {
|
||||||
|
proxy serviceProxy
|
||||||
|
}
|
||||||
|
|
||||||
// ApplicationInsightsQuery is the model that holds the information
|
// ApplicationInsightsQuery is the model that holds the information
|
||||||
// needed to make a metrics query to Application Insights, and the information
|
// needed to make a metrics query to Application Insights, and the information
|
||||||
@ -41,8 +43,12 @@ type ApplicationInsightsQuery struct {
|
|||||||
aggregation string
|
aggregation string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *ApplicationInsightsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||||
|
e.proxy.Do(rw, req, cli)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *ApplicationInsightsDatasource) executeTimeSeriesQuery(ctx context.Context,
|
func (e *ApplicationInsightsDatasource) executeTimeSeriesQuery(ctx context.Context,
|
||||||
originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||||
result := backend.NewQueryDataResponse()
|
result := backend.NewQueryDataResponse()
|
||||||
|
|
||||||
queries, err := e.buildQueries(originalQueries)
|
queries, err := e.buildQueries(originalQueries)
|
||||||
@ -51,7 +57,7 @@ func (e *ApplicationInsightsDatasource) executeTimeSeriesQuery(ctx context.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
queryRes, err := e.executeQuery(ctx, query, dsInfo)
|
queryRes, err := e.executeQuery(ctx, query, dsInfo, client, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -122,11 +128,11 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []backend.DataQuery
|
|||||||
return applicationInsightsQueries, nil
|
return applicationInsightsQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query *ApplicationInsightsQuery, dsInfo datasourceInfo) (
|
func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query *ApplicationInsightsQuery, dsInfo datasourceInfo, client *http.Client, url string) (
|
||||||
backend.DataResponse, error) {
|
backend.DataResponse, error) {
|
||||||
dataResponse := backend.DataResponse{}
|
dataResponse := backend.DataResponse{}
|
||||||
|
|
||||||
req, err := e.createRequest(ctx, dsInfo)
|
req, err := e.createRequest(ctx, dsInfo, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dataResponse.Error = err
|
dataResponse.Error = err
|
||||||
return dataResponse, nil
|
return dataResponse, nil
|
||||||
@ -154,7 +160,7 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query
|
|||||||
}
|
}
|
||||||
|
|
||||||
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
|
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
|
||||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[appInsights].HTTPClient, req)
|
res, err := ctxhttp.Do(ctx, client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dataResponse.Error = err
|
dataResponse.Error = err
|
||||||
return dataResponse, nil
|
return dataResponse, nil
|
||||||
@ -193,16 +199,14 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query
|
|||||||
return dataResponse, nil
|
return dataResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||||
appInsightsAppID := dsInfo.Settings.AppInsightsAppId
|
appInsightsAppID := dsInfo.Settings.AppInsightsAppId
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[appInsights].URL, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
azlog.Debug("Failed to create request", "error", err)
|
azlog.Debug("Failed to create request", "error", err)
|
||||||
return nil, errutil.Wrap("Failed to create request", err)
|
return nil, errutil.Wrap("Failed to create request", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("X-API-Key", dsInfo.DecryptedSecureJSONData["appInsightsApiKey"])
|
|
||||||
|
|
||||||
req.URL.Path = fmt.Sprintf("/v1/apps/%s", appInsightsAppID)
|
req.URL.Path = fmt.Sprintf("/v1/apps/%s", appInsightsAppID)
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
|
@ -3,11 +3,9 @@ package azuremonitor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -207,11 +205,9 @@ func TestInsightsDimensionsUnmarshalJSON(t *testing.T) {
|
|||||||
|
|
||||||
func TestAppInsightsCreateRequest(t *testing.T) {
|
func TestAppInsightsCreateRequest(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
url := "http://ds"
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
Settings: azureMonitorSettings{AppInsightsAppId: "foo"},
|
Settings: azureMonitorSettings{AppInsightsAppId: "foo"},
|
||||||
Services: map[string]datasourceService{
|
|
||||||
appInsights: {URL: "http://ds"},
|
|
||||||
},
|
|
||||||
DecryptedSecureJSONData: map[string]string{
|
DecryptedSecureJSONData: map[string]string{
|
||||||
"appInsightsApiKey": "key",
|
"appInsightsApiKey": "key",
|
||||||
},
|
},
|
||||||
@ -220,15 +216,11 @@ func TestAppInsightsCreateRequest(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
expectedURL string
|
expectedURL string
|
||||||
expectedHeaders http.Header
|
|
||||||
Err require.ErrorAssertionFunc
|
Err require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "creates a request",
|
name: "creates a request",
|
||||||
expectedURL: "http://ds/v1/apps/foo",
|
expectedURL: "http://ds/v1/apps/foo",
|
||||||
expectedHeaders: http.Header{
|
|
||||||
"X-Api-Key": []string{"key"},
|
|
||||||
},
|
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -236,14 +228,11 @@ func TestAppInsightsCreateRequest(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ds := ApplicationInsightsDatasource{}
|
ds := ApplicationInsightsDatasource{}
|
||||||
req, err := ds.createRequest(ctx, dsInfo)
|
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||||
tt.Err(t, err)
|
tt.Err(t, err)
|
||||||
if req.URL.String() != tt.expectedURL {
|
if req.URL.String() != tt.expectedURL {
|
||||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||||
}
|
}
|
||||||
if !cmp.Equal(req.Header, tt.expectedHeaders) {
|
|
||||||
t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, tt.expectedHeaders))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AzureLogAnalyticsDatasource calls the Azure Log Analytics API's
|
// AzureLogAnalyticsDatasource calls the Azure Log Analytics API's
|
||||||
type AzureLogAnalyticsDatasource struct{}
|
type AzureLogAnalyticsDatasource struct {
|
||||||
|
proxy serviceProxy
|
||||||
|
}
|
||||||
|
|
||||||
// AzureLogAnalyticsQuery is the query request that is built from the saved values for
|
// AzureLogAnalyticsQuery is the query request that is built from the saved values for
|
||||||
// from the UI
|
// from the UI
|
||||||
@ -36,11 +38,15 @@ type AzureLogAnalyticsQuery struct {
|
|||||||
TimeRange backend.TimeRange
|
TimeRange backend.TimeRange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *AzureLogAnalyticsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||||
|
e.proxy.Do(rw, req, cli)
|
||||||
|
}
|
||||||
|
|
||||||
// executeTimeSeriesQuery does the following:
|
// executeTimeSeriesQuery does the following:
|
||||||
// 1. build the AzureMonitor url and querystring for each query
|
// 1. build the AzureMonitor url and querystring for each query
|
||||||
// 2. executes each query by calling the Azure Monitor API
|
// 2. executes each query by calling the Azure Monitor API
|
||||||
// 3. parses the responses for each query into data frames
|
// 3. parses the responses for each query into data frames
|
||||||
func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||||
result := backend.NewQueryDataResponse()
|
result := backend.NewQueryDataResponse()
|
||||||
|
|
||||||
queries, err := e.buildQueries(originalQueries, dsInfo)
|
queries, err := e.buildQueries(originalQueries, dsInfo)
|
||||||
@ -49,7 +55,7 @@ func (e *AzureLogAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo)
|
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo, client, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@ -119,7 +125,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []backend.DataQuery,
|
|||||||
return azureLogAnalyticsQueries, nil
|
return azureLogAnalyticsQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo datasourceInfo) backend.DataResponse {
|
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo datasourceInfo, client *http.Client, url string) backend.DataResponse {
|
||||||
dataResponse := backend.DataResponse{}
|
dataResponse := backend.DataResponse{}
|
||||||
|
|
||||||
dataResponseErrorWithExecuted := func(err error) backend.DataResponse {
|
dataResponseErrorWithExecuted := func(err error) backend.DataResponse {
|
||||||
@ -140,8 +146,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
|
|||||||
return dataResponseErrorWithExecuted(fmt.Errorf("Log Analytics credentials are no longer supported. Go to the data source configuration to update Azure Monitor credentials")) //nolint:golint,stylecheck
|
return dataResponseErrorWithExecuted(fmt.Errorf("Log Analytics credentials are no longer supported. Go to the data source configuration to update Azure Monitor credentials")) //nolint:golint,stylecheck
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := e.createRequest(ctx, dsInfo)
|
req, err := e.createRequest(ctx, dsInfo, url)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dataResponse.Error = err
|
dataResponse.Error = err
|
||||||
return dataResponse
|
return dataResponse
|
||||||
@ -167,7 +172,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
|
|||||||
}
|
}
|
||||||
|
|
||||||
azlog.Debug("AzureLogAnalytics", "Request ApiURL", req.URL.String())
|
azlog.Debug("AzureLogAnalytics", "Request ApiURL", req.URL.String())
|
||||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[azureLogAnalytics].HTTPClient, req)
|
res, err := ctxhttp.Do(ctx, client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dataResponseErrorWithExecuted(err)
|
return dataResponseErrorWithExecuted(err)
|
||||||
}
|
}
|
||||||
@ -217,8 +222,8 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
|
|||||||
return dataResponse
|
return dataResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[azureLogAnalytics].URL, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
azlog.Debug("Failed to create request", "error", err)
|
azlog.Debug("Failed to create request", "error", err)
|
||||||
return nil, errutil.Wrap("failed to create request", err)
|
return nil, errutil.Wrap("failed to create request", err)
|
||||||
|
@ -181,11 +181,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
|
|||||||
|
|
||||||
func TestLogAnalyticsCreateRequest(t *testing.T) {
|
func TestLogAnalyticsCreateRequest(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dsInfo := datasourceInfo{
|
url := "http://ds"
|
||||||
Services: map[string]datasourceService{
|
dsInfo := datasourceInfo{}
|
||||||
azureLogAnalytics: {URL: "http://ds"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -204,7 +201,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ds := AzureLogAnalyticsDatasource{}
|
ds := AzureLogAnalyticsDatasource{}
|
||||||
req, err := ds.createRequest(ctx, dsInfo)
|
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||||
tt.Err(t, err)
|
tt.Err(t, err)
|
||||||
if req.URL.String() != tt.expectedURL {
|
if req.URL.String() != tt.expectedURL {
|
||||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||||
@ -231,7 +228,7 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) {
|
|||||||
Params: url.Values{},
|
Params: url.Values{},
|
||||||
TimeRange: backend.TimeRange{},
|
TimeRange: backend.TimeRange{},
|
||||||
}
|
}
|
||||||
res := ds.executeQuery(ctx, query, dsInfo)
|
res := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services[azureLogAnalytics].URL)
|
||||||
if res.Error == nil {
|
if res.Error == nil {
|
||||||
t.Fatal("expecting an error")
|
t.Fatal("expecting an error")
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AzureResourceGraphDatasource calls the Azure Resource Graph API's
|
// AzureResourceGraphDatasource calls the Azure Resource Graph API's
|
||||||
type AzureResourceGraphDatasource struct{}
|
type AzureResourceGraphDatasource struct {
|
||||||
|
proxy serviceProxy
|
||||||
|
}
|
||||||
|
|
||||||
// AzureResourceGraphQuery is the query request that is built from the saved values for
|
// AzureResourceGraphQuery is the query request that is built from the saved values for
|
||||||
// from the UI
|
// from the UI
|
||||||
@ -38,11 +40,15 @@ type AzureResourceGraphQuery struct {
|
|||||||
const argAPIVersion = "2021-03-01"
|
const argAPIVersion = "2021-03-01"
|
||||||
const argQueryProviderName = "/providers/Microsoft.ResourceGraph/resources"
|
const argQueryProviderName = "/providers/Microsoft.ResourceGraph/resources"
|
||||||
|
|
||||||
|
func (e *AzureResourceGraphDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||||
|
e.proxy.Do(rw, req, cli)
|
||||||
|
}
|
||||||
|
|
||||||
// executeTimeSeriesQuery does the following:
|
// executeTimeSeriesQuery does the following:
|
||||||
// 1. builds the AzureMonitor url and querystring for each query
|
// 1. builds the AzureMonitor url and querystring for each query
|
||||||
// 2. executes each query by calling the Azure Monitor API
|
// 2. executes each query by calling the Azure Monitor API
|
||||||
// 3. parses the responses for each query into data frames
|
// 3. parses the responses for each query into data frames
|
||||||
func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||||
result := &backend.QueryDataResponse{
|
result := &backend.QueryDataResponse{
|
||||||
Responses: map[string]backend.DataResponse{},
|
Responses: map[string]backend.DataResponse{},
|
||||||
}
|
}
|
||||||
@ -53,7 +59,7 @@ func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo)
|
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo, client, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@ -95,7 +101,7 @@ func (e *AzureResourceGraphDatasource) buildQueries(queries []backend.DataQuery,
|
|||||||
return azureResourceGraphQueries, nil
|
return azureResourceGraphQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, dsInfo datasourceInfo) backend.DataResponse {
|
func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, dsInfo datasourceInfo, client *http.Client, dsURL string) backend.DataResponse {
|
||||||
dataResponse := backend.DataResponse{}
|
dataResponse := backend.DataResponse{}
|
||||||
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
@ -132,7 +138,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
|||||||
return dataResponse
|
return dataResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := e.createRequest(ctx, dsInfo, reqBody)
|
req, err := e.createRequest(ctx, dsInfo, reqBody, dsURL)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dataResponse.Error = err
|
dataResponse.Error = err
|
||||||
@ -159,7 +165,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
|||||||
}
|
}
|
||||||
|
|
||||||
azlog.Debug("AzureResourceGraph", "Request ApiURL", req.URL.String())
|
azlog.Debug("AzureResourceGraph", "Request ApiURL", req.URL.String())
|
||||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[azureResourceGraph].HTTPClient, req)
|
res, err := ctxhttp.Do(ctx, client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dataResponseErrorWithExecuted(err)
|
return dataResponseErrorWithExecuted(err)
|
||||||
}
|
}
|
||||||
@ -182,8 +188,8 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *
|
|||||||
return dataResponse
|
return dataResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte) (*http.Request, error) {
|
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, reqBody []byte, url string) (*http.Request, error) {
|
||||||
req, err := http.NewRequest(http.MethodPost, dsInfo.Services[azureResourceGraph].URL, bytes.NewBuffer(reqBody))
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
azlog.Debug("Failed to create request", "error", err)
|
azlog.Debug("Failed to create request", "error", err)
|
||||||
return nil, errutil.Wrap("failed to create request", err)
|
return nil, errutil.Wrap("failed to create request", err)
|
||||||
|
@ -76,11 +76,8 @@ func TestBuildingAzureResourceGraphQueries(t *testing.T) {
|
|||||||
|
|
||||||
func TestAzureResourceGraphCreateRequest(t *testing.T) {
|
func TestAzureResourceGraphCreateRequest(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dsInfo := datasourceInfo{
|
url := "http://ds"
|
||||||
Services: map[string]datasourceService{
|
dsInfo := datasourceInfo{}
|
||||||
azureResourceGraph: {URL: "http://ds"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -102,7 +99,7 @@ func TestAzureResourceGraphCreateRequest(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ds := AzureResourceGraphDatasource{}
|
ds := AzureResourceGraphDatasource{}
|
||||||
req, err := ds.createRequest(ctx, dsInfo, []byte{})
|
req, err := ds.createRequest(ctx, dsInfo, []byte{}, url)
|
||||||
tt.Err(t, err)
|
tt.Err(t, err)
|
||||||
if req.URL.String() != tt.expectedURL {
|
if req.URL.String() != tt.expectedURL {
|
||||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||||
|
@ -21,7 +21,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AzureMonitorDatasource calls the Azure Monitor API - one of the four API's supported
|
// AzureMonitorDatasource calls the Azure Monitor API - one of the four API's supported
|
||||||
type AzureMonitorDatasource struct{}
|
type AzureMonitorDatasource struct {
|
||||||
|
proxy serviceProxy
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds
|
// 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds
|
||||||
@ -30,11 +32,15 @@ var (
|
|||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
// executeTimeSeriesQuery does the following:
|
// executeTimeSeriesQuery does the following:
|
||||||
// 1. build the AzureMonitor url and querystring for each query
|
// 1. build the AzureMonitor url and querystring for each query
|
||||||
// 2. executes each query by calling the Azure Monitor API
|
// 2. executes each query by calling the Azure Monitor API
|
||||||
// 3. parses the responses for each query into data frames
|
// 3. parses the responses for each query into data frames
|
||||||
func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||||
result := backend.NewQueryDataResponse()
|
result := backend.NewQueryDataResponse()
|
||||||
|
|
||||||
queries, err := e.buildQueries(originalQueries, dsInfo)
|
queries, err := e.buildQueries(originalQueries, dsInfo)
|
||||||
@ -43,7 +49,7 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
queryRes, resp, err := e.executeQuery(ctx, query, dsInfo)
|
queryRes, resp, err := e.executeQuery(ctx, query, dsInfo, client, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -149,10 +155,10 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf
|
|||||||
return azureMonitorQueries, nil
|
return azureMonitorQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, dsInfo datasourceInfo) (backend.DataResponse, AzureMonitorResponse, error) {
|
func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, dsInfo datasourceInfo, cli *http.Client, url string) (backend.DataResponse, AzureMonitorResponse, error) {
|
||||||
dataResponse := backend.DataResponse{}
|
dataResponse := backend.DataResponse{}
|
||||||
|
|
||||||
req, err := e.createRequest(ctx, dsInfo)
|
req, err := e.createRequest(ctx, dsInfo, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dataResponse.Error = err
|
dataResponse.Error = err
|
||||||
return dataResponse, AzureMonitorResponse{}, nil
|
return dataResponse, AzureMonitorResponse{}, nil
|
||||||
@ -180,7 +186,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM
|
|||||||
|
|
||||||
azlog.Debug("AzureMonitor", "Request ApiURL", req.URL.String())
|
azlog.Debug("AzureMonitor", "Request ApiURL", req.URL.String())
|
||||||
azlog.Debug("AzureMonitor", "Target", query.Target)
|
azlog.Debug("AzureMonitor", "Target", query.Target)
|
||||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[azureMonitor].HTTPClient, req)
|
res, err := ctxhttp.Do(ctx, cli, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dataResponse.Error = err
|
dataResponse.Error = err
|
||||||
return dataResponse, AzureMonitorResponse{}, nil
|
return dataResponse, AzureMonitorResponse{}, nil
|
||||||
@ -200,8 +206,8 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM
|
|||||||
return dataResponse, data, nil
|
return dataResponse, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[azureMonitor].URL, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
azlog.Debug("Failed to create request", "error", err)
|
azlog.Debug("Failed to create request", "error", err)
|
||||||
return nil, errutil.Wrap("Failed to create request", err)
|
return nil, errutil.Wrap("Failed to create request", err)
|
||||||
|
@ -514,11 +514,8 @@ func loadTestFile(t *testing.T, name string) AzureMonitorResponse {
|
|||||||
|
|
||||||
func TestAzureMonitorCreateRequest(t *testing.T) {
|
func TestAzureMonitorCreateRequest(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{}
|
||||||
Services: map[string]datasourceService{
|
url := "http://ds/"
|
||||||
azureMonitor: {URL: "http://ds"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -539,7 +536,7 @@ func TestAzureMonitorCreateRequest(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ds := AzureMonitorDatasource{}
|
ds := AzureMonitorDatasource{}
|
||||||
req, err := ds.createRequest(ctx, dsInfo)
|
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||||
tt.Err(t, err)
|
tt.Err(t, err)
|
||||||
if req.URL.String() != tt.expectedURL {
|
if req.URL.String() != tt.expectedURL {
|
||||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||||
|
125
pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go
Normal file
125
pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package azuremonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTarget(original string) (target string, err error) {
|
||||||
|
splittedPath := strings.Split(original, "/")
|
||||||
|
if len(splittedPath) < 3 {
|
||||||
|
err = fmt.Errorf("the request should contain the service on its path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target = fmt.Sprintf("/%s", strings.Join(splittedPath[2:], "/"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpServiceProxy struct{}
|
||||||
|
|
||||||
|
func (s *httpServiceProxy) Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter {
|
||||||
|
res, err := cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err)))
|
||||||
|
if err != nil {
|
||||||
|
azlog.Error("Unable to write HTTP response", "error", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := res.Body.Close(); err != nil {
|
||||||
|
azlog.Warn("Failed to close response body", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err)))
|
||||||
|
if err != nil {
|
||||||
|
azlog.Error("Unable to write HTTP response", "error", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rw.WriteHeader(res.StatusCode)
|
||||||
|
_, err = rw.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
azlog.Error("Unable to write HTTP response", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range res.Header {
|
||||||
|
rw.Header().Set(k, v[0])
|
||||||
|
for _, v := range v[1:] {
|
||||||
|
rw.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Returning the response write for testing purposes
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getDataSourceFromHTTPReq(req *http.Request) (datasourceInfo, error) {
|
||||||
|
ctx := req.Context()
|
||||||
|
pluginContext := httpadapter.PluginConfigFromContext(ctx)
|
||||||
|
i, err := s.im.Get(pluginContext)
|
||||||
|
if err != nil {
|
||||||
|
return datasourceInfo{}, nil
|
||||||
|
}
|
||||||
|
ds, ok := i.(datasourceInfo)
|
||||||
|
if !ok {
|
||||||
|
return datasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance")
|
||||||
|
}
|
||||||
|
return ds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResponse(rw http.ResponseWriter, code int, msg string) {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, err := rw.Write([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
azlog.Error("Unable to write HTTP response", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) resourceHandler(subDataSource string) func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
return func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
azlog.Debug("Received resource call", "url", req.URL.String(), "method", req.Method)
|
||||||
|
|
||||||
|
newPath, err := getTarget(req.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeResponse(rw, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dsInfo, err := s.getDataSourceFromHTTPReq(req)
|
||||||
|
if err != nil {
|
||||||
|
writeResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service := dsInfo.Services[subDataSource]
|
||||||
|
serviceURL, err := url.Parse(service.URL)
|
||||||
|
if err != nil {
|
||||||
|
writeResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.URL.Path = newPath
|
||||||
|
req.URL.Host = serviceURL.Host
|
||||||
|
req.URL.Scheme = serviceURL.Scheme
|
||||||
|
|
||||||
|
s.executors[subDataSource].resourceRequest(rw, req, service.HTTPClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route definitions shared with the frontend.
|
||||||
|
// Check: /public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts <routeNames>
|
||||||
|
func (s *Service) registerRoutes(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("/azuremonitor/", s.resourceHandler(azureMonitor))
|
||||||
|
mux.HandleFunc("/appinsights/", s.resourceHandler(appInsights))
|
||||||
|
mux.HandleFunc("/loganalytics/", s.resourceHandler(azureLogAnalytics))
|
||||||
|
mux.HandleFunc("/resourcegraph/", s.resourceHandler(azureResourceGraph))
|
||||||
|
}
|
122
pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go
Normal file
122
pkg/tsdb/azuremonitor/azuremonitor-resource-handler_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package azuremonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseResourcePath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
original string
|
||||||
|
expectedTarget string
|
||||||
|
Err require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Path with a subscription",
|
||||||
|
"/azuremonitor/subscriptions/44693801",
|
||||||
|
"/subscriptions/44693801",
|
||||||
|
require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Malformed path",
|
||||||
|
"/subscriptions?44693801",
|
||||||
|
"",
|
||||||
|
require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
target, err := getTarget(tt.original)
|
||||||
|
if target != tt.expectedTarget {
|
||||||
|
t.Errorf("Unexpected target %s expecting %s", target, tt.expectedTarget)
|
||||||
|
}
|
||||||
|
tt.Err(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_proxyRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{"forwards headers and body"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("foo", "bar")
|
||||||
|
_, err := w.Write([]byte("result"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
proxy := httpServiceProxy{}
|
||||||
|
res := proxy.Do(rw, req, srv.Client())
|
||||||
|
if res.Header().Get("foo") != "bar" {
|
||||||
|
t.Errorf("Unexpected headers: %v", res.Header())
|
||||||
|
}
|
||||||
|
result := rw.Result()
|
||||||
|
body, err := ioutil.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
err = result.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if string(body) != "result" {
|
||||||
|
t.Errorf("Unexpected body: %v", string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeProxy struct {
|
||||||
|
requestedURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeProxy) Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter {
|
||||||
|
s.requestedURL = req.URL.String()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_resourceHandler(t *testing.T) {
|
||||||
|
proxy := &fakeProxy{}
|
||||||
|
s := Service{
|
||||||
|
im: &fakeInstance{
|
||||||
|
services: map[string]datasourceService{
|
||||||
|
azureMonitor: {
|
||||||
|
URL: routes[setting.AzurePublic][azureMonitor].URL,
|
||||||
|
HTTPClient: &http.Client{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cfg: &setting.Cfg{},
|
||||||
|
executors: map[string]azDatasourceExecutor{
|
||||||
|
azureMonitor: &AzureMonitorDatasource{
|
||||||
|
proxy: proxy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://foo/azuremonitor/subscriptions/44693801", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error %v", err)
|
||||||
|
}
|
||||||
|
s.resourceHandler(azureMonitor)(rw, req)
|
||||||
|
expectedURL := "https://management.azure.com/subscriptions/44693801"
|
||||||
|
if proxy.requestedURL != expectedURL {
|
||||||
|
t.Errorf("Unexpected result URL. Got %s, expecting %s", proxy.requestedURL, expectedURL)
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
@ -39,10 +40,17 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serviceProxy interface {
|
||||||
|
Do(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
PluginManager plugins.Manager `inject:""`
|
PluginManager plugins.Manager `inject:""`
|
||||||
Cfg *setting.Cfg `inject:""`
|
Cfg *setting.Cfg `inject:""`
|
||||||
BackendPluginManager backendplugin.Manager `inject:""`
|
BackendPluginManager backendplugin.Manager `inject:""`
|
||||||
|
HTTPClientProvider *httpclient.Provider `inject:""`
|
||||||
|
im instancemgmt.InstanceManager
|
||||||
|
executors map[string]azDatasourceExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
type azureMonitorSettings struct {
|
type azureMonitorSettings struct {
|
||||||
@ -55,9 +63,8 @@ type datasourceInfo struct {
|
|||||||
Cloud string
|
Cloud string
|
||||||
Credentials azcredentials.AzureCredentials
|
Credentials azcredentials.AzureCredentials
|
||||||
Settings azureMonitorSettings
|
Settings azureMonitorSettings
|
||||||
Services map[string]datasourceService
|
|
||||||
Routes map[string]azRoute
|
Routes map[string]azRoute
|
||||||
HTTPCliOpts httpclient.Options
|
Services map[string]datasourceService
|
||||||
|
|
||||||
JSONData map[string]interface{}
|
JSONData map[string]interface{}
|
||||||
DecryptedSecureJSONData map[string]string
|
DecryptedSecureJSONData map[string]string
|
||||||
@ -70,7 +77,19 @@ type datasourceService struct {
|
|||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
func getDatasourceService(cfg *setting.Cfg, clientProvider httpclient.Provider, dsInfo datasourceInfo, routeName string) (datasourceService, error) {
|
||||||
|
route := dsInfo.Routes[routeName]
|
||||||
|
client, err := newHTTPClient(route, dsInfo, cfg, clientProvider)
|
||||||
|
if err != nil {
|
||||||
|
return datasourceService{}, err
|
||||||
|
}
|
||||||
|
return datasourceService{
|
||||||
|
URL: dsInfo.Routes[routeName].URL,
|
||||||
|
HTTPClient: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstanceSettings(cfg *setting.Cfg, clientProvider httpclient.Provider, executors map[string]azDatasourceExecutor) datasource.InstanceFactoryFunc {
|
||||||
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||||
jsonData, err := simplejson.NewJson(settings.JSONData)
|
jsonData, err := simplejson.NewJson(settings.JSONData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -99,11 +118,6 @@ func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
|||||||
return nil, fmt.Errorf("error getting credentials: %w", err)
|
return nil, fmt.Errorf("error getting credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpCliOpts, err := settings.HTTPClientOptions()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting http options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := datasourceInfo{
|
model := datasourceInfo{
|
||||||
Cloud: cloud,
|
Cloud: cloud,
|
||||||
Credentials: credentials,
|
Credentials: credentials,
|
||||||
@ -111,9 +125,16 @@ func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
|||||||
JSONData: jsonDataObj,
|
JSONData: jsonDataObj,
|
||||||
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
DecryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||||
DatasourceID: settings.ID,
|
DatasourceID: settings.ID,
|
||||||
Services: map[string]datasourceService{},
|
|
||||||
Routes: routes[cloud],
|
Routes: routes[cloud],
|
||||||
HTTPCliOpts: httpCliOpts,
|
Services: map[string]datasourceService{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for routeName := range executors {
|
||||||
|
service, err := getDatasourceService(cfg, clientProvider, model, routeName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
model.Services[routeName] = service
|
||||||
}
|
}
|
||||||
|
|
||||||
return model, nil
|
return model, nil
|
||||||
@ -121,51 +142,60 @@ func NewInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type azDatasourceExecutor interface {
|
type azDatasourceExecutor interface {
|
||||||
executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error)
|
executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error)
|
||||||
|
resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newExecutor(im instancemgmt.InstanceManager, cfg *setting.Cfg, executors map[string]azDatasourceExecutor) *datasource.QueryTypeMux {
|
func (s *Service) getDataSourceFromPluginReq(req *backend.QueryDataRequest) (datasourceInfo, error) {
|
||||||
|
i, err := s.im.Get(req.PluginContext)
|
||||||
|
if err != nil {
|
||||||
|
return datasourceInfo{}, err
|
||||||
|
}
|
||||||
|
dsInfo, ok := i.(datasourceInfo)
|
||||||
|
if !ok {
|
||||||
|
return datasourceInfo{}, fmt.Errorf("unable to convert datasource from service instance")
|
||||||
|
}
|
||||||
|
dsInfo.OrgID = req.PluginContext.OrgID
|
||||||
|
return dsInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) newMux() *datasource.QueryTypeMux {
|
||||||
mux := datasource.NewQueryTypeMux()
|
mux := datasource.NewQueryTypeMux()
|
||||||
for dsType := range executors {
|
for dsType := range s.executors {
|
||||||
// Make a copy of the string to keep the reference after the iterator
|
// Make a copy of the string to keep the reference after the iterator
|
||||||
dst := dsType
|
dst := dsType
|
||||||
mux.HandleFunc(dsType, func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
mux.HandleFunc(dsType, func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
i, err := im.Get(req.PluginContext)
|
executor := s.executors[dst]
|
||||||
|
dsInfo, err := s.getDataSourceFromPluginReq(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dsInfo := i.(datasourceInfo)
|
service, ok := dsInfo.Services[dst]
|
||||||
dsInfo.OrgID = req.PluginContext.OrgID
|
if !ok {
|
||||||
ds := executors[dst]
|
return nil, fmt.Errorf("missing service for %s", dst)
|
||||||
if _, ok := dsInfo.Services[dst]; !ok {
|
|
||||||
// Create an HTTP Client if it has not been created before
|
|
||||||
route := dsInfo.Routes[dst]
|
|
||||||
client, err := newHTTPClient(route, dsInfo, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
dsInfo.Services[dst] = datasourceService{
|
return executor.executeTimeSeriesQuery(ctx, req.Queries, dsInfo, service.HTTPClient, service.URL)
|
||||||
URL: dsInfo.Routes[dst].URL,
|
|
||||||
HTTPClient: client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ds.executeTimeSeriesQuery(ctx, req.Queries, dsInfo)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Init() error {
|
func (s *Service) Init() error {
|
||||||
im := datasource.NewInstanceManager(NewInstanceSettings(s.Cfg))
|
proxy := &httpServiceProxy{}
|
||||||
executors := map[string]azDatasourceExecutor{
|
s.executors = map[string]azDatasourceExecutor{
|
||||||
azureMonitor: &AzureMonitorDatasource{},
|
azureMonitor: &AzureMonitorDatasource{proxy: proxy},
|
||||||
appInsights: &ApplicationInsightsDatasource{},
|
appInsights: &ApplicationInsightsDatasource{proxy: proxy},
|
||||||
azureLogAnalytics: &AzureLogAnalyticsDatasource{},
|
azureLogAnalytics: &AzureLogAnalyticsDatasource{proxy: proxy},
|
||||||
insightsAnalytics: &InsightsAnalyticsDatasource{},
|
insightsAnalytics: &InsightsAnalyticsDatasource{proxy: proxy},
|
||||||
azureResourceGraph: &AzureResourceGraphDatasource{},
|
azureResourceGraph: &AzureResourceGraphDatasource{proxy: proxy},
|
||||||
}
|
}
|
||||||
|
s.im = datasource.NewInstanceManager(NewInstanceSettings(s.Cfg, *s.HTTPClientProvider, s.executors))
|
||||||
|
mux := s.newMux()
|
||||||
|
resourceMux := http.NewServeMux()
|
||||||
|
s.registerRoutes(resourceMux)
|
||||||
factory := coreplugin.New(backend.ServeOpts{
|
factory := coreplugin.New(backend.ServeOpts{
|
||||||
QueryDataHandler: newExecutor(im, s.Cfg, executors),
|
QueryDataHandler: mux,
|
||||||
|
CallResourceHandler: httpadapter.New(resourceMux),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := s.BackendPluginManager.Register(dsName, factory); err != nil {
|
if err := s.BackendPluginManager.Register(dsName, factory); err != nil {
|
||||||
|
@ -2,11 +2,12 @@ package azuremonitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
|
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
|
||||||
@ -35,6 +36,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
JSONData: map[string]interface{}{"azureAuthType": "msi"},
|
JSONData: map[string]interface{}{"azureAuthType": "msi"},
|
||||||
DatasourceID: 40,
|
DatasourceID: 40,
|
||||||
DecryptedSecureJSONData: map[string]string{"key": "value"},
|
DecryptedSecureJSONData: map[string]string{"key": "value"},
|
||||||
|
Services: map[string]datasourceService{},
|
||||||
},
|
},
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
@ -48,22 +50,25 @@ func TestNewInstanceSettings(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
factory := NewInstanceSettings(cfg)
|
factory := NewInstanceSettings(cfg, httpclient.Provider{}, map[string]azDatasourceExecutor{})
|
||||||
instance, err := factory(tt.settings)
|
instance, err := factory(tt.settings)
|
||||||
tt.Err(t, err)
|
tt.Err(t, err)
|
||||||
if !cmp.Equal(instance, tt.expectedModel, cmpopts.IgnoreFields(datasourceInfo{}, "Services", "HTTPCliOpts")) {
|
if !cmp.Equal(instance, tt.expectedModel) {
|
||||||
t.Errorf("Unexpected instance: %v", cmp.Diff(instance, tt.expectedModel))
|
t.Errorf("Unexpected instance: %v", cmp.Diff(instance, tt.expectedModel))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeInstance struct{}
|
type fakeInstance struct {
|
||||||
|
routes map[string]azRoute
|
||||||
|
services map[string]datasourceService
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeInstance) Get(pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
|
func (f *fakeInstance) Get(pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
|
||||||
return datasourceInfo{
|
return datasourceInfo{
|
||||||
Services: map[string]datasourceService{},
|
Routes: f.routes,
|
||||||
Routes: routes[azureMonitorPublic],
|
Services: f.services,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,19 +82,24 @@ type fakeExecutor struct {
|
|||||||
expectedURL string
|
expectedURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeExecutor) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
func (f *fakeExecutor) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||||
if s, ok := dsInfo.Services[f.queryType]; !ok {
|
}
|
||||||
|
|
||||||
|
func (f *fakeExecutor) executeTimeSeriesQuery(ctx context.Context, originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||||
|
if client == nil {
|
||||||
f.t.Errorf("The HTTP client for %s is missing", f.queryType)
|
f.t.Errorf("The HTTP client for %s is missing", f.queryType)
|
||||||
} else {
|
} else {
|
||||||
if s.URL != f.expectedURL {
|
if url != f.expectedURL {
|
||||||
f.t.Errorf("Unexpected URL %s wanted %s", s.URL, f.expectedURL)
|
f.t.Errorf("Unexpected URL %s wanted %s", url, f.expectedURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &backend.QueryDataResponse{}, nil
|
return &backend.QueryDataResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_newExecutor(t *testing.T) {
|
func Test_newMux(t *testing.T) {
|
||||||
cfg := &setting.Cfg{}
|
cfg := &setting.Cfg{
|
||||||
|
Azure: setting.AzureSettings{},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -113,13 +123,26 @@ func Test_newExecutor(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
mux := newExecutor(&fakeInstance{}, cfg, map[string]azDatasourceExecutor{
|
s := &Service{
|
||||||
|
Cfg: cfg,
|
||||||
|
im: &fakeInstance{
|
||||||
|
routes: routes[azureMonitorPublic],
|
||||||
|
services: map[string]datasourceService{
|
||||||
|
tt.queryType: {
|
||||||
|
URL: routes[azureMonitorPublic][tt.queryType].URL,
|
||||||
|
HTTPClient: &http.Client{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
executors: map[string]azDatasourceExecutor{
|
||||||
tt.queryType: &fakeExecutor{
|
tt.queryType: &fakeExecutor{
|
||||||
t: t,
|
t: t,
|
||||||
queryType: tt.queryType,
|
queryType: tt.queryType,
|
||||||
expectedURL: tt.expectedURL,
|
expectedURL: tt.expectedURL,
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
mux := s.newMux()
|
||||||
res, err := mux.QueryData(context.TODO(), &backend.QueryDataRequest{
|
res, err := mux.QueryData(context.TODO(), &backend.QueryDataRequest{
|
||||||
PluginContext: backend.PluginContext{},
|
PluginContext: backend.PluginContext{},
|
||||||
Queries: []backend.DataQuery{
|
Queries: []backend.DataQuery{
|
||||||
|
@ -8,34 +8,39 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider"
|
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
func httpClientProvider(route azRoute, model datasourceInfo, cfg *setting.Cfg) (*httpclient.Provider, error) {
|
func getMiddlewares(route azRoute, model datasourceInfo, cfg *setting.Cfg) ([]httpclient.Middleware, error) {
|
||||||
var clientProvider *httpclient.Provider
|
middlewares := []httpclient.Middleware{}
|
||||||
|
|
||||||
if len(route.Scopes) > 0 {
|
if len(route.Scopes) > 0 {
|
||||||
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, model.Credentials)
|
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, model.Credentials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
middlewares = append(middlewares, aztokenprovider.AuthMiddleware(tokenProvider, route.Scopes))
|
||||||
|
}
|
||||||
|
|
||||||
clientProvider = httpclient.NewProvider(httpclient.ProviderOptions{
|
if _, ok := model.DecryptedSecureJSONData["appInsightsApiKey"]; ok && (route.URL == azAppInsights.URL || route.URL == azChinaAppInsights.URL) {
|
||||||
Middlewares: []httpclient.Middleware{
|
// Inject API-Key for AppInsights
|
||||||
aztokenprovider.AuthMiddleware(tokenProvider, route.Scopes),
|
apiKeyMiddleware := httpclient.MiddlewareFunc(func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||||
},
|
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||||
|
req.Header.Set("X-API-Key", model.DecryptedSecureJSONData["appInsightsApiKey"])
|
||||||
|
return next.RoundTrip(req)
|
||||||
})
|
})
|
||||||
} else {
|
})
|
||||||
clientProvider = httpclient.NewProvider()
|
middlewares = append(middlewares, apiKeyMiddleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
return clientProvider, nil
|
return middlewares, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPClient(route azRoute, model datasourceInfo, cfg *setting.Cfg) (*http.Client, error) {
|
func newHTTPClient(route azRoute, model datasourceInfo, cfg *setting.Cfg, clientProvider httpclient.Provider) (*http.Client, error) {
|
||||||
model.HTTPCliOpts.Headers = route.Headers
|
m, err := getMiddlewares(route, model, cfg)
|
||||||
|
|
||||||
clientProvider, err := httpClientProvider(route, model, cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return clientProvider.New(model.HTTPCliOpts)
|
return clientProvider.New(httpclient.Options{
|
||||||
|
Headers: route.Headers,
|
||||||
|
Middlewares: m,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -10,21 +10,37 @@ import (
|
|||||||
|
|
||||||
func Test_httpCliProvider(t *testing.T) {
|
func Test_httpCliProvider(t *testing.T) {
|
||||||
cfg := &setting.Cfg{}
|
cfg := &setting.Cfg{}
|
||||||
model := datasourceInfo{
|
|
||||||
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
|
||||||
}
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
route azRoute
|
route azRoute
|
||||||
|
model datasourceInfo
|
||||||
expectedMiddlewares int
|
expectedMiddlewares int
|
||||||
Err require.ErrorAssertionFunc
|
Err require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "creates an HTTP client with a middleware",
|
name: "creates an HTTP client with a middleware due to the scope",
|
||||||
route: azRoute{
|
route: azRoute{
|
||||||
URL: "http://route",
|
URL: "http://route",
|
||||||
Scopes: []string{"http://route/.default"},
|
Scopes: []string{"http://route/.default"},
|
||||||
},
|
},
|
||||||
|
model: datasourceInfo{
|
||||||
|
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||||
|
},
|
||||||
|
expectedMiddlewares: 1,
|
||||||
|
Err: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "creates an HTTP client with a middleware due to an app key",
|
||||||
|
route: azRoute{
|
||||||
|
URL: azAppInsights.URL,
|
||||||
|
Scopes: []string{},
|
||||||
|
},
|
||||||
|
model: datasourceInfo{
|
||||||
|
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||||
|
DecryptedSecureJSONData: map[string]string{
|
||||||
|
"appInsightsApiKey": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
expectedMiddlewares: 1,
|
expectedMiddlewares: 1,
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
@ -34,20 +50,22 @@ func Test_httpCliProvider(t *testing.T) {
|
|||||||
URL: "http://route",
|
URL: "http://route",
|
||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
},
|
},
|
||||||
// httpclient.NewProvider returns a client with 2 middlewares by default
|
model: datasourceInfo{
|
||||||
expectedMiddlewares: 2,
|
Credentials: &azcredentials.AzureClientSecretCredentials{},
|
||||||
|
},
|
||||||
|
expectedMiddlewares: 0,
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cli, err := httpClientProvider(tt.route, model, cfg)
|
m, err := getMiddlewares(tt.route, tt.model, cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Cannot test that the cli middleware works properly since the azcore sdk
|
// Cannot test that the cli middleware works properly since the azcore sdk
|
||||||
// rejects the TLS certs (if provided)
|
// rejects the TLS certs (if provided)
|
||||||
if len(cli.Opts.Middlewares) != tt.expectedMiddlewares {
|
if len(m) != tt.expectedMiddlewares {
|
||||||
t.Errorf("Unexpected middlewares: %v", cli.Opts.Middlewares)
|
t.Errorf("Unexpected middlewares: %v", m)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,9 @@ import (
|
|||||||
"golang.org/x/net/context/ctxhttp"
|
"golang.org/x/net/context/ctxhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InsightsAnalyticsDatasource struct{}
|
type InsightsAnalyticsDatasource struct {
|
||||||
|
proxy serviceProxy
|
||||||
|
}
|
||||||
|
|
||||||
type InsightsAnalyticsQuery struct {
|
type InsightsAnalyticsQuery struct {
|
||||||
RefID string
|
RefID string
|
||||||
@ -31,8 +33,12 @@ type InsightsAnalyticsQuery struct {
|
|||||||
Target string
|
Target string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *InsightsAnalyticsDatasource) resourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) {
|
||||||
|
e.proxy.Do(rw, req, cli)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context,
|
func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context,
|
||||||
originalQueries []backend.DataQuery, dsInfo datasourceInfo) (*backend.QueryDataResponse, error) {
|
originalQueries []backend.DataQuery, dsInfo datasourceInfo, client *http.Client, url string) (*backend.QueryDataResponse, error) {
|
||||||
result := backend.NewQueryDataResponse()
|
result := backend.NewQueryDataResponse()
|
||||||
|
|
||||||
queries, err := e.buildQueries(originalQueries, dsInfo)
|
queries, err := e.buildQueries(originalQueries, dsInfo)
|
||||||
@ -41,7 +47,7 @@ func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo)
|
result.Responses[query.RefID] = e.executeQuery(ctx, query, dsInfo, client, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@ -80,7 +86,7 @@ func (e *InsightsAnalyticsDatasource) buildQueries(queries []backend.DataQuery,
|
|||||||
return iaQueries, nil
|
return iaQueries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery, dsInfo datasourceInfo) backend.DataResponse {
|
func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery, dsInfo datasourceInfo, client *http.Client, url string) backend.DataResponse {
|
||||||
dataResponse := backend.DataResponse{}
|
dataResponse := backend.DataResponse{}
|
||||||
|
|
||||||
dataResponseError := func(err error) backend.DataResponse {
|
dataResponseError := func(err error) backend.DataResponse {
|
||||||
@ -88,7 +94,7 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I
|
|||||||
return dataResponse
|
return dataResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := e.createRequest(ctx, dsInfo)
|
req, err := e.createRequest(ctx, dsInfo, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dataResponseError(err)
|
return dataResponseError(err)
|
||||||
}
|
}
|
||||||
@ -112,7 +118,7 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I
|
|||||||
}
|
}
|
||||||
|
|
||||||
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
|
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
|
||||||
res, err := ctxhttp.Do(ctx, dsInfo.Services[appInsights].HTTPClient, req)
|
res, err := ctxhttp.Do(ctx, client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dataResponseError(err)
|
return dataResponseError(err)
|
||||||
}
|
}
|
||||||
@ -168,15 +174,14 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I
|
|||||||
return dataResponse
|
return dataResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo) (*http.Request, error) {
|
func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo datasourceInfo, url string) (*http.Request, error) {
|
||||||
appInsightsAppID := dsInfo.Settings.AppInsightsAppId
|
appInsightsAppID := dsInfo.Settings.AppInsightsAppId
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, dsInfo.Services[insightsAnalytics].URL, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
azlog.Debug("Failed to create request", "error", err)
|
azlog.Debug("Failed to create request", "error", err)
|
||||||
return nil, errutil.Wrap("Failed to create request", err)
|
return nil, errutil.Wrap("Failed to create request", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("X-API-Key", dsInfo.DecryptedSecureJSONData["appInsightsApiKey"])
|
|
||||||
req.URL.Path = fmt.Sprintf("/v1/apps/%s", appInsightsAppID)
|
req.URL.Path = fmt.Sprintf("/v1/apps/%s", appInsightsAppID)
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInsightsAnalyticsCreateRequest(t *testing.T) {
|
func TestInsightsAnalyticsCreateRequest(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
url := "http://ds"
|
||||||
dsInfo := datasourceInfo{
|
dsInfo := datasourceInfo{
|
||||||
Settings: azureMonitorSettings{AppInsightsAppId: "foo"},
|
Settings: azureMonitorSettings{AppInsightsAppId: "foo"},
|
||||||
Services: map[string]datasourceService{
|
|
||||||
insightsAnalytics: {URL: "http://ds"},
|
|
||||||
},
|
|
||||||
DecryptedSecureJSONData: map[string]string{
|
DecryptedSecureJSONData: map[string]string{
|
||||||
"appInsightsApiKey": "key",
|
"appInsightsApiKey": "key",
|
||||||
},
|
},
|
||||||
@ -30,9 +27,6 @@ func TestInsightsAnalyticsCreateRequest(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "creates a request",
|
name: "creates a request",
|
||||||
expectedURL: "http://ds/v1/apps/foo",
|
expectedURL: "http://ds/v1/apps/foo",
|
||||||
expectedHeaders: http.Header{
|
|
||||||
"X-Api-Key": []string{"key"},
|
|
||||||
},
|
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -40,14 +34,11 @@ func TestInsightsAnalyticsCreateRequest(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ds := InsightsAnalyticsDatasource{}
|
ds := InsightsAnalyticsDatasource{}
|
||||||
req, err := ds.createRequest(ctx, dsInfo)
|
req, err := ds.createRequest(ctx, dsInfo, url)
|
||||||
tt.Err(t, err)
|
tt.Err(t, err)
|
||||||
if req.URL.String() != tt.expectedURL {
|
if req.URL.String() != tt.expectedURL {
|
||||||
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String())
|
||||||
}
|
}
|
||||||
if !cmp.Equal(req.Header, tt.expectedHeaders) {
|
|
||||||
t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, tt.expectedHeaders))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
export function getManagementApiRoute(azureCloud: string): string {
|
|
||||||
switch (azureCloud) {
|
|
||||||
case 'azuremonitor':
|
|
||||||
return 'azuremonitor';
|
|
||||||
case 'chinaazuremonitor':
|
|
||||||
return 'chinaazuremonitor';
|
|
||||||
case 'govazuremonitor':
|
|
||||||
return 'govazuremonitor';
|
|
||||||
case 'germanyazuremonitor':
|
|
||||||
return 'germanyazuremonitor';
|
|
||||||
default:
|
|
||||||
throw new Error('The cloud not supported.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLogAnalyticsApiRoute(azureCloud: string): string {
|
|
||||||
switch (azureCloud) {
|
|
||||||
case 'azuremonitor':
|
|
||||||
return 'loganalyticsazure';
|
|
||||||
case 'chinaazuremonitor':
|
|
||||||
return 'chinaloganalyticsazure';
|
|
||||||
case 'govazuremonitor':
|
|
||||||
return 'govloganalyticsazure';
|
|
||||||
default:
|
|
||||||
throw new Error('The cloud not supported.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAppInsightsApiRoute(azureCloud: string): string {
|
|
||||||
switch (azureCloud) {
|
|
||||||
case 'azuremonitor':
|
|
||||||
return 'appinsights';
|
|
||||||
case 'chinaazuremonitor':
|
|
||||||
return 'chinaappinsights';
|
|
||||||
default:
|
|
||||||
throw new Error('The cloud not supported.');
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,7 +15,6 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AppInsightsDatasource', () => {
|
describe('AppInsightsDatasource', () => {
|
||||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
|
||||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||||
|
|
||||||
const ctx: any = {};
|
const ctx: any = {};
|
||||||
@ -53,8 +52,8 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation(() => {
|
ctx.ds.getResource = jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({ data: response, status: 200 });
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation(() => {
|
ctx.ds.getResource = jest.fn().mockImplementation(() => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -106,7 +105,7 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation(() => {
|
ctx.ds.getResource = jest.fn().mockImplementation(() => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -419,9 +418,9 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.getResource = jest.fn().mockImplementation((path) => {
|
||||||
expect(options.url).toContain('/metrics/metadata');
|
expect(path).toContain('/metrics/metadata');
|
||||||
return Promise.resolve({ data: response, status: 200 });
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -457,9 +456,9 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.getResource = jest.fn().mockImplementation((path) => {
|
||||||
expect(options.url).toContain('/metrics/metadata');
|
expect(path).toContain('/metrics/metadata');
|
||||||
return Promise.resolve({ data: response, status: 200 });
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -485,8 +484,8 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.getResource = jest.fn().mockImplementation((path) => {
|
||||||
expect(options.url).toContain('/metrics/metadata');
|
expect(path).toContain('/metrics/metadata');
|
||||||
return Promise.resolve({ data: response, status: 200 });
|
return Promise.resolve({ data: response, status: 200 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -523,8 +522,8 @@ describe('AppInsightsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.getResource = jest.fn().mockImplementation((path) => {
|
||||||
expect(options.url).toContain('/metrics/metadata');
|
expect(path).toContain('/metrics/metadata');
|
||||||
return Promise.resolve({ data: response, status: 200 });
|
return Promise.resolve({ data: response, status: 200 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { DataQueryRequest, DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data';
|
import { DataQueryRequest, DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data';
|
||||||
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
|
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
import TimegrainConverter from '../time_grain_converter';
|
import TimegrainConverter from '../time_grain_converter';
|
||||||
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types';
|
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types';
|
||||||
|
import { routeNames } from '../utils/common';
|
||||||
import ResponseParser from './response_parser';
|
import ResponseParser from './response_parser';
|
||||||
import { getAzureCloud } from '../credentials';
|
|
||||||
import { getAppInsightsApiRoute } from '../api/routes';
|
|
||||||
|
|
||||||
export interface LogAnalyticsColumn {
|
export interface LogAnalyticsColumn {
|
||||||
text: string;
|
text: string;
|
||||||
@ -14,8 +13,7 @@ export interface LogAnalyticsColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
|
export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
|
||||||
url: string;
|
resourcePath: string;
|
||||||
baseUrl: string;
|
|
||||||
version = 'beta';
|
version = 'beta';
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {};
|
logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {};
|
||||||
@ -24,11 +22,7 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
|
|||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
this.applicationId = instanceSettings.jsonData.appInsightsAppId || '';
|
this.applicationId = instanceSettings.jsonData.appInsightsAppId || '';
|
||||||
|
|
||||||
const cloud = getAzureCloud(instanceSettings);
|
this.resourcePath = `${routeNames.appInsights}/${this.version}/apps/${this.applicationId}`;
|
||||||
const route = getAppInsightsApiRoute(cloud);
|
|
||||||
this.baseUrl = `/${route}/${this.version}/apps/${this.applicationId}`;
|
|
||||||
|
|
||||||
this.url = instanceSettings.url || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isConfigured(): boolean {
|
isConfigured(): boolean {
|
||||||
@ -134,21 +128,14 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
|
|||||||
}
|
}
|
||||||
|
|
||||||
testDatasource(): Promise<DatasourceValidationResult> {
|
testDatasource(): Promise<DatasourceValidationResult> {
|
||||||
const url = `${this.baseUrl}/metrics/metadata`;
|
const path = `${this.resourcePath}/metrics/metadata`;
|
||||||
return this.doRequest(url)
|
return this.getResource(path)
|
||||||
.then<DatasourceValidationResult>((response: any) => {
|
.then<DatasourceValidationResult>((response: any) => {
|
||||||
if (response.status === 200) {
|
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Successfully queried the Application Insights service.',
|
message: 'Successfully queried the Application Insights service.',
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: 'Application Insights: Returned http status code ' + response.status,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
let message = 'Application Insights: ';
|
let message = 'Application Insights: ';
|
||||||
@ -169,29 +156,14 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
doRequest(url: any, maxRetries = 1): Promise<any> {
|
|
||||||
return getBackendSrv()
|
|
||||||
.datasourceRequest({
|
|
||||||
url: this.url + url,
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
if (maxRetries > 0) {
|
|
||||||
return this.doRequest(url, maxRetries - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetricNames() {
|
getMetricNames() {
|
||||||
const url = `${this.baseUrl}/metrics/metadata`;
|
const path = `${this.resourcePath}/metrics/metadata`;
|
||||||
return this.doRequest(url).then(ResponseParser.parseMetricNames);
|
return this.getResource(path).then(ResponseParser.parseMetricNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetricMetadata(metricName: string) {
|
getMetricMetadata(metricName: string) {
|
||||||
const url = `${this.baseUrl}/metrics/metadata`;
|
const path = `${this.resourcePath}/metrics/metadata`;
|
||||||
return this.doRequest(url).then((result: any) => {
|
return this.getResource(path).then((result: any) => {
|
||||||
return new ResponseParser(result).parseMetadata(metricName);
|
return new ResponseParser(result).parseMetadata(metricName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -203,8 +175,8 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
|
|||||||
}
|
}
|
||||||
|
|
||||||
getQuerySchema() {
|
getQuerySchema() {
|
||||||
const url = `${this.baseUrl}/query/schema`;
|
const path = `${this.resourcePath}/query/schema`;
|
||||||
return this.doRequest(url).then((result: any) => {
|
return this.getResource(path).then((result: any) => {
|
||||||
const schema = new ResponseParser(result).parseQuerySchema();
|
const schema = new ResponseParser(result).parseQuerySchema();
|
||||||
// console.log(schema);
|
// console.log(schema);
|
||||||
return schema;
|
return schema;
|
||||||
|
@ -12,11 +12,11 @@ export default class ResponseParser {
|
|||||||
const xaxis = this.results[i].query.xaxis;
|
const xaxis = this.results[i].query.xaxis;
|
||||||
const yaxises = this.results[i].query.yaxis;
|
const yaxises = this.results[i].query.yaxis;
|
||||||
const spliton = this.results[i].query.spliton;
|
const spliton = this.results[i].query.spliton;
|
||||||
columns = this.results[i].result.data.Tables[0].Columns;
|
columns = this.results[i].result.Tables[0].Columns;
|
||||||
const rows = this.results[i].result.data.Tables[0].Rows;
|
const rows = this.results[i].result.Tables[0].Rows;
|
||||||
data = concat(data, this.parseRawQueryResultRow(this.results[i].query, columns, rows, xaxis, yaxises, spliton));
|
data = concat(data, this.parseRawQueryResultRow(this.results[i].query, columns, rows, xaxis, yaxises, spliton));
|
||||||
} else {
|
} else {
|
||||||
const value = this.results[i].result.data.value;
|
const value = this.results[i].result.value;
|
||||||
const alias = this.results[i].query.alias;
|
const alias = this.results[i].query.alias;
|
||||||
data = concat(data, this.parseQueryResultRow(this.results[i].query, value, alias));
|
data = concat(data, this.parseQueryResultRow(this.results[i].query, value, alias));
|
||||||
}
|
}
|
||||||
@ -174,14 +174,14 @@ export default class ResponseParser {
|
|||||||
return dateTime(dateTimeValue).valueOf();
|
return dateTime(dateTimeValue).valueOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseMetricNames(result: { data: { metrics: any } }) {
|
static parseMetricNames(result: { metrics: any }) {
|
||||||
const keys = _keys(result.data.metrics);
|
const keys = _keys(result.metrics);
|
||||||
|
|
||||||
return ResponseParser.toTextValueList(keys);
|
return ResponseParser.toTextValueList(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMetadata(metricName: string) {
|
parseMetadata(metricName: string) {
|
||||||
const metric = this.results.data.metrics[metricName];
|
const metric = this.results.metrics[metricName];
|
||||||
|
|
||||||
if (!metric) {
|
if (!metric) {
|
||||||
throw Error('No data found for metric: ' + metricName);
|
throw Error('No data found for metric: ' + metricName);
|
||||||
@ -203,9 +203,9 @@ export default class ResponseParser {
|
|||||||
Type: 'AppInsights',
|
Type: 'AppInsights',
|
||||||
Tables: {},
|
Tables: {},
|
||||||
};
|
};
|
||||||
if (this.results && this.results.data && this.results.data.Tables) {
|
if (this.results && this.results && this.results.Tables) {
|
||||||
for (let i = 0; i < this.results.data.Tables[0].Rows.length; i++) {
|
for (let i = 0; i < this.results.Tables[0].Rows.length; i++) {
|
||||||
const column = this.results.data.Tables[0].Rows[i];
|
const column = this.results.Tables[0].Rows[i];
|
||||||
const columnTable = column[0];
|
const columnTable = column[0];
|
||||||
const columnName = column[1];
|
const columnName = column[1];
|
||||||
const columnType = column[2];
|
const columnType = column[2];
|
||||||
|
@ -3,14 +3,12 @@ import FakeSchemaData from './__mocks__/schema';
|
|||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { AzureLogsVariable, DatasourceValidationResult } from '../types';
|
import { AzureLogsVariable, DatasourceValidationResult } from '../types';
|
||||||
import { toUtc } from '@grafana/data';
|
import { toUtc } from '@grafana/data';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
|
||||||
|
|
||||||
const templateSrv = new TemplateSrv();
|
const templateSrv = new TemplateSrv();
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||||
getBackendSrv: () => backendSrv,
|
|
||||||
getTemplateSrv: () => templateSrv,
|
getTemplateSrv: () => templateSrv,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -22,13 +20,6 @@ const makeResourceURI = (
|
|||||||
`/subscriptions/${subscriptionID}/resourceGroups/${resourceGroup}/providers/Microsoft.OperationalInsights/workspaces/${resourceName}`;
|
`/subscriptions/${subscriptionID}/resourceGroups/${resourceGroup}/providers/Microsoft.OperationalInsights/workspaces/${resourceName}`;
|
||||||
|
|
||||||
describe('AzureLogAnalyticsDatasource', () => {
|
describe('AzureLogAnalyticsDatasource', () => {
|
||||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
datasourceRequestMock.mockImplementation(jest.fn());
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx: any = {};
|
const ctx: any = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -40,64 +31,6 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When the config option "Same as Azure Monitor" has been chosen', () => {
|
|
||||||
const tableResponseWithOneColumn = {
|
|
||||||
tables: [
|
|
||||||
{
|
|
||||||
name: 'PrimaryResult',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'Category',
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
rows: [['Administrative'], ['Policy']],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const workspaceResponse = {
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
name: 'aworkspace',
|
|
||||||
id: makeResourceURI('a-workspace'),
|
|
||||||
properties: {
|
|
||||||
source: 'Azure',
|
|
||||||
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let workspacesUrl: string;
|
|
||||||
let azureLogAnalyticsUrl: string;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
ctx.instanceSettings.jsonData.subscriptionId = 'xxx';
|
|
||||||
ctx.instanceSettings.jsonData.tenantId = 'xxx';
|
|
||||||
ctx.instanceSettings.jsonData.clientId = 'xxx';
|
|
||||||
ctx.instanceSettings.jsonData.azureLogAnalyticsSameAs = true;
|
|
||||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
|
||||||
|
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
|
||||||
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces?api-version') > -1) {
|
|
||||||
workspacesUrl = options.url;
|
|
||||||
return Promise.resolve({ data: workspaceResponse, status: 200 });
|
|
||||||
} else {
|
|
||||||
azureLogAnalyticsUrl = options.url;
|
|
||||||
return Promise.resolve({ data: tableResponseWithOneColumn, status: 200 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the loganalyticsazure plugin route', async () => {
|
|
||||||
await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
|
|
||||||
|
|
||||||
expect(workspacesUrl).toContain('azuremonitor');
|
|
||||||
expect(azureLogAnalyticsUrl).toContain('loganalyticsazure');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When performing testDatasource', () => {
|
describe('When performing testDatasource', () => {
|
||||||
describe('and an error is returned', () => {
|
describe('and an error is returned', () => {
|
||||||
const error = {
|
const error = {
|
||||||
@ -113,7 +46,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.reject(error));
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockRejectedValue(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error status and a detailed error message', () => {
|
it('should return error status and a detailed error message', () => {
|
||||||
@ -129,9 +62,9 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getSchema', () => {
|
describe('When performing getSchema', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
expect(options.url).toContain('metadata');
|
expect(path).toContain('metadata');
|
||||||
return Promise.resolve({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200, ok: true });
|
return Promise.resolve(FakeSchemaData.getlogAnalyticsFakeMetadata());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -191,9 +124,9 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
|
|
||||||
describe('and is the workspaces() macro', () => {
|
describe('and is the workspaces() macro', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
expect(options.url).toContain('xxx');
|
expect(path).toContain('xxx');
|
||||||
return Promise.resolve({ data: workspacesResponse, status: 200 });
|
return Promise.resolve(workspacesResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
queryResults = await ctx.ds.metricFindQuery('workspaces()');
|
queryResults = await ctx.ds.metricFindQuery('workspaces()');
|
||||||
@ -209,9 +142,9 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
|
|
||||||
describe('and is the workspaces() macro with the subscription parameter', () => {
|
describe('and is the workspaces() macro with the subscription parameter', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123');
|
expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123');
|
||||||
return Promise.resolve({ data: workspacesResponse, status: 200 });
|
return Promise.resolve(workspacesResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
queryResults = await ctx.ds.metricFindQuery('workspaces(11112222-eeee-4949-9b2d-9106972f9123)');
|
queryResults = await ctx.ds.metricFindQuery('workspaces(11112222-eeee-4949-9b2d-9106972f9123)');
|
||||||
@ -227,9 +160,9 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
|
|
||||||
describe('and is the workspaces() macro with the subscription parameter quoted', () => {
|
describe('and is the workspaces() macro with the subscription parameter quoted', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123');
|
expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123');
|
||||||
return Promise.resolve({ data: workspacesResponse, status: 200 });
|
return Promise.resolve(workspacesResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
queryResults = await ctx.ds.metricFindQuery('workspaces("11112222-eeee-4949-9b2d-9106972f9123")');
|
queryResults = await ctx.ds.metricFindQuery('workspaces("11112222-eeee-4949-9b2d-9106972f9123")');
|
||||||
@ -273,11 +206,11 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
if (options.url.indexOf('OperationalInsights/workspaces?api-version=') > -1) {
|
if (path.indexOf('OperationalInsights/workspaces?api-version=') > -1) {
|
||||||
return Promise.resolve({ data: workspaceResponse, status: 200 });
|
return Promise.resolve(workspaceResponse);
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({ data: tableResponseWithOneColumn, status: 200 });
|
return Promise.resolve(tableResponseWithOneColumn);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -337,11 +270,11 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
let annotationResults: any[];
|
let annotationResults: any[];
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
|
if (path.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
|
||||||
return Promise.resolve({ data: workspaceResponse, status: 200 });
|
return Promise.resolve(workspaceResponse);
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({ data: tableResponse, status: 200 });
|
return Promise.resolve(tableResponse);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,17 +15,16 @@ import {
|
|||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
MetricFindValue,
|
MetricFindValue,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend, FetchResponse } from '@grafana/runtime';
|
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
|
||||||
import { Observable, from } from 'rxjs';
|
import { Observable, from } from 'rxjs';
|
||||||
import { mergeMap } from 'rxjs/operators';
|
import { mergeMap } from 'rxjs/operators';
|
||||||
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
||||||
import { getLogAnalyticsApiRoute, getManagementApiRoute } from '../api/routes';
|
|
||||||
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
|
|
||||||
import { isGUIDish } from '../components/ResourcePicker/utils';
|
import { isGUIDish } from '../components/ResourcePicker/utils';
|
||||||
|
import { routeNames } from '../utils/common';
|
||||||
|
|
||||||
interface AdhocQuery {
|
interface AdhocQuery {
|
||||||
datasourceId: number;
|
datasourceId: number;
|
||||||
url: string;
|
path: string;
|
||||||
resultFormat: string;
|
resultFormat: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,14 +32,13 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
AzureMonitorQuery,
|
AzureMonitorQuery,
|
||||||
AzureDataSourceJsonData
|
AzureDataSourceJsonData
|
||||||
> {
|
> {
|
||||||
url: string;
|
resourcePath: string;
|
||||||
baseUrl: string;
|
|
||||||
azurePortalUrl: string;
|
azurePortalUrl: string;
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
|
||||||
defaultSubscriptionId?: string;
|
defaultSubscriptionId?: string;
|
||||||
|
|
||||||
azureMonitorUrl: string;
|
azureMonitorPath: string;
|
||||||
defaultOrFirstWorkspace: string;
|
defaultOrFirstWorkspace: string;
|
||||||
cache: Map<string, any>;
|
cache: Map<string, any>;
|
||||||
|
|
||||||
@ -48,15 +46,11 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
|
|
||||||
|
this.resourcePath = `${routeNames.logAnalytics}`;
|
||||||
|
this.azureMonitorPath = `${routeNames.azureMonitor}/subscriptions`;
|
||||||
const cloud = getAzureCloud(instanceSettings);
|
const cloud = getAzureCloud(instanceSettings);
|
||||||
const logAnalyticsRoute = getLogAnalyticsApiRoute(cloud);
|
|
||||||
this.baseUrl = `/${logAnalyticsRoute}`;
|
|
||||||
this.azurePortalUrl = getAzurePortalUrl(cloud);
|
this.azurePortalUrl = getAzurePortalUrl(cloud);
|
||||||
|
|
||||||
const managementRoute = getManagementApiRoute(cloud);
|
|
||||||
this.azureMonitorUrl = `/${managementRoute}/subscriptions`;
|
|
||||||
|
|
||||||
this.url = instanceSettings.url || '';
|
|
||||||
this.defaultSubscriptionId = this.instanceSettings.jsonData.subscriptionId || '';
|
this.defaultSubscriptionId = this.instanceSettings.jsonData.subscriptionId || '';
|
||||||
this.defaultOrFirstWorkspace = this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace || '';
|
this.defaultOrFirstWorkspace = this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace || '';
|
||||||
}
|
}
|
||||||
@ -71,8 +65,8 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this.azureMonitorUrl}?api-version=2019-03-01`;
|
const path = `${this.azureMonitorPath}?api-version=2019-03-01`;
|
||||||
return await this.doRequest(url).then((result: any) => {
|
return await this.getResource(path).then((result: any) => {
|
||||||
return ResponseParser.parseSubscriptions(result);
|
return ResponseParser.parseSubscriptions(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -81,7 +75,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
const response = await this.getWorkspaceList(subscription);
|
const response = await this.getWorkspaceList(subscription);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
map(response.data.value, (val: any) => {
|
map(response.value, (val: any) => {
|
||||||
return { text: val.name, value: val.id };
|
return { text: val.name, value: val.id };
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
@ -91,20 +85,16 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
const subscriptionId = getTemplateSrv().replace(subscription || this.defaultSubscriptionId);
|
const subscriptionId = getTemplateSrv().replace(subscription || this.defaultSubscriptionId);
|
||||||
|
|
||||||
const workspaceListUrl =
|
const workspaceListUrl =
|
||||||
this.azureMonitorUrl +
|
this.azureMonitorPath +
|
||||||
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
|
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
|
||||||
return this.doRequest(workspaceListUrl, true);
|
return this.getResource(workspaceListUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(resourceUri: string) {
|
async getMetadata(resourceUri: string) {
|
||||||
const url = `${this.baseUrl}/v1${resourceUri}/metadata`;
|
const path = `${this.resourcePath}/v1${resourceUri}/metadata`;
|
||||||
|
|
||||||
const resp = await this.doRequest<AzureLogAnalyticsMetadata>(url);
|
const resp = await this.getResource(path);
|
||||||
if (!resp.ok) {
|
return resp;
|
||||||
throw new Error('Unable to get metadata for workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKustoSchema(resourceUri: string) {
|
async getKustoSchema(resourceUri: string) {
|
||||||
@ -202,7 +192,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
const response = await this.getWorkspaceList(this.defaultSubscriptionId);
|
const response = await this.getWorkspaceList(this.defaultSubscriptionId);
|
||||||
|
|
||||||
const details = response.data.value.find((o: any) => {
|
const details = response.value.find((o: any) => {
|
||||||
return o.properties.customerId === workspaceId;
|
return o.properties.customerId === workspaceId;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -286,14 +276,14 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
);
|
);
|
||||||
|
|
||||||
const querystring = querystringBuilder.generate().uriString;
|
const querystring = querystringBuilder.generate().uriString;
|
||||||
const url = isGUIDish(workspace)
|
const path = isGUIDish(workspace)
|
||||||
? `${this.baseUrl}/v1/workspaces/${workspace}/query?${querystring}`
|
? `${this.resourcePath}/v1/workspaces/${workspace}/query?${querystring}`
|
||||||
: `${this.baseUrl}/v1${workspace}/query?${querystring}`;
|
: `${this.resourcePath}/v1${workspace}/query?${querystring}`;
|
||||||
|
|
||||||
const queries = [
|
const queries = [
|
||||||
{
|
{
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
url: url,
|
path: path,
|
||||||
resultFormat: 'table',
|
resultFormat: 'table',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -370,7 +360,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
|
|
||||||
doQueries(queries: AdhocQuery[]) {
|
doQueries(queries: AdhocQuery[]) {
|
||||||
return map(queries, (query) => {
|
return map(queries, (query) => {
|
||||||
return this.doRequest(query.url)
|
return this.getResource(query.path)
|
||||||
.then((result: any) => {
|
.then((result: any) => {
|
||||||
return {
|
return {
|
||||||
result: result,
|
result: result,
|
||||||
@ -386,32 +376,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRequest<T = any>(url: string, useCache = false, maxRetries = 1): Promise<FetchResponse<T>> {
|
|
||||||
try {
|
|
||||||
if (useCache && this.cache.has(url)) {
|
|
||||||
return this.cache.get(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getBackendSrv().datasourceRequest({
|
|
||||||
url: this.url + url,
|
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (useCache) {
|
|
||||||
this.cache.set(url, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (error) {
|
|
||||||
if (maxRetries > 0) {
|
|
||||||
return this.doRequest(url, useCache, maxRetries - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: update to be completely resource-centric
|
|
||||||
async testDatasource(): Promise<DatasourceValidationResult> {
|
async testDatasource(): Promise<DatasourceValidationResult> {
|
||||||
const validationError = this.validateDatasource();
|
const validationError = this.validateDatasource();
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
@ -437,23 +401,16 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = isGUIDish(resourceOrWorkspace)
|
const path = isGUIDish(resourceOrWorkspace)
|
||||||
? `${this.baseUrl}/v1/workspaces/${resourceOrWorkspace}/metadata`
|
? `${this.resourcePath}/v1/workspaces/${resourceOrWorkspace}/metadata`
|
||||||
: `${this.baseUrl}/v1${resourceOrWorkspace}/metadata`;
|
: `${this.resourcePath}/v1/${resourceOrWorkspace}/metadata`;
|
||||||
|
|
||||||
return await this.doRequest(url).then<DatasourceValidationResult>((response: any) => {
|
return await this.getResource(path).then<DatasourceValidationResult>((response: any) => {
|
||||||
if (response.status === 200) {
|
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Successfully queried the Azure Log Analytics service.',
|
message: 'Successfully queried the Azure Log Analytics service.',
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: 'Returned http status code ' + response.status,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let message = 'Azure Log Analytics: ';
|
let message = 'Azure Log Analytics: ';
|
||||||
|
@ -11,11 +11,11 @@ export default class ResponseParser {
|
|||||||
let data: any[] = [];
|
let data: any[] = [];
|
||||||
let columns: any[] = [];
|
let columns: any[] = [];
|
||||||
for (let i = 0; i < this.results.length; i++) {
|
for (let i = 0; i < this.results.length; i++) {
|
||||||
if (this.results[i].result.data.tables.length === 0) {
|
if (this.results[i].result.tables.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
columns = this.results[i].result.data.tables[0].columns;
|
columns = this.results[i].result.tables[0].columns;
|
||||||
const rows = this.results[i].result.data.tables[0].rows;
|
const rows = this.results[i].result.tables[0].rows;
|
||||||
|
|
||||||
if (this.results[i].query.resultFormat === 'time_series') {
|
if (this.results[i].query.resultFormat === 'time_series') {
|
||||||
data = concat(data, this.parseTimeSeriesResult(this.results[i].query, columns, rows));
|
data = concat(data, this.parseTimeSeriesResult(this.results[i].query, columns, rows));
|
||||||
@ -157,11 +157,11 @@ export default class ResponseParser {
|
|||||||
|
|
||||||
const valueFieldName = 'subscriptionId';
|
const valueFieldName = 'subscriptionId';
|
||||||
const textFieldName = 'displayName';
|
const textFieldName = 'displayName';
|
||||||
for (let i = 0; i < result.data.value.length; i++) {
|
for (let i = 0; i < result.value.length; i++) {
|
||||||
if (!find(list, ['value', get(result.data.value[i], valueFieldName)])) {
|
if (!find(list, ['value', get(result.value[i], valueFieldName)])) {
|
||||||
list.push({
|
list.push({
|
||||||
text: `${get(result.data.value[i], textFieldName)}`,
|
text: `${get(result.value[i], textFieldName)}`,
|
||||||
value: get(result.data.value[i], valueFieldName),
|
value: get(result.value[i], valueFieldName),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,12 @@ import AzureMonitorDatasource from '../datasource';
|
|||||||
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
|
||||||
import { AzureDataSourceJsonData, DatasourceValidationResult } from '../types';
|
import { AzureDataSourceJsonData, DatasourceValidationResult } from '../types';
|
||||||
|
|
||||||
const templateSrv = new TemplateSrv();
|
const templateSrv = new TemplateSrv();
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||||
getBackendSrv: () => backendSrv,
|
|
||||||
getTemplateSrv: () => templateSrv,
|
getTemplateSrv: () => templateSrv,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -20,15 +18,13 @@ interface TestContext {
|
|||||||
|
|
||||||
describe('AzureMonitorDatasource', () => {
|
describe('AzureMonitorDatasource', () => {
|
||||||
const ctx: TestContext = {} as TestContext;
|
const ctx: TestContext = {} as TestContext;
|
||||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
ctx.instanceSettings = ({
|
ctx.instanceSettings = ({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
url: 'http://azuremonitor.com',
|
url: 'http://azuremonitor.com',
|
||||||
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' },
|
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f', cloudName: 'azuremonitor' },
|
||||||
cloudName: 'azuremonitor',
|
|
||||||
} as unknown) as DataSourceInstanceSettings<AzureDataSourceJsonData>;
|
} as unknown) as DataSourceInstanceSettings<AzureDataSourceJsonData>;
|
||||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
||||||
});
|
});
|
||||||
@ -48,7 +44,7 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.reject(error));
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockRejectedValue(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error status and a detailed error message', () => {
|
it('should return error status and a detailed error message', () => {
|
||||||
@ -61,17 +57,13 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('and a list of resource groups is returned', () => {
|
describe('and a list of resource groups is returned', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.instanceSettings.jsonData.tenantId = 'xxx';
|
ctx.instanceSettings.jsonData.tenantId = 'xxx';
|
||||||
ctx.instanceSettings.jsonData.clientId = 'xxx';
|
ctx.instanceSettings.jsonData.clientId = 'xxx';
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 }));
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue({ data: response, status: 200 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return success status', () => {
|
it('should return success status', () => {
|
||||||
@ -85,19 +77,15 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
describe('When performing metricFindQuery', () => {
|
describe('When performing metricFindQuery', () => {
|
||||||
describe('with a subscriptions query', () => {
|
describe('with a subscriptions query', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{ displayName: 'Primary', subscriptionId: 'sub1' },
|
{ displayName: 'Primary', subscriptionId: 'sub1' },
|
||||||
{ displayName: 'Secondary', subscriptionId: 'sub2' },
|
{ displayName: 'Secondary', subscriptionId: 'sub2' },
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a list of subscriptions', async () => {
|
it('should return a list of subscriptions', async () => {
|
||||||
@ -112,15 +100,11 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with a resource groups query', () => {
|
describe('with a resource groups query', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a list of resource groups', async () => {
|
it('should return a list of resource groups', async () => {
|
||||||
@ -135,16 +119,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with a resource groups query that specifies a subscription id', () => {
|
describe('with a resource groups query that specifies a subscription id', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123');
|
expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123');
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -161,23 +141,18 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with namespaces query', () => {
|
describe('with namespaces query', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'test',
|
||||||
type: 'Microsoft.Network/networkInterfaces',
|
type: 'Microsoft.Network/networkInterfaces',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -192,23 +167,18 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with namespaces query that specifies a subscription id', () => {
|
describe('with namespaces query that specifies a subscription id', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'test',
|
||||||
type: 'Microsoft.Network/networkInterfaces',
|
type: 'Microsoft.Network/networkInterfaces',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -223,7 +193,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with resource names query', () => {
|
describe('with resource names query', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'Failure Anomalies - nodeapp',
|
name: 'Failure Anomalies - nodeapp',
|
||||||
@ -234,16 +203,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
type: 'microsoft.insights/components',
|
type: 'microsoft.insights/components',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -258,7 +223,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with resource names query and that specifies a subscription id', () => {
|
describe('with resource names query and that specifies a subscription id', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'Failure Anomalies - nodeapp',
|
name: 'Failure Anomalies - nodeapp',
|
||||||
@ -269,16 +233,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
type: 'microsoft.insights/components',
|
type: 'microsoft.insights/components',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -298,7 +258,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with metric names query', () => {
|
describe('with metric names query', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
@ -313,17 +272,13 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(
|
||||||
expect(options.url).toBe(
|
basePath +
|
||||||
baseUrl +
|
|
||||||
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
|
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
|
||||||
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
|
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
|
||||||
);
|
);
|
||||||
@ -346,7 +301,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with metric names query and specifies a subscription id', () => {
|
describe('with metric names query and specifies a subscription id', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
@ -361,17 +315,13 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
expect(path).toBe(
|
||||||
expect(options.url).toBe(
|
basePath +
|
||||||
baseUrl +
|
|
||||||
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
|
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
|
||||||
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
|
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
|
||||||
);
|
);
|
||||||
@ -394,7 +344,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with metric namespace query', () => {
|
describe('with metric namespace query', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'Microsoft.Compute-virtualMachines',
|
name: 'Microsoft.Compute-virtualMachines',
|
||||||
@ -409,17 +358,13 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(
|
||||||
expect(options.url).toBe(
|
basePath +
|
||||||
baseUrl +
|
|
||||||
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
|
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
|
||||||
);
|
);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
@ -439,7 +384,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('with metric namespace query and specifies a subscription id', () => {
|
describe('with metric namespace query and specifies a subscription id', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'Microsoft.Compute-virtualMachines',
|
name: 'Microsoft.Compute-virtualMachines',
|
||||||
@ -454,17 +398,13 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
|
expect(path).toBe(
|
||||||
expect(options.url).toBe(
|
basePath +
|
||||||
baseUrl +
|
|
||||||
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
|
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
|
||||||
);
|
);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
@ -487,7 +427,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getSubscriptions', () => {
|
describe('When performing getSubscriptions', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
|
id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
|
||||||
@ -507,14 +446,11 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
type: 'Total',
|
type: 'Total',
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return list of subscriptions', () => {
|
it('should return list of subscriptions', () => {
|
||||||
@ -528,15 +464,11 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getResourceGroups', () => {
|
describe('When performing getResourceGroups', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation(() => Promise.resolve(response));
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return list of Resource Groups', () => {
|
it('should return list of Resource Groups', () => {
|
||||||
@ -552,7 +484,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getMetricDefinitions', () => {
|
describe('When performing getMetricDefinitions', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'test',
|
name: 'test',
|
||||||
@ -577,16 +508,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
type: 'Microsoft.Storage/storageAccounts',
|
type: 'Microsoft.Storage/storageAccounts',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -617,7 +544,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
describe('When performing getResourceNames', () => {
|
describe('When performing getResourceNames', () => {
|
||||||
describe('and there are no special cases', () => {
|
describe('and there are no special cases', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'Failure Anomalies - nodeapp',
|
name: 'Failure Anomalies - nodeapp',
|
||||||
@ -628,16 +554,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
type: 'microsoft.insights/components',
|
type: 'microsoft.insights/components',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -655,7 +577,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('and the metric definition is blobServices', () => {
|
describe('and the metric definition is blobServices', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'Failure Anomalies - nodeapp',
|
name: 'Failure Anomalies - nodeapp',
|
||||||
@ -666,16 +587,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
type: 'Microsoft.Storage/storageAccounts',
|
type: 'Microsoft.Storage/storageAccounts',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01');
|
||||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -698,7 +615,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getMetricNames', () => {
|
describe('When performing getMetricNames', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
@ -731,20 +647,16 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
|
||||||
const expected =
|
const expected =
|
||||||
baseUrl +
|
basePath +
|
||||||
'/providers/microsoft.insights/components/resource1' +
|
'/providers/microsoft.insights/components/resource1' +
|
||||||
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
|
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
|
||||||
expect(options.url).toBe(expected);
|
expect(path).toBe(expected);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -770,7 +682,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getMetricMetadata', () => {
|
describe('When performing getMetricMetadata', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
@ -803,20 +714,16 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
|
||||||
const expected =
|
const expected =
|
||||||
baseUrl +
|
basePath +
|
||||||
'/providers/microsoft.insights/components/resource1' +
|
'/providers/microsoft.insights/components/resource1' +
|
||||||
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
|
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
|
||||||
expect(options.url).toBe(expected);
|
expect(path).toBe(expected);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -841,7 +748,6 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getMetricMetadata on metrics with dimensions', () => {
|
describe('When performing getMetricMetadata on metrics with dimensions', () => {
|
||||||
const response = {
|
const response = {
|
||||||
data: {
|
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: {
|
name: {
|
||||||
@ -877,20 +783,16 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
supportedAggregationTypes: ['None', 'Average'],
|
supportedAggregationTypes: ['None', 'Average'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const baseUrl =
|
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
|
||||||
const expected =
|
const expected =
|
||||||
baseUrl +
|
basePath +
|
||||||
'/providers/microsoft.insights/components/resource1' +
|
'/providers/microsoft.insights/components/resource1' +
|
||||||
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
|
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
|
||||||
expect(options.url).toBe(expected);
|
expect(path).toBe(expected);
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
AzureMonitorMetricDefinitionsResponse,
|
AzureMonitorMetricDefinitionsResponse,
|
||||||
AzureMonitorResourceGroupsResponse,
|
AzureMonitorResourceGroupsResponse,
|
||||||
AzureQueryType,
|
AzureQueryType,
|
||||||
AzureMonitorMetricsMetadataResponse,
|
|
||||||
AzureMetricQuery,
|
AzureMetricQuery,
|
||||||
DatasourceValidationResult,
|
DatasourceValidationResult,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
@ -21,14 +20,14 @@ import {
|
|||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getBackendSrv, DataSourceWithBackend, getTemplateSrv, FetchResponse } from '@grafana/runtime';
|
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
|
||||||
import { from, Observable } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
import { mergeMap } from 'rxjs/operators';
|
import { mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
||||||
import { getManagementApiRoute } from '../api/routes';
|
|
||||||
import { resourceTypeDisplayNames } from '../azureMetadata';
|
import { resourceTypeDisplayNames } from '../azureMetadata';
|
||||||
|
import { routeNames } from '../utils/common';
|
||||||
|
|
||||||
const defaultDropdownValue = 'select';
|
const defaultDropdownValue = 'select';
|
||||||
|
|
||||||
@ -46,11 +45,10 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
apiVersion = '2018-01-01';
|
apiVersion = '2018-01-01';
|
||||||
apiPreviewVersion = '2017-12-01-preview';
|
apiPreviewVersion = '2017-12-01-preview';
|
||||||
defaultSubscriptionId?: string;
|
defaultSubscriptionId?: string;
|
||||||
baseUrl: string;
|
resourcePath: string;
|
||||||
azurePortalUrl: string;
|
azurePortalUrl: string;
|
||||||
resourceGroup: string;
|
resourceGroup: string;
|
||||||
resourceName: string;
|
resourceName: string;
|
||||||
url: string;
|
|
||||||
supportedMetricNamespaces: string[] = [];
|
supportedMetricNamespaces: string[] = [];
|
||||||
timeSrv: TimeSrv;
|
timeSrv: TimeSrv;
|
||||||
|
|
||||||
@ -61,12 +59,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
|
this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
|
||||||
|
|
||||||
const cloud = getAzureCloud(instanceSettings);
|
const cloud = getAzureCloud(instanceSettings);
|
||||||
const route = getManagementApiRoute(cloud);
|
this.resourcePath = `${routeNames.azureMonitor}/subscriptions`;
|
||||||
this.baseUrl = `/${route}/subscriptions`;
|
|
||||||
this.azurePortalUrl = getAzurePortalUrl(cloud);
|
|
||||||
|
|
||||||
this.url = instanceSettings.url!;
|
|
||||||
this.supportedMetricNamespaces = new SupportedNamespaces(cloud).get();
|
this.supportedMetricNamespaces = new SupportedNamespaces(cloud).get();
|
||||||
|
this.azurePortalUrl = getAzurePortalUrl(cloud);
|
||||||
}
|
}
|
||||||
|
|
||||||
isConfigured(): boolean {
|
isConfigured(): boolean {
|
||||||
@ -344,22 +339,23 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this.baseUrl}?api-version=2019-03-01`;
|
return this.getResource(`${this.resourcePath}?api-version=2019-03-01`).then((result: any) => {
|
||||||
return await this.doRequest(url).then((result: any) => {
|
|
||||||
return ResponseParser.parseSubscriptions(result);
|
return ResponseParser.parseSubscriptions(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceGroups(subscriptionId: string) {
|
getResourceGroups(subscriptionId: string) {
|
||||||
const url = `${this.baseUrl}/${subscriptionId}/resourceGroups?api-version=${this.apiVersion}`;
|
return this.getResource(
|
||||||
return this.doRequest(url).then((result: AzureMonitorResourceGroupsResponse) => {
|
`${this.resourcePath}/${subscriptionId}/resourceGroups?api-version=${this.apiVersion}`
|
||||||
|
).then((result: AzureMonitorResourceGroupsResponse) => {
|
||||||
return ResponseParser.parseResponseValues(result, 'name', 'name');
|
return ResponseParser.parseResponseValues(result, 'name', 'name');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
|
getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
|
||||||
const url = `${this.baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${this.apiVersion}`;
|
return this.getResource(
|
||||||
return this.doRequest(url)
|
`${this.resourcePath}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${this.apiVersion}`
|
||||||
|
)
|
||||||
.then((result: AzureMonitorMetricDefinitionsResponse) => {
|
.then((result: AzureMonitorMetricDefinitionsResponse) => {
|
||||||
return ResponseParser.parseResponseValues(result, 'type', 'type');
|
return ResponseParser.parseResponseValues(result, 'type', 'type');
|
||||||
})
|
})
|
||||||
@ -410,9 +406,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
}
|
}
|
||||||
|
|
||||||
getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) {
|
getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) {
|
||||||
const url = `${this.baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${this.apiVersion}`;
|
return this.getResource(
|
||||||
|
`${this.resourcePath}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${this.apiVersion}`
|
||||||
return this.doRequest(url).then((result: any) => {
|
).then((result: any) => {
|
||||||
if (!startsWith(metricDefinition, 'Microsoft.Storage/storageAccounts/')) {
|
if (!startsWith(metricDefinition, 'Microsoft.Storage/storageAccounts/')) {
|
||||||
return ResponseParser.parseResourceNames(result, metricDefinition);
|
return ResponseParser.parseResourceNames(result, metricDefinition);
|
||||||
}
|
}
|
||||||
@ -429,7 +425,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
|
|
||||||
getMetricNamespaces(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
|
getMetricNamespaces(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
|
||||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||||
this.baseUrl,
|
this.resourcePath,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
metricDefinition,
|
metricDefinition,
|
||||||
@ -437,7 +433,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
this.apiPreviewVersion
|
this.apiPreviewVersion
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.doRequest(url).then((result: any) => {
|
return this.getResource(url).then((result: any) => {
|
||||||
return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName');
|
return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -450,7 +446,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
metricNamespace: string
|
metricNamespace: string
|
||||||
) {
|
) {
|
||||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||||
this.baseUrl,
|
this.resourcePath,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
metricDefinition,
|
metricDefinition,
|
||||||
@ -459,7 +455,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
this.apiVersion
|
this.apiVersion
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.doRequest(url).then((result: any) => {
|
return this.getResource(url).then((result: any) => {
|
||||||
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
|
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -473,7 +469,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
metricName: string
|
metricName: string
|
||||||
) {
|
) {
|
||||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||||
this.baseUrl,
|
this.resourcePath,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
metricDefinition,
|
metricDefinition,
|
||||||
@ -482,8 +478,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
this.apiVersion
|
this.apiVersion
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.doRequest<AzureMonitorMetricsMetadataResponse>(url).then((result) => {
|
return this.getResource(url).then((result: any) => {
|
||||||
return ResponseParser.parseMetadata(result.data, metricName);
|
return ResponseParser.parseMetadata(result, metricName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,21 +490,14 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${this.baseUrl}?api-version=2019-03-01`;
|
const url = `${this.resourcePath}?api-version=2019-03-01`;
|
||||||
|
|
||||||
return await this.doRequest(url).then<DatasourceValidationResult>((response: any) => {
|
return await this.getResource(url).then<DatasourceValidationResult>((response: any) => {
|
||||||
if (response.status === 200) {
|
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Successfully queried the Azure Monitor service.',
|
message: 'Successfully queried the Azure Monitor service.',
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: 'Returned http status code ' + response.status,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let message = 'Azure Monitor: ';
|
let message = 'Azure Monitor: ';
|
||||||
@ -555,19 +544,4 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
|||||||
private isValidConfigField(field?: string): boolean {
|
private isValidConfigField(field?: string): boolean {
|
||||||
return typeof field === 'string' && field.length > 0;
|
return typeof field === 'string' && field.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
doRequest<T = any>(url: string, maxRetries = 1): Promise<FetchResponse<T>> {
|
|
||||||
return getBackendSrv()
|
|
||||||
.datasourceRequest<T>({
|
|
||||||
url: this.url + url,
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
if (maxRetries > 0) {
|
|
||||||
return this.doRequest<T>(url, maxRetries - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,10 @@ export default class ResponseParser {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < result.data.value.length; i++) {
|
for (let i = 0; i < result.value.length; i++) {
|
||||||
if (!find(list, ['value', get(result.data.value[i], valueFieldName)])) {
|
if (!find(list, ['value', get(result.value[i], valueFieldName)])) {
|
||||||
const value = get(result.data.value[i], valueFieldName);
|
const value = get(result.value[i], valueFieldName);
|
||||||
const text = get(result.data.value[i], textFieldName, value);
|
const text = get(result.value[i], textFieldName, value);
|
||||||
|
|
||||||
list.push({
|
list.push({
|
||||||
text: text,
|
text: text,
|
||||||
@ -39,11 +39,11 @@ export default class ResponseParser {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < result.data.value.length; i++) {
|
for (let i = 0; i < result.value.length; i++) {
|
||||||
if (result.data.value[i].type === metricDefinition) {
|
if (result.value[i].type === metricDefinition) {
|
||||||
list.push({
|
list.push({
|
||||||
text: result.data.value[i].name,
|
text: result.value[i].name,
|
||||||
value: result.data.value[i].name,
|
value: result.value[i].name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,11 +113,11 @@ export default class ResponseParser {
|
|||||||
|
|
||||||
const valueFieldName = 'subscriptionId';
|
const valueFieldName = 'subscriptionId';
|
||||||
const textFieldName = 'displayName';
|
const textFieldName = 'displayName';
|
||||||
for (let i = 0; i < result.data.value.length; i++) {
|
for (let i = 0; i < result.value.length; i++) {
|
||||||
if (!find(list, ['value', get(result.data.value[i], valueFieldName)])) {
|
if (!find(list, ['value', get(result.value[i], valueFieldName)])) {
|
||||||
list.push({
|
list.push({
|
||||||
text: `${get(result.data.value[i], textFieldName)}`,
|
text: `${get(result.value[i], textFieldName)}`,
|
||||||
value: get(result.data.value[i], valueFieldName),
|
value: get(result.value[i], valueFieldName),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ import { getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
|||||||
import { InsightsConfig } from './InsightsConfig';
|
import { InsightsConfig } from './InsightsConfig';
|
||||||
import ResponseParser from '../azure_monitor/response_parser';
|
import ResponseParser from '../azure_monitor/response_parser';
|
||||||
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types';
|
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types';
|
||||||
import { getAzureCloud, isAppInsightsConfigured } from '../credentials';
|
import { isAppInsightsConfigured } from '../credentials';
|
||||||
import { getManagementApiRoute } from '../api/routes';
|
import { routeNames } from '../utils/common';
|
||||||
|
|
||||||
export type Props = DataSourcePluginOptionsEditorProps<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
export type Props = DataSourcePluginOptionsEditorProps<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ export interface State {
|
|||||||
|
|
||||||
export class ConfigEditor extends PureComponent<Props, State> {
|
export class ConfigEditor extends PureComponent<Props, State> {
|
||||||
templateSrv: TemplateSrv = getTemplateSrv();
|
templateSrv: TemplateSrv = getTemplateSrv();
|
||||||
|
baseURL: string;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -33,10 +34,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
|||||||
unsaved: false,
|
unsaved: false,
|
||||||
appInsightsInitiallyConfigured: isAppInsightsConfigured(props.options),
|
appInsightsInitiallyConfigured: isAppInsightsConfigured(props.options),
|
||||||
};
|
};
|
||||||
|
this.baseURL = `/api/datasources/${this.props.options.id}/resources/${routeNames.azureMonitor}/subscriptions`;
|
||||||
if (this.props.options.id) {
|
|
||||||
updateDatasourcePluginOption(this.props, 'url', '/api/datasources/proxy/' + this.props.options.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateOptions = (optionsFunc: (options: AzureDataSourceSettings) => AzureDataSourceSettings): void => {
|
private updateOptions = (optionsFunc: (options: AzureDataSourceSettings) => AzureDataSourceSettings): void => {
|
||||||
@ -61,12 +59,9 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
|||||||
private getSubscriptions = async (): Promise<Array<SelectableValue<string>>> => {
|
private getSubscriptions = async (): Promise<Array<SelectableValue<string>>> => {
|
||||||
await this.saveOptions();
|
await this.saveOptions();
|
||||||
|
|
||||||
const cloud = getAzureCloud(this.props.options);
|
const query = `?api-version=2019-03-01`;
|
||||||
const route = getManagementApiRoute(cloud);
|
|
||||||
const url = `/${route}/subscriptions?api-version=2019-03-01`;
|
|
||||||
|
|
||||||
const result = await getBackendSrv().datasourceRequest({
|
const result = await getBackendSrv().datasourceRequest({
|
||||||
url: this.props.options.url + url,
|
url: this.baseURL + query,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,12 +71,9 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
|||||||
private getLogAnalyticsSubscriptions = async (): Promise<Array<SelectableValue<string>>> => {
|
private getLogAnalyticsSubscriptions = async (): Promise<Array<SelectableValue<string>>> => {
|
||||||
await this.saveOptions();
|
await this.saveOptions();
|
||||||
|
|
||||||
const cloud = getAzureCloud(this.props.options);
|
const query = `?api-version=2019-03-01`;
|
||||||
const route = getManagementApiRoute(cloud);
|
|
||||||
const url = `/${route}/subscriptions?api-version=2019-03-01`;
|
|
||||||
|
|
||||||
const result = await getBackendSrv().datasourceRequest({
|
const result = await getBackendSrv().datasourceRequest({
|
||||||
url: this.props.options.url + url,
|
url: this.baseURL + query,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,12 +83,9 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
|||||||
private getWorkspaces = async (subscriptionId: string): Promise<Array<SelectableValue<string>>> => {
|
private getWorkspaces = async (subscriptionId: string): Promise<Array<SelectableValue<string>>> => {
|
||||||
await this.saveOptions();
|
await this.saveOptions();
|
||||||
|
|
||||||
const cloud = getAzureCloud(this.props.options);
|
const workspaceURL = `/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
|
||||||
const route = getManagementApiRoute(cloud);
|
|
||||||
const url = `/${route}/subscriptions/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
|
|
||||||
|
|
||||||
const result = await getBackendSrv().datasourceRequest({
|
const result = await getBackendSrv().datasourceRequest({
|
||||||
url: this.props.options.url + url,
|
url: this.baseURL + workspaceURL,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -113,20 +113,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -316,20 +305,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -525,17 +503,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Maximum", "Average"],
|
||||||
"Maximum",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Maximum",
|
"aggregation": "Maximum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 3600000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
3600000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -701,14 +671,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -764,14 +729,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -925,20 +885,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1036,9 +985,7 @@
|
|||||||
{
|
{
|
||||||
"id": "reduce",
|
"id": "reduce",
|
||||||
"options": {
|
"options": {
|
||||||
"reducers": [
|
"reducers": ["sum"]
|
||||||
"sum"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -1112,14 +1059,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1177,9 +1119,7 @@
|
|||||||
{
|
{
|
||||||
"id": "reduce",
|
"id": "reduce",
|
||||||
"options": {
|
"options": {
|
||||||
"reducers": [
|
"reducers": ["sum"]
|
||||||
"sum"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -1253,14 +1193,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1318,9 +1253,7 @@
|
|||||||
{
|
{
|
||||||
"id": "reduce",
|
"id": "reduce",
|
||||||
"options": {
|
"options": {
|
||||||
"reducers": [
|
"reducers": ["sum"]
|
||||||
"sum"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -1394,14 +1327,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1459,9 +1387,7 @@
|
|||||||
{
|
{
|
||||||
"id": "reduce",
|
"id": "reduce",
|
||||||
"options": {
|
"options": {
|
||||||
"reducers": [
|
"reducers": ["sum"]
|
||||||
"sum"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -1535,13 +1461,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Maximum"],
|
||||||
"Maximum"
|
|
||||||
],
|
|
||||||
"aggregation": "Maximum",
|
"aggregation": "Maximum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1595,9 +1517,7 @@
|
|||||||
{
|
{
|
||||||
"id": "reduce",
|
"id": "reduce",
|
||||||
"options": {
|
"options": {
|
||||||
"reducers": [
|
"reducers": ["sum"]
|
||||||
"sum"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -1671,17 +1591,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Maximum", "Average"],
|
||||||
"Maximum",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Maximum",
|
"aggregation": "Maximum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 3600000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
3600000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1755,9 +1667,7 @@
|
|||||||
{
|
{
|
||||||
"id": "reduce",
|
"id": "reduce",
|
||||||
"options": {
|
"options": {
|
||||||
"reducers": [
|
"reducers": ["sum"]
|
||||||
"sum"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -1837,21 +1747,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -2037,17 +1935,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Maximum", "Average"],
|
||||||
"Maximum",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Maximum",
|
"aggregation": "Maximum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 3600000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
3600000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -2183,9 +2073,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -2206,13 +2094,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Maximum"],
|
||||||
"Maximum"
|
|
||||||
],
|
|
||||||
"aggregation": "Maximum",
|
"aggregation": "Maximum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -2337,20 +2221,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -2540,20 +2413,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -2743,20 +2605,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -2961,14 +2812,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3029,14 +2875,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3187,14 +3028,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3323,9 +3159,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -3346,14 +3180,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3414,14 +3243,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Total",
|
"aggregation": "Total",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3483,14 +3307,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Total", "Average"],
|
||||||
"Total",
|
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3627,15 +3446,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Minimum", "Average", "Maximum"],
|
||||||
"Minimum",
|
|
||||||
"Average",
|
|
||||||
"Maximum"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [3600000],
|
||||||
3600000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3683,15 +3496,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Minimum", "Average", "Maximum"],
|
||||||
"Minimum",
|
|
||||||
"Average",
|
|
||||||
"Maximum"
|
|
||||||
],
|
|
||||||
"aggregation": "Minimum",
|
"aggregation": "Minimum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [3600000],
|
||||||
3600000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3740,15 +3547,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Minimum", "Average", "Maximum"],
|
||||||
"Minimum",
|
|
||||||
"Average",
|
|
||||||
"Maximum"
|
|
||||||
],
|
|
||||||
"aggregation": "Maximum",
|
"aggregation": "Maximum",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [3600000],
|
||||||
3600000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -3902,13 +3703,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Average"],
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000],
|
||||||
60000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4070,13 +3867,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Average"],
|
||||||
"Average"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000],
|
||||||
60000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4253,20 +4046,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4448,20 +4230,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4658,13 +4429,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4712,13 +4479,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4767,13 +4530,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4916,13 +4675,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -4979,13 +4734,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -5034,13 +4785,9 @@
|
|||||||
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
"workspace": "657b3e91-7c0b-438b-86a5-f769445e237d"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["Count"],
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [300000],
|
||||||
300000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -5131,8 +4878,7 @@
|
|||||||
"templating": {
|
"templating": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {},
|
||||||
},
|
|
||||||
"description": null,
|
"description": null,
|
||||||
"error": null,
|
"error": null,
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
|
@ -86,9 +86,7 @@
|
|||||||
"justifyMode": "center",
|
"justifyMode": "center",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -109,25 +107,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -214,25 +196,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -320,25 +286,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -477,25 +427,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -669,25 +603,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -863,25 +781,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Average",
|
"aggregation": "Average",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -1057,25 +959,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [],
|
"dimensionFilters": [],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
@ -1257,25 +1143,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1457,25 +1327,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1657,25 +1511,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1857,25 +1695,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -1968,25 +1790,9 @@
|
|||||||
"workspace": "$ws"
|
"workspace": "$ws"
|
||||||
},
|
},
|
||||||
"azureMonitor": {
|
"azureMonitor": {
|
||||||
"aggOptions": [
|
"aggOptions": ["None", "Average", "Minimum", "Maximum", "Total", "Count"],
|
||||||
"None",
|
|
||||||
"Average",
|
|
||||||
"Minimum",
|
|
||||||
"Maximum",
|
|
||||||
"Total",
|
|
||||||
"Count"
|
|
||||||
],
|
|
||||||
"aggregation": "Count",
|
"aggregation": "Count",
|
||||||
"allowedTimeGrainsMs": [
|
"allowedTimeGrainsMs": [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||||
60000,
|
|
||||||
300000,
|
|
||||||
900000,
|
|
||||||
1800000,
|
|
||||||
3600000,
|
|
||||||
21600000,
|
|
||||||
43200000,
|
|
||||||
86400000
|
|
||||||
],
|
|
||||||
"dimensionFilter": "*",
|
"dimensionFilter": "*",
|
||||||
"dimensionFilters": [
|
"dimensionFilters": [
|
||||||
{
|
{
|
||||||
@ -2153,9 +1959,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -2358,9 +2162,7 @@
|
|||||||
"justifyMode": "center",
|
"justifyMode": "center",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["changeCount"],
|
||||||
"changeCount"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": true
|
"values": true
|
||||||
},
|
},
|
||||||
@ -2744,8 +2546,7 @@
|
|||||||
"templating": {
|
"templating": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {},
|
||||||
},
|
|
||||||
"description": null,
|
"description": null,
|
||||||
"error": null,
|
"error": null,
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
|
@ -26,8 +26,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"list": [
|
"list": []
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"gnetId": null,
|
"gnetId": null,
|
||||||
@ -155,9 +154,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": true
|
"values": true
|
||||||
},
|
},
|
||||||
@ -357,9 +354,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": true
|
"values": true
|
||||||
},
|
},
|
||||||
@ -1895,9 +1890,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -2442,9 +2435,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -3129,9 +3120,7 @@
|
|||||||
"justifyMode": "auto",
|
"justifyMode": "auto",
|
||||||
"orientation": "auto",
|
"orientation": "auto",
|
||||||
"reduceOptions": {
|
"reduceOptions": {
|
||||||
"calcs": [
|
"calcs": ["lastNotNull"],
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
"fields": "",
|
||||||
"values": false
|
"values": false
|
||||||
},
|
},
|
||||||
@ -4445,8 +4434,7 @@
|
|||||||
"templating": {
|
"templating": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {},
|
||||||
},
|
|
||||||
"description": null,
|
"description": null,
|
||||||
"error": null,
|
"error": null,
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -35,155 +35,6 @@
|
|||||||
"updated": "2018-12-06"
|
"updated": "2018-12-06"
|
||||||
},
|
},
|
||||||
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"path": "azuremonitor",
|
|
||||||
"method": "*",
|
|
||||||
"url": "https://management.azure.com",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://management.azure.com/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureCloud",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "govazuremonitor",
|
|
||||||
"method": "*",
|
|
||||||
"url": "https://management.usgovcloudapi.net",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://management.usgovcloudapi.net/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureUSGovernment",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "germanyazuremonitor",
|
|
||||||
"method": "*",
|
|
||||||
"url": "https://management.microsoftazure.de",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://management.microsoftazure.de/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureGermanCloud",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "chinaazuremonitor",
|
|
||||||
"method": "*",
|
|
||||||
"url": "https://management.chinacloudapi.cn",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://management.chinacloudapi.cn/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureChinaCloud",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "appinsights",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://api.applicationinsights.io",
|
|
||||||
"headers": [
|
|
||||||
{ "name": "X-API-Key", "content": "{{.SecureJsonData.appInsightsApiKey}}" },
|
|
||||||
{ "name": "x-ms-app", "content": "Grafana" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "chinaappinsights",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://api.applicationinsights.azure.cn",
|
|
||||||
"headers": [
|
|
||||||
{ "name": "X-API-Key", "content": "{{.SecureJsonData.appInsightsApiKey}}" },
|
|
||||||
{ "name": "x-ms-app", "content": "Grafana" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "loganalyticsazure",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://api.loganalytics.io/",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://api.loganalytics.io/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureCloud",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [
|
|
||||||
{ "name": "x-ms-app", "content": "Grafana" },
|
|
||||||
{ "name": "Cache-Control", "content": "public, max-age=60" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "chinaloganalyticsazure",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://api.loganalytics.azure.cn/",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://api.loganalytics.azure.cn/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureChinaCloud",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [
|
|
||||||
{ "name": "x-ms-app", "content": "Grafana" },
|
|
||||||
{ "name": "Cache-Control", "content": "public, max-age=60" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "govloganalyticsazure",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://api.loganalytics.us/",
|
|
||||||
"authType": "azure",
|
|
||||||
"tokenAuth": {
|
|
||||||
"scopes": ["https://api.loganalytics.us/.default"],
|
|
||||||
"params": {
|
|
||||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
|
||||||
"azure_cloud": "AzureUSGovernment",
|
|
||||||
"tenant_id": "{{.JsonData.tenantId | orEmpty}}",
|
|
||||||
"client_id": "{{.JsonData.clientId | orEmpty}}",
|
|
||||||
"client_secret": "{{.SecureJsonData.clientSecret | orEmpty}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"headers": [
|
|
||||||
{ "name": "x-ms-app", "content": "Grafana" },
|
|
||||||
{ "name": "Cache-Control", "content": "public, max-age=60" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"grafanaVersion": "5.2.x",
|
"grafanaVersion": "5.2.x",
|
||||||
"plugins": []
|
"plugins": []
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
|
||||||
|
|
||||||
import ResourcePickerData from './resourcePickerData';
|
import ResourcePickerData from './resourcePickerData';
|
||||||
import {
|
import {
|
||||||
createMockARGResourceContainersResponse,
|
createMockARGResourceContainersResponse,
|
||||||
@ -11,47 +6,34 @@ import {
|
|||||||
import { ResourceRowType } from '../components/ResourcePicker/types';
|
import { ResourceRowType } from '../components/ResourcePicker/types';
|
||||||
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
|
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
|
||||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
|
||||||
getBackendSrv: () => backendSrv,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const instanceSettings = createMockInstanceSetttings();
|
const instanceSettings = createMockInstanceSetttings();
|
||||||
|
const resourcePickerData = new ResourcePickerData(instanceSettings);
|
||||||
|
let postResource: jest.Mock;
|
||||||
|
|
||||||
describe('AzureMonitor resourcePickerData', () => {
|
describe('AzureMonitor resourcePickerData', () => {
|
||||||
describe('getResourcePickerData', () => {
|
describe('getResourcePickerData', () => {
|
||||||
let fetchMock: jest.SpyInstance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock = jest.spyOn(backendSrv, 'fetch');
|
postResource = jest.fn().mockResolvedValue(createMockARGResourceContainersResponse());
|
||||||
fetchMock.mockImplementation(() => {
|
resourcePickerData.postResource = postResource;
|
||||||
const data = createMockARGResourceContainersResponse();
|
|
||||||
return of(createFetchResponse(data));
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => fetchMock.mockReset());
|
|
||||||
|
|
||||||
it('calls ARG API', async () => {
|
it('calls ARG API', async () => {
|
||||||
const resourcePickerData = new ResourcePickerData(instanceSettings);
|
|
||||||
await resourcePickerData.getResourcePickerData();
|
await resourcePickerData.getResourcePickerData();
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalled();
|
expect(postResource).toHaveBeenCalled();
|
||||||
const argQuery = fetchMock.mock.calls[0][0].data.query;
|
const argQuery = postResource.mock.calls[0][1].query;
|
||||||
|
|
||||||
expect(argQuery).toContain(`where type == 'microsoft.resources/subscriptions'`);
|
expect(argQuery).toContain(`where type == 'microsoft.resources/subscriptions'`);
|
||||||
expect(argQuery).toContain(`where type == 'microsoft.resources/subscriptions/resourcegroups'`);
|
expect(argQuery).toContain(`where type == 'microsoft.resources/subscriptions/resourcegroups'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns only subscriptions at the top level', async () => {
|
it('returns only subscriptions at the top level', async () => {
|
||||||
const resourcePickerData = new ResourcePickerData(instanceSettings);
|
|
||||||
const results = await resourcePickerData.getResourcePickerData();
|
const results = await resourcePickerData.getResourcePickerData();
|
||||||
|
|
||||||
expect(results.map((v) => v.id)).toEqual(['/subscriptions/abc-123', '/subscription/def-456']);
|
expect(results.map((v) => v.id)).toEqual(['/subscriptions/abc-123', '/subscription/def-456']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('nests resource groups under their subscriptions', async () => {
|
it('nests resource groups under their subscriptions', async () => {
|
||||||
const resourcePickerData = new ResourcePickerData(instanceSettings);
|
|
||||||
const results = await resourcePickerData.getResourcePickerData();
|
const results = await resourcePickerData.getResourcePickerData();
|
||||||
|
|
||||||
expect(results[0].children?.map((v) => v.id)).toEqual([
|
expect(results[0].children?.map((v) => v.id)).toEqual([
|
||||||
@ -68,8 +50,6 @@ describe('AzureMonitor resourcePickerData', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getResourcesForResourceGroup', () => {
|
describe('getResourcesForResourceGroup', () => {
|
||||||
let fetchMock: jest.SpyInstance;
|
|
||||||
|
|
||||||
const resourceRow = {
|
const resourceRow = {
|
||||||
id: '/subscription/def-456/resourceGroups/dev',
|
id: '/subscription/def-456/resourceGroups/dev',
|
||||||
name: 'Dev',
|
name: 'Dev',
|
||||||
@ -78,27 +58,20 @@ describe('AzureMonitor resourcePickerData', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock = jest.spyOn(backendSrv, 'fetch');
|
postResource = jest.fn().mockResolvedValue(createARGResourcesResponse());
|
||||||
fetchMock.mockImplementation(() => {
|
resourcePickerData.postResource = postResource;
|
||||||
const data = createARGResourcesResponse();
|
|
||||||
return of(createFetchResponse(data));
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => fetchMock.mockReset());
|
|
||||||
|
|
||||||
it('requests resources for the specified resource row', async () => {
|
it('requests resources for the specified resource row', async () => {
|
||||||
const resourcePickerData = new ResourcePickerData(instanceSettings);
|
|
||||||
await resourcePickerData.getResourcesForResourceGroup(resourceRow);
|
await resourcePickerData.getResourcesForResourceGroup(resourceRow);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalled();
|
expect(postResource).toHaveBeenCalled();
|
||||||
const argQuery = fetchMock.mock.calls[0][0].data.query;
|
const argQuery = postResource.mock.calls[0][1].query;
|
||||||
|
|
||||||
expect(argQuery).toContain(resourceRow.id);
|
expect(argQuery).toContain(resourceRow.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns formatted resources', async () => {
|
it('returns formatted resources', async () => {
|
||||||
const resourcePickerData = new ResourcePickerData(instanceSettings);
|
|
||||||
const results = await resourcePickerData.getResourcesForResourceGroup(resourceRow);
|
const results = await resourcePickerData.getResourcesForResourceGroup(resourceRow);
|
||||||
|
|
||||||
expect(results.map((v) => v.id)).toEqual([
|
expect(results.map((v) => v.id)).toEqual([
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FetchResponse, getBackendSrv } from '@grafana/runtime';
|
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||||
import { getManagementApiRoute } from '../api/routes';
|
import { DataSourceInstanceSettings } from '../../../../../../packages/grafana-data/src';
|
||||||
import {
|
import {
|
||||||
locationDisplayNames,
|
locationDisplayNames,
|
||||||
logsSupportedLocationsKusto,
|
logsSupportedLocationsKusto,
|
||||||
@ -8,24 +8,24 @@ import {
|
|||||||
} from '../azureMetadata';
|
} from '../azureMetadata';
|
||||||
import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types';
|
import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types';
|
||||||
import { parseResourceURI } from '../components/ResourcePicker/utils';
|
import { parseResourceURI } from '../components/ResourcePicker/utils';
|
||||||
import { getAzureCloud } from '../credentials';
|
|
||||||
import {
|
import {
|
||||||
AzureDataSourceInstanceSettings,
|
AzureDataSourceJsonData,
|
||||||
AzureGraphResponse,
|
AzureGraphResponse,
|
||||||
|
AzureMonitorQuery,
|
||||||
AzureResourceSummaryItem,
|
AzureResourceSummaryItem,
|
||||||
RawAzureResourceGroupItem,
|
RawAzureResourceGroupItem,
|
||||||
RawAzureResourceItem,
|
RawAzureResourceItem,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { routeNames } from '../utils/common';
|
||||||
|
|
||||||
const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01';
|
const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01';
|
||||||
|
|
||||||
export default class ResourcePickerData {
|
export default class ResourcePickerData extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
|
||||||
private proxyUrl: string;
|
private resourcePath: string;
|
||||||
private cloud: string;
|
|
||||||
|
|
||||||
constructor(instanceSettings: AzureDataSourceInstanceSettings) {
|
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
|
||||||
this.proxyUrl = instanceSettings.url!;
|
super(instanceSettings);
|
||||||
this.cloud = getAzureCloud(instanceSettings);
|
this.resourcePath = `${routeNames.resourceGraph}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly templateVariableGroupID = '$$grafana-templateVariables$$';
|
static readonly templateVariableGroupID = '$$grafana-templateVariables$$';
|
||||||
@ -54,29 +54,19 @@ export default class ResourcePickerData {
|
|||||||
| order by subscriptionURI asc
|
| order by subscriptionURI asc
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { ok, data: response } = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query);
|
const response = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query);
|
||||||
|
|
||||||
// TODO: figure out desired error handling strategy
|
|
||||||
if (!ok) {
|
|
||||||
throw new Error('unable to fetch resource containers');
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatResourceGroupData(response.data);
|
return formatResourceGroupData(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourcesForResourceGroup(resourceGroup: ResourceRow) {
|
async getResourcesForResourceGroup(resourceGroup: ResourceRow) {
|
||||||
const { ok, data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
||||||
resources
|
resources
|
||||||
| where id hasprefix "${resourceGroup.id}"
|
| where id hasprefix "${resourceGroup.id}"
|
||||||
| where type in (${logsSupportedResourceTypesKusto}) and location in (${logsSupportedLocationsKusto})
|
| where type in (${logsSupportedResourceTypesKusto}) and location in (${logsSupportedLocationsKusto})
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// TODO: figure out desired error handling strategy
|
return formatResourceGroupChildren(response);
|
||||||
if (!ok) {
|
|
||||||
throw new Error('unable to fetch resource containers');
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatResourceGroupChildren(response.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
|
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
|
||||||
@ -113,51 +103,37 @@ export default class ResourcePickerData {
|
|||||||
| project subscriptionName, resourceGroupName, resourceName
|
| project subscriptionName, resourceGroupName, resourceName
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { ok, data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
|
const { data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
|
||||||
|
|
||||||
if (!ok || !response.data[0]) {
|
if (!response.length) {
|
||||||
throw new Error('unable to fetch resource details');
|
throw new Error('unable to fetch resource details');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data[0];
|
return response[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourceURIFromWorkspace(workspace: string) {
|
async getResourceURIFromWorkspace(workspace: string) {
|
||||||
const { ok, data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
||||||
resources
|
resources
|
||||||
| where properties['customerId'] == "${workspace}"
|
| where properties['customerId'] == "${workspace}"
|
||||||
| project id
|
| project id
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// TODO: figure out desired error handling strategy
|
if (!response.length) {
|
||||||
if (!ok) {
|
|
||||||
throw new Error('unable to fetch resource containers');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.data.length) {
|
|
||||||
throw new Error('unable to find resource for workspace ' + workspace);
|
throw new Error('unable to find resource for workspace ' + workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data[0].id;
|
return response[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeResourceGraphRequest<T = unknown>(
|
async makeResourceGraphRequest<T = unknown>(query: string, maxRetries = 1): Promise<AzureGraphResponse<T>> {
|
||||||
query: string,
|
|
||||||
maxRetries = 1
|
|
||||||
): Promise<FetchResponse<AzureGraphResponse<T>>> {
|
|
||||||
try {
|
try {
|
||||||
return await getBackendSrv()
|
return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, {
|
||||||
.fetch<AzureGraphResponse<T>>({
|
|
||||||
url: this.proxyUrl + '/' + getManagementApiRoute(this.cloud) + RESOURCE_GRAPH_URL,
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
query: query,
|
query: query,
|
||||||
options: {
|
options: {
|
||||||
resultFormat: 'objectArray',
|
resultFormat: 'objectArray',
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.toPromise();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (maxRetries > 0) {
|
if (maxRetries > 0) {
|
||||||
return this.makeResourceGraphRequest(query, maxRetries - 1);
|
return this.makeResourceGraphRequest(query, maxRetries - 1);
|
||||||
|
@ -28,3 +28,12 @@ export function convertTimeGrainsToMs<T extends { value: string }>(timeGrains: T
|
|||||||
});
|
});
|
||||||
return allowedTimeGrainsMs;
|
return allowedTimeGrainsMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route definitions shared with the backend.
|
||||||
|
// Check: /pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go <registerRoutes>
|
||||||
|
export const routeNames = {
|
||||||
|
azureMonitor: 'azuremonitor',
|
||||||
|
logAnalytics: 'loganalytics',
|
||||||
|
appInsights: 'appinsights',
|
||||||
|
resourceGraph: 'resourcegraph',
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user