azuremonitor: Deep linking from Log Analytic queries to the Azure Portal (#24417)

* azuremonitor: add gzipped and base64 encoded query to metadata

for Azure Log Analytic query responses

* azure monitor: add fields to metadata for log analytics

* azuremonitor: correction to text in query editor

* azuremonitor: adds subscription id to result metadata

* azuremonitor: build deep link url for Log Analytics

Most of the information needed for building the url
comes from the backend. The workspace friendly name
and the resource group that the workspace belongs
to are fetched in a separate API call. This call is
cached otherwise there would be a workspaces call
per query on the dashboard.

* docs: azure log analytics deep linking

* Apply suggestions from code review

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* docs: fixing review comments for azure monitor

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
Daniel Lee
2020-05-11 19:11:03 +02:00
committed by GitHub
co-authored by Diana Payton
parent ae7f0aeb7a
commit 67ed579647
9 changed files with 250 additions and 72 deletions
@@ -1,7 +1,10 @@
package azuremonitor
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -35,6 +38,7 @@ type AzureLogAnalyticsQuery struct {
RefID string
ResultFormat string
URL string
Model *simplejson.Json
Params url.Values
Target string
}
@@ -77,7 +81,6 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []*tsdb.Query, timeRa
}
urlComponents := map[string]string{}
urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
urlComponents["workspace"] = fmt.Sprintf("%v", azureLogAnalyticsTarget["workspace"])
apiURL := fmt.Sprintf("%s/query", urlComponents["workspace"])
@@ -92,6 +95,7 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []*tsdb.Query, timeRa
RefID: query.RefId,
ResultFormat: resultFormat,
URL: apiURL,
Model: query.Model,
Params: params,
Target: params.Encode(),
})
@@ -145,12 +149,12 @@ func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *A
azlog.Debug("AzureLogsAnalytics", "Response", queryResult)
if query.ResultFormat == "table" {
queryResult.Tables, queryResult.Meta, err = e.parseToTables(data, query.Params.Get("query"))
queryResult.Tables, queryResult.Meta, err = e.parseToTables(data, query.Model, query.Params)
if err != nil {
return nil, err
}
} else {
queryResult.Series, queryResult.Meta, err = e.parseToTimeSeries(data, query.Params.Get("query"))
queryResult.Series, queryResult.Meta, err = e.parseToTimeSeries(data, query.Model, query.Params)
if err != nil {
return nil, err
}
@@ -233,9 +237,10 @@ func (e *AzureLogAnalyticsDatasource) unmarshalResponse(res *http.Response) (Azu
return data, nil
}
func (e *AzureLogAnalyticsDatasource) parseToTables(data AzureLogAnalyticsResponse, query string) ([]*tsdb.Table, *simplejson.Json, error) {
meta := metadata{
Query: query,
func (e *AzureLogAnalyticsDatasource) parseToTables(data AzureLogAnalyticsResponse, model *simplejson.Json, params url.Values) ([]*tsdb.Table, *simplejson.Json, error) {
meta, err := createMetadata(model, params)
if err != nil {
return nil, simplejson.NewFromAny(meta), err
}
tables := make([]*tsdb.Table, 0)
@@ -267,9 +272,10 @@ func (e *AzureLogAnalyticsDatasource) parseToTables(data AzureLogAnalyticsRespon
return nil, nil, errors.New("no data as no PrimaryResult table was returned in the response")
}
func (e *AzureLogAnalyticsDatasource) parseToTimeSeries(data AzureLogAnalyticsResponse, query string) (tsdb.TimeSeriesSlice, *simplejson.Json, error) {
meta := metadata{
Query: query,
func (e *AzureLogAnalyticsDatasource) parseToTimeSeries(data AzureLogAnalyticsResponse, model *simplejson.Json, params url.Values) (tsdb.TimeSeriesSlice, *simplejson.Json, error) {
meta, err := createMetadata(model, params)
if err != nil {
return nil, simplejson.NewFromAny(meta), err
}
for _, t := range data.Tables {
@@ -352,3 +358,32 @@ func (e *AzureLogAnalyticsDatasource) parseToTimeSeries(data AzureLogAnalyticsRe
return nil, nil, errors.New("no data as no PrimaryResult table was returned in the response")
}
func createMetadata(model *simplejson.Json, params url.Values) (metadata, error) {
meta := metadata{
Query: params.Get("query"),
Subscription: model.Get("subscriptionId").MustString(),
Workspace: model.Get("azureLogAnalytics").Get("workspace").MustString(),
}
encQuery, err := encodeQuery(meta.Query)
if err != nil {
return meta, err
}
meta.EncodedQuery = encQuery
return meta, nil
}
func encodeQuery(rawQuery string) (string, error) {
var b bytes.Buffer
gz := gzip.NewWriter(&b)
if _, err := gz.Write([]byte(rawQuery)); err != nil {
return "", err
}
if err := gz.Close(); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b.Bytes()), nil
}
@@ -57,8 +57,15 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
RefID: "A",
ResultFormat: "time_series",
URL: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query",
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",
Model: simplejson.NewFromAny(map[string]interface{}{
"azureLogAnalytics": map[string]interface{}{
"query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer",
"resultFormat": "time_series",
"workspace": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
},
}),
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",
},
},
Err: require.NoError,
@@ -69,7 +76,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
queries, err := datasource.buildQueries(tt.queryModel, tt.timeRange)
tt.Err(t, err)
if diff := cmp.Diff(tt.azureLogAnalyticsQueries, queries, cmpopts.EquateNaNs()); diff != "" {
if diff := cmp.Diff(tt.azureLogAnalyticsQueries, queries, cmpopts.IgnoreUnexported(simplejson.Json{})); diff != "" {
t.Errorf("Result mismatch (-want +got):\n%s", diff)
}
})
@@ -100,7 +107,7 @@ func TestParsingAzureLogAnalyticsResponses(t *testing.T) {
},
},
},
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"Computer","type":"string"},{"name":"avg_CounterValue","type":"real"}],"query":"test query"}`,
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"Computer","type":"string"},{"name":"avg_CounterValue","type":"real"}],"subscription":"1234","workspace":"aworkspace","query":"test query","encodedQuery":"H4sIAAAAAAAA/ypJLS5RKCxNLaoEBAAA///0rBfVCgAAAA=="}`,
Err: require.NoError,
},
{
@@ -133,7 +140,7 @@ func TestParsingAzureLogAnalyticsResponses(t *testing.T) {
},
},
},
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"ObjectName","type":"string"},{"name":"avg_CounterValue","type":"real"}],"query":"test query"}`,
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"ObjectName","type":"string"},{"name":"avg_CounterValue","type":"real"}],"subscription":"1234","workspace":"aworkspace","query":"test query","encodedQuery":"H4sIAAAAAAAA/ypJLS5RKCxNLaoEBAAA///0rBfVCgAAAA=="}`,
Err: require.NoError,
},
{
@@ -150,7 +157,7 @@ func TestParsingAzureLogAnalyticsResponses(t *testing.T) {
},
},
},
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"avg_CounterValue","type":"int"}],"query":"test query"}`,
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"avg_CounterValue","type":"int"}],"subscription":"1234","workspace":"aworkspace","query":"test query","encodedQuery":"H4sIAAAAAAAA/ypJLS5RKCxNLaoEBAAA///0rBfVCgAAAA=="}`,
Err: require.NoError,
},
{
@@ -158,7 +165,7 @@ func TestParsingAzureLogAnalyticsResponses(t *testing.T) {
testFile: "loganalytics/4-log-analytics-response-metrics-no-time-column.json",
query: "test query",
series: nil,
meta: `{"columns":[{"name":"Computer","type":"string"},{"name":"avg_CounterValue","type":"real"}],"query":"test query"}`,
meta: `{"columns":[{"name":"Computer","type":"string"},{"name":"avg_CounterValue","type":"real"}],"subscription":"1234","workspace":"aworkspace","query":"test query","encodedQuery":"H4sIAAAAAAAA/ypJLS5RKCxNLaoEBAAA///0rBfVCgAAAA=="}`,
Err: require.NoError,
},
{
@@ -166,7 +173,7 @@ func TestParsingAzureLogAnalyticsResponses(t *testing.T) {
testFile: "loganalytics/5-log-analytics-response-metrics-no-value-column.json",
query: "test query",
series: nil,
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"Computer","type":"string"}],"query":"test query"}`,
meta: `{"columns":[{"name":"TimeGenerated","type":"datetime"},{"name":"Computer","type":"string"}],"subscription":"1234","workspace":"aworkspace","query":"test query","encodedQuery":"H4sIAAAAAAAA/ypJLS5RKCxNLaoEBAAA///0rBfVCgAAAA=="}`,
Err: require.NoError,
},
}
@@ -175,7 +182,15 @@ func TestParsingAzureLogAnalyticsResponses(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
data, _ := loadLogAnalyticsTestFile(tt.testFile)
series, meta, err := datasource.parseToTimeSeries(data, tt.query)
model := simplejson.NewFromAny(map[string]interface{}{
"subscriptionId": "1234",
"azureLogAnalytics": map[string]interface{}{
"workspace": "aworkspace",
},
})
params := url.Values{}
params.Add("query", tt.query)
series, meta, err := datasource.parseToTimeSeries(data, model, params)
tt.Err(t, err)
if diff := cmp.Diff(tt.series, series, cmpopts.EquateNaNs()); diff != "" {
@@ -262,7 +277,7 @@ func TestParsingAzureLogAnalyticsTableResponses(t *testing.T) {
},
meta: `{"columns":[{"name":"TenantId","type":"string"},{"name":"Computer","type":"string"},{"name":"ObjectName","type":"string"},{"name":"CounterName","type":"string"},` +
`{"name":"InstanceName","type":"string"},{"name":"Min","type":"real"},{"name":"Max","type":"real"},{"name":"SampleCount","type":"int"},{"name":"CounterValue","type":"real"},` +
`{"name":"TimeGenerated","type":"datetime"}],"query":"test query"}`,
`{"name":"TimeGenerated","type":"datetime"}],"subscription":"1234","workspace":"aworkspace","query":"test query","encodedQuery":"H4sIAAAAAAAA/ypJLS5RKCxNLaoEBAAA///0rBfVCgAAAA=="}`,
Err: require.NoError,
},
}
@@ -271,7 +286,15 @@ func TestParsingAzureLogAnalyticsTableResponses(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
data, _ := loadLogAnalyticsTestFile(tt.testFile)
tables, meta, err := datasource.parseToTables(data, tt.query)
model := simplejson.NewFromAny(map[string]interface{}{
"subscriptionId": "1234",
"azureLogAnalytics": map[string]interface{}{
"workspace": "aworkspace",
},
})
params := url.Values{}
params.Add("query", tt.query)
tables, meta, err := datasource.parseToTables(data, model, params)
tt.Err(t, err)
if diff := cmp.Diff(tt.tables, tables, cmpopts.EquateNaNs()); diff != "" {
+5 -2
View File
@@ -79,8 +79,11 @@ type AzureLogAnalyticsTable struct {
}
type metadata struct {
Columns []column `json:"columns"`
Query string `json:"query"`
Columns []column `json:"columns"`
Subscription string `json:"subscription"`
Workspace string `json:"workspace"`
Query string `json:"query"`
EncodedQuery string `json:"encodedQuery"`
}
type column struct {