Commit bc9c5338 by Kyle Brandt Committed by GitHub

Azure: Split insights into two services (#25410)

Azure Application Insights Analytics is no longer accessed by the edit button from within the Application Insights service. Instead, there is now an Insights Analytics option in the Service drop down.

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
parent af0c7372
......@@ -68,7 +68,9 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
const times: number[] = [];
const values: TimeSeriesValue[] = [];
for (const point of timeSeries.datapoints) {
// Sometimes the points are sent as datapoints
const points = timeSeries.datapoints || (timeSeries as any).points;
for (const point of points) {
values.push(point[0]);
times.push(point[1] as number);
}
......@@ -96,7 +98,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
}
return {
name: timeSeries.target,
name: timeSeries.target || (timeSeries as any).name,
refId: timeSeries.refId,
meta: timeSeries.meta,
fields,
......@@ -293,7 +295,7 @@ export function toDataFrame(data: any): DataFrame {
return convertJSONDocumentDataToDataFrame(data);
}
if (data.hasOwnProperty('datapoints')) {
if (data.hasOwnProperty('datapoints') || data.hasOwnProperty('points')) {
return convertTimeSeriesToDataFrame(data);
}
......
......@@ -5,14 +5,17 @@ import {
KeyValue,
LoadingState,
DataQueryError,
TimeSeries,
TableData,
toDataFrame,
} from '@grafana/data';
interface DataResponse {
error?: string;
refId?: string;
dataframes?: string[];
// series: null,
// tables: null,
series?: TimeSeries[];
tables?: TableData[];
}
/**
......@@ -35,6 +38,24 @@ export function toDataQueryResponse(res: any): DataQueryResponse {
}
}
if (dr.series && dr.series.length) {
for (const s of dr.series) {
if (!s.refId) {
s.refId = refId;
}
rsp.data.push(toDataFrame(s));
}
}
if (dr.tables && dr.tables.length) {
for (const s of dr.tables) {
if (!s.refId) {
s.refId = refId;
}
rsp.data.push(toDataFrame(s));
}
}
if (dr.dataframes) {
for (const b64 of dr.dataframes) {
try {
......
......@@ -92,72 +92,43 @@ func (e *ApplicationInsightsDatasource) buildQueries(queries []*tsdb.Query, time
insightsJSONModel := queryJSONModel.AppInsights
azlog.Debug("Application Insights", "target", insightsJSONModel)
if insightsJSONModel.RawQuery == nil {
return nil, fmt.Errorf("missing the 'rawQuery' property")
}
if *insightsJSONModel.RawQuery {
var rawQueryString string
if insightsJSONModel.RawQueryString == "" {
return nil, errors.New("rawQuery requires rawQueryString")
}
rawQueryString, err := KqlInterpolate(query, timeRange, insightsJSONModel.RawQueryString)
azureURL := fmt.Sprintf("metrics/%s", insightsJSONModel.MetricName)
timeGrain := insightsJSONModel.TimeGrain
timeGrains := insightsJSONModel.AllowedTimeGrainsMs
if timeGrain == "auto" {
timeGrain, err = setAutoTimeGrain(query.IntervalMs, timeGrains)
if err != nil {
return nil, err
}
}
params := url.Values{}
params.Add("query", rawQueryString)
applicationInsightsQueries = append(applicationInsightsQueries, &ApplicationInsightsQuery{
RefID: query.RefId,
IsRaw: true,
ApiURL: "query",
Params: params,
TimeColumnName: insightsJSONModel.TimeColumn,
ValueColumnName: insightsJSONModel.ValueColumn,
SegmentColumnName: insightsJSONModel.SegmentColumn,
Target: params.Encode(),
})
} else {
azureURL := fmt.Sprintf("metrics/%s", insightsJSONModel.MetricName)
timeGrain := insightsJSONModel.TimeGrain
timeGrains := insightsJSONModel.AllowedTimeGrainsMs
if timeGrain == "auto" {
timeGrain, err = setAutoTimeGrain(query.IntervalMs, timeGrains)
if err != nil {
return nil, err
}
}
params := url.Values{}
params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339)))
if timeGrain != "none" {
params.Add("interval", timeGrain)
}
params.Add("aggregation", insightsJSONModel.Aggregation)
params := url.Values{}
params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339)))
if timeGrain != "none" {
params.Add("interval", timeGrain)
}
params.Add("aggregation", insightsJSONModel.Aggregation)
dimension := strings.TrimSpace(insightsJSONModel.Dimension)
// Azure Monitor combines this and the following logic such that if dimensionFilter, must also Dimension, should that be done here as well?
if dimension != "" && !strings.EqualFold(dimension, "none") {
params.Add("segment", dimension)
}
dimension := strings.TrimSpace(insightsJSONModel.Dimension)
// Azure Monitor combines this and the following logic such that if dimensionFilter, must also Dimension, should that be done here as well?
if dimension != "" && !strings.EqualFold(dimension, "none") {
params.Add("segment", dimension)
}
dimensionFilter := strings.TrimSpace(insightsJSONModel.DimensionFilter)
if dimensionFilter != "" {
params.Add("filter", dimensionFilter)
}
dimensionFilter := strings.TrimSpace(insightsJSONModel.DimensionFilter)
if dimensionFilter != "" {
params.Add("filter", dimensionFilter)
}
applicationInsightsQueries = append(applicationInsightsQueries, &ApplicationInsightsQuery{
RefID: query.RefId,
IsRaw: false,
ApiURL: azureURL,
Params: params,
Alias: insightsJSONModel.Alias,
Target: params.Encode(),
})
applicationInsightsQueries = append(applicationInsightsQueries, &ApplicationInsightsQuery{
RefID: query.RefId,
IsRaw: false,
ApiURL: azureURL,
Params: params,
Alias: insightsJSONModel.Alias,
Target: params.Encode(),
})
}
}
return applicationInsightsQueries, nil
......@@ -209,18 +180,10 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query
return nil, fmt.Errorf("Request failed status: %v", res.Status)
}
if query.IsRaw {
queryResult.Series, queryResult.Meta, err = e.parseTimeSeriesFromQuery(body, query)
if err != nil {
queryResult.Error = err
return queryResult, nil
}
} else {
queryResult.Series, err = e.parseTimeSeriesFromMetrics(body, query)
if err != nil {
queryResult.Error = err
return queryResult, nil
}
queryResult.Series, err = e.parseTimeSeriesFromMetrics(body, query)
if err != nil {
queryResult.Error = err
return queryResult, nil
}
return queryResult, nil
......@@ -280,96 +243,6 @@ func (e *ApplicationInsightsDatasource) getPluginRoute(plugin *plugins.DataSourc
return pluginRoute, pluginRouteName, nil
}
func (e *ApplicationInsightsDatasource) parseTimeSeriesFromQuery(body []byte, query *ApplicationInsightsQuery) (tsdb.TimeSeriesSlice, *simplejson.Json, error) {
var data ApplicationInsightsQueryResponse
err := json.Unmarshal(body, &data)
if err != nil {
azlog.Debug("Failed to unmarshal Application Insights response", "error", err, "body", string(body))
return nil, nil, err
}
type Metadata struct {
Columns []string `json:"columns"`
}
meta := Metadata{}
for _, t := range data.Tables {
if t.Name == "PrimaryResult" {
timeIndex, valueIndex, segmentIndex := -1, -1, -1
meta.Columns = make([]string, 0)
for i, v := range t.Columns {
meta.Columns = append(meta.Columns, v.Name)
switch v.Name {
case query.TimeColumnName:
timeIndex = i
case query.ValueColumnName:
valueIndex = i
case query.SegmentColumnName:
segmentIndex = i
}
}
if timeIndex == -1 {
azlog.Info("no time column specified, returning existing columns, no data")
return nil, simplejson.NewFromAny(meta), nil
}
if valueIndex == -1 {
azlog.Info("no value column specified, returning existing columns, no data")
return nil, simplejson.NewFromAny(meta), nil
}
var getPoints func([]interface{}) *tsdb.TimeSeriesPoints
slice := tsdb.TimeSeriesSlice{}
if segmentIndex == -1 {
legend := formatApplicationInsightsLegendKey(query.Alias, query.ValueColumnName, "", "")
series := tsdb.NewTimeSeries(legend, []tsdb.TimePoint{})
slice = append(slice, series)
getPoints = func(row []interface{}) *tsdb.TimeSeriesPoints {
return &series.Points
}
} else {
mapping := map[string]*tsdb.TimeSeriesPoints{}
getPoints = func(row []interface{}) *tsdb.TimeSeriesPoints {
segment := fmt.Sprintf("%v", row[segmentIndex])
if points, ok := mapping[segment]; ok {
return points
}
legend := formatApplicationInsightsLegendKey(query.Alias, query.ValueColumnName, query.SegmentColumnName, segment)
series := tsdb.NewTimeSeries(legend, []tsdb.TimePoint{})
slice = append(slice, series)
mapping[segment] = &series.Points
return &series.Points
}
}
for _, r := range t.Rows {
timeStr, ok := r[timeIndex].(string)
if !ok {
return nil, simplejson.NewFromAny(meta), errors.New("invalid time value")
}
timeValue, err := time.Parse(time.RFC3339Nano, timeStr)
if err != nil {
return nil, simplejson.NewFromAny(meta), err
}
var value float64
if value, err = getFloat(r[valueIndex]); err != nil {
return nil, simplejson.NewFromAny(meta), err
}
points := getPoints(r)
*points = append(*points, tsdb.NewTimePoint(null.FloatFrom(value), float64(timeValue.Unix()*1000)))
}
return slice, simplejson.NewFromAny(meta), nil
}
}
return nil, nil, errors.New("could not find table")
}
func (e *ApplicationInsightsDatasource) parseTimeSeriesFromMetrics(body []byte, query *ApplicationInsightsQuery) (tsdb.TimeSeriesSlice, error) {
doc, err := simplejson.NewJson(body)
if err != nil {
......
......@@ -142,98 +142,6 @@ func TestApplicationInsightsDatasource(t *testing.T) {
So(queries[0].Target, ShouldEqual, "aggregation=Average&interval=PT1M&timespan=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() {
......
......@@ -51,6 +51,7 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
var azureMonitorQueries []*tsdb.Query
var applicationInsightsQueries []*tsdb.Query
var azureLogAnalyticsQueries []*tsdb.Query
var insightsAnalyticsQueries []*tsdb.Query
for _, query := range tsdbQuery.Queries {
queryType := query.Model.Get("queryType").MustString("")
......@@ -62,6 +63,8 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
applicationInsightsQueries = append(applicationInsightsQueries, query)
case "Azure Log Analytics":
azureLogAnalyticsQueries = append(azureLogAnalyticsQueries, query)
case "Insights Analytics":
insightsAnalyticsQueries = append(insightsAnalyticsQueries, query)
default:
return nil, fmt.Errorf("Alerting not supported for %s", queryType)
}
......@@ -82,6 +85,11 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
dsInfo: e.dsInfo,
}
iaDatasource := &InsightsAnalyticsDatasource{
httpClient: e.httpClient,
dsInfo: e.dsInfo,
}
azResult, err := azDatasource.executeTimeSeriesQuery(ctx, azureMonitorQueries, tsdbQuery.TimeRange)
if err != nil {
return nil, err
......@@ -97,6 +105,11 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
return nil, err
}
iaResult, err := iaDatasource.executeTimeSeriesQuery(ctx, insightsAnalyticsQueries, tsdbQuery.TimeRange)
if err != nil {
return nil, err
}
for k, v := range aiResult.Results {
azResult.Results[k] = v
}
......@@ -105,5 +118,9 @@ func (e *AzureMonitorExecutor) Query(ctx context.Context, dsInfo *models.DataSou
azResult.Results[k] = v
}
for k, v := range iaResult.Results {
azResult.Results[k] = v
}
return azResult, nil
}
package azuremonitor
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/opentracing/opentracing-go"
"golang.org/x/net/context/ctxhttp"
)
type InsightsAnalyticsDatasource struct {
httpClient *http.Client
dsInfo *models.DataSource
}
type InsightsAnalyticsQuery struct {
RefID string
RawQuery string
InterpolatedQuery string
ResultFormat string
Params url.Values
Target string
}
func (e *InsightsAnalyticsDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []*tsdb.Query, timeRange *tsdb.TimeRange) (*tsdb.Response, error) {
result := &tsdb.Response{
Results: map[string]*tsdb.QueryResult{},
}
queries, err := e.buildQueries(originalQueries, timeRange)
if err != nil {
return nil, err
}
for _, query := range queries {
result.Results[query.RefID] = e.executeQuery(ctx, query)
}
return result, nil
}
func (e *InsightsAnalyticsDatasource) buildQueries(queries []*tsdb.Query, timeRange *tsdb.TimeRange) ([]*InsightsAnalyticsQuery, error) {
iaQueries := []*InsightsAnalyticsQuery{}
for _, query := range queries {
queryBytes, err := query.Model.Encode()
if err != nil {
return nil, fmt.Errorf("failed to re-encode the Azure Application Insights Analytics query into JSON: %w", err)
}
qm := InsightsAnalyticsQuery{}
queryJSONModel := insightsAnalyticsJSONQuery{}
err = json.Unmarshal(queryBytes, &queryJSONModel)
if err != nil {
return nil, fmt.Errorf("failed to decode the Azure Application Insights Analytics query object from JSON: %w", err)
}
qm.RawQuery = queryJSONModel.InsightsAnalytics.Query
qm.ResultFormat = queryJSONModel.InsightsAnalytics.ResultFormat
qm.RefID = query.RefId
if qm.RawQuery == "" {
return nil, fmt.Errorf("query is missing query string property")
}
qm.InterpolatedQuery, err = KqlInterpolate(query, timeRange, qm.RawQuery)
if err != nil {
return nil, err
}
qm.Params = url.Values{}
qm.Params.Add("query", qm.InterpolatedQuery)
qm.Target = qm.Params.Encode()
iaQueries = append(iaQueries, &qm)
}
return iaQueries, nil
}
func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *InsightsAnalyticsQuery) *tsdb.QueryResult {
queryResult := &tsdb.QueryResult{RefId: query.RefID}
queryResultError := func(err error) *tsdb.QueryResult {
queryResult.Error = err
return queryResult
}
req, err := e.createRequest(ctx, e.dsInfo)
if err != nil {
queryResultError(err)
}
req.URL.Path = path.Join(req.URL.Path, "query")
req.URL.RawQuery = query.Params.Encode()
span, ctx := opentracing.StartSpanFromContext(ctx, "application insights analytics query")
span.SetTag("target", query.Target)
span.SetTag("datasource_id", e.dsInfo.Id)
span.SetTag("org_id", e.dsInfo.OrgId)
defer span.Finish()
err = opentracing.GlobalTracer().Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header))
if err != nil {
azlog.Warn("failed to inject global tracer")
}
azlog.Debug("ApplicationInsights", "Request URL", req.URL.String())
res, err := ctxhttp.Do(ctx, e.httpClient, req)
if err != nil {
queryResultError(err)
}
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
queryResultError(err)
}
if res.StatusCode/100 != 2 {
azlog.Debug("Request failed", "status", res.Status, "body", string(body))
queryResultError(fmt.Errorf("Request failed status: %v", res.Status))
}
var logResponse AzureLogAnalyticsResponse
d := json.NewDecoder(bytes.NewReader(body))
d.UseNumber()
err = d.Decode(&logResponse)
if err != nil {
queryResultError(err)
}
t, err := logResponse.GetPrimaryResultTable()
if err != nil {
queryResultError(err)
}
frame, err := LogTableToFrame(t)
if err != nil {
return queryResultError(err)
}
if query.ResultFormat == "time_series" {
tsSchema := frame.TimeSeriesSchema()
if tsSchema.Type == data.TimeSeriesTypeLong {
wideFrame, err := data.LongToWide(frame, &data.FillMissing{})
if err == nil {
frame = wideFrame
} else {
frame.AppendNotices(data.Notice{Severity: data.NoticeSeverityWarning, Text: "could not convert frame to time series, returning raw table: " + err.Error()})
}
}
}
frames := data.Frames{frame}
queryResult.Dataframes = tsdb.NewDecodedDataFrames(frames)
return queryResult
}
func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) {
// find plugin
plugin, ok := plugins.DataSources[dsInfo.Type]
if !ok {
return nil, errors.New("Unable to find datasource plugin Azure Application Insights")
}
cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor")
appInsightsRoute, pluginRouteName, err := e.getPluginRoute(plugin, cloudName)
if err != nil {
return nil, err
}
appInsightsAppID := dsInfo.JsonData.Get("appInsightsAppId").MustString()
proxyPass := fmt.Sprintf("%s/v1/apps/%s", pluginRouteName, appInsightsAppID)
u, err := url.Parse(dsInfo.Url)
if err != nil {
return nil, fmt.Errorf("unable to parse url for Application Insights Analytics datasource: %w", err)
}
u.Path = path.Join(u.Path, fmt.Sprintf("/v1/apps/%s", appInsightsAppID))
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
azlog.Debug("Failed to create request", "error", err)
return nil, errutil.Wrap("Failed to create request", err)
}
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
pluginproxy.ApplyRoute(ctx, req, proxyPass, appInsightsRoute, dsInfo)
return req, nil
}
func (e *InsightsAnalyticsDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin, cloudName string) (*plugins.AppPluginRoute, string, error) {
pluginRouteName := "appinsights"
if cloudName == "chinaazuremonitor" {
pluginRouteName = "chinaappinsights"
}
var pluginRoute *plugins.AppPluginRoute
for _, route := range plugin.Routes {
if route.Path == pluginRouteName {
pluginRoute = route
break
}
}
return pluginRoute, pluginRouteName, nil
}
......@@ -107,16 +107,18 @@ type insightsJSONQuery struct {
Dimension string `json:"dimension"`
DimensionFilter string `json:"dimensionFilter"`
MetricName string `json:"metricName"`
RawQuery *bool `json:"rawQuery"`
RawQueryString string `json:"rawQueryString"`
TimeGrain string `json:"timeGrain"`
TimeColumn string `json:"timeColumn"`
ValueColumn string `json:"valueColumn"`
SegmentColumn string `json:"segmentColumn"`
} `json:"appInsights"`
Raw *bool `json:"raw"`
}
type insightsAnalyticsJSONQuery struct {
InsightsAnalytics struct {
Query string `json:"query"`
ResultFormat string `json:"resultFormat"`
} `json:"insightsAnalytics"`
}
// logJSONQuery is the frontend JSON query model for an Azure Log Analytics query.
type logJSONQuery struct {
AzureLogAnalytics struct {
......
import { TimeSeries, toDataFrame } from '@grafana/data';
import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
import { ScopedVars } from '@grafana/data';
import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
import _ from 'lodash';
import TimegrainConverter from '../time_grain_converter';
import { AzureDataSourceJsonData, AzureMonitorQuery } from '../types';
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from '../types';
import ResponseParser from './response_parser';
export interface LogAnalyticsColumn {
text: string;
value: string;
}
export default class AppInsightsDatasource {
id: number;
export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
url: string;
baseUrl: string;
version = 'beta';
......@@ -20,7 +19,7 @@ export default class AppInsightsDatasource {
logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {};
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
this.id = instanceSettings.id;
super(instanceSettings);
this.applicationId = instanceSettings.jsonData.appInsightsAppId || '';
switch (instanceSettings.jsonData?.cloudName) {
......@@ -72,123 +71,45 @@ export default class AppInsightsDatasource {
};
}
createMetricsRequest(item: any, options: DataQueryRequest<AzureMonitorQuery>, target: AzureMonitorQuery) {
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> {
const item = target.appInsights;
const old: any = item;
// fix for timeGrainUnit which is a deprecated/removed field name
if (item.timeGrainCount) {
item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrainCount, item.timeGrainUnit);
if (old.timeGrainCount) {
item.timeGrain = TimegrainConverter.createISO8601Duration(old.timeGrainCount, item.timeGrainUnit);
} else if (item.timeGrainUnit && item.timeGrain !== 'auto') {
item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
}
// migration for non-standard names
if (item.groupBy && !item.dimension) {
item.dimension = item.groupBy;
if (old.groupBy && !item.dimension) {
item.dimension = old.groupBy;
}
if (item.filter && !item.dimensionFilter) {
item.dimensionFilter = item.filter;
if (old.filter && !item.dimensionFilter) {
item.dimensionFilter = old.filter;
}
const templateSrv = getTemplateSrv();
return {
type: 'timeSeriesQuery',
raw: false,
refId: target.refId,
format: target.format,
queryType: AzureQueryType.ApplicationInsights,
appInsights: {
rawQuery: false,
timeGrain: templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars),
timeGrain: templateSrv.replace((item.timeGrain || '').toString(), scopedVars),
allowedTimeGrainsMs: item.allowedTimeGrainsMs,
metricName: templateSrv.replace(item.metricName, options.scopedVars),
aggregation: templateSrv.replace(item.aggregation, options.scopedVars),
dimension: templateSrv.replace(item.dimension, options.scopedVars),
dimensionFilter: templateSrv.replace(item.dimensionFilter, options.scopedVars),
metricName: templateSrv.replace(item.metricName, scopedVars),
aggregation: templateSrv.replace(item.aggregation, scopedVars),
dimension: templateSrv.replace(item.dimension, scopedVars),
dimensionFilter: templateSrv.replace(item.dimensionFilter, 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) {
// @ts-ignore
return;
}
const { data } = await getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries,
},
});
const result: DataQueryResponseData[] = [];
if (data.results) {
Object.values(data.results).forEach((queryRes: any) => {
if (queryRes.meta && queryRes.meta.columns) {
const columnNames = queryRes.meta.columns as string[];
this.logAnalyticsColumns[queryRes.refId] = _.map(columnNames, n => ({ text: n, value: n }));
}
if (!queryRes.series) {
return;
}
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) {
return _.map(queries, query => {
return this.doRequest(query.url)
.then((result: any) => {
return {
result: result,
query: query,
};
})
.catch((err: any) => {
throw {
error: err,
query: query,
};
});
});
}
annotationQuery(options: any) {}
metricFindQuery(query: string) {
const appInsightsMetricNameQuery = query.match(/^AppInsightsMetricNames\(\)/i);
if (appInsightsMetricNameQuery) {
......
import _ from 'lodash';
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
import ResponseParser from './response_parser';
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureLogsVariable } from '../types';
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureLogsVariable, AzureQueryType } from '../types';
import {
DataQueryResponse,
ScopedVars,
......@@ -109,10 +109,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
});
}
filterQuery(item: AzureMonitorQuery): boolean {
return item.hide !== true && !!item.azureLogAnalytics;
}
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> {
const item = target.azureLogAnalytics;
......@@ -129,7 +125,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
return {
refId: target.refId,
format: target.format,
queryType: 'Azure Log Analytics',
queryType: AzureQueryType.LogAnalytics,
subscriptionId: subscriptionId,
azureLogAnalytics: {
resultFormat: item.resultFormat,
......
......@@ -8,6 +8,7 @@ import {
AzureDataSourceJsonData,
AzureMonitorMetricDefinitionsResponse,
AzureMonitorResourceGroupsResponse,
AzureQueryType,
} from '../types';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { getBackendSrv, DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
......@@ -76,9 +77,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return {
refId: target.refId,
subscription: subscriptionId,
queryType: 'Azure Monitor',
queryType: AzureQueryType.AzureMonitor,
type: 'timeSeriesQuery',
raw: false,
azureMonitor: {
resourceGroup,
resourceName,
......
......@@ -2,72 +2,106 @@ import _ from 'lodash';
import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
import AppInsightsDatasource from './app_insights/app_insights_datasource';
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
import { AzureMonitorQuery, AzureDataSourceJsonData } from './types';
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureQueryType, InsightsAnalyticsQuery } from './types';
import {
DataSourceApi,
DataQueryRequest,
DataSourceInstanceSettings,
DataQueryResponse,
DataQueryResponseData,
LoadingState,
} from '@grafana/data';
import { Observable } from 'rxjs';
import { Observable, of, from } from 'rxjs';
import { DataSourceWithBackend } from '@grafana/runtime';
import InsightsAnalyticsDatasource from './insights_analytics/insights_analytics_datasource';
export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> {
azureMonitorDatasource: AzureMonitorDatasource;
appInsightsDatasource: AppInsightsDatasource;
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
insightsAnalyticsDatasource: InsightsAnalyticsDatasource;
pseudoDatasource: Record<AzureQueryType, DataSourceWithBackend>;
optionsKey: Record<AzureQueryType, string>;
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
super(instanceSettings);
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings);
this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings);
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
}
query(options: DataQueryRequest<AzureMonitorQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponseData> {
const promises: any[] = [];
const azureMonitorOptions = _.cloneDeep(options);
const appInsightsOptions = _.cloneDeep(options);
const azureLogAnalyticsOptions = _.cloneDeep(options);
azureMonitorOptions.targets = _.filter(azureMonitorOptions.targets, ['queryType', 'Azure Monitor']);
appInsightsOptions.targets = _.filter(appInsightsOptions.targets, ['queryType', 'Application Insights']);
azureLogAnalyticsOptions.targets = _.filter(azureLogAnalyticsOptions.targets, ['queryType', 'Azure Log Analytics']);
if (appInsightsOptions.targets.length > 0) {
const aiPromise = this.appInsightsDatasource.query(appInsightsOptions);
if (aiPromise) {
promises.push(aiPromise);
this.insightsAnalyticsDatasource = new InsightsAnalyticsDatasource(instanceSettings);
const pseudoDatasource: any = {};
pseudoDatasource[AzureQueryType.ApplicationInsights] = this.appInsightsDatasource;
pseudoDatasource[AzureQueryType.AzureMonitor] = this.azureMonitorDatasource;
pseudoDatasource[AzureQueryType.InsightsAnalytics] = this.insightsAnalyticsDatasource;
pseudoDatasource[AzureQueryType.LogAnalytics] = this.azureLogAnalyticsDatasource;
this.pseudoDatasource = pseudoDatasource;
const optionsKey: any = {};
optionsKey[AzureQueryType.ApplicationInsights] = 'appInsights';
optionsKey[AzureQueryType.AzureMonitor] = 'azureMonitor';
optionsKey[AzureQueryType.InsightsAnalytics] = 'insightsAnalytics';
optionsKey[AzureQueryType.LogAnalytics] = 'azureLogAnalytics';
this.optionsKey = optionsKey;
}
query(options: DataQueryRequest<AzureMonitorQuery>): Observable<DataQueryResponseData> {
const byType: Record<AzureQueryType, DataQueryRequest<AzureMonitorQuery>> = ({} as unknown) as Record<
AzureQueryType,
DataQueryRequest<AzureMonitorQuery>
>;
for (const target of options.targets) {
// Migrate old query structure
if (target.queryType === AzureQueryType.ApplicationInsights) {
if ((target.appInsights as any).rawQuery) {
target.queryType = AzureQueryType.InsightsAnalytics;
target.insightsAnalytics = (target.appInsights as unknown) as InsightsAnalyticsQuery;
delete target.appInsights;
}
}
}
if (azureLogAnalyticsOptions.targets.length > 0) {
const obs = this.azureLogAnalyticsDatasource.query(azureLogAnalyticsOptions);
if (!promises.length) {
return obs; // return the observable directly
if (!target.queryType) {
target.queryType = AzureQueryType.AzureMonitor;
}
// NOTE: this only includes the data!
// When all three query types are ready to be observale, they should all use observable
promises.push(obs.toPromise().then(r => r.data));
}
if (azureMonitorOptions.targets.length > 0) {
const obs = this.azureMonitorDatasource.query(azureMonitorOptions);
if (!promises.length) {
return obs; // return the observable directly
// Check that we have options
const opts = (target as any)[this.optionsKey[target.queryType]];
// Skip hidden queries or ones without properties
if (target.hide || !opts) {
continue;
}
// NOTE: this only includes the data!
// When all three query types are ready to be observale, they should all use observable
promises.push(obs.toPromise().then(r => r.data));
}
if (promises.length === 0) {
return Promise.resolve({ data: [] });
// Initalize the list of queries
let q = byType[target.queryType];
if (!q) {
q = _.cloneDeep(options);
q.targets = [];
byType[target.queryType] = q;
}
q.targets.push(target);
}
return Promise.all(promises).then(results => {
return { data: _.flatten(results) };
// Distinct types are managed by distinct requests
const obs = Object.keys(byType).map((type: AzureQueryType) => {
const req = byType[type];
return this.pseudoDatasource[type].query(req);
});
// Single query can skip merge
if (obs.length === 1) {
return obs[0];
}
if (obs.length > 1) {
// Not accurate, but simple and works
// should likely be more like the mixed data source
const promises = obs.map(o => o.toPromise());
return from(
Promise.all(promises).then(results => {
return { data: _.flatten(results) };
})
);
}
return of({ state: LoadingState.Done });
}
async annotationQuery(options: any) {
......
import { ScopedVars } from '@grafana/data';
import { DataSourceInstanceSettings } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from '../types';
import AppInsightsDatasource from '../app_insights/app_insights_datasource';
export default class InsightsAnalyticsDatasource extends AppInsightsDatasource {
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
super(instanceSettings);
}
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> {
const item = target.insightsAnalytics;
// Old name migrations
const old: any = item;
if (old.rawQueryString && !item.query) {
item.query = old.rawQueryString;
}
return {
refId: target.refId,
queryType: AzureQueryType.InsightsAnalytics,
insightsAnalytics: {
query: getTemplateSrv().replace(item.query, scopedVars),
resultFormat: item.resultFormat,
},
};
}
}
<query-editor-row
query-ctrl="ctrl"
can-collapse="false"
has-text-edit-mode="ctrl.target.queryType === 'Application Insights'"
>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Service</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select
class="gf-form-input service-dropdown"
class="gf-form-input service-dropdown min-width-12"
ng-model="ctrl.target.queryType"
ng-options="f as f for f in ['Application Insights', 'Azure Monitor', 'Azure Log Analytics']"
ng-options="f as f for f in ['Application Insights', 'Azure Monitor', 'Azure Log Analytics', 'Insights Analytics']"
ng-change="ctrl.onQueryTypeChange()"
></select>
</div>
......@@ -300,8 +299,39 @@
</div>
</div>
<div ng-if="ctrl.target.queryType === 'Insights Analytics'">
<div class="gf-form gf-form--grow">
<kusto-editor
class="gf-form gf-form--grow"
query="ctrl.target.insightsAnalytics.query"
placeholder="'Application Insights Query'"
change="ctrl.onInsightsAnalyticsQueryChange"
execute="ctrl.onQueryExecute"
variables="ctrl.templateVariables"
getSchema="ctrl.getAppInsightsQuerySchema"
/>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Format As</label>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input gf-size-auto"
ng-model="ctrl.target.insightsAnalytics.resultFormat"
ng-options="f.value as f.text for f in ctrl.resultFormats"
ng-change="ctrl.refresh()"
></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</div>
<div ng-if="ctrl.target.queryType === 'Application Insights'">
<div ng-show="!ctrl.target.appInsights.rawQuery">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Metric</label>
......@@ -426,73 +456,12 @@
ng-blur="ctrl.refresh()"
/>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div ng-show="ctrl.target.appInsights.rawQuery">
<!-- <div class="gf-form">
<textarea rows="3" class="gf-form-input" ng-model="ctrl.target.appInsights.rawQueryString" spellcheck="false"
placeholder="Application Insights Query" ng-model-onblur ng-change="ctrl.refresh()"></textarea>
</div> -->
<div class="gf-form gf-form--grow">
<kusto-editor
class="gf-form gf-form--grow"
query="ctrl.target.appInsights.rawQueryString"
placeholder="'Application Insights Query'"
change="ctrl.onAppInsightsQueryChange"
execute="ctrl.onAppInsightsQueryExecute"
variables="ctrl.templateVariables"
getSchema="ctrl.getAppInsightsQuerySchema"
/>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">X-axis</label>
<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"
>
</gf-form-dropdown>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Y-axis</label>
<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"
>
</gf-form-dropdown>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Split On</label>
<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"
>
</gf-form-dropdown>
</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-show="ctrl.lastQueryError">
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
</div>
......
......@@ -31,7 +31,7 @@ describe('AzureMonitorQueryCtrl', () => {
});
it('should set default App Insights editor to be builder', () => {
expect(queryCtrl.target.appInsights.rawQuery).toBe(false);
expect(!!(queryCtrl.target.appInsights as any).rawQuery).toBe(false);
});
it('should set query parts to select', () => {
......
......@@ -8,6 +8,7 @@ import kbn from 'app/core/utils/kbn';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { auto, IPromise } from 'angular';
import { DataFrame, PanelEvents } from '@grafana/data';
import { AzureQueryType } from './types';
export interface ResultFormat {
text: string;
......@@ -20,8 +21,9 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
defaultDropdownValue = 'select';
target: {
// should be: AzureMonitorQuery
refId: string;
queryType: string;
queryType: AzureQueryType;
subscription: string;
azureMonitor: {
resourceGroup: string;
......@@ -46,7 +48,6 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
workspace: string;
};
appInsights: {
rawQuery: boolean;
// metric style query when rawQuery == false
metricName: string;
dimension: any;
......@@ -62,12 +63,10 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
timeGrain: string;
timeGrains: Array<{ text: string; value: string }>;
allowedTimeGrainsMs: number[];
// query style query when rawQuery == true
rawQueryString: string;
timeColumn: string;
valueColumn: string;
segmentColumn: string;
};
insightsAnalytics: {
query: any;
resultFormat: string;
};
};
......@@ -105,12 +104,12 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
},
appInsights: {
metricName: this.defaultDropdownValue,
rawQuery: false,
rawQueryString: '',
dimension: 'none',
timeGrain: 'auto',
timeColumn: 'timestamp',
valueColumn: '',
},
insightsAnalytics: {
query: '',
resultFormat: 'time_series',
},
};
......@@ -614,11 +613,11 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
.catch(this.handleQueryCtrlError.bind(this));
}
onAppInsightsQueryChange = (nextQuery: string) => {
this.target.appInsights.rawQueryString = nextQuery;
onInsightsAnalyticsQueryChange = (nextQuery: string) => {
this.target.insightsAnalytics.query = nextQuery;
};
onAppInsightsQueryExecute = () => {
onQueryExecute = () => {
return this.refresh();
};
......@@ -637,10 +636,6 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.refresh();
}
toggleEditorMode() {
this.target.appInsights.rawQuery = !this.target.appInsights.rawQuery;
}
updateTimeGrainType() {
if (this.target.appInsights.timeGrainType === 'specific') {
this.target.appInsights.timeGrainCount = '1';
......
......@@ -2,12 +2,22 @@ import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@g
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
export enum AzureQueryType {
AzureMonitor = 'Azure Monitor',
ApplicationInsights = 'Application Insights',
InsightsAnalytics = 'Insights Analytics',
LogAnalytics = 'Azure Log Analytics',
}
export interface AzureMonitorQuery extends DataQuery {
queryType: AzureQueryType;
format: string;
subscription: string;
azureMonitor: AzureMetricQuery;
azureLogAnalytics: AzureLogsQuery;
appInsights: ApplicationInsightsQuery;
insightsAnalytics: InsightsAnalyticsQuery;
}
export interface AzureDataSourceJsonData extends DataSourceJsonData {
......@@ -58,8 +68,6 @@ export interface AzureLogsQuery {
}
export interface ApplicationInsightsQuery {
rawQuery: boolean;
rawQueryString: any;
metricName: string;
timeGrainUnit: string;
timeGrain: string;
......@@ -70,6 +78,11 @@ export interface ApplicationInsightsQuery {
alias: string;
}
export interface InsightsAnalyticsQuery {
query: string;
resultFormat: string;
}
// Azure Monitor API Types
export interface AzureMonitorMetricDefinitionsResponse {
......
......@@ -85,6 +85,9 @@ def remove_long_paths():
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/insights_analytics/insights_analytics_datasource.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts',
'/tmp/a/grafana/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/AnalyticsConfig.test.tsx',
......
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