AzureMonitor: API support for multiple resources (#61315)

This commit is contained in:
Andres Martinez Gotor 2023-01-12 17:25:13 +01:00 committed by GitHub
parent 6822298953
commit 9055e1993d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 67 deletions

View File

@ -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
}

View File

@ -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()

View File

@ -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"`
}