From fcb9dcb0b8ab15ff9a19c1c59bf93423fcb5e4ee Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 26 May 2020 10:52:33 -0400 Subject: [PATCH] AzureMonitor: Types for front-end queries (#24937) --- .../applicationinsights-datasource.go | 70 +++++++++---------- .../applicationinsights-datasource_test.go | 3 +- .../azure-log-analytics-datasource.go | 19 +++-- .../azuremonitor/azuremonitor-datasource.go | 55 +++++++++------ .../azuremonitor-datasource_test.go | 2 +- pkg/tsdb/azuremonitor/azuremonitor-time.go | 21 +----- pkg/tsdb/azuremonitor/types.go | 48 +++++++++++++ 7 files changed, 131 insertions(+), 87 deletions(-) diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go b/pkg/tsdb/azuremonitor/applicationinsights-datasource.go index b6e5914454a..495d43ce5be 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go +++ b/pkg/tsdb/azuremonitor/applicationinsights-datasource.go @@ -79,32 +79,30 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []*tsdb.Query, time } for _, query := range queries { - applicationInsightsTarget := query.Model.Get("appInsights").MustMap() - azlog.Debug("Application Insights", "target", applicationInsightsTarget) - - rawQuery := false - if asInterface, ok := applicationInsightsTarget["rawQuery"]; ok { - if asBool, ok := asInterface.(bool); ok { - rawQuery = asBool - } else { - return nil, errors.New("'rawQuery' should be a boolean") - } - } else { - return nil, errors.New("missing 'rawQuery' property") + queryBytes, err := query.Model.Encode() + if err != nil { + return nil, fmt.Errorf("failed to re-encode the Azure Application Insights query into JSON: %w", err) + } + queryJSONModel := insightsJSONQuery{} + err = json.Unmarshal(queryBytes, &queryJSONModel) + if err != nil { + return nil, fmt.Errorf("failed to decode the Azure Application Insights query object from JSON: %w", err) } - if rawQuery { + insightsJSONModel := queryJSONModel.AppInsights + azlog.Debug("Application Insights", "target", insightsJSONModel) + + if insightsJSONModel.RawQuery == nil { + return nil, fmt.Errorf("missing the 'rawQuery' property") + } + + if *insightsJSONModel.RawQuery { var rawQueryString string - if asInterface, ok := applicationInsightsTarget["rawQueryString"]; ok { - if asString, ok := asInterface.(string); ok { - rawQueryString = asString - } - } - if rawQueryString == "" { + if insightsJSONModel.RawQueryString == "" { return nil, errors.New("rawQuery requires rawQueryString") } - rawQueryString, err := KqlInterpolate(query, timeRange, fmt.Sprintf("%v", rawQueryString)) + rawQueryString, err := KqlInterpolate(query, timeRange, insightsJSONModel.RawQueryString) if err != nil { return nil, err } @@ -117,20 +115,15 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []*tsdb.Query, time IsRaw: true, ApiURL: "query", Params: params, - TimeColumnName: fmt.Sprintf("%v", applicationInsightsTarget["timeColumn"]), - ValueColumnName: fmt.Sprintf("%v", applicationInsightsTarget["valueColumn"]), - SegmentColumnName: fmt.Sprintf("%v", applicationInsightsTarget["segmentColumn"]), + TimeColumnName: insightsJSONModel.TimeColumn, + ValueColumnName: insightsJSONModel.ValueColumn, + SegmentColumnName: insightsJSONModel.SegmentColumn, Target: params.Encode(), }) } else { - alias := "" - if val, ok := applicationInsightsTarget["alias"]; ok { - alias = fmt.Sprintf("%v", val) - } - - azureURL := fmt.Sprintf("metrics/%s", fmt.Sprintf("%v", applicationInsightsTarget["metricName"])) - timeGrain := fmt.Sprintf("%v", applicationInsightsTarget["timeGrain"]) - timeGrains := applicationInsightsTarget["allowedTimeGrainsMs"] + azureURL := fmt.Sprintf("metrics/%s", insightsJSONModel.MetricName) + timeGrain := insightsJSONModel.TimeGrain + timeGrains := insightsJSONModel.AllowedTimeGrainsMs if timeGrain == "auto" { timeGrain, err = setAutoTimeGrain(query.IntervalMs, timeGrains) if err != nil { @@ -143,16 +136,17 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []*tsdb.Query, time if timeGrain != "none" { params.Add("interval", timeGrain) } - params.Add("aggregation", fmt.Sprintf("%v", applicationInsightsTarget["aggregation"])) + params.Add("aggregation", insightsJSONModel.Aggregation) - dimension := strings.TrimSpace(fmt.Sprintf("%v", applicationInsightsTarget["dimension"])) - if applicationInsightsTarget["dimension"] != nil && len(dimension) > 0 && !strings.EqualFold(dimension, "none") { + dimension := strings.TrimSpace(insightsJSONModel.Dimension) + // Azure Monitor combines this and the following logic such that if dimensionFilter, must also Dimension, should that be done here as well? + if dimension != "" && !strings.EqualFold(dimension, "none") { params.Add("segment", dimension) } - dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", applicationInsightsTarget["dimensionFilter"])) - if applicationInsightsTarget["dimensionFilter"] != nil && len(dimensionFilter) > 0 { - params.Add("filter", fmt.Sprintf("%v", dimensionFilter)) + dimensionFilter := strings.TrimSpace(insightsJSONModel.DimensionFilter) + if dimensionFilter != "" { + params.Add("filter", dimensionFilter) } applicationInsightsQueries = append(applicationInsightsQueries, &ApplicationInsightsQuery{ @@ -160,7 +154,7 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []*tsdb.Query, time IsRaw: false, ApiURL: azureURL, Params: params, - Alias: alias, + Alias: insightsJSONModel.Alias, Target: params.Encode(), }) } diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go b/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go index 85de8a3622d..9798b478003 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go +++ b/pkg/tsdb/azuremonitor/applicationinsights-datasource_test.go @@ -1,7 +1,6 @@ package azuremonitor import ( - "encoding/json" "fmt" "io/ioutil" "testing" @@ -92,7 +91,7 @@ func TestApplicationInsightsDatasource(t *testing.T) { "metricName": "Percentage CPU", "alias": "testalias", "queryType": "Application Insights", - "allowedTimeGrainsMs": []interface{}{"auto", json.Number("60000"), json.Number("300000")}, + "allowedTimeGrainsMs": []int64{60000, 300000}, }, }) tsdbQuery.Queries[0].IntervalMs = 400000 diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go index c9c58f7915d..e65a1bedb20 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go @@ -72,20 +72,31 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []*tsdb.Query, timeRa azureLogAnalyticsQueries := []*AzureLogAnalyticsQuery{} for _, query := range queries { - azureLogAnalyticsTarget := query.Model.Get("azureLogAnalytics").MustMap() + queryBytes, err := query.Model.Encode() + if err != nil { + return nil, fmt.Errorf("failed to re-encode the Azure Log Analytics query into JSON: %w", err) + } + + queryJSONModel := logJSONQuery{} + err = json.Unmarshal(queryBytes, &queryJSONModel) + if err != nil { + return nil, fmt.Errorf("failed to decode the Azure Log Analytics query object from JSON: %w", err) + } + + azureLogAnalyticsTarget := queryJSONModel.AzureLogAnalytics azlog.Debug("AzureLogAnalytics", "target", azureLogAnalyticsTarget) - resultFormat := fmt.Sprintf("%v", azureLogAnalyticsTarget["resultFormat"]) + resultFormat := azureLogAnalyticsTarget.ResultFormat if resultFormat == "" { resultFormat = "time_series" } urlComponents := map[string]string{} - urlComponents["workspace"] = fmt.Sprintf("%v", azureLogAnalyticsTarget["workspace"]) + urlComponents["workspace"] = azureLogAnalyticsTarget.Workspace apiURL := fmt.Sprintf("%s/query", urlComponents["workspace"]) params := url.Values{} - rawQuery, err := KqlInterpolate(query, timeRange, fmt.Sprintf("%v", azureLogAnalyticsTarget["query"]), "TimeGenerated") + rawQuery, err := KqlInterpolate(query, timeRange, azureLogAnalyticsTarget.Query, "TimeGenerated") if err != nil { return nil, err } diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go index ce72800a467..e1f94042d54 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go @@ -36,6 +36,8 @@ var ( defaultAllowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000} ) +const azureMonitorAPIVersion = "2018-01-01" + // executeTimeSeriesQuery does the following: // 1. build the AzureMonitor url and querystring for each query // 2. executes each query by calling the Azure Monitor API @@ -81,31 +83,38 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * for _, query := range queries { var target string + queryBytes, err := query.Model.Encode() + if err != nil { + return nil, fmt.Errorf("failed to re-encode the Azure Monitor query into JSON: %w", err) + } - azureMonitorTarget := query.Model.Get("azureMonitor").MustMap() + queryJSONModel := azureMonitorJSONQuery{} + err = json.Unmarshal(queryBytes, &queryJSONModel) + if err != nil { + return nil, fmt.Errorf("failed to decode the Azure Monitor query object from JSON: %w", err) + } + + azJSONModel := queryJSONModel.AzureMonitor urlComponents := map[string]string{} - urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString()) - urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"]) - urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"]) - urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"]) + urlComponents["subscription"] = queryJSONModel.Subscription + urlComponents["resourceGroup"] = azJSONModel.ResourceGroup + urlComponents["metricDefinition"] = azJSONModel.MetricDefinition + urlComponents["resourceName"] = azJSONModel.ResourceName ub := urlBuilder{ DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(), - Subscription: urlComponents["subscription"], - ResourceGroup: urlComponents["resourceGroup"], - MetricDefinition: urlComponents["metricDefinition"], - ResourceName: urlComponents["resourceName"], + Subscription: queryJSONModel.Subscription, + ResourceGroup: queryJSONModel.AzureMonitor.ResourceGroup, + MetricDefinition: azJSONModel.MetricDefinition, + ResourceName: azJSONModel.ResourceName, } azureURL := ub.Build() - alias := "" - if val, ok := azureMonitorTarget["alias"]; ok { - alias = fmt.Sprintf("%v", val) - } + alias := azJSONModel.Alias - timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"]) - timeGrains := azureMonitorTarget["allowedTimeGrainsMs"] + timeGrain := azJSONModel.TimeGrain + timeGrains := azJSONModel.AllowedTimeGrainsMs if timeGrain == "auto" { timeGrain, err = setAutoTimeGrain(query.IntervalMs, timeGrains) if err != nil { @@ -114,18 +123,18 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * } params := url.Values{} - params.Add("api-version", "2018-01-01") + params.Add("api-version", azureMonitorAPIVersion) params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339))) params.Add("interval", timeGrain) - params.Add("aggregation", fmt.Sprintf("%v", azureMonitorTarget["aggregation"])) - params.Add("metricnames", fmt.Sprintf("%v", azureMonitorTarget["metricName"])) - params.Add("metricnamespace", fmt.Sprintf("%v", azureMonitorTarget["metricNamespace"])) + params.Add("aggregation", azJSONModel.Aggregation) + params.Add("metricnames", azJSONModel.MetricName) // MetricName or MetricNames ? + params.Add("metricnamespace", azJSONModel.MetricNamespace) - dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimension"])) - dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimensionFilter"])) - if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" { + dimension := strings.TrimSpace(azJSONModel.Dimension) + dimensionFilter := strings.TrimSpace(azJSONModel.DimensionFilter) + if dimension != "" && dimensionFilter != "" && dimension != "None" { params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter)) - params.Add("top", fmt.Sprintf("%v", azureMonitorTarget["top"])) + params.Add("top", azJSONModel.Top) } target = params.Encode() diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go index 16e09e6e0ac..9186f75fb23 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go @@ -104,7 +104,7 @@ func TestAzureMonitorDatasource(t *testing.T) { "metricName": "Percentage CPU", "alias": "testalias", "queryType": "Azure Monitor", - "allowedTimeGrainsMs": []interface{}{"auto", json.Number("60000"), json.Number("300000")}, + "allowedTimeGrainsMs": []int64{60000, 300000}, }, }) tsdbQuery.Queries[0].IntervalMs = 400000 diff --git a/pkg/tsdb/azuremonitor/azuremonitor-time.go b/pkg/tsdb/azuremonitor/azuremonitor-time.go index 3c7643a0a08..f631b864c9c 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-time.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-time.go @@ -1,27 +1,10 @@ package azuremonitor -import "encoding/json" - // 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 interface{}) (string, error) { - // parses array of numbers from the timeGrains json field - allowedTimeGrains := []int64{} - tgs, ok := timeGrains.([]interface{}) - if ok { - for _, v := range tgs { - jsonNumber, ok := v.(json.Number) - if ok { - tg, err := jsonNumber.Int64() - if err == nil { - allowedTimeGrains = append(allowedTimeGrains, tg) - } - } - } - } - - autoInterval := findClosestAllowedIntervalMS(intervalMs, allowedTimeGrains) +func setAutoTimeGrain(intervalMs int64, timeGrains []int64) (string, error) { + autoInterval := findClosestAllowedIntervalMS(intervalMs, timeGrains) tg := &TimeGrain{} autoTimeGrain, err := tg.createISO8601DurationFromIntervalMS(autoInterval) if err != nil { diff --git a/pkg/tsdb/azuremonitor/types.go b/pkg/tsdb/azuremonitor/types.go index 491e77fe67e..c31be5f98ab 100644 --- a/pkg/tsdb/azuremonitor/types.go +++ b/pkg/tsdb/azuremonitor/types.go @@ -90,3 +90,51 @@ type column struct { Name string `json:"name"` Type string `json:"type"` } + +// azureMonitorJSONQuery is the frontend JSON query model for an Azure Monitor query. +type azureMonitorJSONQuery struct { + AzureMonitor struct { + Aggregation string `json:"aggregation"` + Alias string `json:"alias"` + AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"` + Dimension string `json:"dimension"` + DimensionFilter string `json:"dimensionFilter"` + Format string `json:"format"` + MetricDefinition string `json:"metricDefinition"` + MetricName string `json:"metricName"` + MetricNamespace string `json:"metricNamespace"` + ResourceGroup string `json:"resourceGroup"` + ResourceName string `json:"resourceName"` + TimeGrain string `json:"timeGrain"` + Top string `json:"top"` + } `json:"azureMonitor"` + Subscription string `json:"subscription"` +} + +// insightsJSONQuery is the frontend JSON query model for an Azure Application Insights query. +type insightsJSONQuery struct { + AppInsights struct { + Aggregation string `json:"aggregation"` + Alias string `json:"alias"` + AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"` + Dimension string `json:"dimension"` + DimensionFilter string `json:"dimensionFilter"` + MetricName string `json:"metricName"` + RawQuery *bool `json:"rawQuery"` + RawQueryString string `json:"rawQueryString"` + TimeGrain string `json:"timeGrain"` + TimeColumn string `json:"timeColumn"` + ValueColumn string `json:"valueColumn"` + SegmentColumn string `json:"segmentColumn"` + } `json:"appInsights"` + Raw *bool `json:"raw"` +} + +// 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"` + Workspace string `json:"workspace"` + } `json:"azureLogAnalytics"` +}