mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 09:26:43 -06:00
c05049f395
* azuremonitor: add support for log analytics macros Also adds tests for the kql macros * azuremonitor: backend implementation for Log Analytics * azuremonitor: remove gzip header from plugin route The Go net/http library adds an accept encoding header for gzip automatically. https://golang.org/src/net/http/transport.go\#L2454 So no need to specify it manually * azuremonitor: parses log analytics time series * azuremonitor: support for table data for Log Analytics * azuremonitor: for log analytics switch to calling the API... ...from the backend for time series and table queries. * azuremonitor: fix missing err check * azuremonitor: support Azure China, Azure Gov... for log analytics on the backend. * azuremonitor: review fixes * azuremonitor: rename test files folder to testdata To follow Go conventions for test data in tests * azuremonitor: review fixes * azuremonitor: better error message for http requests * azuremonitor: fix for load workspaces on config page * azuremonitor: strict null check fixes Co-authored-by: bergquist <carl.bergquist@gmail.com>
386 lines
14 KiB
Go
386 lines
14 KiB
Go
package azuremonitor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/tsdb"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
)
|
|
|
|
func TestApplicationInsightsDatasource(t *testing.T) {
|
|
Convey("ApplicationInsightsDatasource", t, func() {
|
|
datasource := &ApplicationInsightsDatasource{}
|
|
|
|
Convey("Parse queries from frontend and build AzureMonitor API queries", func() {
|
|
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
|
|
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{}{}),
|
|
},
|
|
Model: simplejson.NewFromAny(map[string]interface{}{
|
|
"appInsights": map[string]interface{}{
|
|
"rawQuery": false,
|
|
"timeGrain": "PT1M",
|
|
"aggregation": "Average",
|
|
"metricName": "server/exceptions",
|
|
"alias": "testalias",
|
|
"queryType": "Application Insights",
|
|
},
|
|
}),
|
|
RefId: "A",
|
|
IntervalMs: 1234,
|
|
},
|
|
},
|
|
}
|
|
Convey("and is a normal query", func() {
|
|
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(queries), ShouldEqual, 1)
|
|
So(queries[0].RefID, ShouldEqual, "A")
|
|
So(queries[0].ApiURL, ShouldEqual, "metrics/server/exceptions")
|
|
So(queries[0].Target, ShouldEqual, "aggregation=Average&interval=PT1M×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
|
So(len(queries[0].Params), ShouldEqual, 3)
|
|
So(queries[0].Params["timespan"][0], ShouldEqual, "2018-03-15T13:00:00Z/2018-03-15T13:34:00Z")
|
|
So(queries[0].Params["aggregation"][0], ShouldEqual, "Average")
|
|
So(queries[0].Params["interval"][0], ShouldEqual, "PT1M")
|
|
So(queries[0].Alias, ShouldEqual, "testalias")
|
|
})
|
|
|
|
Convey("and has a time grain set to auto", func() {
|
|
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
|
"appInsights": map[string]interface{}{
|
|
"rawQuery": false,
|
|
"timeGrain": "auto",
|
|
"aggregation": "Average",
|
|
"metricName": "Percentage CPU",
|
|
"alias": "testalias",
|
|
"queryType": "Application Insights",
|
|
},
|
|
})
|
|
tsdbQuery.Queries[0].IntervalMs = 400000
|
|
|
|
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(queries[0].Params["interval"][0], ShouldEqual, "PT15M")
|
|
})
|
|
|
|
Convey("and has a time grain set to auto and the metric has a limited list of allowed time grains", func() {
|
|
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
|
"appInsights": map[string]interface{}{
|
|
"rawQuery": false,
|
|
"timeGrain": "auto",
|
|
"aggregation": "Average",
|
|
"metricName": "Percentage CPU",
|
|
"alias": "testalias",
|
|
"queryType": "Application Insights",
|
|
"allowedTimeGrainsMs": []interface{}{"auto", json.Number("60000"), json.Number("300000")},
|
|
},
|
|
})
|
|
tsdbQuery.Queries[0].IntervalMs = 400000
|
|
|
|
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(queries[0].Params["interval"][0], ShouldEqual, "PT5M")
|
|
})
|
|
|
|
Convey("and has a dimension filter", func() {
|
|
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
|
"appInsights": map[string]interface{}{
|
|
"rawQuery": false,
|
|
"timeGrain": "PT1M",
|
|
"aggregation": "Average",
|
|
"metricName": "Percentage CPU",
|
|
"alias": "testalias",
|
|
"queryType": "Application Insights",
|
|
"dimension": "blob",
|
|
"dimensionFilter": "blob eq '*'",
|
|
},
|
|
})
|
|
|
|
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(queries[0].Target, ShouldEqual, "aggregation=Average&filter=blob+eq+%27%2A%27&interval=PT1M&segment=blob×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
|
So(queries[0].Params["filter"][0], ShouldEqual, "blob eq '*'")
|
|
|
|
})
|
|
|
|
Convey("and has a dimension filter set to None", func() {
|
|
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
|
"appInsights": map[string]interface{}{
|
|
"rawQuery": false,
|
|
"timeGrain": "PT1M",
|
|
"aggregation": "Average",
|
|
"metricName": "Percentage CPU",
|
|
"alias": "testalias",
|
|
"queryType": "Application Insights",
|
|
"dimension": "None",
|
|
},
|
|
})
|
|
|
|
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(queries[0].Target, ShouldEqual, "aggregation=Average&interval=PT1M×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
|
})
|
|
|
|
Convey("id a raw query", func() {
|
|
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
|
"appInsights": map[string]interface{}{
|
|
"rawQuery": true,
|
|
"rawQueryString": "exceptions | where $__timeFilter(timestamp) | summarize count=count() by bin(timestamp, $__interval)",
|
|
"timeColumn": "timestamp",
|
|
"valueColumn": "count",
|
|
},
|
|
})
|
|
|
|
queries, err := datasource.buildQueries(tsdbQuery.Queries, tsdbQuery.TimeRange)
|
|
So(err, ShouldBeNil)
|
|
So(queries[0].Params["query"][0], ShouldEqual, "exceptions | where ['timestamp'] >= datetime('2018-03-15T13:00:00Z') and ['timestamp'] <= datetime('2018-03-15T13:34:00Z') | summarize count=count() by bin(timestamp, 1234ms)")
|
|
So(queries[0].Target, ShouldEqual, "query=exceptions+%7C+where+%5B%27timestamp%27%5D+%3E%3D+datetime%28%272018-03-15T13%3A00%3A00Z%27%29+and+%5B%27timestamp%27%5D+%3C%3D+datetime%28%272018-03-15T13%3A34%3A00Z%27%29+%7C+summarize+count%3Dcount%28%29+by+bin%28timestamp%2C+1234ms%29")
|
|
})
|
|
})
|
|
|
|
Convey("Parse Application Insights query API response in the time series format", func() {
|
|
Convey("no segments", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/1-application-insights-response-raw-query.json")
|
|
So(err, ShouldBeNil)
|
|
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: true,
|
|
TimeColumnName: "timestamp",
|
|
ValueColumnName: "value",
|
|
}
|
|
series, _, err := datasource.parseTimeSeriesFromQuery(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 1)
|
|
So(series[0].Name, ShouldEqual, "value")
|
|
So(len(series[0].Points), ShouldEqual, 2)
|
|
|
|
So(series[0].Points[0][0].Float64, ShouldEqual, 1)
|
|
So(series[0].Points[0][1].Float64, ShouldEqual, int64(1568336523000))
|
|
|
|
So(series[0].Points[1][0].Float64, ShouldEqual, 2)
|
|
So(series[0].Points[1][1].Float64, ShouldEqual, int64(1568340123000))
|
|
})
|
|
|
|
Convey("with segments", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/2-application-insights-response-raw-query-segmented.json")
|
|
So(err, ShouldBeNil)
|
|
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: true,
|
|
TimeColumnName: "timestamp",
|
|
ValueColumnName: "value",
|
|
SegmentColumnName: "segment",
|
|
}
|
|
series, _, err := datasource.parseTimeSeriesFromQuery(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 2)
|
|
So(series[0].Name, ShouldEqual, "{segment=a}.value")
|
|
So(len(series[0].Points), ShouldEqual, 2)
|
|
|
|
So(series[0].Points[0][0].Float64, ShouldEqual, 1)
|
|
So(series[0].Points[0][1].Float64, ShouldEqual, int64(1568336523000))
|
|
|
|
So(series[0].Points[1][0].Float64, ShouldEqual, 3)
|
|
So(series[0].Points[1][1].Float64, ShouldEqual, int64(1568426523000))
|
|
|
|
So(series[1].Name, ShouldEqual, "{segment=b}.value")
|
|
So(series[1].Points[0][0].Float64, ShouldEqual, 2)
|
|
So(series[1].Points[0][1].Float64, ShouldEqual, int64(1568336523000))
|
|
|
|
So(series[1].Points[1][0].Float64, ShouldEqual, 4)
|
|
So(series[1].Points[1][1].Float64, ShouldEqual, int64(1568426523000))
|
|
|
|
Convey("with alias", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/2-application-insights-response-raw-query-segmented.json")
|
|
So(err, ShouldBeNil)
|
|
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: true,
|
|
TimeColumnName: "timestamp",
|
|
ValueColumnName: "value",
|
|
SegmentColumnName: "segment",
|
|
Alias: "{{metric}} {{dimensionname}} {{dimensionvalue}}",
|
|
}
|
|
series, _, err := datasource.parseTimeSeriesFromQuery(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 2)
|
|
So(series[0].Name, ShouldEqual, "value segment a")
|
|
So(series[1].Name, ShouldEqual, "value segment b")
|
|
|
|
})
|
|
})
|
|
})
|
|
|
|
Convey("Parse Application Insights metrics API", func() {
|
|
Convey("single value", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/3-application-insights-response-metrics-single-value.json")
|
|
So(err, ShouldBeNil)
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: false,
|
|
}
|
|
series, err := datasource.parseTimeSeriesFromMetrics(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 1)
|
|
So(series[0].Name, ShouldEqual, "value")
|
|
So(len(series[0].Points), ShouldEqual, 1)
|
|
|
|
So(series[0].Points[0][0].Float64, ShouldEqual, 1.2)
|
|
So(series[0].Points[0][1].Float64, ShouldEqual, int64(1568340123000))
|
|
})
|
|
|
|
Convey("1H separation", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/4-application-insights-response-metrics-no-segment.json")
|
|
So(err, ShouldBeNil)
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: false,
|
|
}
|
|
series, err := datasource.parseTimeSeriesFromMetrics(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 1)
|
|
So(series[0].Name, ShouldEqual, "value")
|
|
So(len(series[0].Points), ShouldEqual, 2)
|
|
|
|
So(series[0].Points[0][0].Float64, ShouldEqual, 1)
|
|
So(series[0].Points[0][1].Float64, ShouldEqual, int64(1568340123000))
|
|
So(series[0].Points[1][0].Float64, ShouldEqual, 2)
|
|
So(series[0].Points[1][1].Float64, ShouldEqual, int64(1568343723000))
|
|
|
|
Convey("with segmentation", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/4-application-insights-response-metrics-segmented.json")
|
|
So(err, ShouldBeNil)
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: false,
|
|
}
|
|
series, err := datasource.parseTimeSeriesFromMetrics(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 2)
|
|
So(series[0].Name, ShouldEqual, "{blob=a}.value")
|
|
So(len(series[0].Points), ShouldEqual, 2)
|
|
|
|
So(series[0].Points[0][0].Float64, ShouldEqual, 1)
|
|
So(series[0].Points[0][1].Float64, ShouldEqual, int64(1568340123000))
|
|
So(series[0].Points[1][0].Float64, ShouldEqual, 2)
|
|
So(series[0].Points[1][1].Float64, ShouldEqual, int64(1568343723000))
|
|
|
|
So(series[1].Name, ShouldEqual, "{blob=b}.value")
|
|
So(len(series[1].Points), ShouldEqual, 2)
|
|
|
|
So(series[1].Points[0][0].Float64, ShouldEqual, 3)
|
|
So(series[1].Points[0][1].Float64, ShouldEqual, int64(1568340123000))
|
|
So(series[1].Points[1][0].Float64, ShouldEqual, 4)
|
|
So(series[1].Points[1][1].Float64, ShouldEqual, int64(1568343723000))
|
|
|
|
Convey("with alias", func() {
|
|
data, err := ioutil.ReadFile("testdata/applicationinsights/4-application-insights-response-metrics-segmented.json")
|
|
So(err, ShouldBeNil)
|
|
query := &ApplicationInsightsQuery{
|
|
IsRaw: false,
|
|
Alias: "{{metric}} {{dimensionname}} {{dimensionvalue}}",
|
|
}
|
|
series, err := datasource.parseTimeSeriesFromMetrics(data, query)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(len(series), ShouldEqual, 2)
|
|
So(series[0].Name, ShouldEqual, "value blob a")
|
|
So(series[1].Name, ShouldEqual, "value blob b")
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestAppInsightsPluginRoutes(t *testing.T) {
|
|
datasource := &ApplicationInsightsDatasource{}
|
|
plugin := &plugins.DataSourcePlugin{
|
|
Routes: []*plugins.AppPluginRoute{
|
|
{
|
|
Path: "appinsights",
|
|
Method: "GET",
|
|
URL: "https://api.applicationinsights.io",
|
|
Headers: []plugins.AppPluginRouteHeader{
|
|
{Name: "X-API-Key", Content: "{{.SecureJsonData.appInsightsApiKey}}"},
|
|
{Name: "x-ms-app", Content: "Grafana"},
|
|
},
|
|
},
|
|
{
|
|
Path: "chinaappinsights",
|
|
Method: "GET",
|
|
URL: "https://api.applicationinsights.azure.cn",
|
|
Headers: []plugins.AppPluginRouteHeader{
|
|
{Name: "X-API-Key", Content: "{{.SecureJsonData.appInsightsApiKey}}"},
|
|
{Name: "x-ms-app", Content: "Grafana"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cloudName string
|
|
expectedRouteName string
|
|
expectedRouteURL string
|
|
Err require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "plugin proxy route for the Azure public cloud",
|
|
cloudName: "azuremonitor",
|
|
expectedRouteName: "appinsights",
|
|
expectedRouteURL: "https://api.applicationinsights.io",
|
|
Err: require.NoError,
|
|
},
|
|
{
|
|
name: "plugin proxy route for the Azure China cloud",
|
|
cloudName: "chinaazuremonitor",
|
|
expectedRouteName: "chinaappinsights",
|
|
expectedRouteURL: "https://api.applicationinsights.azure.cn",
|
|
Err: require.NoError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
route, routeName, err := datasource.getPluginRoute(plugin, tt.cloudName)
|
|
tt.Err(t, err)
|
|
|
|
if diff := cmp.Diff(tt.expectedRouteURL, route.URL, cmpopts.EquateNaNs()); diff != "" {
|
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.expectedRouteName, routeName, cmpopts.EquateNaNs()); diff != "" {
|
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|