mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
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:
co-authored by
Diana Payton
parent
ae7f0aeb7a
commit
67ed579647
@@ -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 != "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user