diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index 7a3c00fb367..e554fcd71ca 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "net/url" "path" "regexp" "time" @@ -36,9 +35,9 @@ type AzureLogAnalyticsQuery struct { ResultFormat string URL string JSON json.RawMessage - Params url.Values - Target string TimeRange backend.TimeRange + Query string + Resources []string } func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) { @@ -72,7 +71,9 @@ func getApiURL(queryJSONModel types.LogJSONQuery) string { azureLogAnalyticsTarget := queryJSONModel.AzureLogAnalytics var resourceOrWorkspace string - if azureLogAnalyticsTarget.Resource != "" { + if len(azureLogAnalyticsTarget.Resources) > 0 { + resourceOrWorkspace = azureLogAnalyticsTarget.Resources[0] + } else if azureLogAnalyticsTarget.Resource != "" { resourceOrWorkspace = azureLogAnalyticsTarget.Resource } else { resourceOrWorkspace = azureLogAnalyticsTarget.Workspace @@ -107,21 +108,25 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(logger log.Logger, queries [] apiURL := getApiURL(queryJSONModel) - params := url.Values{} rawQuery, err := macros.KqlInterpolate(logger, query, dsInfo, azureLogAnalyticsTarget.Query, "TimeGenerated") if err != nil { return nil, err } - params.Add("query", rawQuery) + resources := []string{} + if len(azureLogAnalyticsTarget.Resources) > 0 { + resources = azureLogAnalyticsTarget.Resources + } else if azureLogAnalyticsTarget.Resource != "" { + resources = []string{azureLogAnalyticsTarget.Resource} + } azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, &AzureLogAnalyticsQuery{ RefID: query.RefID, ResultFormat: resultFormat, URL: apiURL, JSON: query.JSON, - Params: params, - Target: params.Encode(), TimeRange: query.TimeRange, + Query: rawQuery, + Resources: resources, }) } @@ -138,7 +143,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, logger l &data.Frame{ RefID: query.RefID, Meta: &data.FrameMeta{ - ExecutedQueryString: query.Params.Get("query"), + ExecutedQueryString: query.Query, }, }, } @@ -150,17 +155,14 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, logger l return dataResponseErrorWithExecuted(fmt.Errorf("credentials for Log Analytics are no longer supported. Go to the data source configuration to update Azure Monitor credentials")) } - req, err := e.createRequest(ctx, logger, url) + req, err := e.createRequest(ctx, logger, url, query) if err != nil { dataResponse.Error = err return dataResponse } - req.URL.Path = path.Join(req.URL.Path, query.URL) - req.URL.RawQuery = query.Params.Encode() - ctx, span := tracer.Start(ctx, "azure log analytics query") - span.SetAttributes("target", query.Target, attribute.Key("target").String(query.Target)) + span.SetAttributes("target", query.Query, attribute.Key("target").String(query.Query)) span.SetAttributes("from", query.TimeRange.From.UnixNano()/int64(time.Millisecond), attribute.Key("from").Int64(query.TimeRange.From.UnixNano()/int64(time.Millisecond))) span.SetAttributes("until", query.TimeRange.To.UnixNano()/int64(time.Millisecond), attribute.Key("until").Int64(query.TimeRange.To.UnixNano()/int64(time.Millisecond))) span.SetAttributes("datasource_id", dsInfo.DatasourceID, attribute.Key("datasource_id").Int64(dsInfo.DatasourceID)) @@ -186,7 +188,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, logger l return dataResponseErrorWithExecuted(err) } - frame, err := ResponseTableToFrame(t, query.RefID, query.Params.Get("query")) + frame, err := ResponseTableToFrame(t, query.RefID, query.Query) if err != nil { return dataResponseErrorWithExecuted(err) } @@ -201,7 +203,7 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, logger l } err = setAdditionalFrameMeta(frame, - query.Params.Get("query"), + query.Query, model.Get("azureLogAnalytics").Get("resource").MustString()) if err != nil { frame.AppendNotices(data.Notice{Severity: data.NoticeSeverityWarning, Text: "could not add custom metadata: " + err.Error()}) @@ -235,14 +237,26 @@ func appendErrorNotice(frame *data.Frame, err *AzureLogAnalyticsAPIError) *data. return frame } -func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, logger log.Logger, url string) (*http.Request, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) +func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, logger log.Logger, queryURL string, query *AzureLogAnalyticsQuery) (*http.Request, error) { + body := map[string]interface{}{ + "query": query.Query, + } + if len(query.Resources) > 1 { + body["resources"] = query.Resources + } + jsonValue, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("%v: %w", "failed to create request", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, queryURL, bytes.NewBuffer(jsonValue)) if err != nil { logger.Debug("Failed to create request", "error", err) return nil, fmt.Errorf("%v: %w", "failed to create request", err) } req.URL.Path = "/" req.Header.Set("Content-Type", "application/json") + req.URL.Path = path.Join(req.URL.Path, query.URL) return req, nil } diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go index 8926dae9389..41b21d28ad5 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -3,8 +3,8 @@ package loganalytics import ( "context" "fmt" + "io" "net/http" - "net/url" "strings" "testing" "time" @@ -40,7 +40,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", - "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", + "query": "Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", "resultFormat": "%s" } }`, types.TimeSeries)), @@ -57,12 +57,12 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", - "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", + "query": "Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", "resultFormat": "%s" } }`, types.TimeSeries)), - Params: url.Values{"query": {"query=Perf | where ['TimeGenerated'] >= datetime('2018-03-15T13:00:00Z') and ['TimeGenerated'] <= datetime('2018-03-15T13:34:00Z') | where ['Computer'] in ('comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, 34000ms), Computer"}}, - Target: "query=query%3DPerf+%7C+where+%5B%27TimeGenerated%27%5D+%3E%3D+datetime%28%272018-03-15T13%3A00%3A00Z%27%29+and+%5B%27TimeGenerated%27%5D+%3C%3D+datetime%28%272018-03-15T13%3A34%3A00Z%27%29+%7C+where+%5B%27Computer%27%5D+in+%28%27comp1%27%2C%27comp2%27%29+%7C+summarize+avg%28CounterValue%29+by+bin%28TimeGenerated%2C+34000ms%29%2C+Computer", + Query: "Perf | where ['TimeGenerated'] >= datetime('2018-03-15T13:00:00Z') and ['TimeGenerated'] <= datetime('2018-03-15T13:34:00Z') | where ['Computer'] in ('comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, 34000ms), Computer", + Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, TimeRange: timeRange, }, }, @@ -77,7 +77,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "workspace": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "query": "query=Perf", + "query": "Perf", "resultFormat": "%s" } }`, types.TimeSeries)), @@ -93,12 +93,12 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "workspace": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "query": "query=Perf", + "query": "Perf", "resultFormat": "%s" } }`, types.TimeSeries)), - Params: url.Values{"query": {"query=Perf"}}, - Target: "query=query%3DPerf", + Query: "Perf", + Resources: []string{}, }, }, Err: require.NoError, @@ -112,7 +112,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "workspace": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", - "query": "query=Perf", + "query": "Perf", "resultFormat": "%s" } }`, types.TimeSeries)), @@ -128,26 +128,26 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "workspace": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", - "query": "query=Perf", + "query": "Perf", "resultFormat": "%s" } }`, types.TimeSeries)), - Params: url.Values{"query": {"query=Perf"}}, - Target: "query=query%3DPerf", + Query: "Perf", + Resources: []string{}, }, }, Err: require.NoError, }, { - name: "Queries with a Resource should use resource-centric url", + name: "Queries with multiple resources", queryModel: []backend.DataQuery{ { JSON: []byte(fmt.Sprintf(`{ "queryType": "Azure Log Analytics", "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", - "query": "query=Perf", + "query": "Perf", "resultFormat": "%s" } }`, types.TimeSeries)), @@ -163,12 +163,48 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "queryType": "Azure Log Analytics", "azureLogAnalytics": { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", - "query": "query=Perf", + "query": "Perf", "resultFormat": "%s" } }`, types.TimeSeries)), - Params: url.Values{"query": {"query=Perf"}}, - Target: "query=query%3DPerf", + Query: "Perf", + Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, + }, + }, + Err: require.NoError, + }, + { + name: "Query with multiple resources", + queryModel: []backend.DataQuery{ + { + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/r1","/subscriptions/r2"], + "query": "Perf", + "resultFormat": "%s" + } + }`, types.TimeSeries)), + RefID: "A", + TimeRange: timeRange, + }, + }, + azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ + { + RefID: "A", + ResultFormat: types.TimeSeries, + URL: "v1/subscriptions/r1/query", + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/r1","/subscriptions/r2"], + "query": "Perf", + "resultFormat": "%s" + } + }`, types.TimeSeries)), + Query: "Perf", + Resources: []string{"/subscriptions/r1", "/subscriptions/r2"}, + TimeRange: timeRange, }, }, Err: require.NoError, @@ -188,35 +224,44 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { func TestLogAnalyticsCreateRequest(t *testing.T) { ctx := context.Background() - url := "http://ds" + url := "http://ds/" - tests := []struct { - name string - expectedURL string - expectedHeaders http.Header - Err require.ErrorAssertionFunc - }{ - { - name: "creates a request", - expectedURL: "http://ds/", - expectedHeaders: http.Header{"Content-Type": []string{"application/json"}}, - Err: require.NoError, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ds := AzureLogAnalyticsDatasource{} - req, err := ds.createRequest(ctx, logger, url) - tt.Err(t, err) - if req.URL.String() != tt.expectedURL { - t.Errorf("Expecting %s, got %s", tt.expectedURL, req.URL.String()) - } - if !cmp.Equal(req.Header, tt.expectedHeaders) { - t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, tt.expectedHeaders)) - } + t.Run("creates a request", func(t *testing.T) { + ds := AzureLogAnalyticsDatasource{} + req, err := ds.createRequest(ctx, logger, url, &AzureLogAnalyticsQuery{ + Resources: []string{"r"}, + Query: "Perf", }) - } + require.NoError(t, err) + if req.URL.String() != url { + t.Errorf("Expecting %s, got %s", url, req.URL.String()) + } + expectedHeaders := http.Header{"Content-Type": []string{"application/json"}} + if !cmp.Equal(req.Header, expectedHeaders) { + t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, expectedHeaders)) + } + expectedBody := `{"query":"Perf"}` + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + if !cmp.Equal(string(body), expectedBody) { + t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) + } + }) + + t.Run("creates a request with multiple resources", func(t *testing.T) { + ds := AzureLogAnalyticsDatasource{} + req, err := ds.createRequest(ctx, logger, url, &AzureLogAnalyticsQuery{ + Resources: []string{"r1", "r2"}, + Query: "Perf", + }) + require.NoError(t, err) + expectedBody := `{"query":"Perf","resources":["r1","r2"]}` + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + if !cmp.Equal(string(body), expectedBody) { + t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) + } + }) } func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) { @@ -231,7 +276,6 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) { } ctx := context.Background() query := &AzureLogAnalyticsQuery{ - Params: url.Values{}, TimeRange: backend.TimeRange{}, } tracer := tracing.InitializeTracerForTest() diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index fa9bd6fc3a7..73fc3160a34 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -180,12 +180,14 @@ func (a AzureMonitorDimensionFilter) ConstructFiltersString() string { // 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"` - Resource string `json:"resource"` + Query string `json:"query"` + ResultFormat string `json:"resultFormat"` + Resources []string `json:"resources"` // Deprecated: Queries should be migrated to use Resource instead Workspace string `json:"workspace"` + // Deprecated: Use Resources instead + Resource string `json:"resource"` } `json:"azureLogAnalytics"` }