The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/tsdb/azuremonitor/azuremonitor-datasource_tes...

441 lines
14 KiB

package azuremonitor
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"path/filepath"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/stretchr/testify/require"
)
func TestAzureMonitorBuildQueries(t *testing.T) {
datasource := &AzureMonitorDatasource{}
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
tests := []struct {
name string
azureMonitorVariedProperties map[string]interface{}
azureMonitorQueryTarget string
expectedInterval string
queryIntervalMS int64
}{
{
name: "Parse queries from frontend and build AzureMonitor API queries",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"top": "10",
},
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",
},
{
name: "time grain set to auto",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "auto",
"top": "10",
},
queryIntervalMS: 400000,
expectedInterval: "PT15M",
azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT15M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z",
},
{
name: "time grain set to auto",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "auto",
"allowedTimeGrainsMs": []int64{60000, 300000},
"top": "10",
},
queryIntervalMS: 400000,
expectedInterval: "PT5M",
azureMonitorQueryTarget: "aggregation=Average&api-version=2018-01-01&interval=PT5M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z",
},
{
name: "has a dimension filter",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimension": "blob",
"dimensionFilter": "*",
"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 a dimension filter",
azureMonitorVariedProperties: map[string]interface{}{
"timeGrain": "PT1M",
"dimension": "None",
"dimensionFilter": "*",
"top": "10",
},
queryIntervalMS: 400000,
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",
},
}
commonAzureModelProps := map[string]interface{}{
"aggregation": "Average",
"resourceGroup": "grafanastaging",
"resourceName": "grafana",
"metricDefinition": "Microsoft.Compute/virtualMachines",
"metricNamespace": "Microsoft.Compute-virtualMachines",
"metricName": "Percentage CPU",
"alias": "testalias",
"queryType": "Azure Monitor",
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range commonAzureModelProps {
tt.azureMonitorVariedProperties[k] = v
}
tsdbQuery := &tsdb.TsdbQuery{
TimeRange: &tsdb.TimeRange{
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
},
Queries: []*tsdb.Query{
{
DataSource: &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"subscriptionId": "default-subscription",
}),
},
Model: simplejson.NewFromAny(map[string]interface{}{
"subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
"azureMonitor": tt.azureMonitorVariedProperties,
},
),
RefId: "A",
IntervalMs: tt.queryIntervalMS,
},
},
}
azureMonitorQuery := &AzureMonitorQuery{
URL: "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics",
UrlComponents: map[string]string{
"metricDefinition": "Microsoft.Compute/virtualMachines",
"resourceGroup": "grafanastaging",
"resourceName": "grafana",
"subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
},
Target: tt.azureMonitorQueryTarget,
RefID: "A",
Alias: "testalias",
}
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
if err != nil {
t.Error(err)
}
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)
}
})
}
}
func makeDates(startDate time.Time, count int, interval time.Duration) (times []time.Time) {
for i := 0; i < count; i++ {
times = append(times, startDate.Add(interval*time.Duration(i)))
}
return
}
func TestAzureMonitorParseResponse(t *testing.T) {
tests := []struct {
name string
responseFile string
mockQuery *AzureMonitorQuery
expectedFrames data.Frames
queryIntervalMS int64
}{
{
name: "average aggregate time series response",
responseFile: "1-azure-monitor-response-avg.json",
mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Average"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 8, 10, 13, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{
2.0875, 2.1525, 2.155, 3.6925, 2.44,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})),
},
},
{
name: "total aggregate time series response",
responseFile: "2-azure-monitor-response-total.json",
mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Total"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 13, 29, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{
8.26, 8.7, 14.82, 10.07, 8.52,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})),
},
},
{
name: "maximum aggregate time series response",
responseFile: "3-azure-monitor-response-maximum.json",
mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Maximum"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 14, 26, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{
3.07, 2.92, 2.87, 2.27, 2.52,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})),
},
},
{
name: "minimum aggregate time series response",
responseFile: "4-azure-monitor-response-minimum.json",
mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Minimum"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 14, 43, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{
1.51, 2.38, 1.69, 2.27, 1.96,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})),
},
},
{
name: "count aggregate time series response",
responseFile: "5-azure-monitor-response-count.json",
mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Count"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 14, 44, 0, 0, time.UTC), 5, time.Minute)),
data.NewField("grafana.Percentage CPU", nil, []float64{
4, 4, 4, 4, 4,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})),
},
},
{
name: "multi dimension time series response",
responseFile: "6-azure-monitor-response-multi-dimension.json",
mockQuery: &AzureMonitorQuery{
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"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{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("grafana{blobtype=PageBlob}.Blob Count", nil, []float64{
3, 3, 3, 3, 3, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("grafana{blobtype=BlockBlob}.Blob Count", nil, []float64{
1, 1, 1, 1, 1, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("",
data.NewField("", nil,
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{
0, 0, 0, 0, 0, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
},
},
{
name: "with alias patterns in the query",
responseFile: "2-azure-monitor-response-total.json",
mockQuery: &AzureMonitorQuery{
Alias: "custom {{resourcegroup}} {{namespace}} {{resourceName}} {{metric}}",
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Total"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
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{
8.26, 8.7, 14.82, 10.07, 8.52,
}).SetConfig(&data.FieldConfig{Unit: "Percent"})),
},
},
{
name: "multi dimension with alias",
responseFile: "6-azure-monitor-response-multi-dimension.json",
mockQuery: &AzureMonitorQuery{
Alias: "{{dimensionname}}={{DimensionValue}}",
UrlComponents: map[string]string{
"resourceName": "grafana",
},
Params: url.Values{
"aggregation": {"Average"},
},
},
expectedFrames: data.Frames{
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("blobtype=PageBlob", nil, []float64{
3, 3, 3, 3, 3, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("blobtype=BlockBlob", nil, []float64{
1, 1, 1, 1, 1, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
data.NewFrame("",
data.NewField("", nil,
makeDates(time.Date(2019, 2, 9, 15, 21, 0, 0, time.UTC), 6, time.Hour)),
data.NewField("blobtype=Azure Data Lake Storage", nil, []float64{
0, 0, 0, 0, 0, 0,
}).SetConfig(&data.FieldConfig{Unit: "Count"})),
},
},
}
datasource := &AzureMonitorDatasource{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
azData, err := loadTestFile("azuremonitor/" + tt.responseFile)
require.NoError(t, err)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
err = datasource.parseResponse(res, azData, tt.mockQuery)
require.NoError(t, err)
frames, err := res.Dataframes.Decoded()
require.NoError(t, err)
if diff := cmp.Diff(tt.expectedFrames, frames, data.FrameTestCompareOptions()...); diff != "" {
t.Errorf("Result mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestFindClosestAllowIntervalMS(t *testing.T) {
humanIntervalToMS := map[string]int64{
"3m": 180000,
"5m": 300000,
"10m": 600000,
"15m": 900000,
"1d": 86400000,
"2d": 172800000,
}
tests := []struct {
name string
allowedTimeGrains []int64 // Note: Uses defaults when empty list
inputInterval int64
expectedInterval int64
}{
{
name: "closest to 3m is 5m",
allowedTimeGrains: []int64{},
inputInterval: humanIntervalToMS["3m"],
expectedInterval: humanIntervalToMS["5m"],
},
{
name: "closest to 10m is 15m",
allowedTimeGrains: []int64{},
inputInterval: humanIntervalToMS["10m"],
expectedInterval: humanIntervalToMS["15m"],
},
{
name: "closest to 2d is 1d",
allowedTimeGrains: []int64{},
inputInterval: humanIntervalToMS["2d"],
expectedInterval: humanIntervalToMS["1d"],
},
{
name: "closest to 3m is 1d when 1d is only allowed interval",
allowedTimeGrains: []int64{humanIntervalToMS["1d"]},
inputInterval: humanIntervalToMS["2d"],
expectedInterval: humanIntervalToMS["1d"],
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
interval := findClosestAllowedIntervalMS(tt.inputInterval, tt.allowedTimeGrains)
require.Equal(t, tt.expectedInterval, interval)
})
}
}
func loadTestFile(name string) (AzureMonitorResponse, error) {
var azData AzureMonitorResponse
path := filepath.Join("testdata", name)
jsonBody, err := ioutil.ReadFile(path)
if err != nil {
return azData, err
}
err = json.Unmarshal(jsonBody, &azData)
return azData, err
}