Commit 20faef8d by Chad Nedzlek Committed by Daniel Lee

AzureMonitor: Alerting for Azure Application Insights (#19381)

* Convert Azure Application Insights datasource to Go

Allows for alerting of Application Insights data source

Closes: #15153

* Fix timeGrainReset

* Default time interval for querys for alerts

* Fix a few rename related bugs

* Update readme to indicate App Insights alerting

* Fix typo and add tests to ensure migration is happening

* Address code review feedback (mostly typos and unintended changes)
parent 92765a6c
...@@ -216,7 +216,9 @@ Examples: ...@@ -216,7 +216,9 @@ Examples:
### Application Insights Alerting ### Application Insights Alerting
Not implemented yet. Grafana alerting is supported for Application Insights. This is not Azure Alerts support. Read more about how alerting in Grafana works [here]({{< relref "alerting/rules.md" >}}).
{{< docs-imagebox img="/img/docs/v60/azuremonitor-alerting.png" class="docs-image--no-shadow" caption="Azure Monitor Alerting" >}}
## Querying the Azure Log Analytics Service ## Querying the Azure Log Analytics Service
......
...@@ -107,7 +107,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * ...@@ -107,7 +107,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"]) timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"])
timeGrains := azureMonitorTarget["allowedTimeGrainsMs"] timeGrains := azureMonitorTarget["allowedTimeGrainsMs"]
if timeGrain == "auto" { if timeGrain == "auto" {
timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains) timeGrain, err = setAutoTimeGrain(query.IntervalMs, timeGrains)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -147,35 +147,6 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * ...@@ -147,35 +147,6 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
return azureMonitorQueries, nil return azureMonitorQueries, nil
} }
// setAutoTimeGrain tries to find the closest interval to the query's intervalMs value
// if the metric has a limited set of possible intervals/time grains then use those
// instead of the default list of intervals
func (e *AzureMonitorDatasource) setAutoTimeGrain(intervalMs int64, timeGrains interface{}) (string, error) {
// parses array of numbers from the timeGrains json field
allowedTimeGrains := []int64{}
tgs, ok := timeGrains.([]interface{})
if ok {
for _, v := range tgs {
jsonNumber, ok := v.(json.Number)
if ok {
tg, err := jsonNumber.Int64()
if err == nil {
allowedTimeGrains = append(allowedTimeGrains, tg)
}
}
}
}
autoInterval := e.findClosestAllowedIntervalMS(intervalMs, allowedTimeGrains)
tg := &TimeGrain{}
autoTimeGrain, err := tg.createISO8601DurationFromIntervalMS(autoInterval)
if err != nil {
return "", err
}
return autoTimeGrain, nil
}
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{Meta: simplejson.New(), RefId: query.RefID}
...@@ -203,7 +174,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM ...@@ -203,7 +174,7 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM
opentracing.HTTPHeaders, opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header)) opentracing.HTTPHeadersCarrier(req.Header))
azlog.Debug("AzureMonitor", "Request URL", req.URL.String()) azlog.Debug("AzureMonitor", "Request ApiURL", req.URL.String())
res, err := ctxhttp.Do(ctx, e.httpClient, req) res, err := ctxhttp.Do(ctx, e.httpClient, req)
if err != nil { if err != nil {
queryResult.Error = err queryResult.Error = err
...@@ -290,7 +261,7 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data ...@@ -290,7 +261,7 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data
metadataName = series.Metadatavalues[0].Name.LocalizedValue metadataName = series.Metadatavalues[0].Name.LocalizedValue
metadataValue = series.Metadatavalues[0].Value metadataValue = series.Metadatavalues[0].Value
} }
metricName := formatLegendKey(query.Alias, query.UrlComponents["resourceName"], data.Value[0].Name.LocalizedValue, metadataName, metadataValue, data.Namespace, data.Value[0].ID) metricName := formatAzureMonitorLegendKey(query.Alias, query.UrlComponents["resourceName"], data.Value[0].Name.LocalizedValue, metadataName, metadataValue, data.Namespace, data.Value[0].ID)
for _, point := range series.Data { for _, point := range series.Data {
var value float64 var value float64
...@@ -321,35 +292,9 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data ...@@ -321,35 +292,9 @@ func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data
return nil return nil
} }
// findClosestAllowedIntervalMs is used for the auto time grain setting. // formatAzureMonitorLegendKey builds the legend key or timeseries name
// It finds the closest time grain from the list of allowed time grains for Azure Monitor
// using the Grafana interval in milliseconds
// Some metrics only allow a limited list of time grains. The allowedTimeGrains parameter
// allows overriding the default list of allowed time grains.
func (e *AzureMonitorDatasource) findClosestAllowedIntervalMS(intervalMs int64, allowedTimeGrains []int64) int64 {
allowedIntervals := defaultAllowedIntervalsMS
if len(allowedTimeGrains) > 0 {
allowedIntervals = allowedTimeGrains
}
closest := allowedIntervals[0]
for i, allowed := range allowedIntervals {
if intervalMs > allowed {
if i+1 < len(allowedIntervals) {
closest = allowedIntervals[i+1]
} else {
closest = allowed
}
}
}
return closest
}
// formatLegendKey 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 formatLegendKey(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) string {
if alias == "" { if alias == "" {
if len(metadataName) > 0 { if len(metadataName) > 0 {
return fmt.Sprintf("%s{%s=%s}.%s", resourceName, metadataName, metadataValue, metricName) return fmt.Sprintf("%s{%s=%s}.%s", resourceName, metadataName, metadataValue, metricName)
......
...@@ -167,7 +167,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -167,7 +167,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
Convey("Parse AzureMonitor API response in the time series format", func() { Convey("Parse AzureMonitor API response in the time series format", func() {
Convey("when data from query aggregated as average to one time series", func() { Convey("when data from query aggregated as average to one time series", func() {
data, err := loadTestFile("./test-data/1-azure-monitor-response-avg.json") data, err := loadTestFile("./test-data/azuremonitor/1-azure-monitor-response-avg.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(data.Interval, ShouldEqual, "PT1M") So(data.Interval, ShouldEqual, "PT1M")
...@@ -204,7 +204,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -204,7 +204,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data from query aggregated as total to one time series", func() { Convey("when data from query aggregated as total to one time series", func() {
data, err := loadTestFile("./test-data/2-azure-monitor-response-total.json") data, err := loadTestFile("./test-data/azuremonitor/2-azure-monitor-response-total.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -224,7 +224,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -224,7 +224,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data from query aggregated as maximum to one time series", func() { Convey("when data from query aggregated as maximum to one time series", func() {
data, err := loadTestFile("./test-data/3-azure-monitor-response-maximum.json") data, err := loadTestFile("./test-data/azuremonitor/3-azure-monitor-response-maximum.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -244,7 +244,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -244,7 +244,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data from query aggregated as minimum to one time series", func() { Convey("when data from query aggregated as minimum to one time series", func() {
data, err := loadTestFile("./test-data/4-azure-monitor-response-minimum.json") data, err := loadTestFile("./test-data/azuremonitor/4-azure-monitor-response-minimum.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -264,7 +264,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -264,7 +264,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data from query aggregated as Count to one time series", func() { Convey("when data from query aggregated as Count to one time series", func() {
data, err := loadTestFile("./test-data/5-azure-monitor-response-count.json") data, err := loadTestFile("./test-data/azuremonitor/5-azure-monitor-response-count.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -284,7 +284,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -284,7 +284,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data from query aggregated as total and has dimension filter", func() { Convey("when data from query aggregated as total and has dimension filter", func() {
data, err := loadTestFile("./test-data/6-azure-monitor-response-multi-dimension.json") data, err := loadTestFile("./test-data/azuremonitor/6-azure-monitor-response-multi-dimension.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -311,7 +311,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -311,7 +311,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data from query has alias patterns", func() { Convey("when data from query has alias patterns", func() {
data, err := loadTestFile("./test-data/2-azure-monitor-response-total.json") data, err := loadTestFile("./test-data/azuremonitor/2-azure-monitor-response-total.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -331,7 +331,7 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -331,7 +331,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
}) })
Convey("when data has dimension filters and alias patterns", func() { Convey("when data has dimension filters and alias patterns", func() {
data, err := loadTestFile("./test-data/6-azure-monitor-response-multi-dimension.json") data, err := loadTestFile("./test-data/azuremonitor/6-azure-monitor-response-multi-dimension.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
...@@ -363,16 +363,16 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -363,16 +363,16 @@ func TestAzureMonitorDatasource(t *testing.T) {
"2d": 172800000, "2d": 172800000,
} }
closest := datasource.findClosestAllowedIntervalMS(intervals["3m"], []int64{}) closest := findClosestAllowedIntervalMS(intervals["3m"], []int64{})
So(closest, ShouldEqual, intervals["5m"]) So(closest, ShouldEqual, intervals["5m"])
closest = datasource.findClosestAllowedIntervalMS(intervals["10m"], []int64{}) closest = findClosestAllowedIntervalMS(intervals["10m"], []int64{})
So(closest, ShouldEqual, intervals["15m"]) So(closest, ShouldEqual, intervals["15m"])
closest = datasource.findClosestAllowedIntervalMS(intervals["2d"], []int64{}) closest = findClosestAllowedIntervalMS(intervals["2d"], []int64{})
So(closest, ShouldEqual, intervals["1d"]) So(closest, ShouldEqual, intervals["1d"])
closest = datasource.findClosestAllowedIntervalMS(intervals["3m"], []int64{intervals["1d"]}) closest = findClosestAllowedIntervalMS(intervals["3m"], []int64{intervals["1d"]})
So(closest, ShouldEqual, intervals["1d"]) So(closest, ShouldEqual, intervals["1d"])
}) })
}) })
......
package azuremonitor
import "encoding/json"
// setAutoTimeGrain tries to find the closest interval to the query's intervalMs value
// if the metric has a limited set of possible intervals/time grains then use those
// instead of the default list of intervals
func setAutoTimeGrain(intervalMs int64, timeGrains interface{}) (string, error) {
// parses array of numbers from the timeGrains json field
allowedTimeGrains := []int64{}
tgs, ok := timeGrains.([]interface{})
if ok {
for _, v := range tgs {
jsonNumber, ok := v.(json.Number)
if ok {
tg, err := jsonNumber.Int64()
if err == nil {
allowedTimeGrains = append(allowedTimeGrains, tg)
}
}
}
}
autoInterval := findClosestAllowedIntervalMS(intervalMs, allowedTimeGrains)
tg := &TimeGrain{}
autoTimeGrain, err := tg.createISO8601DurationFromIntervalMS(autoInterval)
if err != nil {
return "", err
}
return autoTimeGrain, nil
}
// findClosestAllowedIntervalMs is used for the auto time grain setting.
// It finds the closest time grain from the list of allowed time grains for Azure Monitor
// using the Grafana interval in milliseconds
// Some metrics only allow a limited list of time grains. The allowedTimeGrains parameter
// allows overriding the default list of allowed time grains.
func findClosestAllowedIntervalMS(intervalMs int64, allowedTimeGrains []int64) int64 {
allowedIntervals := defaultAllowedIntervalsMS
if len(allowedTimeGrains) > 0 {
allowedIntervals = allowedTimeGrains
}
closest := allowedIntervals[0]
for i, allowed := range allowedIntervals {
if intervalMs > allowed {
if i+1 < len(allowedIntervals) {
closest = allowedIntervals[i+1]
} else {
closest = allowed
}
}
}
return closest
}
...@@ -46,10 +46,10 @@ func init() { ...@@ -46,10 +46,10 @@ func init() {
// executes the queries against the API and parses the response into // executes the queries against the API and parses the response into
// the right format // the right format
func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
var result *tsdb.Response
var err error var err error
var azureMonitorQueries []*tsdb.Query var azureMonitorQueries []*tsdb.Query
var applicationInsightsQueries []*tsdb.Query
for _, query := range tsdbQuery.Queries { for _, query := range tsdbQuery.Queries {
queryType := query.Model.Get("queryType").MustString("") queryType := query.Model.Get("queryType").MustString("")
...@@ -57,6 +57,8 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou ...@@ -57,6 +57,8 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
switch queryType { switch queryType {
case "Azure Monitor": case "Azure Monitor":
azureMonitorQueries = append(azureMonitorQueries, query) azureMonitorQueries = append(azureMonitorQueries, query)
case "Application Insights":
applicationInsightsQueries = append(applicationInsightsQueries, query)
default: default:
return nil, fmt.Errorf("Alerting not supported for %s", queryType) return nil, fmt.Errorf("Alerting not supported for %s", queryType)
} }
...@@ -67,7 +69,24 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou ...@@ -67,7 +69,24 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
dsInfo: e.dsInfo, dsInfo: e.dsInfo,
} }
result, err = azDatasource.executeTimeSeriesQuery(ctx, azureMonitorQueries, tsdbQuery.TimeRange) aiDatasource := &ApplicationInsightsDatasource{
httpClient: e.httpClient,
dsInfo: e.dsInfo,
}
azResult, err := azDatasource.executeTimeSeriesQuery(ctx, azureMonitorQueries, tsdbQuery.TimeRange)
if err != nil {
return nil, err
}
aiResult, err := aiDatasource.executeTimeSeriesQuery(ctx, applicationInsightsQueries, tsdbQuery.TimeRange)
if err != nil {
return nil, err
}
for k, v := range aiResult.Results {
azResult.Results[k] = v
}
return result, err return azResult, nil
} }
package azuremonitor
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
const rsIdentifier = `([_a-zA-Z0-9]+)`
const sExpr = `\$` + rsIdentifier + `(?:\(([^\)]*)\))?`
type kqlMacroEngine struct {
timeRange *tsdb.TimeRange
query *tsdb.Query
}
func KqlInterpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, kql string) (string, error) {
engine := kqlMacroEngine{}
return engine.Interpolate(query, timeRange, kql)
}
func (m *kqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, kql string) (string, error) {
m.timeRange = timeRange
m.query = query
rExp, _ := regexp.Compile(sExpr)
var macroError error
kql = m.ReplaceAllStringSubmatchFunc(rExp, kql, func(groups []string) string {
args := []string{}
if len(groups) > 2 {
args = strings.Split(groups[2], ",")
}
for i, arg := range args {
args[i] = strings.Trim(arg, " ")
}
res, err := m.evaluateMacro(groups[1], args)
if err != nil && macroError == nil {
macroError = err
return "macro_error()"
}
return res
})
if macroError != nil {
return "", macroError
}
return kql, nil
}
func (m *kqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
switch name {
case "__timeFilter":
timeColumn := "timestamp"
if len(args) > 0 && args[0] != "" {
timeColumn = args[0]
}
return fmt.Sprintf("['%s'] >= datetime('%s') and ['%s'] <= datetime('%s')", timeColumn, m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), timeColumn, m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__from":
return fmt.Sprintf("datetime('%s')", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__to":
return fmt.Sprintf("datetime('%s')", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__interval":
var interval time.Duration
if m.query.IntervalMs == 0 {
to := m.timeRange.MustGetTo().UnixNano()
from := m.timeRange.MustGetFrom().UnixNano()
// default to "100 datapoints" if nothing in the query is more specific
defaultInterval := time.Duration((to - from) / 60)
var err error
interval, err = tsdb.GetIntervalFrom(m.query.DataSource, m.query.Model, defaultInterval)
if err != nil {
azlog.Warn("Unable to get interval from query", "datasource", m.query.DataSource, "model", m.query.Model)
interval = defaultInterval
}
} else {
interval = time.Millisecond * time.Duration(m.query.IntervalMs)
}
return fmt.Sprintf("%dms", int(interval/time.Millisecond)), nil
case "__contains":
if len(args) < 2 || args[0] == "" || args[1] == "" {
return "", fmt.Errorf("macro %v needs colName and variableSet", name)
}
if args[1] == "all" {
return "1 == 1", nil
}
return fmt.Sprintf("['%s'] in ('%s')", args[0], args[1]), nil
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
}
func (m *kqlMacroEngine) ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
result := ""
lastIndex := 0
for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
groups := []string{}
for i := 0; i < len(v); i += 2 {
if v[i] < 0 {
groups = append(groups, "")
} else {
groups = append(groups, str[v[i]:v[i+1]])
}
}
result += str[lastIndex:v[0]] + repl(groups)
lastIndex = v[1]
}
return result + str[lastIndex:]
}
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{
"name": "timestamp",
"type": "datetime"
},
{
"name": "value",
"type": "int"
}
],
"rows": [
[
"2019-09-13T01:02:03.456789Z",
1
],
[
"2019-09-13T02:02:03.456789Z",
2
]
]
}
]
}
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{
"name": "timestamp",
"type": "datetime"
},
{
"name": "value",
"type": "int"
},
{
"name": "segment",
"type": "string"
}
],
"rows": [
[
"2019-09-13T01:02:03.456789Z",
1,
"a"
],
[
"2019-09-13T01:02:03.456789Z",
2,
"b"
],
[
"2019-09-14T02:02:03.456789Z",
3,
"a"
],
[
"2019-09-14T02:02:03.456789Z",
4,
"b"
]
]
}
]
}
{
"value": {
"start": "2019-09-13T01:02:03.456789Z",
"end": "2019-09-13T02:02:03.456789Z",
"value": {
"avg": 1.2
}
}
}
{
"value": {
"start": "2019-09-13T01:02:03.456789Z",
"end": "2019-09-13T03:02:03.456789Z",
"interval": "PT1H",
"segments": [
{
"start": "2019-09-13T01:02:03.456789Z",
"end": "2019-09-13T02:02:03.456789Z",
"value": {
"avg": 1
}
},
{
"start": "2019-09-13T02:02:03.456789Z",
"end": "2019-09-13T03:02:03.456789Z",
"value": {
"avg": 2
}
}
]
}
}
{
"value": {
"start": "2019-09-13T01:02:03.456789Z",
"end": "2019-09-13T03:02:03.456789Z",
"interval": "PT1H",
"segments": [
{
"start": "2019-09-13T01:02:03.456789Z",
"end": "2019-09-13T02:02:03.456789Z",
"segments": [
{
"value": {
"avg": 1
},
"blob": "a"
},
{
"value": {
"avg": 3
},
"blob": "b"
}
]
},
{
"start": "2019-09-13T02:02:03.456789Z",
"end": "2019-09-13T03:02:03.456789Z",
"segments": [
{
"value": {
"avg": 2
},
"blob": "a"
},
{
"value": {
"avg": 4
},
"blob": "b"
}
]
}
]
}
}
...@@ -51,17 +51,32 @@ type AzureMonitorResponse struct { ...@@ -51,17 +51,32 @@ type AzureMonitorResponse struct {
Resourceregion string `json:"resourceregion"` Resourceregion string `json:"resourceregion"`
} }
// ApplicationInsightsResponse is the json response from the Application Insights API
type ApplicationInsightsResponse struct { type ApplicationInsightsResponse struct {
MetricResponse *ApplicationInsightsMetricsResponse
QueryResponse *ApplicationInsightsQueryResponse
}
// ApplicationInsightsResponse is the json response from the Application Insights API
type ApplicationInsightsQueryResponse struct {
Tables []struct { Tables []struct {
TableName string `json:"TableName"` Name string `json:"name"`
Columns []struct { Columns []struct {
ColumnName string `json:"ColumnName"` Name string `json:"name"`
DataType string `json:"DataType"` Type string `json:"type"`
ColumnType string `json:"ColumnType"` } `json:"columns"`
} `json:"Columns"` Rows [][]interface{} `json:"rows"`
Rows [][]interface{} `json:"Rows"` } `json:"tables"`
} `json:"Tables"` }
// ApplicationInsightsMetricsResponse is the json response from the Application Insights API
type ApplicationInsightsMetricsResponse struct {
Name string
Segments []struct {
Start time.Time
End time.Time
Segmented map[string]float64
Value float64
}
} }
// AzureLogAnalyticsResponse is the json response object from the Azure Log Analytics API. // AzureLogAnalyticsResponse is the json response object from the Azure Log Analytics API.
......
...@@ -5,7 +5,7 @@ import ( ...@@ -5,7 +5,7 @@ import (
"strings" "strings"
) )
// URLQueryReader is a URL query type. // URLQueryReader is a ApiURL query type.
type URLQueryReader struct { type URLQueryReader struct {
values url.Values values url.Values
} }
...@@ -22,7 +22,7 @@ func NewURLQueryReader(urlInfo *url.URL) (*URLQueryReader, error) { ...@@ -22,7 +22,7 @@ func NewURLQueryReader(urlInfo *url.URL) (*URLQueryReader, error) {
}, nil }, nil
} }
// Get parse parameters from an URL. If the parameter does not exist, it returns // Get parse parameters from an ApiURL. If the parameter does not exist, it returns
// the default value. // the default value.
func (r *URLQueryReader) Get(name string, def string) string { func (r *URLQueryReader) Get(name string, def string) string {
val := r.values[name] val := r.values[name]
...@@ -33,7 +33,7 @@ func (r *URLQueryReader) Get(name string, def string) string { ...@@ -33,7 +33,7 @@ func (r *URLQueryReader) Get(name string, def string) string {
return val[0] return val[0]
} }
// JoinURLFragments joins two URL fragments into only one URL string. // JoinURLFragments joins two ApiURL fragments into only one ApiURL string.
func JoinURLFragments(a, b string) string { func JoinURLFragments(a, b string) string {
aslash := strings.HasSuffix(a, "/") aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/") bslash := strings.HasPrefix(b, "/")
......
import _ from 'lodash'; import { TimeSeries, toDataFrame } from '@grafana/data';
import AppInsightsQuerystringBuilder from './app_insights_querystring_builder'; import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/ui';
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
import ResponseParser from './response_parser';
import { DataSourceInstanceSettings } from '@grafana/ui';
import { AzureDataSourceJsonData } from '../types';
import { BackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { IQService } from 'angular'; import _ from 'lodash';
import TimegrainConverter from '../time_grain_converter';
import { AzureDataSourceJsonData, AzureMonitorQuery } from '../types';
import ResponseParser from './response_parser';
export interface LogAnalyticsColumn { export interface LogAnalyticsColumn {
text: string; text: string;
...@@ -24,8 +24,7 @@ export default class AppInsightsDatasource { ...@@ -24,8 +24,7 @@ export default class AppInsightsDatasource {
constructor( constructor(
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>, instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
private backendSrv: BackendSrv, private backendSrv: BackendSrv,
private templateSrv: TemplateSrv, private templateSrv: TemplateSrv
private $q: IQService
) { ) {
this.id = instanceSettings.id; this.id = instanceSettings.id;
this.applicationId = instanceSettings.jsonData.appInsightsAppId; this.applicationId = instanceSettings.jsonData.appInsightsAppId;
...@@ -37,73 +36,82 @@ export default class AppInsightsDatasource { ...@@ -37,73 +36,82 @@ export default class AppInsightsDatasource {
return !!this.applicationId && this.applicationId.length > 0; return !!this.applicationId && this.applicationId.length > 0;
} }
query(options: any) { createRawQueryRequest(item: any, options: DataQueryRequest<AzureMonitorQuery>, target: AzureMonitorQuery) {
const queries = _.filter(options.targets, item => { if (item.xaxis && !item.timeColumn) {
return item.hide !== true; item.timeColumn = item.xaxis;
}).map(target => { }
const item = target.appInsights;
if (item.rawQuery) {
const querystringBuilder = new LogAnalyticsQuerystringBuilder(
this.templateSrv.replace(item.rawQueryString, options.scopedVars),
options,
'timestamp'
);
const generated = querystringBuilder.generate();
const url = `${this.baseUrl}/query?${generated.uriString}`; if (item.yaxis && !item.valueColumn) {
item.valueColumn = item.yaxis;
}
return { if (item.spliton && !item.segmentColumn) {
refId: target.refId, item.segmentColumn = item.spliton;
intervalMs: options.intervalMs, }
maxDataPoints: options.maxDataPoints,
datasourceId: this.id,
url: url,
format: options.format,
alias: item.alias,
query: generated.rawQuery,
xaxis: item.xaxis,
yaxis: item.yaxis,
spliton: item.spliton,
raw: true,
};
} else {
const querystringBuilder = new AppInsightsQuerystringBuilder(
options.range.from,
options.range.to,
options.interval
);
if (item.groupBy !== 'none') {
querystringBuilder.setGroupBy(this.templateSrv.replace(item.groupBy, options.scopedVars));
}
querystringBuilder.setAggregation(item.aggregation);
querystringBuilder.setInterval(
item.timeGrainType,
this.templateSrv.replace(item.timeGrain, options.scopedVars),
item.timeGrainUnit
);
querystringBuilder.setFilter(this.templateSrv.replace(item.filter || '')); return {
type: 'timeSeriesQuery',
raw: false,
appInsights: {
rawQuery: true,
rawQueryString: this.templateSrv.replace(item.rawQueryString, options.scopedVars),
timeColumn: item.timeColumn,
valueColumn: item.valueColumn,
segmentColumn: item.segmentColumn,
},
};
}
const url = `${this.baseUrl}/metrics/${this.templateSrv.replace( createMetricsRequest(item: any, options: DataQueryRequest<AzureMonitorQuery>, target: AzureMonitorQuery) {
encodeURI(item.metricName), // fix for timeGrainUnit which is a deprecated/removed field name
options.scopedVars if (item.timeGrainCount) {
)}?${querystringBuilder.generate()}`; item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrainCount, item.timeGrainUnit);
} else if (item.timeGrainUnit && item.timeGrain !== 'auto') {
item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
}
return { // migration for non-standard names
refId: target.refId, if (item.groupBy && !item.dimension) {
intervalMs: options.intervalMs, item.dimension = item.groupBy;
maxDataPoints: options.maxDataPoints, }
datasourceId: this.id,
url: url, if (item.filter && !item.dimensionFilter) {
format: options.format, item.dimensionFilter = item.filter;
alias: item.alias, }
xaxis: '',
yaxis: '', return {
spliton: '', type: 'timeSeriesQuery',
raw: false, raw: false,
}; appInsights: {
rawQuery: false,
timeGrain: this.templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars),
allowedTimeGrainsMs: item.allowedTimeGrainsMs,
metricName: this.templateSrv.replace(item.metricName, options.scopedVars),
aggregation: this.templateSrv.replace(item.aggregation, options.scopedVars),
dimension: this.templateSrv.replace(item.dimension, options.scopedVars),
dimensionFilter: this.templateSrv.replace(item.dimensionFilter, options.scopedVars),
alias: item.alias,
format: target.format,
},
};
}
async query(options: DataQueryRequest<AzureMonitorQuery>): Promise<DataQueryResponseData[]> {
const queries = _.filter(options.targets, item => {
return item.hide !== true;
}).map((target: AzureMonitorQuery) => {
const item = target.appInsights;
let query: any;
if (item.rawQuery) {
query = this.createRawQueryRequest(item, options, target);
} else {
query = this.createMetricsRequest(item, options, target);
} }
query.refId = target.refId;
query.intervalMs = options.intervalMs;
query.datasourceId = this.id;
query.queryType = 'Application Insights';
return query;
}); });
if (!queries || queries.length === 0) { if (!queries || queries.length === 0) {
...@@ -111,25 +119,42 @@ export default class AppInsightsDatasource { ...@@ -111,25 +119,42 @@ export default class AppInsightsDatasource {
return; return;
} }
const promises = this.doQueries(queries); const { data } = await this.backendSrv.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
},
});
return this.$q const result: DataQueryResponseData[] = [];
.all(promises) if (data.results) {
.then(results => { Object.values(data.results).forEach((queryRes: any) => {
return new ResponseParser(results).parseQueryResult(); if (queryRes.meta && queryRes.meta.columns) {
}) const columnNames = queryRes.meta.columns as string[];
.then(results => { this.logAnalyticsColumns[queryRes.refId] = _.map(columnNames, n => ({ text: n, value: n }));
const flattened: any[] = []; }
for (let i = 0; i < results.length; i++) { if (!queryRes.series) {
if (results[i].columnsForDropdown) { return;
this.logAnalyticsColumns[results[i].refId] = results[i].columnsForDropdown;
}
flattened.push(results[i]);
} }
return flattened; queryRes.series.forEach((series: any) => {
const timeSerie: TimeSeries = {
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
};
result.push(toDataFrame(timeSerie));
});
}); });
return result;
}
return Promise.resolve([]);
} }
doQueries(queries: any) { doQueries(queries: any) {
......
import AppInsightsQuerystringBuilder from './app_insights_querystring_builder';
import { toUtc } from '@grafana/data';
describe('AppInsightsQuerystringBuilder', () => {
let builder: AppInsightsQuerystringBuilder;
beforeEach(() => {
builder = new AppInsightsQuerystringBuilder(toUtc('2017-08-22 06:00'), toUtc('2017-08-22 07:00'), '1h');
});
describe('with only from/to date range', () => {
it('should always add datetime filtering to the querystring', () => {
const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z`;
expect(builder.generate()).toEqual(querystring);
});
});
describe('with from/to date range and aggregation type', () => {
beforeEach(() => {
builder.setAggregation('avg');
});
it('should add datetime filtering and aggregation to the querystring', () => {
const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&aggregation=avg`;
expect(builder.generate()).toEqual(querystring);
});
});
describe('with from/to date range and group by segment', () => {
beforeEach(() => {
builder.setGroupBy('client/city');
});
it('should add datetime filtering and segment to the querystring', () => {
const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&segment=client/city`;
expect(builder.generate()).toEqual(querystring);
});
});
describe('with from/to date range and specific group by interval', () => {
beforeEach(() => {
builder.setInterval('specific', 1, 'hour');
});
it('should add datetime filtering and interval to the querystring', () => {
const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&interval=PT1H`;
expect(builder.generate()).toEqual(querystring);
});
});
describe('with from/to date range and auto group by interval', () => {
beforeEach(() => {
builder.setInterval('auto', '', '');
});
it('should add datetime filtering and interval to the querystring', () => {
const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&interval=PT1H`;
expect(builder.generate()).toEqual(querystring);
});
});
describe('with filter', () => {
beforeEach(() => {
builder.setFilter(`client/city eq 'Boydton'`);
});
it('should add datetime filtering and interval to the querystring', () => {
const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&filter=client/city eq 'Boydton'`;
expect(builder.generate()).toEqual(querystring);
});
});
});
import TimeGrainConverter from '../time_grain_converter';
export default class AppInsightsQuerystringBuilder {
aggregation = '';
groupBy = '';
timeGrainType = '';
timeGrain = '';
timeGrainUnit = '';
filter = '';
constructor(private from: any, private to: any, public grafanaInterval: any) {}
setAggregation(aggregation: string) {
this.aggregation = aggregation;
}
setGroupBy(groupBy: string) {
this.groupBy = groupBy;
}
setInterval(timeGrainType: string, timeGrain: any, timeGrainUnit: string) {
this.timeGrainType = timeGrainType;
this.timeGrain = timeGrain;
this.timeGrainUnit = timeGrainUnit;
}
setFilter(filter: string) {
this.filter = filter;
}
generate() {
let querystring = `timespan=${this.from.utc().format()}/${this.to.utc().format()}`;
if (this.aggregation && this.aggregation.length > 0) {
querystring += `&aggregation=${this.aggregation}`;
}
if (this.groupBy && this.groupBy.length > 0) {
querystring += `&segment=${this.groupBy}`;
}
if (this.timeGrainType === 'specific' && this.timeGrain && this.timeGrainUnit) {
querystring += `&interval=${TimeGrainConverter.createISO8601Duration(this.timeGrain, this.timeGrainUnit)}`;
}
if (this.timeGrainType === 'auto') {
querystring += `&interval=${TimeGrainConverter.createISO8601DurationFromInterval(this.grafanaInterval)}`;
}
if (this.filter) {
querystring += `&filter=${this.filter}`;
}
return querystring;
}
}
...@@ -22,12 +22,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa ...@@ -22,12 +22,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
) { ) {
super(instanceSettings); super(instanceSettings);
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.backendSrv, this.templateSrv); this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.backendSrv, this.templateSrv);
this.appInsightsDatasource = new AppInsightsDatasource( this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.backendSrv, this.templateSrv);
instanceSettings,
this.backendSrv,
this.templateSrv,
this.$q
);
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource( this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(
instanceSettings, instanceSettings,
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
get-options="ctrl.getMetricNamespaces($query)" on-change="ctrl.onMetricNamespacesChange()" css-class="min-width-12"> get-options="ctrl.getMetricNamespaces($query)" on-change="ctrl.onMetricNamespacesChange()" css-class="min-width-12">
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-9">Metric</label> <label class="gf-form-label query-keyword width-9">Metric</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.metricName" allow-custom="true" lookup-text="true" <gf-form-dropdown model="ctrl.target.azureMonitor.metricName" allow-custom="true" lookup-text="true"
get-options="ctrl.getMetricNames($query)" on-change="ctrl.onMetricNameChange()" css-class="min-width-12"> get-options="ctrl.getMetricNames($query)" on-change="ctrl.onMetricNameChange()" css-class="min-width-12">
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
</div> </div>
</div> </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">Time Grain</label> <label class="gf-form-label query-keyword width-9">Time Grain</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper"> <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper">
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
</div> </div>
<div class="gf-form" ng-show="ctrl.target.azureMonitor.timeGrain.trim() === 'auto'"> <div class="gf-form" ng-show="ctrl.target.azureMonitor.timeGrain.trim() === 'auto'">
<label class="gf-form-label">Auto Interval</label> <label class="gf-form-label">Auto Interval</label>
<label class="gf-form-label">{{ctrl.getAutoInterval()}}</label> <label class="gf-form-label">{{ctrl.getAzureMonitorAutoInterval()}}</label>
</div> </div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div> <div class="gf-form-label gf-form-label--grow"></div>
...@@ -238,19 +238,19 @@ ...@@ -238,19 +238,19 @@
<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">Group By</label> <label class="gf-form-label query-keyword width-9">Group By</label>
<gf-form-dropdown allow-custom="true" ng-hide="ctrl.target.appInsights.groupBy !== 'none'" model="ctrl.target.appInsights.groupBy" <gf-form-dropdown allow-custom="true" ng-hide="ctrl.target.appInsights.dimension !== 'none'" model="ctrl.target.appInsights.dimension"
lookup-text="true" get-options="ctrl.getAppInsightsGroupBySegments($query)" on-change="ctrl.refresh()" lookup-text="true" get-options="ctrl.getAppInsightsGroupBySegments($query)" on-change="ctrl.refresh()"
css-class="min-width-20"> css-class="min-width-20">
</gf-form-dropdown> </gf-form-dropdown>
<label class="gf-form-label min-width-20 pointer" ng-hide="ctrl.target.appInsights.groupBy === 'none'" <label class="gf-form-label min-width-20 pointer" ng-hide="ctrl.target.appInsights.dimension === 'none'"
ng-click="ctrl.resetAppInsightsGroupBy()">{{ctrl.target.appInsights.groupBy}} ng-click="ctrl.resetAppInsightsGroupBy()">{{ctrl.target.appInsights.dimension}}
<i class="fa fa-remove"></i> <i class="fa fa-remove"></i>
</label> </label>
</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">Filter</label> <label class="gf-form-label query-keyword width-9">Filter</label>
<input type="text" class="gf-form-input width-17" ng-model="ctrl.target.appInsights.filter" spellcheck="false" <input type="text" class="gf-form-input width-17" ng-model="ctrl.target.appInsights.dimensionFilter" spellcheck="false"
placeholder="your/groupby eq 'a_value'" ng-blur="ctrl.refresh()"> placeholder="your/groupby eq 'a_value'" ng-blur="ctrl.refresh()">
</div> </div>
</div> </div>
...@@ -258,7 +258,6 @@ ...@@ -258,7 +258,6 @@
<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"> <div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-9">Time Grain</label> <label class="gf-form-label query-keyword width-9">Time Grain</label>
...@@ -268,17 +267,17 @@ ...@@ -268,17 +267,17 @@
</div> </div>
</div> </div>
<div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType === 'auto' || ctrl.target.appInsights.timeGrainType === 'none'"> <div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType === 'auto' || ctrl.target.appInsights.timeGrainType === 'none'">
<input type="text" class="gf-form-input width-3" ng-model="ctrl.target.appInsights.timeGrain" spellcheck="false" <input type="text" class="gf-form-input width-3" ng-model="ctrl.target.appInsights.timeGrainCount" spellcheck="false"
placeholder="" ng-blur="ctrl.refresh()"> placeholder="" ng-blur="ctrl.updateAppInsightsTimeGrain()">
</div> </div>
<div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType === 'auto' || ctrl.target.appInsights.timeGrainType === 'none'"> <div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType === 'auto' || ctrl.target.appInsights.timeGrainType === 'none'">
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper"> <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper">
<select class="gf-form-input" ng-model="ctrl.target.appInsights.timeGrainUnit" ng-options="f as f for f in ['minute', 'hour', 'day', 'month', 'year']" <select class="gf-form-input" ng-model="ctrl.target.appInsights.timeGrainUnit" ng-options="f as f for f in ['minute', 'hour', 'day', 'month', 'year']"
ng-change="ctrl.refresh()"></select> ng-change="ctrl.updateAppInsightsTimeGrain()"></select>
</div>
</div> </div>
</div>
<div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType !== 'auto'"> <div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType !== 'auto'">
<label class="gf-form-label">Auto Interval</label> <label class="gf-form-label">Auto Interval</label>
<label class="gf-form-label">{{ctrl.getAppInsightsAutoInterval()}}</label> <label class="gf-form-label">{{ctrl.getAppInsightsAutoInterval()}}</label>
</div> </div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
...@@ -291,10 +290,9 @@ ...@@ -291,10 +290,9 @@
<input type="text" class="gf-form-input width-30" ng-model="ctrl.target.appInsights.alias" spellcheck="false" <input type="text" class="gf-form-input width-30" ng-model="ctrl.target.appInsights.alias" spellcheck="false"
placeholder="alias patterns (see help for more info)" ng-blur="ctrl.refresh()"> placeholder="alias patterns (see help for more info)" ng-blur="ctrl.refresh()">
</div> </div>
</div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<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>
<div ng-show="ctrl.target.appInsights.rawQuery"> <div ng-show="ctrl.target.appInsights.rawQuery">
...@@ -316,13 +314,13 @@ ...@@ -316,13 +314,13 @@
<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">X-axis</label> <label class="gf-form-label query-keyword width-9">X-axis</label>
<gf-form-dropdown model="ctrl.target.appInsights.xaxis" allow-custom="true" placeholder="eg. 'timestamp'" <gf-form-dropdown model="ctrl.target.appInsights.timeColumn" allow-custom="true" placeholder="eg. 'timestamp'"
get-options="ctrl.getAppInsightsColumns($query)" on-change="ctrl.onAppInsightsColumnChange()" css-class="min-width-20"> get-options="ctrl.getAppInsightsColumns($query)" on-change="ctrl.onAppInsightsColumnChange()" css-class="min-width-20">
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-9">Y-axis(es)</label> <label class="gf-form-label query-keyword width-9">Y-axis</label>
<gf-form-dropdown model="ctrl.target.appInsights.yaxis" allow-custom="true" get-options="ctrl.getAppInsightsColumns($query)" <gf-form-dropdown model="ctrl.target.appInsights.valueColumn" allow-custom="true" get-options="ctrl.getAppInsightsColumns($query)"
on-change="ctrl.onAppInsightsColumnChange()" css-class="min-width-20"> on-change="ctrl.onAppInsightsColumnChange()" css-class="min-width-20">
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
...@@ -333,7 +331,7 @@ ...@@ -333,7 +331,7 @@
<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">Split On</label> <label class="gf-form-label query-keyword width-9">Split On</label>
<gf-form-dropdown model="ctrl.target.appInsights.spliton" allow-custom="true" get-options="ctrl.getAppInsightsColumns($query)" <gf-form-dropdown model="ctrl.target.appInsights.segmentColumn" allow-custom="true" get-options="ctrl.getAppInsightsColumns($query)"
on-change="ctrl.onAppInsightsColumnChange()" css-class="min-width-20"> on-change="ctrl.onAppInsightsColumnChange()" css-class="min-width-20">
</gf-form-dropdown> </gf-form-dropdown>
</div> </div>
......
...@@ -41,7 +41,7 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -41,7 +41,7 @@ describe('AzureMonitorQueryCtrl', () => {
expect(queryCtrl.target.azureMonitor.resourceName).toBe('select'); expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select'); expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
expect(queryCtrl.target.azureMonitor.metricName).toBe('select'); expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
expect(queryCtrl.target.appInsights.groupBy).toBe('none'); expect(queryCtrl.target.appInsights.dimension).toBe('none');
}); });
}); });
...@@ -239,6 +239,35 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -239,6 +239,35 @@ describe('AzureMonitorQueryCtrl', () => {
}); });
describe('and query type is Application Insights', () => { describe('and query type is Application Insights', () => {
describe('and target is in old format', () => {
it('data is migrated', () => {
queryCtrl.target.appInsights.xaxis = 'sample-x';
queryCtrl.target.appInsights.yaxis = 'sample-y';
queryCtrl.target.appInsights.spliton = 'sample-split';
queryCtrl.target.appInsights.groupBy = 'sample-group';
queryCtrl.target.appInsights.groupByOptions = ['sample-group-1', 'sample-group-2'];
queryCtrl.target.appInsights.filter = 'sample-filter';
queryCtrl.target.appInsights.metricName = 'sample-metric';
queryCtrl.migrateApplicationInsightsKeys();
expect(queryCtrl.target.appInsights.xaxis).toBeUndefined();
expect(queryCtrl.target.appInsights.yaxis).toBeUndefined();
expect(queryCtrl.target.appInsights.spliton).toBeUndefined();
expect(queryCtrl.target.appInsights.groupBy).toBeUndefined();
expect(queryCtrl.target.appInsights.groupByOptions).toBeUndefined();
expect(queryCtrl.target.appInsights.filter).toBeUndefined();
expect(queryCtrl.target.appInsights.timeColumn).toBe('sample-x');
expect(queryCtrl.target.appInsights.valueColumn).toBe('sample-y');
expect(queryCtrl.target.appInsights.segmentColumn).toBe('sample-split');
expect(queryCtrl.target.appInsights.dimension).toBe('sample-group');
expect(queryCtrl.target.appInsights.dimensions).toEqual(['sample-group-1', 'sample-group-2']);
expect(queryCtrl.target.appInsights.dimensionFilter).toBe('sample-filter');
expect(queryCtrl.target.appInsights.metricName).toBe('sample-metric');
});
});
describe('when getOptions for the Metric Names dropdown is called', () => { describe('when getOptions for the Metric Names dropdown is called', () => {
const response = [{ text: 'metric1', value: 'metric1' }, { text: 'metric2', value: 'metric2' }]; const response = [{ text: 'metric1', value: 'metric1' }, { text: 'metric2', value: 'metric2' }];
...@@ -259,7 +288,7 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -259,7 +288,7 @@ describe('AzureMonitorQueryCtrl', () => {
describe('when getOptions for the GroupBy segments dropdown is called', () => { describe('when getOptions for the GroupBy segments dropdown is called', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.appInsights.groupByOptions = ['opt1', 'opt2']; queryCtrl.target.appInsights.dimensions = ['opt1', 'opt2'];
}); });
it('should return a list of GroupBy segments', () => { it('should return a list of GroupBy segments', () => {
...@@ -291,8 +320,8 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -291,8 +320,8 @@ describe('AzureMonitorQueryCtrl', () => {
expect(queryCtrl.target.appInsights.aggregation).toBe('avg'); expect(queryCtrl.target.appInsights.aggregation).toBe('avg');
expect(queryCtrl.target.appInsights.aggOptions).toContain('avg'); expect(queryCtrl.target.appInsights.aggOptions).toContain('avg');
expect(queryCtrl.target.appInsights.aggOptions).toContain('sum'); expect(queryCtrl.target.appInsights.aggOptions).toContain('sum');
expect(queryCtrl.target.appInsights.groupByOptions).toContain('client/os'); expect(queryCtrl.target.appInsights.dimensions).toContain('client/os');
expect(queryCtrl.target.appInsights.groupByOptions).toContain('client/city'); expect(queryCtrl.target.appInsights.dimensions).toContain('client/city');
}); });
}); });
}); });
......
...@@ -32,13 +32,13 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -32,13 +32,13 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
dimensionFilter: string; dimensionFilter: string;
timeGrain: string; timeGrain: string;
timeGrainUnit: string; timeGrainUnit: string;
timeGrains: Array<{ text: string; value: string }>;
allowedTimeGrainsMs: number[]; allowedTimeGrainsMs: number[];
dimensions: any[]; dimensions: any[];
dimension: any; dimension: any;
top: string; top: string;
aggregation: string; aggregation: string;
aggOptions: string[]; aggOptions: string[];
timeGrains: Array<{ text: string; value: string }>;
}; };
azureLogAnalytics: { azureLogAnalytics: {
query: string; query: string;
...@@ -46,19 +46,28 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -46,19 +46,28 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
workspace: string; workspace: string;
}; };
appInsights: { appInsights: {
metricName: string;
rawQuery: boolean; rawQuery: boolean;
rawQueryString: string; // metric style query when rawQuery == false
groupBy: string; metricName: string;
timeGrainType: string; dimension: any;
xaxis: string; dimensionFilter: string;
yaxis: string; dimensions: string[];
spliton: string;
aggOptions: string[]; aggOptions: string[];
aggregation: string; aggregation: string;
groupByOptions: string[];
timeGrainType: string;
timeGrainCount: string;
timeGrainUnit: string; timeGrainUnit: string;
timeGrain: string; timeGrain: string;
timeGrains: Array<{ text: string; value: string }>;
allowedTimeGrainsMs: number[];
// query style query when rawQuery == true
rawQueryString: string;
timeColumn: string;
valueColumn: string;
segmentColumn: string;
}; };
}; };
...@@ -73,6 +82,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -73,6 +82,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
dimensionFilter: '*', dimensionFilter: '*',
timeGrain: 'auto', timeGrain: 'auto',
top: '10', top: '10',
aggOptions: [] as string[],
timeGrains: [] as string[],
}, },
azureLogAnalytics: { azureLogAnalytics: {
query: [ query: [
...@@ -96,11 +107,10 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -96,11 +107,10 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
metricName: this.defaultDropdownValue, metricName: this.defaultDropdownValue,
rawQuery: false, rawQuery: false,
rawQueryString: '', rawQueryString: '',
groupBy: 'none', dimension: 'none',
timeGrainType: 'auto', timeGrain: 'auto',
xaxis: 'timestamp', timeColumn: 'timestamp',
yaxis: '', valueColumn: '',
spliton: '',
}, },
}; };
...@@ -124,6 +134,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -124,6 +134,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.migrateToDefaultNamespace(); this.migrateToDefaultNamespace();
this.migrateApplicationInsightsKeys();
this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
...@@ -184,6 +196,23 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -184,6 +196,23 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.onMetricNameChange(); this.onMetricNameChange();
} }
if (this.target.appInsights.timeGrainUnit) {
if (this.target.appInsights.timeGrain !== 'auto') {
if (this.target.appInsights.timeGrainCount) {
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration(
this.target.appInsights.timeGrainCount,
this.target.appInsights.timeGrainUnit
);
} else {
this.target.appInsights.timeGrainCount = this.target.appInsights.timeGrain;
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration(
this.target.appInsights.timeGrain,
this.target.appInsights.timeGrainUnit
);
}
}
}
if ( if (
this.target.azureMonitor.timeGrains && this.target.azureMonitor.timeGrains &&
this.target.azureMonitor.timeGrains.length > 0 && this.target.azureMonitor.timeGrains.length > 0 &&
...@@ -191,6 +220,14 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -191,6 +220,14 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
) { ) {
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.azureMonitor.timeGrains); this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.azureMonitor.timeGrains);
} }
if (
this.target.appInsights.timeGrains &&
this.target.appInsights.timeGrains.length > 0 &&
(!this.target.appInsights.allowedTimeGrainsMs || this.target.appInsights.allowedTimeGrainsMs.length === 0)
) {
this.target.appInsights.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.appInsights.timeGrains);
}
} }
migrateToFromTimes() { migrateToFromTimes() {
...@@ -210,6 +247,27 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -210,6 +247,27 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.target.azureMonitor.metricNamespace = this.target.azureMonitor.metricDefinition; this.target.azureMonitor.metricNamespace = this.target.azureMonitor.metricDefinition;
} }
migrateApplicationInsightsKeys(): void {
const appInsights = this.target.appInsights as any;
// Migrate old app insights data keys to match other datasources
const mappings = {
xaxis: 'timeColumn',
yaxis: 'valueColumn',
spliton: 'segmentColumn',
groupBy: 'dimension',
groupByOptions: 'dimensions',
filter: 'dimensionFilter',
} as { [old: string]: string };
for (const old in mappings) {
if (appInsights[old]) {
appInsights[mappings[old]] = appInsights[old];
delete appInsights[old];
}
}
}
replace(variable: string) { replace(variable: string) {
return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars); return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars);
} }
...@@ -424,6 +482,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -424,6 +482,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
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();
}) })
.catch(this.handleQueryCtrlError.bind(this)); .catch(this.handleQueryCtrlError.bind(this));
...@@ -439,19 +498,34 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -439,19 +498,34 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return allowedTimeGrainsMs; return allowedTimeGrainsMs;
} }
getAutoInterval() { generateAutoUnits(timeGrain: string, timeGrains: Array<{ value: string }>) {
if (this.target.azureMonitor.timeGrain === 'auto') { if (timeGrain === 'auto') {
return TimegrainConverter.findClosestTimeGrain( return TimegrainConverter.findClosestTimeGrain(
this.templateSrv.getBuiltInIntervalValue(), this.templateSrv.getBuiltInIntervalValue(),
_.map(this.target.azureMonitor.timeGrains, o => _.map(timeGrains, o => TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)) || [
TimegrainConverter.createKbnUnitFromISO8601Duration(o.value) '1m',
) || ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'] '5m',
'15m',
'30m',
'1h',
'6h',
'12h',
'1d',
]
); );
} }
return ''; return '';
} }
getAzureMonitorAutoInterval() {
return this.generateAutoUnits(this.target.azureMonitor.timeGrain, this.target.azureMonitor.timeGrains);
}
getApplicationInsightAutoInterval() {
return this.generateAutoUnits(this.target.appInsights.timeGrain, this.target.appInsights.timeGrains);
}
/* Azure Log Analytics */ /* Azure Log Analytics */
getWorkspaces = () => { getWorkspaces = () => {
...@@ -521,7 +595,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -521,7 +595,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
.getAppInsightsMetricMetadata(this.replace(this.target.appInsights.metricName)) .getAppInsightsMetricMetadata(this.replace(this.target.appInsights.metricName))
.then((aggData: { supportedAggTypes: string[]; supportedGroupBy: string[]; primaryAggType: string }) => { .then((aggData: { supportedAggTypes: string[]; supportedGroupBy: string[]; primaryAggType: string }) => {
this.target.appInsights.aggOptions = aggData.supportedAggTypes; this.target.appInsights.aggOptions = aggData.supportedAggTypes;
this.target.appInsights.groupByOptions = aggData.supportedGroupBy; this.target.appInsights.dimensions = aggData.supportedGroupBy;
this.target.appInsights.aggregation = aggData.primaryAggType; this.target.appInsights.aggregation = aggData.primaryAggType;
return this.refresh(); return this.refresh();
}) })
...@@ -541,27 +615,41 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { ...@@ -541,27 +615,41 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}; };
getAppInsightsGroupBySegments(query: any) { getAppInsightsGroupBySegments(query: any) {
return _.map(this.target.appInsights.groupByOptions, option => { return _.map(this.target.appInsights.dimensions, (option: string) => {
return { text: option, value: option }; return { text: option, value: option };
}); });
} }
resetAppInsightsGroupBy() { resetAppInsightsGroupBy() {
this.target.appInsights.groupBy = 'none'; this.target.appInsights.dimension = 'none';
this.refresh(); this.refresh();
} }
toggleEditorMode() {
this.target.appInsights.rawQuery = !this.target.appInsights.rawQuery;
}
updateTimeGrainType() { updateTimeGrainType() {
if (this.target.appInsights.timeGrainType === 'specific') { if (this.target.appInsights.timeGrainType === 'specific') {
this.target.appInsights.timeGrain = '1'; this.target.appInsights.timeGrainCount = '1';
this.target.appInsights.timeGrainUnit = 'minute'; this.target.appInsights.timeGrainUnit = 'minute';
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration(
this.target.appInsights.timeGrainCount,
this.target.appInsights.timeGrainUnit
);
} else { } else {
this.target.appInsights.timeGrain = ''; this.target.appInsights.timeGrainCount = '';
this.target.appInsights.timeGrainUnit = '';
} }
this.refresh();
} }
toggleEditorMode() { updateAppInsightsTimeGrain() {
this.target.appInsights.rawQuery = !this.target.appInsights.rawQuery; if (this.target.appInsights.timeGrainUnit && this.target.appInsights.timeGrainCount) {
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration(
this.target.appInsights.timeGrainCount,
this.target.appInsights.timeGrainUnit
);
}
this.refresh();
} }
} }
import { DataQuery, DataSourceJsonData } from '@grafana/ui'; import { DataQuery, DataSourceJsonData } from '@grafana/ui';
export interface AzureMonitorQuery extends DataQuery { export interface AzureMonitorQuery extends DataQuery {
refId: string;
format: string; format: string;
subscription: string; subscription: string;
azureMonitor: AzureMetricQuery; azureMonitor: AzureMetricQuery;
azureLogAnalytics: AzureLogsQuery; azureLogAnalytics: AzureLogsQuery;
// appInsights: any; appInsights: ApplicationInsightsQuery;
} }
export interface AzureDataSourceJsonData extends DataSourceJsonData { export interface AzureDataSourceJsonData extends DataSourceJsonData {
...@@ -35,7 +36,6 @@ export interface AzureMetricQuery { ...@@ -35,7 +36,6 @@ export interface AzureMetricQuery {
metricName: string; metricName: string;
timeGrainUnit: string; timeGrainUnit: string;
timeGrain: string; timeGrain: string;
timeGrains: string[];
allowedTimeGrainsMs: number[]; allowedTimeGrainsMs: number[];
aggregation: string; aggregation: string;
dimension: string; dimension: string;
...@@ -50,6 +50,19 @@ export interface AzureLogsQuery { ...@@ -50,6 +50,19 @@ export interface AzureLogsQuery {
workspace: string; workspace: string;
} }
export interface ApplicationInsightsQuery {
rawQuery: boolean;
rawQueryString: any;
metricName: string;
timeGrainUnit: string;
timeGrain: string;
allowedTimeGrainsMs: number[];
aggregation: string;
dimension: string;
dimensionFilter: string;
alias: string;
}
// Azure Monitor API Types // Azure Monitor API Types
export interface AzureMonitorMetricDefinitionsResponse { export interface AzureMonitorMetricDefinitionsResponse {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment