Azuremonitor schematize backend (#69822)

* metrics schematize

* metrics now use schemas

* logs schema

* remove unused code

* fix tests
This commit is contained in:
Andrew Hackmann 2023-06-10 15:30:49 -05:00 committed by GitHub
parent d29124214a
commit 8d37d8f60b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 244 additions and 246 deletions

View File

@ -89,7 +89,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
resources := []string{}
var resourceOrWorkspace string
var queryString string
var resultFormat string
var resultFormat dataquery.ResultFormat
traceExploreQuery := ""
traceParentExploreQuery := ""
traceLogsExploreQuery := ""
@ -103,7 +103,9 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
azureLogAnalyticsTarget := queryJSONModel.AzureLogAnalytics
logger.Debug("AzureLogAnalytics", "target", azureLogAnalyticsTarget)
resultFormat = azureLogAnalyticsTarget.ResultFormat
if azureLogAnalyticsTarget.ResultFormat != nil {
resultFormat = *azureLogAnalyticsTarget.ResultFormat
}
if resultFormat == "" {
resultFormat = types.TimeSeries
}
@ -115,14 +117,16 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
if len(azureLogAnalyticsTarget.Resources) > 0 {
resources = azureLogAnalyticsTarget.Resources
resourceOrWorkspace = azureLogAnalyticsTarget.Resources[0]
} else if azureLogAnalyticsTarget.Resource != "" {
resources = []string{azureLogAnalyticsTarget.Resource}
resourceOrWorkspace = azureLogAnalyticsTarget.Resource
} else {
resourceOrWorkspace = azureLogAnalyticsTarget.Workspace
} else if azureLogAnalyticsTarget.Resource != nil && *azureLogAnalyticsTarget.Resource != "" {
resources = []string{*azureLogAnalyticsTarget.Resource}
resourceOrWorkspace = *azureLogAnalyticsTarget.Resource
} else if azureLogAnalyticsTarget.Workspace != nil {
resourceOrWorkspace = *azureLogAnalyticsTarget.Workspace
}
queryString = azureLogAnalyticsTarget.Query
if azureLogAnalyticsTarget.Query != nil {
queryString = *azureLogAnalyticsTarget.Query
}
}
if query.QueryType == string(dataquery.AzureQueryTypeAzureTraces) {
@ -205,7 +209,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, logger l
azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, &AzureLogAnalyticsQuery{
RefID: query.RefID,
ResultFormat: resultFormat,
ResultFormat: string(resultFormat),
URL: apiURL,
JSON: query.JSON,
TimeRange: query.TimeRange,
@ -701,7 +705,7 @@ func encodeQuery(rawQuery string) (string, error) {
return base64.StdEncoding.EncodeToString(b.Bytes()), nil
}
func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []string, filters []types.TracesFilters, resultFormat *string, resources []string) string {
func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []string, filters []dataquery.AzureTracesFilter, resultFormat *dataquery.ResultFormat, resources []string) string {
types := traceTypes
if len(types) == 0 {
types = Tables
@ -709,7 +713,7 @@ func buildTracesQuery(operationId string, parentSpanID *string, traceTypes []str
filteredTypes := make([]string, 0)
// If the result format is set to trace then we filter out all events that are of the type traces as they don't make sense when visualised as a span
if resultFormat != nil && dataquery.ResultFormat(*resultFormat) == dataquery.ResultFormatTrace {
if resultFormat != nil && *resultFormat == dataquery.ResultFormatTrace {
filteredTypes = slices.Filter(filteredTypes, types, func(s string) bool { return s != "traces" })
} else {
filteredTypes = types

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics"
azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
@ -69,7 +70,7 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
for _, query := range queries {
var target string
queryJSONModel := types.AzureMonitorJSONQuery{}
queryJSONModel := dataquery.AzureMonitorQuery{}
err := json.Unmarshal(query.JSON, &queryJSONModel)
if err != nil {
return nil, fmt.Errorf("failed to decode the Azure Monitor query object from JSON: %w", err)
@ -77,49 +78,29 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
azJSONModel := queryJSONModel.AzureMonitor
// Legacy: If only MetricDefinition is set, use it as namespace
if azJSONModel.MetricDefinition != "" && azJSONModel.MetricNamespace == "" {
if azJSONModel.MetricDefinition != nil && *azJSONModel.MetricDefinition != "" &&
azJSONModel.MetricNamespace != nil && *azJSONModel.MetricNamespace == "" {
azJSONModel.MetricNamespace = azJSONModel.MetricDefinition
}
azJSONModel.DimensionFilters = MigrateDimensionFilters(azJSONModel.DimensionFilters)
alias := azJSONModel.Alias
timeGrain := azJSONModel.TimeGrain
timeGrains := azJSONModel.AllowedTimeGrainsMs
if timeGrain == "auto" {
timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains)
if err != nil {
return nil, err
}
alias := ""
if azJSONModel.Alias != nil {
alias = *azJSONModel.Alias
}
params := url.Values{}
params.Add("api-version", AzureMonitorAPIVersion)
params.Add("timespan", fmt.Sprintf("%v/%v", query.TimeRange.From.UTC().Format(time.RFC3339), query.TimeRange.To.UTC().Format(time.RFC3339)))
params.Add("interval", timeGrain)
params.Add("aggregation", azJSONModel.Aggregation)
params.Add("metricnames", azJSONModel.MetricName)
if azJSONModel.CustomNamespace != "" {
params.Add("metricnamespace", azJSONModel.CustomNamespace)
} else {
params.Add("metricnamespace", azJSONModel.MetricNamespace)
azureURL := ""
if queryJSONModel.Subscription != nil {
azureURL = BuildSubscriptionMetricsURL(*queryJSONModel.Subscription)
}
azureURL := BuildSubscriptionMetricsURL(queryJSONModel.Subscription)
filterInBody := true
if azJSONModel.Region != "" {
params.Add("region", azJSONModel.Region)
}
resourceIDs := []string{}
resourceMap := map[string]types.AzureMonitorResource{}
resourceMap := map[string]dataquery.AzureMonitorResource{}
if hasOne, resourceGroup, resourceName := hasOneResource(queryJSONModel); hasOne {
ub := urlBuilder{
ResourceURI: azJSONModel.ResourceURI,
ResourceURI: azJSONModel.ResourceUri,
// Alternative, used to reconstruct resource URI if it's not present
DefaultSubscription: dsInfo.Settings.SubscriptionId,
DefaultSubscription: &dsInfo.Settings.SubscriptionId,
Subscription: queryJSONModel.Subscription,
ResourceGroup: resourceGroup,
MetricNamespace: azJSONModel.MetricNamespace,
@ -128,25 +109,36 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
azureURL = ub.BuildMetricsURL()
// POST requests are only supported at the subscription level
filterInBody = false
resourceMap[ub.buildResourceURI()] = types.AzureMonitorResource{ResourceGroup: resourceGroup, ResourceName: resourceName}
resourceUri := ub.buildResourceURI()
if resourceUri != nil {
resourceMap[*resourceUri] = dataquery.AzureMonitorResource{ResourceGroup: resourceGroup, ResourceName: resourceName}
}
} else {
for _, r := range azJSONModel.Resources {
ub := urlBuilder{
DefaultSubscription: dsInfo.Settings.SubscriptionId,
DefaultSubscription: &dsInfo.Settings.SubscriptionId,
Subscription: queryJSONModel.Subscription,
ResourceGroup: r.ResourceGroup,
MetricNamespace: azJSONModel.MetricNamespace,
ResourceName: r.ResourceName,
}
resourceUri := ub.buildResourceURI()
resourceMap[resourceUri] = r
resourceIDs = append(resourceIDs, fmt.Sprintf("Microsoft.ResourceId eq '%s'", resourceUri))
if resourceUri != nil {
resourceMap[*resourceUri] = r
}
resourceIDs = append(resourceIDs, fmt.Sprintf("Microsoft.ResourceId eq '%s'", *resourceUri))
}
}
// old model
dimension := strings.TrimSpace(azJSONModel.Dimension)
dimensionFilter := strings.TrimSpace(azJSONModel.DimensionFilter)
dimension := ""
if azJSONModel.Dimension != nil {
dimension = strings.TrimSpace(*azJSONModel.Dimension)
}
dimensionFilter := ""
if azJSONModel.DimensionFilter != nil {
dimensionFilter = strings.TrimSpace(*azJSONModel.DimensionFilter)
}
dimSB := strings.Builder{}
@ -155,9 +147,9 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
} else {
for i, filter := range azJSONModel.DimensionFilters {
if len(filter.Filters) == 0 {
dimSB.WriteString(fmt.Sprintf("%s eq '*'", filter.Dimension))
dimSB.WriteString(fmt.Sprintf("%s eq '*'", *filter.Dimension))
} else {
dimSB.WriteString(filter.ConstructFiltersString())
dimSB.WriteString(types.ConstructFiltersString(filter))
}
if i != len(azJSONModel.DimensionFilters)-1 {
dimSB.WriteString(" and ")
@ -175,16 +167,21 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
}
}
if azJSONModel.Top != "" {
params.Add("top", azJSONModel.Top)
params, err := getParams(azJSONModel, query)
if err != nil {
return nil, err
}
target = params.Encode()
if setting.Env == setting.Dev {
logger.Debug("Azuremonitor request", "params", params)
}
sub := ""
if queryJSONModel.Subscription != nil {
sub = *queryJSONModel.Subscription
}
query := &types.AzureMonitorQuery{
URL: azureURL,
Target: target,
@ -194,7 +191,7 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
TimeRange: query.TimeRange,
Dimensions: azJSONModel.DimensionFilters,
Resources: resourceMap,
Subscription: queryJSONModel.Subscription,
Subscription: sub,
}
if filterString != "" {
if filterInBody {
@ -209,6 +206,45 @@ func (e *AzureMonitorDatasource) buildQueries(logger log.Logger, queries []backe
return azureMonitorQueries, nil
}
func getParams(azJSONModel *dataquery.AzureMetricQuery, query backend.DataQuery) (url.Values, error) {
params := url.Values{}
timeGrain := azJSONModel.TimeGrain
timeGrains := azJSONModel.AllowedTimeGrainsMs
if timeGrain != nil && *timeGrain == "auto" {
var err error
timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains)
if err != nil {
return nil, err
}
}
params.Add("api-version", AzureMonitorAPIVersion)
params.Add("timespan", fmt.Sprintf("%v/%v", query.TimeRange.From.UTC().Format(time.RFC3339), query.TimeRange.To.UTC().Format(time.RFC3339)))
if timeGrain != nil {
params.Add("interval", *timeGrain)
}
if azJSONModel.Aggregation != nil {
params.Add("aggregation", *azJSONModel.Aggregation)
}
if azJSONModel.MetricName != nil {
params.Add("metricnames", *azJSONModel.MetricName)
}
if azJSONModel.CustomNamespace != nil && *azJSONModel.CustomNamespace != "" {
params.Add("metricnamespace", *azJSONModel.CustomNamespace)
} else if azJSONModel.MetricNamespace != nil {
params.Add("metricnamespace", *azJSONModel.MetricNamespace)
}
if azJSONModel.Region != nil && *azJSONModel.Region != "" {
params.Add("region", *azJSONModel.Region)
}
if azJSONModel.Top != nil && *azJSONModel.Top != "" {
params.Add("top", *azJSONModel.Top)
}
return params, nil
}
func retrieveSubscriptionDetails(e *AzureMonitorDatasource, cli *http.Client, ctx context.Context, logger log.Logger, tracer tracing.Tracer, subscriptionId string, baseUrl string, dsId int64, orgId int64) {
req, err := e.createRequest(ctx, logger, fmt.Sprintf("%s/subscriptions/%s", baseUrl, subscriptionId))
if err != nil {
@ -489,7 +525,20 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, res
continue
}
switch dimension.Operator {
if dimension.Dimension == nil {
continue
}
if dimension.Operator == nil {
filter := types.AzureMonitorDimensionFilterBackend{
Key: *dimension.Dimension,
Operator: 0,
Values: dimensionFilters,
}
filters = append(filters, filter)
continue
}
switch *dimension.Operator {
case "eq":
dimensionInt = 0
case "ne":
@ -499,7 +548,7 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, res
}
filter := types.AzureMonitorDimensionFilterBackend{
Key: dimension.Dimension,
Key: *dimension.Dimension,
Operator: dimensionInt,
Values: dimensionFilters,
}
@ -552,7 +601,7 @@ func getQueryUrl(query *types.AzureMonitorQuery, azurePortalUrl, resourceID, res
// formatAzureMonitorLegendKey builds the legend key or timeseries name
// Alias patterns like {{resourcename}} are replaced with the appropriate data values.
func formatAzureMonitorLegendKey(alias string, subscriptionId string, resource types.AzureMonitorResource, metricName string, metadataName string,
func formatAzureMonitorLegendKey(alias string, subscriptionId string, resource dataquery.AzureMonitorResource, metricName string, metadataName string,
metadataValue string, namespace string, seriesID string, labels data.Labels) string {
// Could be a collision problem if there were two keys that varied only in case, but I don't think that would happen in azure.
lowerLabels := data.Labels{}
@ -582,16 +631,16 @@ func formatAzureMonitorLegendKey(alias string, subscriptionId string, resource t
}
}
if metaPartName == "resourcegroup" {
return []byte(resource.ResourceGroup)
if metaPartName == "resourcegroup" && resource.ResourceGroup != nil {
return []byte(*resource.ResourceGroup)
}
if metaPartName == "namespace" {
return []byte(namespace)
}
if metaPartName == "resourcename" {
return []byte(resource.ResourceName)
if metaPartName == "resourcename" && resource.ResourceName != nil {
return []byte(*resource.ResourceName)
}
if metaPartName == "metric" {
@ -674,17 +723,17 @@ func extractResourceIDFromMetricsURL(url string) string {
return strings.Split(url, "/providers/microsoft.insights/metrics")[0]
}
func hasOneResource(query types.AzureMonitorJSONQuery) (bool, string, string) {
func hasOneResource(query dataquery.AzureMonitorQuery) (bool, *string, *string) {
azJSONModel := query.AzureMonitor
if len(azJSONModel.Resources) > 1 {
return false, "", ""
return false, nil, nil
}
if len(azJSONModel.Resources) == 1 {
return true, azJSONModel.Resources[0].ResourceGroup, azJSONModel.Resources[0].ResourceName
}
if azJSONModel.ResourceGroup != "" || azJSONModel.ResourceName != "" {
if (azJSONModel.ResourceGroup != nil && *azJSONModel.ResourceGroup != "") || (azJSONModel.ResourceName != nil && *azJSONModel.ResourceName != "") {
// Deprecated, Resources should be used instead
return true, azJSONModel.ResourceGroup, azJSONModel.ResourceName
}
return false, "", ""
return false, nil, nil
}

View File

@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/testdata"
azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
@ -48,7 +49,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
expectedBodyFilter string
expectedParamFilter string
expectedPortalURL *string
resources map[string]types.AzureMonitorResource
resources map[string]dataquery.AzureMonitorResource
}{
{
name: "Parse queries from frontend and build AzureMonitor API queries",
@ -110,7 +111,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "legacy query without resourceURI and has dimensionFilter*s* property with one dimension",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("eq"), Filter: &wildcardFilter}},
"top": "30",
},
queryInterval: duration,
@ -123,7 +124,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "eq", Filter: &wildcardFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("eq"), Filter: &wildcardFilter}, {Dimension: strPtr("tier"), Operator: strPtr("eq"), Filter: &wildcardFilter}},
"top": "30",
},
queryInterval: duration,
@ -148,7 +149,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "has dimensionFilter*s* property with not equals operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test"}}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("ne"), Filter: &wildcardFilter, Filters: []string{"test"}}},
"top": "30",
},
queryInterval: duration,
@ -161,7 +162,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "has dimensionFilter*s* property with startsWith operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &testFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("sw"), Filter: &testFilter}},
"top": "30",
},
queryInterval: duration,
@ -174,7 +175,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "correctly sets dimension operator to eq (irrespective of operator) when filter value is '*'",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "ne", Filter: &wildcardFilter}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("sw"), Filter: &wildcardFilter}, {Dimension: strPtr("tier"), Operator: strPtr("ne"), Filter: &wildcardFilter}},
"top": "30",
},
queryInterval: duration,
@ -187,7 +188,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "correctly constructs target when multiple filter values are provided for the 'eq' operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("eq"), Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"top": "30",
},
queryInterval: duration,
@ -200,7 +201,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "correctly constructs target when multiple filter values are provided for ne 'eq' operator",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("ne"), Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"top": "30",
},
queryInterval: duration,
@ -226,7 +227,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
"timeGrain": "PT1M",
"top": "10",
"region": "westus",
"resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}, {ResourceGroup: "rg2", ResourceName: "vm2"}},
"resources": []dataquery.AzureMonitorResource{{ResourceGroup: strPtr("rg"), ResourceName: strPtr("vm")}, {ResourceGroup: strPtr("rg2"), ResourceName: strPtr("vm2")}},
},
expectedInterval: "PT1M",
azureMonitorQueryTarget: "aggregation=Average&api-version=2021-05-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute%2FvirtualMachines&region=westus&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=10",
@ -237,7 +238,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "includes a single resource as a parameter filter",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}},
"resources": []dataquery.AzureMonitorResource{{ResourceGroup: strPtr("rg"), ResourceName: strPtr("vm")}},
},
queryInterval: duration,
expectedInterval: "PT1M",
@ -248,8 +249,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
name: "includes a resource and a dimesion as filters",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"resources": []types.AzureMonitorResource{{ResourceGroup: "rg", ResourceName: "vm"}, {ResourceGroup: "rg2", ResourceName: "vm2"}},
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"resources": []dataquery.AzureMonitorResource{{ResourceGroup: strPtr("rg"), ResourceName: strPtr("vm")}, {ResourceGroup: strPtr("rg2"), ResourceName: strPtr("vm2")}},
"dimensionFilters": []dataquery.AzureMetricDimension{{Dimension: strPtr("blob"), Operator: strPtr("ne"), Filter: &wildcardFilter, Filters: []string{"test", "test2"}}},
"top": "30",
},
queryInterval: duration,
@ -296,14 +297,14 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
queries, err := datasource.buildQueries(log.New("test"), tsdbQuery, dsInfo)
require.NoError(t, err)
resources := map[string]types.AzureMonitorResource{}
resources := map[string]dataquery.AzureMonitorResource{}
if tt.azureMonitorVariedProperties["resources"] != nil {
resourceSlice := tt.azureMonitorVariedProperties["resources"].([]types.AzureMonitorResource)
resourceSlice := tt.azureMonitorVariedProperties["resources"].([]dataquery.AzureMonitorResource)
for _, resource := range resourceSlice {
resources[fmt.Sprintf("/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", resource.ResourceGroup, resource.ResourceName)] = resource
resources[fmt.Sprintf("/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", *resource.ResourceGroup, *resource.ResourceName)] = resource
}
} else {
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] = types.AzureMonitorResource{ResourceGroup: "grafanastaging", ResourceName: "grafana"}
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] = dataquery.AzureMonitorResource{ResourceGroup: strPtr("grafanastaging"), ResourceName: strPtr("grafana")}
}
azureMonitorQuery := &types.AzureMonitorQuery{
@ -366,8 +367,9 @@ func TestCustomNamespace(t *testing.T) {
}
func TestAzureMonitorParseResponse(t *testing.T) {
resources := map[string]types.AzureMonitorResource{}
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] = types.AzureMonitorResource{ResourceGroup: "grafanastaging", ResourceName: "grafana"}
resources := map[string]dataquery.AzureMonitorResource{}
resources["/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana"] =
dataquery.AzureMonitorResource{ResourceGroup: strPtr("grafanastaging"), ResourceName: strPtr("grafana")}
subscription := "12345678-aaaa-bbbb-cccc-123456789abc"
tests := []struct {
@ -484,7 +486,7 @@ func TestAzureMonitorParseResponse(t *testing.T) {
Params: url.Values{
"aggregation": {"Average"},
},
Resources: map[string]types.AzureMonitorResource{"/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanatest/providers/Microsoft.Storage/storageAccounts/testblobaccount/blobServices/default/providers/Microsoft.Insights/metrics": {ResourceGroup: "grafanatest", ResourceName: "testblobaccount"}},
Resources: map[string]dataquery.AzureMonitorResource{"/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanatest/providers/Microsoft.Storage/storageAccounts/testblobaccount/blobServices/default/providers/Microsoft.Insights/metrics": {ResourceGroup: strPtr("grafanatest"), ResourceName: strPtr("testblobaccount")}},
Subscription: subscription,
},
},
@ -681,3 +683,7 @@ func TestExtractResourceNameFromMetricsURL(t *testing.T) {
require.Equal(t, expected, extractResourceNameFromMetricsURL((url)))
})
}
func strPtr(s string) *string {
return &s
}

View File

@ -1,11 +1,9 @@
package metrics
import (
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
)
import "github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
func MigrateDimensionFilters(filters []types.AzureMonitorDimensionFilter) []types.AzureMonitorDimensionFilter {
var newFilters []types.AzureMonitorDimensionFilter
func MigrateDimensionFilters(filters []dataquery.AzureMetricDimension) []dataquery.AzureMetricDimension {
var newFilters []dataquery.AzureMetricDimension
for _, filter := range filters {
newFilter := filter
// Ignore the deprecation check as this is a migration

View File

@ -5,7 +5,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
)
func TestDimensionFiltersMigration(t *testing.T) {
@ -14,38 +14,38 @@ func TestDimensionFiltersMigration(t *testing.T) {
additionalTestFilter := "testFilter2"
tests := []struct {
name string
dimensionFilters []types.AzureMonitorDimensionFilter
expectedDimensionFilters []types.AzureMonitorDimensionFilter
dimensionFilters []dataquery.AzureMetricDimension
expectedDimensionFilters []dataquery.AzureMetricDimension
}{
{
name: "will return new format unchanged",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{"testFilter"}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{"testFilter"}}},
},
{
name: "correctly updates old format with wildcard",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq"}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &wildcard}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq")}},
},
{
name: "correctly updates old format with a value",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &testFilter}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter}}},
},
{
name: "correctly ignores wildcard if filters has a value",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard, Filters: []string{testFilter}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &wildcard, Filters: []string{testFilter}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter}}},
},
{
name: "correctly merges values if filters has a value (ignores duplicates)",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &testFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter}}},
},
{
name: "correctly merges values if filters has a value",
dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &additionalTestFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter, additionalTestFilter}}},
dimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filter: &additionalTestFilter, Filters: []string{testFilter}}},
expectedDimensionFilters: []dataquery.AzureMetricDimension{{Dimension: strPtr("testDimension"), Operator: strPtr("eq"), Filters: []string{testFilter, additionalTestFilter}}},
},
}

View File

@ -7,42 +7,54 @@ import (
// urlBuilder builds the URL for calling the Azure Monitor API
type urlBuilder struct {
ResourceURI string
ResourceURI *string
// Following fields will be used to generate a ResourceURI
DefaultSubscription string
Subscription string
ResourceGroup string
MetricNamespace string
ResourceName string
DefaultSubscription *string
Subscription *string
ResourceGroup *string
MetricNamespace *string
ResourceName *string
}
func (params *urlBuilder) buildResourceURI() string {
if params.ResourceURI != "" {
func (params *urlBuilder) buildResourceURI() *string {
if params.ResourceURI != nil && *params.ResourceURI != "" {
return params.ResourceURI
}
subscription := params.Subscription
if params.Subscription == "" {
if params.Subscription == nil || *params.Subscription == "" {
subscription = params.DefaultSubscription
}
metricNamespaceArray := strings.Split(params.MetricNamespace, "/")
resourceNameArray := strings.Split(params.ResourceName, "/")
if params.MetricNamespace == nil || *params.MetricNamespace == "" {
return nil
}
metricNamespaceArray := strings.Split(*params.MetricNamespace, "/")
var resourceNameArray []string
if params.ResourceName != nil && *params.ResourceName != "" {
resourceNameArray = strings.Split(*params.ResourceName, "/")
}
provider := metricNamespaceArray[0]
metricNamespaceArray = metricNamespaceArray[1:]
if strings.HasPrefix(strings.ToLower(params.MetricNamespace), "microsoft.storage/storageaccounts/") &&
!strings.HasSuffix(params.ResourceName, "default") {
if strings.HasPrefix(strings.ToLower(*params.MetricNamespace), "microsoft.storage/storageaccounts/") &&
params.ResourceName != nil &&
!strings.HasSuffix(*params.ResourceName, "default") {
resourceNameArray = append(resourceNameArray, "default")
}
resGroup := ""
if params.ResourceGroup != nil {
resGroup = *params.ResourceGroup
}
urlArray := []string{
"/subscriptions",
subscription,
*subscription,
"resourceGroups",
params.ResourceGroup,
resGroup,
"providers",
provider,
}
@ -52,7 +64,7 @@ func (params *urlBuilder) buildResourceURI() string {
}
resourceURI := strings.Join(urlArray, "/")
return resourceURI
return &resourceURI
}
// BuildMetricsURL checks the metric properties to see which form of the url
@ -61,11 +73,11 @@ func (params *urlBuilder) BuildMetricsURL() string {
resourceURI := params.ResourceURI
// Prior to Grafana 9, we had a legacy query object rather than a resourceURI, so we manually create the resource URI
if resourceURI == "" {
if resourceURI == nil || *resourceURI == "" {
resourceURI = params.buildResourceURI()
}
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI)
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", *resourceURI)
}
// BuildSubscriptionMetricsURL returns a URL for querying metrics for all resources in a subscription

View File

@ -10,7 +10,7 @@ func TestURLBuilder(t *testing.T) {
t.Run("AzureMonitor URL Builder", func(t *testing.T) {
t.Run("when only resource uri is provided it returns resource/uri/providers/microsoft.insights/metrics", func(t *testing.T) {
ub := &urlBuilder{
ResourceURI: "/subscriptions/sub/resource/uri",
ResourceURI: strPtr("/subscriptions/sub/resource/uri"),
}
url := ub.BuildMetricsURL()
@ -19,11 +19,11 @@ func TestURLBuilder(t *testing.T) {
t.Run("when resource uri and legacy fields are provided the legacy fields are ignored", func(t *testing.T) {
ub := &urlBuilder{
ResourceURI: "/subscriptions/sub/resource/uri",
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
ResourceName: "rn1/rn2/rn3",
ResourceURI: strPtr("/subscriptions/sub/resource/uri"),
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.NetApp/netAppAccounts/capacityPools/volumes"),
ResourceName: strPtr("rn1/rn2/rn3"),
}
url := ub.BuildMetricsURL()
@ -33,10 +33,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("Legacy URL Builder params", func(t *testing.T) {
t.Run("when metric definition is in the short form", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Compute/virtualMachines"),
ResourceName: strPtr("rn"),
}
url := ub.BuildMetricsURL()
@ -45,11 +45,11 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is in the short form and a subscription is defined", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
Subscription: "specified-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
DefaultSubscription: strPtr("default-sub"),
Subscription: strPtr("specified-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Compute/virtualMachines"),
ResourceName: strPtr("rn"),
}
url := ub.BuildMetricsURL()
@ -58,10 +58,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Storage/storageAccounts/blobServices",
ResourceName: "rn1/default",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Storage/storageAccounts/blobServices"),
ResourceName: strPtr("rn1/default"),
}
url := ub.BuildMetricsURL()
@ -70,10 +70,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Storage/storageAccounts/fileServices",
ResourceName: "rn1/default",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Storage/storageAccounts/fileServices"),
ResourceName: strPtr("rn1/default"),
}
url := ub.BuildMetricsURL()
@ -82,10 +82,10 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
ResourceName: "rn1/rn2/rn3",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.NetApp/netAppAccounts/capacityPools/volumes"),
ResourceName: strPtr("rn1/rn2/rn3"),
}
url := ub.BuildMetricsURL()
@ -94,13 +94,13 @@ func TestURLBuilder(t *testing.T) {
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricNamespace: "Microsoft.Storage/storageAccounts/blobServices",
ResourceName: "rn1",
DefaultSubscription: strPtr("default-sub"),
ResourceGroup: strPtr("rg"),
MetricNamespace: strPtr("Microsoft.Storage/storageAccounts/blobServices"),
ResourceName: strPtr("rn1"),
}
url := ub.buildResourceURI()
url := *ub.buildResourceURI()
assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default", url)
})
})

View File

@ -8,14 +8,14 @@ var (
// SetAutoTimeGrain tries to find the closest interval to the query's intervalMs value
// if the metric has a limited set of possible intervals/time grains then use those
// instead of the default list of intervals
func SetAutoTimeGrain(intervalMs int64, timeGrains []int64) (string, error) {
func SetAutoTimeGrain(intervalMs int64, timeGrains []int64) (*string, error) {
autoInterval := FindClosestAllowedIntervalMS(intervalMs, timeGrains)
autoTimeGrain, err := CreateISO8601DurationFromIntervalMS(autoInterval)
if err != nil {
return "", err
return nil, err
}
return autoTimeGrain, nil
return &autoTimeGrain, nil
}
// FindClosestAllowedIntervalMS is used for the auto time grain setting.

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
)
const (
@ -83,8 +84,8 @@ type AzureMonitorQuery struct {
Alias string
TimeRange backend.TimeRange
BodyFilter string
Dimensions []AzureMonitorDimensionFilter
Resources map[string]AzureMonitorResource
Dimensions []dataquery.AzureMetricDimension
Resources map[string]dataquery.AzureMonitorResource
Subscription string
}
@ -138,110 +139,38 @@ type AzureMonitorResource struct {
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"`
Region string `json:"region"`
Resources []AzureMonitorResource `json:"resources"`
Aggregation string `json:"aggregation"`
Alias string `json:"alias"`
DimensionFilters []AzureMonitorDimensionFilter `json:"dimensionFilters"` // new model
TimeGrain string `json:"timeGrain"`
Top string `json:"top"`
AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"`
Dimension string `json:"dimension"` // old model
DimensionFilter string `json:"dimensionFilter"` // old model
Format string `json:"format"`
// 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"`
}
// AzureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric
// queries like "BlobType", "eq", "*"
type AzureMonitorDimensionFilter struct {
Dimension string `json:"dimension"`
Operator string `json:"operator"`
Filters []string `json:"filters,omitempty"`
// Deprecated: To support multiselection, filters are passed in a slice now. Also migrated in frontend.
Filter *string `json:"filter,omitempty"`
}
type AzureMonitorDimensionFilterBackend struct {
Key string `json:"key"`
Operator int `json:"operator"`
Values []string `json:"values"`
}
func (a AzureMonitorDimensionFilter) ConstructFiltersString() string {
func ConstructFiltersString(a dataquery.AzureMetricDimension) string {
var filterStrings []string
for _, filter := range a.Filters {
filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter))
dimension := ""
operator := ""
if a.Dimension != nil {
dimension = *a.Dimension
}
if a.Operator != nil {
operator = *a.Operator
}
filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", dimension, operator, filter))
}
if a.Operator == "eq" {
if a.Operator != nil && *a.Operator == "eq" {
return strings.Join(filterStrings, " or ")
} else {
return strings.Join(filterStrings, " and ")
}
return strings.Join(filterStrings, " and ")
}
// LogJSONQuery is the frontend JSON query model for an Azure Log Analytics query.
type LogJSONQuery struct {
AzureLogAnalytics struct {
Query string `json:"query"`
ResultFormat string `json:"resultFormat"`
Resources []string `json:"resources"`
OperationId string `json:"operationId"`
// Deprecated: Queries should be migrated to use Resource instead
Workspace string `json:"workspace"`
// Deprecated: Use Resources instead
Resource string `json:"resource"`
} `json:"azureLogAnalytics"`
AzureLogAnalytics dataquery.AzureLogsQuery `json:"azureLogAnalytics"`
}
type TracesJSONQuery struct {
AzureTraces struct {
// Filters for property values.
Filters []TracesFilters `json:"filters"`
// Operation ID. Used only for Traces queries.
OperationId *string `json:"operationId"`
// KQL query to be executed.
Query *string `json:"query"`
// Array of resource URIs to be queried.
Resources []string `json:"resources"`
// Specifies the format results should be returned as.
ResultFormat *string `json:"resultFormat"`
// Types of events to filter by.
TraceTypes []string `json:"traceTypes"`
} `json:"azureTraces"`
}
type TracesFilters struct {
// Values to filter by.
Filters []string `json:"filters"`
// Comparison operator to use. Either equals or not equals.
Operation string `json:"operation"`
// Property name, auto-populated based on available traces.
Property string `json:"property"`
AzureTraces dataquery.AzureTracesQuery `json:"azureTraces"`
}
// MetricChartDefinition is the JSON model for a metrics chart definition