Azure: Multiple dimension support for Azure Monitor Service (#25947)

Azure Monitor (metrics) support multiple dimensions instead of just one.

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Kyle Brandt 2020-06-30 16:26:46 -04:00 committed by GitHub
parent 72fa5ccb7b
commit 4be56cde0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 403 additions and 142 deletions

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"sort"
"strings" "strings"
"time" "time"
@ -21,7 +22,6 @@ import (
opentracing "github.com/opentracing/opentracing-go" opentracing "github.com/opentracing/opentracing-go"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
) )
@ -57,7 +57,6 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori
if err != nil { if err != nil {
return nil, err return nil, err
} }
// azlog.Debug("AzureMonitor", "Response", resp)
err = e.parseResponse(queryRes, resp, query) err = e.parseResponse(queryRes, resp, query)
if err != nil { if err != nil {
@ -130,10 +129,25 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
params.Add("metricnames", azJSONModel.MetricName) // MetricName or MetricNames ? params.Add("metricnames", azJSONModel.MetricName) // MetricName or MetricNames ?
params.Add("metricnamespace", azJSONModel.MetricNamespace) params.Add("metricnamespace", azJSONModel.MetricNamespace)
// old model
dimension := strings.TrimSpace(azJSONModel.Dimension) dimension := strings.TrimSpace(azJSONModel.Dimension)
dimensionFilter := strings.TrimSpace(azJSONModel.DimensionFilter) dimensionFilter := strings.TrimSpace(azJSONModel.DimensionFilter)
if dimension != "" && dimensionFilter != "" && dimension != "None" {
params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter)) dimSB := strings.Builder{}
if dimension != "" && dimensionFilter != "" && dimension != "None" && len(azJSONModel.DimensionsFilters) == 0 {
dimSB.WriteString(fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
} else {
for i, filter := range azJSONModel.DimensionsFilters {
dimSB.WriteString(filter.String())
if i != len(azJSONModel.DimensionsFilters)-1 {
dimSB.WriteString(" and ")
}
}
}
if dimSB.String() != "" {
params.Add("$filter", dimSB.String())
params.Add("top", azJSONModel.Top) params.Add("top", azJSONModel.Top)
} }
@ -157,7 +171,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
} }
func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, queries []*tsdb.Query, timeRange *tsdb.TimeRange) (*tsdb.QueryResult, AzureMonitorResponse, error) { func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, queries []*tsdb.Query, timeRange *tsdb.TimeRange) (*tsdb.QueryResult, AzureMonitorResponse, error) {
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefID} queryResult := &tsdb.QueryResult{RefId: query.RefID}
req, err := e.createRequest(ctx, e.dsInfo) req, err := e.createRequest(ctx, e.dsInfo)
if err != nil { if err != nil {
@ -167,7 +181,6 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM
req.URL.Path = path.Join(req.URL.Path, query.URL) req.URL.Path = path.Join(req.URL.Path, query.URL)
req.URL.RawQuery = query.Params.Encode() req.URL.RawQuery = query.Params.Encode()
queryResult.Meta.Set("rawQuery", req.URL.RawQuery)
span, ctx := opentracing.StartSpanFromContext(ctx, "azuremonitor query") span, ctx := opentracing.StartSpanFromContext(ctx, "azuremonitor query")
span.SetTag("target", query.Target) span.SetTag("target", query.Target)
@ -270,20 +283,23 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, amr A
frames := data.Frames{} frames := data.Frames{}
for _, series := range amr.Value[0].Timeseries { for _, series := range amr.Value[0].Timeseries {
metadataName := "" labels := data.Labels{}
metadataValue := "" for _, md := range series.Metadatavalues {
if len(series.Metadatavalues) > 0 { labels[md.Name.LocalizedValue] = md.Value
metadataName = series.Metadatavalues[0].Name.LocalizedValue
metadataValue = series.Metadatavalues[0].Value
} }
metricName := formatAzureMonitorLegendKey(query.Alias, query.UrlComponents["resourceName"], amr.Value[0].Name.LocalizedValue, metadataName, metadataValue, amr.Namespace, amr.Value[0].ID)
frame := data.NewFrameOfFieldTypes("", len(series.Data), data.FieldTypeTime, data.FieldTypeFloat64) frame := data.NewFrameOfFieldTypes("", len(series.Data), data.FieldTypeTime, data.FieldTypeFloat64)
frame.RefID = query.RefID frame.RefID = query.RefID
frame.Fields[1].Name = metricName dataField := frame.Fields[1]
frame.Fields[1].SetConfig(&data.FieldConfig{ dataField.Name = amr.Value[0].Name.LocalizedValue
dataField.Labels = labels
dataField.SetConfig(&data.FieldConfig{
Unit: amr.Value[0].Unit, Unit: amr.Value[0].Unit,
}) })
if query.Alias != "" {
dataField.Config.DisplayName = formatAzureMonitorLegendKey(query.Alias, query.UrlComponents["resourceName"],
amr.Value[0].Name.LocalizedValue, "", "", amr.Namespace, amr.Value[0].ID, labels)
}
requestedAgg := query.Params.Get("aggregation") requestedAgg := query.Params.Get("aggregation")
@ -317,14 +333,7 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, amr A
// formatAzureMonitorLegendKey builds the legend key or timeseries name // formatAzureMonitorLegendKey builds the legend key or timeseries name
// Alias patterns like {{resourcename}} are replaced with the appropriate data values. // Alias patterns like {{resourcename}} are replaced with the appropriate data values.
func formatAzureMonitorLegendKey(alias string, resourceName string, metricName string, metadataName string, metadataValue string, namespace string, seriesID string) string { func formatAzureMonitorLegendKey(alias string, resourceName string, metricName string, metadataName string, metadataValue string, namespace string, seriesID string, labels data.Labels) string {
if alias == "" {
if len(metadataName) > 0 {
return fmt.Sprintf("%s{%s=%s}.%s", resourceName, metadataName, metadataValue, metricName)
}
return fmt.Sprintf("%s.%s", resourceName, metricName)
}
startIndex := strings.Index(seriesID, "/resourceGroups/") + 16 startIndex := strings.Index(seriesID, "/resourceGroups/") + 16
endIndex := strings.Index(seriesID, "/providers") endIndex := strings.Index(seriesID, "/providers")
resourceGroup := seriesID[startIndex:endIndex] resourceGroup := seriesID[startIndex:endIndex]
@ -350,14 +359,25 @@ func formatAzureMonitorLegendKey(alias string, resourceName string, metricName s
return []byte(metricName) return []byte(metricName)
} }
keys := make([]string, 0, len(labels))
if metaPartName == "dimensionname" || metaPartName == "dimensionvalue" {
for k := range labels {
keys = append(keys, k)
}
keys = sort.StringSlice(keys)
}
if metaPartName == "dimensionname" { if metaPartName == "dimensionname" {
return []byte(metadataName) return []byte(keys[0])
} }
if metaPartName == "dimensionvalue" { if metaPartName == "dimensionvalue" {
return []byte(metadataValue) return []byte(labels[keys[0]])
} }
if v, ok := labels[metaPartName]; ok {
return []byte(v)
}
return in return in
}) })

View File

@ -72,7 +72,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30",
}, },
{ {
name: "has a dimension filter", name: "has a dimension filter and none Dimension",
azureMonitorVariedProperties: map[string]interface{}{ azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M", "timeGrain": "PT1M",
"dimension": "None", "dimension": "None",
@ -83,6 +83,28 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
expectedInterval: "PT1M", expectedInterval: "PT1M",
azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z", azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z",
}, },
{
name: "has dimensionFilter*s* property with one dimension",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionsFilters": []azureMonitorDimensionFilter{{"blob", "eq", "*"}},
"top": "30",
},
queryIntervalMS: 400000,
expectedInterval: "PT1M",
azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30",
},
{
name: "has dimensionFilter*s* property with two dimensions",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimensionsFilters": []azureMonitorDimensionFilter{{"blob", "eq", "*"}, {"tier", "eq", "*"}},
"top": "30",
},
queryIntervalMS: 400000,
expectedInterval: "PT1M",
azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27+and+tier+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30",
},
} }
commonAzureModelProps := map[string]interface{}{ commonAzureModelProps := map[string]interface{}{
@ -139,9 +161,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
} }
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange) queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
if err != nil { require.NoError(t, err)
t.Error(err)
}
if diff := cmp.Diff(azureMonitorQuery, queries[0], cmpopts.IgnoreUnexported(simplejson.Json{}), cmpopts.IgnoreFields(AzureMonitorQuery{}, "Params")); diff != "" { if diff := cmp.Diff(azureMonitorQuery, queries[0], cmpopts.IgnoreUnexported(simplejson.Json{}), cmpopts.IgnoreFields(AzureMonitorQuery{}, "Params")); diff != "" {
t.Errorf("Result mismatch (-want +got):\n%s", diff) t.Errorf("Result mismatch (-want +got):\n%s", diff)
} }
@ -179,7 +199,7 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 8, 10, 13, 0, 0, time.UTC), 5, time.Minute)), makeDates(time.Date(2019, 2, 8, 10, 13, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{ data.NewField("Percentage CPU", nil, []float64{
2.0875, 2.1525, 2.155, 3.6925, 2.44, 2.0875, 2.1525, 2.155, 3.6925, 2.44,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})), }).SetConfig(&data.FieldConfig{Unit: "Percent"})),
}, },
@ -199,7 +219,7 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 13, 29, 0, 0, time.UTC), 5, time.Minute)), makeDates(time.Date(2019, 2, 9, 13, 29, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{ data.NewField("Percentage CPU", nil, []float64{
8.26, 8.7, 14.82, 10.07, 8.52, 8.26, 8.7, 14.82, 10.07, 8.52,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})), }).SetConfig(&data.FieldConfig{Unit: "Percent"})),
}, },
@ -219,7 +239,7 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 14, 26, 0, 0, time.UTC), 5, time.Minute)), makeDates(time.Date(2019, 2, 9, 14, 26, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{ data.NewField("Percentage CPU", nil, []float64{
3.07, 2.92, 2.87, 2.27, 2.52, 3.07, 2.92, 2.87, 2.27, 2.52,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})), }).SetConfig(&data.FieldConfig{Unit: "Percent"})),
}, },
@ -239,7 +259,7 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 14, 43, 0, 0, time.UTC), 5, time.Minute)), makeDates(time.Date(2019, 2, 9, 14, 43, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{ data.NewField("Percentage CPU", nil, []float64{
1.51, 2.38, 1.69, 2.27, 1.96, 1.51, 2.38, 1.69, 2.27, 1.96,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})), }).SetConfig(&data.FieldConfig{Unit: "Percent"})),
}, },
@ -259,14 +279,14 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 14, 44, 0, 0, time.UTC), 5, time.Minute)), makeDates(time.Date(2019, 2, 9, 14, 44, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{ data.NewField("Percentage CPU", nil, []float64{
4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})), }).SetConfig(&data.FieldConfig{Unit: "Percent"})),
}, },
}, },
{ {
name: "multi dimension time series response", name: "single dimension time series response",
responseFile: "6-azure-monitor-response-multi-dimension.json", responseFile: "6-azure-monitor-response-single-dimension.json",
mockQuery: &AzureMonitorQuery{ mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{ UrlComponents: map[string]string{
"resourceName": "grafana", "resourceName": "grafana",
@ -275,31 +295,24 @@ func TestAzureMonitorParseResponse(t *testing.T) {
"aggregation": {"Average"}, "aggregation": {"Average"},
}, },
}, },
// Regarding multi-dimensional response:
// - It seems they all share the same time index, so maybe can be a wide frame.
// - Due to the type for the Azure monitor response, nulls currently become 0.
// - blogtype=X should maybe become labels.
expectedFrames: data.Frames{ expectedFrames: data.Frames{
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)), makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("grafana{blobtype=PageBlob}.Blob Count", nil, []float64{ data.NewField("Blob Count", data.Labels{"blobtype": "PageBlob"},
3, 3, 3, 3, 3, 0, []float64{3, 3, 3, 3, 3, 0}).SetConfig(&data.FieldConfig{Unit: "Count"})),
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)), makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("grafana{blobtype=BlockBlob}.Blob Count", nil, []float64{ data.NewField("Blob Count", data.Labels{"blobtype": "BlockBlob"},
1, 1, 1, 1, 1, 0, []float64{1, 1, 1, 1, 1, 0}).SetConfig(&data.FieldConfig{Unit: "Count"})),
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)), makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("grafana{blobtype=Azure Data Lake Storage}.Blob Count", nil, []float64{ data.NewField("Blob Count", data.Labels{"blobtype": "Azure Data Lake Storage"},
0, 0, 0, 0, 0, 0, []float64{0, 0, 0, 0, 0, 0}).SetConfig(&data.FieldConfig{Unit: "Count"})),
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
}, },
}, },
{ {
@ -318,14 +331,14 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 13, 29, 0, 0, time.UTC), 5, time.Minute)), makeDates(time.Date(2019, 2, 9, 13, 29, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("custom grafanastaging Microsoft.Compute/virtualMachines grafana Percentage CPU", nil, []float64{ data.NewField("Percentage CPU", nil, []float64{
8.26, 8.7, 14.82, 10.07, 8.52, 8.26, 8.7, 14.82, 10.07, 8.52,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})), }).SetConfig(&data.FieldConfig{Unit: "Percent", DisplayName: "custom grafanastaging Microsoft.Compute/virtualMachines grafana Percentage CPU"})),
}, },
}, },
{ {
name: "multi dimension with alias", name: "single dimension with alias",
responseFile: "6-azure-monitor-response-multi-dimension.json", responseFile: "6-azure-monitor-response-single-dimension.json",
mockQuery: &AzureMonitorQuery{ mockQuery: &AzureMonitorQuery{
Alias: "{{dimensionname}}={{DimensionValue}}", Alias: "{{dimensionname}}={{DimensionValue}}",
UrlComponents: map[string]string{ UrlComponents: map[string]string{
@ -339,23 +352,57 @@ func TestAzureMonitorParseResponse(t *testing.T) {
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)), makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("blobtype=PageBlob", nil, []float64{ data.NewField("Blob Count", data.Labels{"blobtype": "PageBlob"},
3, 3, 3, 3, 3, 0, []float64{3, 3, 3, 3, 3, 0}).SetConfig(&data.FieldConfig{Unit: "Count", DisplayName: "blobtype=PageBlob"})),
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)), makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("blobtype=BlockBlob", nil, []float64{ data.NewField("Blob Count", data.Labels{"blobtype": "BlockBlob"}, []float64{
1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})), }).SetConfig(&data.FieldConfig{Unit: "Count", DisplayName: "blobtype=BlockBlob"})),
data.NewFrame("", data.NewFrame("",
data.NewField("", nil, data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)), makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("blobtype=Azure Data Lake Storage", nil, []float64{ data.NewField("Blob Count", data.Labels{"blobtype": "Azure Data Lake Storage"}, []float64{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})), }).SetConfig(&data.FieldConfig{Unit: "Count", DisplayName: "blobtype=Azure Data Lake Storage"})),
},
},
{
name: "multiple dimension time series response with label alias",
responseFile: "7-azure-monitor-response-multi-dimension.json",
mockQuery: &AzureMonitorQuery{
Alias: "{{resourcegroup}} {Blob Type={{blobtype}}, Tier={{tier}}}",
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Average"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2020, 06, 30, 9, 58, 0, 0, time.UTC), 3, time.Hour)),
data.NewField("Blob Capacity", data.Labels{"blobtype": "PageBlob", "tier": "Standard"},
[]float64{675530, 675530, 675530}).SetConfig(
&data.FieldConfig{Unit: "Bytes", DisplayName: "danieltest {Blob Type=PageBlob, Tier=Standard}"})),
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2020, 06, 30, 9, 58, 0, 0, time.UTC), 3, time.Hour)),
data.NewField("Blob Capacity", data.Labels{"blobtype": "BlockBlob", "tier": "Hot"},
[]float64{0, 0, 0}).SetConfig(
&data.FieldConfig{Unit: "Bytes", DisplayName: "danieltest {Blob Type=BlockBlob, Tier=Hot}"})),
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2020, 06, 30, 9, 58, 0, 0, time.UTC), 3, time.Hour)),
data.NewField("Blob Capacity", data.Labels{"blobtype": "Azure Data Lake Storage", "tier": "Cool"},
[]float64{0, 0, 0}).SetConfig(
&data.FieldConfig{Unit: "Bytes", DisplayName: "danieltest {Blob Type=Azure Data Lake Storage, Tier=Cool}"})),
}, },
}, },
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,119 @@
{
"cost": 0,
"timespan": "2020-06-30T09:58:58Z/2020-06-30T12:58:58Z",
"interval": "PT1H",
"value": [
{
"id": "/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/danieltest/providers/Microsoft.Storage/storageAccounts/danieltestdiag187/blobServices/default/providers/Microsoft.Insights/metrics/BlobCapacity",
"type": "Microsoft.Insights/metrics",
"name": {
"value": "BlobCapacity",
"localizedValue": "Blob Capacity"
},
"displayDescription": "The amount of storage used by the storage accounts Blob service in bytes.",
"unit": "Bytes",
"timeseries": [
{
"metadatavalues": [
{
"name": {
"value": "blobtype",
"localizedValue": "blobtype"
},
"value": "PageBlob"
},
{
"name": {
"value": "tier",
"localizedValue": "tier"
},
"value": "Standard"
}
],
"data": [
{
"timeStamp": "2020-06-30T09:58:00Z",
"average": 675530
},
{
"timeStamp": "2020-06-30T10:58:00Z",
"average": 675530
},
{
"timeStamp": "2020-06-30T11:58:00Z",
"average": 675530
}
]
},
{
"metadatavalues": [
{
"name": {
"value": "blobtype",
"localizedValue": "blobtype"
},
"value": "BlockBlob"
},
{
"name": {
"value": "tier",
"localizedValue": "tier"
},
"value": "Hot"
}
],
"data": [
{
"timeStamp": "2020-06-30T09:58:00Z",
"average": 0
},
{
"timeStamp": "2020-06-30T10:58:00Z",
"average": 0
},
{
"timeStamp": "2020-06-30T11:58:00Z",
"average": 0
}
]
},
{
"metadatavalues": [
{
"name": {
"value": "blobtype",
"localizedValue": "blobtype"
},
"value": "Azure Data Lake Storage"
},
{
"name": {
"value": "tier",
"localizedValue": "tier"
},
"value": "Cool"
}
],
"data": [
{
"timeStamp": "2020-06-30T09:58:00Z",
"average": 0
},
{
"timeStamp": "2020-06-30T10:58:00Z",
"average": 0
},
{
"timeStamp": "2020-06-30T11:58:00Z",
"average": 0
}
]
}
],
"errorCode": "Success"
}
],
"namespace": "Microsoft.Storage/storageAccounts/blobServices",
"resourceregion": "westeurope"
}

View File

@ -87,8 +87,8 @@ type azureMonitorJSONQuery struct {
Aggregation string `json:"aggregation"` Aggregation string `json:"aggregation"`
Alias string `json:"alias"` Alias string `json:"alias"`
AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"` AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"`
Dimension string `json:"dimension"` Dimension string `json:"dimension"` // old model
DimensionFilter string `json:"dimensionFilter"` DimensionFilter string `json:"dimensionFilter"` // old model
Format string `json:"format"` Format string `json:"format"`
MetricDefinition string `json:"metricDefinition"` MetricDefinition string `json:"metricDefinition"`
MetricName string `json:"metricName"` MetricName string `json:"metricName"`
@ -97,10 +97,24 @@ type azureMonitorJSONQuery struct {
ResourceName string `json:"resourceName"` ResourceName string `json:"resourceName"`
TimeGrain string `json:"timeGrain"` TimeGrain string `json:"timeGrain"`
Top string `json:"top"` Top string `json:"top"`
DimensionsFilters []azureMonitorDimensionFilter `json:"dimensionsFilters"` // new model
} `json:"azureMonitor"` } `json:"azureMonitor"`
Subscription string `json:"subscription"` Subscription string `json:"subscription"`
} }
// azureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric
// queries like "BlobType", "eq", "*"
type azureMonitorDimensionFilter struct {
Dimension string `json:"dimension"`
Operator string `json:"operator"`
Filter string `json:"filter"`
}
func (a azureMonitorDimensionFilter) String() string {
return fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, a.Filter)
}
// insightsJSONQuery is the frontend JSON query model for an Azure Application Insights query. // insightsJSONQuery is the frontend JSON query model for an Azure Application Insights query.
type insightsJSONQuery struct { type insightsJSONQuery struct {
AppInsights struct { AppInsights struct {

View File

@ -74,11 +74,18 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const aggregation = templateSrv.replace(item.aggregation, scopedVars); const aggregation = templateSrv.replace(item.aggregation, scopedVars);
const top = templateSrv.replace(item.top || '', scopedVars); const top = templateSrv.replace(item.top || '', scopedVars);
const dimensionsFilters = item.dimensionFilters.map(f => {
return {
dimension: templateSrv.replace(f.dimension, scopedVars),
operator: f.operator || 'eq',
filter: templateSrv.replace(f.filter, scopedVars),
};
});
return { return {
refId: target.refId, refId: target.refId,
subscription: subscriptionId, subscription: subscriptionId,
queryType: AzureQueryType.AzureMonitor, queryType: AzureQueryType.AzureMonitor,
type: 'timeSeriesQuery',
azureMonitor: { azureMonitor: {
resourceGroup, resourceGroup,
resourceName, resourceName,
@ -89,9 +96,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
metricNamespace: metricNamespace:
metricNamespace && metricNamespace !== defaultDropdownValue ? metricNamespace : metricDefinition, metricNamespace && metricNamespace !== defaultDropdownValue ? metricNamespace : metricDefinition,
aggregation: aggregation, aggregation: aggregation,
dimension: templateSrv.replace(item.dimension, scopedVars), dimensionsFilters,
top: top || '10', top: top || '10',
dimensionFilter: templateSrv.replace(item.dimensionFilter, scopedVars),
alias: item.alias, alias: item.alias,
format: target.format, format: target.format,
}, },

View File

@ -13,6 +13,7 @@ import {
import { Observable, of, from } from 'rxjs'; import { Observable, of, from } from 'rxjs';
import { DataSourceWithBackend } from '@grafana/runtime'; import { DataSourceWithBackend } from '@grafana/runtime';
import InsightsAnalyticsDatasource from './insights_analytics/insights_analytics_datasource'; import InsightsAnalyticsDatasource from './insights_analytics/insights_analytics_datasource';
import { migrateMetricsDimensionFilters } from './query_ctrl';
export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> { export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> {
azureMonitorDatasource: AzureMonitorDatasource; azureMonitorDatasource: AzureMonitorDatasource;
@ -64,6 +65,10 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
target.queryType = AzureQueryType.AzureMonitor; target.queryType = AzureQueryType.AzureMonitor;
} }
if (target.queryType === AzureQueryType.AzureMonitor) {
migrateMetricsDimensionFilters(target.azureMonitor);
}
// Check that we have options // Check that we have options
const opts = (target as any)[this.optionsKey[target.queryType]]; const opts = (target as any)[this.optionsKey[target.queryType]];

View File

@ -132,44 +132,73 @@
<div class="gf-form-label gf-form-label--grow"></div> <div class="gf-form-label gf-form-label--grow"></div>
</div> </div>
</div> </div>
<div class="gf-form-inline" ng-show="ctrl.target.azureMonitor.dimensions.length > 0">
<div class="gf-form"> <!-- NO Filters-->
<label class="gf-form-label query-keyword width-9">Dimension</label> <ng-container ng-if="ctrl.target.azureMonitor.dimensionFilters.length < 1">
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> <div class="gf-form-inline">
<select <div class="gf-form">
class="gf-form-input min-width-12" <label class="gf-form-label query-keyword width-9">Dimension</label>
ng-model="ctrl.target.azureMonitor.dimension" </div>
ng-options="f.value as f.text for f in ctrl.target.azureMonitor.dimensions" <div class="gf-form">
ng-change="ctrl.refresh()" <a ng-click="ctrl.azureMonitorAddDimensionFilter()" class="gf-form-label query-part"><icon name="'plus'"></icon></a>
></select> </div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div> </div>
</div> </div>
<div class="gf-form"> </ng-container>
<label class="gf-form-label query-keyword width-3">eq</label>
<input <!-- YES Filters-->
type="text" <ng-container ng-if="ctrl.target.azureMonitor.dimensionFilters.length > 0">
class="gf-form-input width-17" <div ng-repeat="dim in ctrl.target.azureMonitor.dimensionFilters track by $index" class="gf-form-inline">
ng-model="ctrl.target.azureMonitor.dimensionFilter" <div class="gf-form">
spellcheck="false" <label class="gf-form-label query-keyword width-9">Dimension</label>
placeholder="auto" <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
ng-blur="ctrl.refresh()" <select
/> class="gf-form-input min-width-12"
ng-model="dim.dimension"
ng-options="f.value as f.text for f in ctrl.target.azureMonitor.dimensions"
ng-change="ctrl.refresh()"
></select>
</div>
<label class="gf-form-label query-keyword width-3">eq</label>
<input
type="text"
class="gf-form-input width-17"
ng-model="dim.filter"
spellcheck="false"
placeholder="Anything (*)"
ng-blur="ctrl.refresh()"
/>
</div>
<div class="gf-form">
<a ng-click="ctrl.azureMonitorRemoveDimensionFilter($index)" class="gf-form-label query-part"><icon name="'minus'"></icon></a>
</div>
<div class="gf-form" ng-if="$last">
<a ng-click="ctrl.azureMonitorAddDimensionFilter()" class="gf-form-label query-part"><icon name="'plus'"></icon></a>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div> </div>
<div class="gf-form"> <div class="gf-form-inline">
<label class="gf-form-label query-keyword width-9">Top</label> <div class="gf-form">
<input <label class="gf-form-label query-keyword width-9">Top</label>
type="text" <input
class="gf-form-input width-3" type="text"
ng-model="ctrl.target.azureMonitor.top" class="gf-form-input width-3"
spellcheck="false" ng-model="ctrl.target.azureMonitor.top"
placeholder="10" spellcheck="false"
ng-blur="ctrl.refresh()" placeholder="10"
/> ng-blur="ctrl.refresh()"
/>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div> </div>
<div class="gf-form gf-form--grow"> </ng-container>
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-9">Legend Format</label> <label class="gf-form-label query-keyword width-9">Legend Format</label>

View File

@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { auto, IPromise } from 'angular'; import { auto, IPromise } from 'angular';
import { DataFrame, PanelEvents } from '@grafana/data'; import { DataFrame, PanelEvents } from '@grafana/data';
import { AzureQueryType } from './types'; import { AzureQueryType, AzureMetricQuery } from './types';
export interface ResultFormat { export interface ResultFormat {
text: string; text: string;
@ -27,23 +27,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
refId: string; refId: string;
queryType: AzureQueryType; queryType: AzureQueryType;
subscription: string; subscription: string;
azureMonitor: { azureMonitor: AzureMetricQuery;
resourceGroup: string;
resourceName: string;
metricDefinition: string;
metricNamespace: string;
metricName: string;
dimensionFilter: string;
timeGrain: string;
timeGrainUnit: string;
allowedTimeGrainsMs: number[];
dimensions: any[];
dimension: any;
top: string;
aggregation: string;
aggOptions: string[];
timeGrains: Array<{ text: string; value: string }>;
};
azureLogAnalytics: { azureLogAnalytics: {
query: string; query: string;
resultFormat: string; resultFormat: string;
@ -139,6 +123,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.migrateApplicationInsightsDimensions(); this.migrateApplicationInsightsDimensions();
migrateMetricsDimensionFilters(this.target.azureMonitor);
this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope);
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope); this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
this.resultFormats = [ this.resultFormats = [
@ -219,12 +205,13 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
} }
} }
const oldAzureTimeGrains = (this.target.azureMonitor as any).timeGrains;
if ( if (
this.target.azureMonitor.timeGrains && oldAzureTimeGrains &&
this.target.azureMonitor.timeGrains.length > 0 && oldAzureTimeGrains.length > 0 &&
(!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0) (!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0)
) { ) {
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.azureMonitor.timeGrains); this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(oldAzureTimeGrains);
} }
if ( if (
@ -328,10 +315,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.target.azureMonitor.resourceName = this.defaultDropdownValue; this.target.azureMonitor.resourceName = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue; this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = ''; this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = ''; this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimensionFilters = [];
this.target.azureMonitor.dimension = '';
} }
} }
@ -439,10 +424,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue; this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue; this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = ''; this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = ''; this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimensionFilters = [];
this.target.azureMonitor.dimension = '';
this.refresh(); this.refresh();
} }
@ -451,27 +434,22 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue; this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue; this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = ''; this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = ''; this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimensionFilters = [];
this.target.azureMonitor.dimension = '';
} }
onResourceNameChange() { onResourceNameChange() {
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue; this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue; this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = ''; this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = ''; this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimensionFilters = [];
this.target.azureMonitor.dimension = '';
this.refresh(); this.refresh();
} }
onMetricNamespacesChange() { onMetricNamespacesChange() {
this.target.azureMonitor.metricName = this.defaultDropdownValue; this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.dimensions = []; this.target.azureMonitor.dimensionFilters = [];
this.target.azureMonitor.dimension = '';
} }
onMetricNameChange(): IPromise<void> { onMetricNameChange(): IPromise<void> {
@ -489,16 +467,20 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.replace(this.target.azureMonitor.metricName) this.replace(this.target.azureMonitor.metricName)
) )
.then((metadata: any) => { .then((metadata: any) => {
this.target.azureMonitor.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType]; console.log('Update metadata', metadata);
this.target.azureMonitor.aggregation = metadata.primaryAggType;
this.target.azureMonitor.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
this.target.azureMonitor.timeGrain = 'auto';
this.target.azureMonitor.aggregation = metadata.primaryAggType;
this.target.azureMonitor.timeGrain = 'auto';
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []); this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []);
this.target.azureMonitor.dimensions = metadata.dimensions; // HACK: this saves the last metadata values in the panel json ¯\_(ツ)_/¯
const hackState = this.target.azureMonitor as any;
hackState.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
hackState.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
hackState.dimensions = metadata.dimensions;
if (metadata.dimensions.length > 0) { if (metadata.dimensions.length > 0) {
this.target.azureMonitor.dimension = metadata.dimensions[0].value; // this.target.azureMonitor.dimension = metadata.dimensions[0].value;
} }
return this.refresh(); return this.refresh();
@ -537,13 +519,29 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
} }
getAzureMonitorAutoInterval() { getAzureMonitorAutoInterval() {
return this.generateAutoUnits(this.target.azureMonitor.timeGrain, this.target.azureMonitor.timeGrains); return this.generateAutoUnits(this.target.azureMonitor.timeGrain, (this.target.azureMonitor as any).timeGrains);
} }
getApplicationInsightAutoInterval() { getApplicationInsightAutoInterval() {
return this.generateAutoUnits(this.target.appInsights.timeGrain, this.target.appInsights.timeGrains); return this.generateAutoUnits(this.target.appInsights.timeGrain, this.target.appInsights.timeGrains);
} }
azureMonitorAddDimensionFilter() {
console.log('Add dimension', this.target.azureMonitor);
this.target.azureMonitor.dimensionFilters.push({
dimension: '',
operator: 'eq',
filter: '',
});
this.refresh();
}
azureMonitorRemoveDimensionFilter(index: number) {
this.target.azureMonitor.dimensionFilters.splice(index, 1);
this.refresh();
console.log('Remove dimension', index, this.target.azureMonitor);
}
/* Azure Log Analytics */ /* Azure Log Analytics */
getWorkspaces = () => { getWorkspaces = () => {
@ -695,3 +693,20 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.refresh(); this.refresh();
} }
} }
// Modifies the actual query object
export function migrateMetricsDimensionFilters(item: AzureMetricQuery) {
if (!item.dimensionFilters) {
item.dimensionFilters = [];
}
const oldDimension = (item as any).dimension;
if (oldDimension && oldDimension !== 'None') {
item.dimensionFilters.push({
dimension: oldDimension,
operator: 'eq',
filter: (item as any).dimensionFilter,
});
delete (item as any).dimension;
delete (item as any).dimensionFilter;
}
}

View File

@ -45,6 +45,12 @@ export interface AzureDataSourceSecureJsonData {
appInsightsApiKey?: string; appInsightsApiKey?: string;
} }
export interface AzureMetricDimension {
dimension: string;
operator: 'eq'; // future proof
filter?: string; // *
}
export interface AzureMetricQuery { export interface AzureMetricQuery {
resourceGroup: string; resourceGroup: string;
resourceName: string; resourceName: string;
@ -55,8 +61,7 @@ export interface AzureMetricQuery {
timeGrain: string; timeGrain: string;
allowedTimeGrainsMs: number[]; allowedTimeGrainsMs: number[];
aggregation: string; aggregation: string;
dimension: string; dimensionFilters: AzureMetricDimension[];
dimensionFilter: string;
alias: string; alias: string;
top: string; top: string;
} }