From 76868bad0498a0692acf7f0f7da5846ce642cf29 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 7 Oct 2022 14:57:01 +0200 Subject: [PATCH] AzureMonitor: Allow multiple resources in the API for Metrics (#56118) --- pkg/tsdb/azuremonitor/azuremonitor_test.go | 2 +- .../metrics/azuremonitor-datasource.go | 105 ++++++++---- .../metrics/azuremonitor-datasource_test.go | 158 ++++++++++++------ pkg/tsdb/azuremonitor/metrics/url-builder.go | 6 + .../9-azure-monitor-response-multi.json | 53 ++++++ pkg/tsdb/azuremonitor/types/types.go | 32 ++-- 6 files changed, 256 insertions(+), 100 deletions(-) create mode 100644 pkg/tsdb/azuremonitor/testdata/azuremonitor/9-azure-monitor-response-multi.json diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index 8f9218bec95..1cc87c46717 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -425,7 +425,7 @@ func TestCheckHealth(t *testing.T) { Status: backend.HealthStatusError, Message: "One or more health checks failed. See details below.", JSONDetails: []byte( - `{"verboseMessage": "1. Error connecting to Azure Monitor endpoint: health check failed: Get \"https://management.azure.com/subscriptions?api-version=2018-01-01\": not found\n2. Error connecting to Azure Log Analytics endpoint: health check failed: Get \"https://management.azure.com/subscriptions//providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview\": not found\n3. Error connecting to Azure Resource Graph endpoint: health check failed: Post \"https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-06-01-preview\": not found" }`), + `{"verboseMessage": "1. Error connecting to Azure Monitor endpoint: health check failed: Get \"https://management.azure.com/subscriptions?api-version=2021-05-01\": not found\n2. Error connecting to Azure Log Analytics endpoint: health check failed: Get \"https://management.azure.com/subscriptions//providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview\": not found\n3. Error connecting to Azure Resource Graph endpoint: health check failed: Post \"https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-06-01-preview\": not found" }`), }, customServices: map[string]types.DatasourceService{ azureMonitor: { diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index 5ba18e3056b..4404cb91b86 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -36,7 +36,7 @@ var ( resourceNameLandmark = regexp.MustCompile(`(?i)(/(?P[\w-\.]+)/providers/Microsoft\.Insights/metrics)`) ) -const AzureMonitorAPIVersion = "2018-01-01" +const AzureMonitorAPIVersion = "2021-05-01" func (e *AzureMonitorDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { e.Proxy.Do(rw, req, cli) @@ -79,22 +79,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf azJSONModel.MetricNamespace = azJSONModel.MetricDefinition } - ub := urlBuilder{ - ResourceURI: azJSONModel.ResourceURI, - // Alternative, used to reconstruct resource URI if it's not present - DefaultSubscription: dsInfo.Settings.SubscriptionId, - Subscription: queryJSONModel.Subscription, - ResourceGroup: azJSONModel.ResourceGroup, - MetricNamespace: azJSONModel.MetricNamespace, - ResourceName: azJSONModel.ResourceName, - } - azJSONModel.DimensionFilters = MigrateDimensionFilters(azJSONModel.DimensionFilters) - azureURL := ub.BuildMetricsURL() - - if azJSONModel.ResourceName == "" { - azJSONModel.ResourceName = extractResourceNameFromMetricsURL(azureURL) - } alias := azJSONModel.Alias @@ -121,6 +106,23 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf params.Add("metricnamespace", azJSONModel.MetricNamespace) } + azureURL := BuildSubscriptionMetricsURL(queryJSONModel.Subscription) + if azJSONModel.Region != "" { + params.Add("region", azJSONModel.Region) + } else { + // Deprecated, if no region is specified, only one resource group and name is supported + ub := urlBuilder{ + ResourceURI: azJSONModel.ResourceURI, + // Alternative, used to reconstruct resource URI if it's not present + DefaultSubscription: dsInfo.Settings.SubscriptionId, + Subscription: queryJSONModel.Subscription, + ResourceGroup: azJSONModel.ResourceGroup, + MetricNamespace: azJSONModel.MetricNamespace, + ResourceName: azJSONModel.ResourceName, + } + azureURL = ub.BuildMetricsURL() + } + // old model dimension := strings.TrimSpace(azJSONModel.Dimension) dimensionFilter := strings.TrimSpace(azJSONModel.DimensionFilter) @@ -142,8 +144,25 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf } } + resourceIDs := []string{} + for _, r := range azJSONModel.Resources { + ub := urlBuilder{ + DefaultSubscription: dsInfo.Settings.SubscriptionId, + Subscription: queryJSONModel.Subscription, + ResourceGroup: r.ResourceGroup, + MetricNamespace: azJSONModel.MetricNamespace, + ResourceName: r.ResourceName, + } + resourceIDs = append(resourceIDs, fmt.Sprintf("Microsoft.ResourceId eq '%s'", ub.buildResourceURI())) + } + filterString := strings.Join(resourceIDs, " or ") + if dimSB.String() != "" { - params.Add("$filter", dimSB.String()) + if filterString != "" { + filterString = fmt.Sprintf("(%s) and (%s)", filterString, dimSB.String()) + } else { + filterString = dimSB.String() + } if azJSONModel.Top != "" { params.Add("top", azJSONModel.Top) } @@ -156,14 +175,13 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf } azureMonitorQueries = append(azureMonitorQueries, &types.AzureMonitorQuery{ - ResourceName: azJSONModel.ResourceName, - ResourceURI: ub.buildResourceURI(), - URL: azureURL, - Target: target, - Params: params, - RefID: query.RefID, - Alias: alias, - TimeRange: query.TimeRange, + URL: azureURL, + Target: target, + Params: params, + RefID: query.RefID, + Alias: alias, + TimeRange: query.TimeRange, + Filter: filterString, }) } @@ -182,6 +200,10 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *types. req.URL.Path = path.Join(req.URL.Path, query.URL) req.URL.RawQuery = query.Params.Encode() + if query.Filter != "" { + req.Method = http.MethodPost + req.Body = io.NopCloser(strings.NewReader(fmt.Sprintf(`{"filter": "%s"}`, query.Filter))) + } ctx, span := tracer.Start(ctx, "azuremonitor query") span.SetAttributes("target", query.Target, attribute.Key("target").String(query.Target)) @@ -264,11 +286,6 @@ func (e *AzureMonitorDatasource) parseResponse(amr types.AzureMonitorResponse, q return nil, nil } - queryUrl, err := getQueryUrl(query, azurePortalUrl) - if err != nil { - return nil, err - } - frames := data.Frames{} for _, series := range amr.Value[0].Timeseries { labels := data.Labels{} @@ -288,8 +305,19 @@ func (e *AzureMonitorDatasource) parseResponse(amr types.AzureMonitorResponse, q Unit: toGrafanaUnit(amr.Value[0].Unit), }) } + resourceID := labels["microsoft.resourceid"] + resourceIDSlice := strings.Split(resourceID, "/") + resourceName := "" + if len(resourceIDSlice) > 1 { + resourceName = resourceIDSlice[len(resourceIDSlice)-1] + } else { + // Deprecated: This is for backward compatibility, the URL should contain + // the resource ID + resourceName = extractResourceNameFromMetricsURL(query.URL) + resourceID = extractResourceIDFromMetricsURL(query.URL) + } if query.Alias != "" { - displayName := formatAzureMonitorLegendKey(query.Alias, query.ResourceName, + displayName := formatAzureMonitorLegendKey(query.Alias, resourceName, amr.Value[0].Name.LocalizedValue, "", "", amr.Namespace, amr.Value[0].ID, labels) if dataField.Config != nil { @@ -323,6 +351,11 @@ func (e *AzureMonitorDatasource) parseResponse(amr types.AzureMonitorResponse, q frame.SetRow(i, point.TimeStamp, value) } + queryUrl, err := getQueryUrl(query, azurePortalUrl, resourceID, resourceName) + if err != nil { + return nil, err + } + frameWithLink := resourcegraph.AddConfigLinks(*frame, queryUrl) frames = append(frames, &frameWithLink) } @@ -331,7 +364,7 @@ func (e *AzureMonitorDatasource) parseResponse(amr types.AzureMonitorResponse, q } // Gets the deep link for the given query -func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl string) (string, error) { +func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, resourceName string) (string, error) { aggregationType := aggregationTypeMap["Average"] aggregation := query.Params.Get("aggregation") if aggregation != "" { @@ -360,14 +393,14 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl string) (string, "metrics": []types.MetricChartDefinition{ { ResourceMetadata: map[string]string{ - "id": query.ResourceURI, + "id": resourceID, }, Name: query.Params.Get("metricnames"), AggregationType: aggregationType, Namespace: query.Params.Get("metricnamespace"), MetricVisualization: types.MetricVisualization{ DisplayName: query.Params.Get("metricnames"), - ResourceDisplayName: query.ResourceName, + ResourceDisplayName: resourceName, }, }, }, @@ -497,3 +530,7 @@ func extractResourceNameFromMetricsURL(url string) string { return resourceName } + +func extractResourceIDFromMetricsURL(url string) string { + return strings.Split(url, "/providers/microsoft.insights/metrics")[0] +} diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go index 3c9c16920d8..ffb962c6087 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go @@ -39,8 +39,9 @@ func TestAzureMonitorBuildQueries(t *testing.T) { azureMonitorVariedProperties map[string]interface{} azureMonitorQueryTarget string expectedInterval string - resourceURI string queryInterval time.Duration + expectedURL string + expectedFilter string }{ { name: "Parse queries from frontend and build AzureMonitor API queries", @@ -49,9 +50,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { "timeGrain": "PT1M", "top": "10", }, - resourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", expectedInterval: "PT1M", - azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", }, { name: "legacy query without resourceURI and time grain set to auto", @@ -61,7 +61,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT15M", - azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT15M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT15M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", }, { name: "legacy query without resourceURI and time grain set to auto", @@ -72,7 +72,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT5M", - azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT5M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT5M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", }, { name: "legacy query without resourceURI and has a dimension filter", @@ -84,7 +84,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob eq '*'", }, { name: "legacy query without resourceURI and has a dimension filter and none Dimension", @@ -96,7 +97,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", }, { name: "legacy query without resourceURI and has dimensionFilter*s* property with one dimension", @@ -107,7 +108,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob eq '*'", }, { name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions", @@ -118,7 +120,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27+and+tier+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob eq '*' and tier eq '*'", }, { name: "legacy query without resourceURI and has a dimension filter without specifying a top", @@ -129,7 +132,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + expectedFilter: "blob eq '*'", }, { name: "has dimensionFilter*s* property with not equals operator", @@ -140,7 +144,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+ne+%27test%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob ne 'test'", }, { name: "has dimensionFilter*s* property with startsWith operator", @@ -151,7 +156,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+sw+%27test%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob sw 'test'", }, { name: "correctly sets dimension operator to eq (irrespective of operator) when filter value is '*'", @@ -162,7 +168,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27+and+tier+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob eq '*' and tier eq '*'", }, { name: "correctly constructs target when multiple filter values are provided for the 'eq' operator", @@ -173,7 +180,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+eq+%27test%27+or+blob+eq+%27test2%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob eq 'test' or blob eq 'test2'", }, { name: "correctly constructs target when multiple filter values are provided for ne 'eq' operator", @@ -184,7 +192,43 @@ func TestAzureMonitorBuildQueries(t *testing.T) { }, queryInterval: duration, expectedInterval: "PT1M", - azureMonitorQueryTarget: "%24filter=blob+ne+%27test%27+and+blob+ne+%27test2%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "blob ne 'test' and blob ne 'test2'", + }, + { + name: "Includes a region", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "top": "10", + "region": "westus", + }, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines®ion=westus×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + expectedURL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/providers/microsoft.insights/metrics", + }, + { + name: "includes a resource as a filter", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}}, + }, + queryInterval: duration, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", + expectedFilter: "Microsoft.ResourceId eq '/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm'", + }, + { + name: "includes a resource and a dimesion as filters", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}}, + "top": "30", + }, + queryInterval: duration, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + expectedFilter: "(Microsoft.ResourceId eq '/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm') and (blob ne 'test' and blob ne 'test2')", }, } @@ -221,16 +265,18 @@ func TestAzureMonitorBuildQueries(t *testing.T) { } azureMonitorQuery := &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", - Target: tt.azureMonitorQueryTarget, - RefID: "A", - Alias: "testalias", + URL: tt.expectedURL, + Target: tt.azureMonitorQueryTarget, + RefID: "A", + Alias: "testalias", TimeRange: backend.TimeRange{ From: fromStart, To: fromStart.Add(34 * time.Minute), }, + Filter: tt.expectedFilter, + } + if azureMonitorQuery.URL == "" { + azureMonitorQuery.URL = "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics" } queries, err := datasource.buildQueries(tsdbQuery, dsInfo) @@ -243,7 +289,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { `TimeContext/%7B%22absolute%22%3A%7B%22startTime%22%3A%222018-03-15T13%3A00%3A00Z%22%2C%22endTime%22%3A%222018-03-15T13%3A34%3A00Z%22%7D%7D/` + `ChartDefinition/%7B%22v2charts%22%3A%5B%7B%22metrics%22%3A%5B%7B%22resourceMetadata%22%3A%7B%22id%22%3A%22%2Fsubscriptions%2F12345678-aaaa-bbbb-cccc-123456789abc%2FresourceGroups%2Fgrafanastaging%2Fproviders%2FMicrosoft.Compute%2FvirtualMachines%2Fgrafana%22%7D%2C` + `%22name%22%3A%22Percentage%20CPU%22%2C%22aggregationType%22%3A4%2C%22namespace%22%3A%22Microsoft.Compute%2FvirtualMachines%22%2C%22metricVisualization%22%3A%7B%22displayName%22%3A%22Percentage%20CPU%22%2C%22resourceDisplayName%22%3A%22grafana%22%7D%7D%5D%7D%5D%7D` - actual, err := getQueryUrl(queries[0], "http://ds") + actual, err := getQueryUrl(queries[0], "http://ds", "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", "grafana") require.NoError(t, err) require.Equal(t, expected, actual) }) @@ -315,8 +361,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "average aggregate time series response", responseFile: "1-azure-monitor-response-avg.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", Params: url.Values{ "aggregation": {"Average"}, }, @@ -335,8 +380,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "total aggregate time series response", responseFile: "2-azure-monitor-response-total.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", Params: url.Values{ "aggregation": {"Total"}, }, @@ -355,8 +399,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "maximum aggregate time series response", responseFile: "3-azure-monitor-response-maximum.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", Params: url.Values{ "aggregation": {"Maximum"}, }, @@ -375,8 +418,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "minimum aggregate time series response", responseFile: "4-azure-monitor-response-minimum.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", Params: url.Values{ "aggregation": {"Minimum"}, }, @@ -395,8 +437,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "count aggregate time series response", responseFile: "5-azure-monitor-response-count.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", Params: url.Values{ "aggregation": {"Count"}, }, @@ -415,8 +456,7 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "single dimension time series response", responseFile: "6-azure-monitor-response-single-dimension.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", Params: url.Values{ "aggregation": {"Average"}, }, @@ -448,9 +488,8 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "with alias patterns in the query", responseFile: "2-azure-monitor-response-total.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", + Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", Params: url.Values{ "aggregation": {"Total"}, }, @@ -469,9 +508,8 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "single dimension with alias", responseFile: "6-azure-monitor-response-single-dimension.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - Alias: "{{dimensionname}}={{DimensionValue}}", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", + Alias: "{{dimensionname}}={{DimensionValue}}", Params: url.Values{ "aggregation": {"Average"}, }, @@ -505,9 +543,8 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "multiple dimension time series response with label alias", responseFile: "7-azure-monitor-response-multi-dimension.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - Alias: "{{resourcegroup}} {Blob Type={{blobtype}}, Tier={{Tier}}}", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", + Alias: "{{resourcegroup}} {Blob Type={{blobtype}}, Tier={{Tier}}}", Params: url.Values{ "aggregation": {"Average"}, }, @@ -542,9 +579,8 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "unspecified unit with alias should not panic", responseFile: "8-azure-monitor-response-unspecified-unit.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - Alias: "custom", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", + Alias: "custom", Params: url.Values{ "aggregation": {"Average"}, }, @@ -563,9 +599,8 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "with legacy azure monitor query properties and without a resource uri", responseFile: "2-azure-monitor-response-total.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", + Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", Params: url.Values{ "aggregation": {"Total"}, }, @@ -584,9 +619,8 @@ func TestAzureMonitorParseResponse(t *testing.T) { name: "with legacy azure monitor query properties and with a resource uri it should use the resource uri", responseFile: "2-azure-monitor-response-total.json", mockQuery: &types.AzureMonitorQuery{ - ResourceName: "grafana", - ResourceURI: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana", - Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", + Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}", Params: url.Values{ "aggregation": {"Total"}, }, @@ -601,6 +635,26 @@ func TestAzureMonitorParseResponse(t *testing.T) { }).SetConfig(&data.FieldConfig{Unit: "percent", DisplayName: "custom grafanastaging Microsoft.Compute/virtualMachines grafana Percentage CPU", Links: []data.DataLink{totalLink}})), }, }, + { + name: "multiple time series response", + responseFile: "9-azure-monitor-response-multi.json", + mockQuery: &types.AzureMonitorQuery{ + URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/providers/microsoft.insights/metrics", + Params: url.Values{ + "aggregation": {"Average"}, + }, + }, + expectedFrames: data.Frames{ + data.NewFrame("", + data.NewField("Time", nil, + makeDates(time.Date(2019, 2, 8, 10, 13, 0, 0, time.UTC), 5, time.Minute), + ).SetConfig(&data.FieldConfig{Links: []data.DataLink{averageLink}}), + data.NewField("Percentage CPU", data.Labels{"microsoft.resourceid": "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"}, []*float64{ + ptr.Float64(2.0875), ptr.Float64(2.1525), ptr.Float64(2.155), ptr.Float64(3.6925), ptr.Float64(2.44), + }).SetConfig(&data.FieldConfig{Unit: "percent", Links: []data.DataLink{averageLink}}), + ), + }, + }, } datasource := &AzureMonitorDatasource{} diff --git a/pkg/tsdb/azuremonitor/metrics/url-builder.go b/pkg/tsdb/azuremonitor/metrics/url-builder.go index 5129da529c3..849465f90a4 100644 --- a/pkg/tsdb/azuremonitor/metrics/url-builder.go +++ b/pkg/tsdb/azuremonitor/metrics/url-builder.go @@ -67,3 +67,9 @@ func (params *urlBuilder) BuildMetricsURL() string { return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI) } + +// BuildSubscriptionMetricsURL returns a URL for querying metrics for all resources in a subscription +// It requires to set a $filter and a region parameter +func BuildSubscriptionMetricsURL(subscription string) string { + return fmt.Sprintf("/subscriptions/%s/providers/microsoft.insights/metrics", subscription) +} diff --git a/pkg/tsdb/azuremonitor/testdata/azuremonitor/9-azure-monitor-response-multi.json b/pkg/tsdb/azuremonitor/testdata/azuremonitor/9-azure-monitor-response-multi.json new file mode 100644 index 00000000000..b944e495792 --- /dev/null +++ b/pkg/tsdb/azuremonitor/testdata/azuremonitor/9-azure-monitor-response-multi.json @@ -0,0 +1,53 @@ +{ + "cost": 1439, + "timespan": "2022-09-28T15:33:05Z/2022-09-29T15:33:05Z", + "interval": "PT5M", + "value": [ + { + "id": "subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/providers/Microsoft.Insights/metrics/Percentage CPU", + "type": "Microsoft.Insights/metrics", + "name": { + "value": "Percentage CPU", + "localizedValue": "Percentage CPU" + }, + "unit": "Percent", + "timeseries": [ + { + "metadatavalues": [ + { + "name": { + "value": "microsoft.resourceid", + "localizedValue": "microsoft.resourceid" + }, + "value": "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana" + } + ], + "data": [ + { + "timeStamp": "2019-02-08T10:13:00Z", + "average": 2.0875 + }, + { + "timeStamp": "2019-02-08T10:14:00Z", + "average": 2.1525 + }, + { + "timeStamp": "2019-02-08T10:15:00Z", + "average": 2.155 + }, + { + "timeStamp": "2019-02-08T10:16:00Z", + "average": 3.6925 + }, + { + "timeStamp": "2019-02-08T10:17:00Z", + "average": 2.44 + } + ] + } + ] + } + ], + "namespace": "Microsoft.Compute/virtualMachines", + "resourceregion": "northeurope" +} diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index 8193a7b3f7d..f7b8a1b04d5 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -59,14 +59,13 @@ type DatasourceInfo struct { // AzureMonitorQuery is the query for all the services as they have similar queries // with a url, a querystring and an alias field type AzureMonitorQuery struct { - ResourceName string - ResourceURI string - URL string - Target string - Params url.Values - RefID string - Alias string - TimeRange backend.TimeRange + URL string + Target string + Params url.Values + RefID string + Alias string + TimeRange backend.TimeRange + Filter string } // AzureMonitorResponse is the json response from the Azure Monitor API @@ -114,16 +113,21 @@ type AzureResponseTable struct { Rows [][]interface{} `json:"rows"` } +type AzureMonitorResource struct { + ResourceGroup string `json:"resourceGroup"` + ResourceName string `json:"resourceName"` +} + // AzureMonitorJSONQuery is the frontend JSON query model for an Azure Monitor query. type AzureMonitorJSONQuery struct { AzureMonitor struct { ResourceURI string `json:"resourceUri"` // These are used to reconstruct a resource URI - MetricNamespace string `json:"metricNamespace"` - CustomNamespace string `json:"customNamespace"` - MetricName string `json:"metricName"` - ResourceGroup string `json:"resourceGroup"` - ResourceName string `json:"resourceName"` + MetricNamespace string `json:"metricNamespace"` + CustomNamespace string `json:"customNamespace"` + MetricName string `json:"metricName"` + Region string `json:"region"` + Resources []AzureMonitorResource `json:"resources"` Aggregation string `json:"aggregation"` Alias string `json:"alias"` @@ -138,6 +142,8 @@ type AzureMonitorJSONQuery struct { // Deprecated, MetricNamespace should be used instead MetricDefinition string `json:"metricDefinition"` + // Deprecated: Use Resources with a single element instead + AzureMonitorResource } `json:"azureMonitor"` Subscription string `json:"subscription"` }