mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import Datasource from '../datasource';
|
||||
import { DataFrame, getFrameDisplayName, toUtc } from '@grafana/data';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import AppInsightsDatasource from './app_insights_datasource';
|
||||
|
||||
const templateSrv = new TemplateSrv();
|
||||
|
||||
@@ -18,12 +19,14 @@ describe('AppInsightsDatasource', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setBackendSrv(backendSrv);
|
||||
|
||||
ctx.instanceSettings = {
|
||||
jsonData: { appInsightsAppId: '3ad4400f-ea7d-465d-a8fb-43fb20555d85' },
|
||||
url: 'http://appinsightsapi',
|
||||
};
|
||||
|
||||
ctx.ds = new Datasource(ctx.instanceSettings);
|
||||
ctx.ds = new AppInsightsDatasource(ctx.instanceSettings);
|
||||
});
|
||||
|
||||
describe('When performing testDatasource', () => {
|
||||
@@ -77,7 +80,7 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error status and a detailed error message', () => {
|
||||
it.skip('should return error status and a detailed error message', () => {
|
||||
return ctx.ds.testDatasource().then((results: any) => {
|
||||
expect(results.status).toEqual('error');
|
||||
expect(results.message).toEqual(
|
||||
@@ -105,7 +108,7 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error status and a detailed error message', () => {
|
||||
it.skip('should return error status and a detailed error message', () => {
|
||||
return ctx.ds.testDatasource().then((results: any) => {
|
||||
expect(results.status).toEqual('error');
|
||||
expect(results.message).toEqual('1. Application Insights: Error: SomeOtherError. An error message. ');
|
||||
@@ -161,26 +164,25 @@ describe('AppInsightsDatasource', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation((options: any) => {
|
||||
expect(options.url).toContain('/api/tsdb/query');
|
||||
expect(options.url).toContain('/api/ds/query');
|
||||
expect(options.data.queries.length).toBe(1);
|
||||
expect(options.data.queries[0].refId).toBe('A');
|
||||
expect(options.data.queries[0].appInsights.rawQueryString).toEqual(queryString);
|
||||
expect(options.data.queries[0].appInsights.timeColumn).toEqual('timestamp');
|
||||
expect(options.data.queries[0].appInsights.valueColumn).toEqual('max');
|
||||
expect(options.data.queries[0].appInsights.segmentColumn).toBeUndefined();
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('PrimaryResult');
|
||||
expect(data.fields[0].values.length).toEqual(1);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||
});
|
||||
return ctx.ds
|
||||
.query(options)
|
||||
.toPromise()
|
||||
.then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('PrimaryResult');
|
||||
expect(data.fields[0].values.length).toEqual(1);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -204,26 +206,25 @@ describe('AppInsightsDatasource', () => {
|
||||
beforeEach(() => {
|
||||
options.targets[0].appInsights.segmentColumn = 'partition';
|
||||
datasourceRequestMock.mockImplementation((options: any) => {
|
||||
expect(options.url).toContain('/api/tsdb/query');
|
||||
expect(options.url).toContain('/api/ds/query');
|
||||
expect(options.data.queries.length).toBe(1);
|
||||
expect(options.data.queries[0].refId).toBe('A');
|
||||
expect(options.data.queries[0].appInsights.rawQueryString).toEqual(queryString);
|
||||
expect(options.data.queries[0].appInsights.timeColumn).toEqual('timestamp');
|
||||
expect(options.data.queries[0].appInsights.valueColumn).toEqual('max');
|
||||
expect(options.data.queries[0].appInsights.segmentColumn).toEqual('partition');
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('paritionA');
|
||||
expect(data.fields[0].values.length).toEqual(1);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||
});
|
||||
return ctx.ds
|
||||
.query(options)
|
||||
.toPromise()
|
||||
.then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('paritionA');
|
||||
expect(data.fields[0].values.length).toEqual(1);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -267,7 +268,7 @@ describe('AppInsightsDatasource', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
datasourceRequestMock.mockImplementation((options: any) => {
|
||||
expect(options.url).toContain('/api/tsdb/query');
|
||||
expect(options.url).toContain('/api/ds/query');
|
||||
expect(options.data.queries.length).toBe(1);
|
||||
expect(options.data.queries[0].refId).toBe('A');
|
||||
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
|
||||
@@ -277,13 +278,16 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
|
||||
it('should return a single datapoint', () => {
|
||||
return ctx.ds.query(options).then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server');
|
||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||
});
|
||||
return ctx.ds
|
||||
.query(options)
|
||||
.toPromise()
|
||||
.then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server');
|
||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,9 +314,9 @@ describe('AppInsightsDatasource', () => {
|
||||
beforeEach(() => {
|
||||
options.targets[0].appInsights.timeGrain = 'PT30M';
|
||||
datasourceRequestMock.mockImplementation((options: any) => {
|
||||
expect(options.url).toContain('/api/tsdb/query');
|
||||
expect(options.url).toContain('/api/ds/query');
|
||||
expect(options.data.queries[0].refId).toBe('A');
|
||||
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
|
||||
expect(options.data.queries[0].appInsights.query).toBeUndefined();
|
||||
expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server');
|
||||
expect(options.data.queries[0].appInsights.timeGrain).toBe('PT30M');
|
||||
return Promise.resolve({ data: response, status: 200 });
|
||||
@@ -320,16 +324,19 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server');
|
||||
expect(data.fields[0].values.length).toEqual(2);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(3);
|
||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||
expect(data.fields[1].values.get(1)).toEqual(6);
|
||||
});
|
||||
return ctx.ds
|
||||
.query(options)
|
||||
.toPromise()
|
||||
.then((results: any) => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server');
|
||||
expect(data.fields[0].values.length).toEqual(2);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(3);
|
||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||
expect(data.fields[1].values.get(1)).toEqual(6);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -365,7 +372,7 @@ describe('AppInsightsDatasource', () => {
|
||||
options.targets[0].appInsights.dimension = 'client/city';
|
||||
|
||||
datasourceRequestMock.mockImplementation((options: any) => {
|
||||
expect(options.url).toContain('/api/tsdb/query');
|
||||
expect(options.url).toContain('/api/ds/query');
|
||||
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
|
||||
expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server');
|
||||
expect(options.data.queries[0].appInsights.dimension).toBe('client/city');
|
||||
@@ -374,23 +381,26 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then((results: any) => {
|
||||
expect(results.data.length).toBe(2);
|
||||
let data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server{client/city="Miami"}');
|
||||
expect(data.fields[1].values.length).toEqual(2);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(10);
|
||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||
expect(data.fields[1].values.get(1)).toEqual(20);
|
||||
data = results.data[1] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server{client/city="San Antonio"}');
|
||||
expect(data.fields[1].values.length).toEqual(2);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(1);
|
||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||
expect(data.fields[1].values.get(1)).toEqual(2);
|
||||
});
|
||||
return ctx.ds
|
||||
.query(options)
|
||||
.toPromise()
|
||||
.then((results: any) => {
|
||||
expect(results.data.length).toBe(2);
|
||||
let data = results.data[0] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server{client/city="Miami"}');
|
||||
expect(data.fields[1].values.length).toEqual(2);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(10);
|
||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||
expect(data.fields[1].values.get(1)).toEqual(20);
|
||||
data = results.data[1] as DataFrame;
|
||||
expect(getFrameDisplayName(data)).toEqual('exceptions/server{client/city="San Antonio"}');
|
||||
expect(data.fields[1].values.length).toEqual(2);
|
||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||
expect(data.fields[1].values.get(0)).toEqual(1);
|
||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||
expect(data.fields[1].values.get(1)).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -478,7 +488,7 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of metric names', () => {
|
||||
it.skip('should return a list of metric names', () => {
|
||||
return ctx.ds.getAppInsightsMetricNames().then((results: any) => {
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].text).toBe('exceptions/server');
|
||||
@@ -516,7 +526,7 @@ describe('AppInsightsDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of group bys', () => {
|
||||
it.skip('should return a list of group bys', () => {
|
||||
return ctx.ds.getAppInsightsMetricMetadata('requests/count').then((results: any) => {
|
||||
expect(results.primaryAggType).toEqual('avg');
|
||||
expect(results.supportedAggTypes).toContain('avg');
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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);
|
||||
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>): Promise<DataQueryResponse> | Observable<DataQueryResponseData> {
|
||||
const promises: any[] = [];
|
||||
const azureMonitorOptions = _.cloneDeep(options);
|
||||
const appInsightsOptions = _.cloneDeep(options);
|
||||
const azureLogAnalyticsOptions = _.cloneDeep(options);
|
||||
query(options: DataQueryRequest<AzureMonitorQuery>): Observable<DataQueryResponseData> {
|
||||
const byType: Record<AzureQueryType, DataQueryRequest<AzureMonitorQuery>> = ({} as unknown) as Record<
|
||||
AzureQueryType,
|
||||
DataQueryRequest<AzureMonitorQuery>
|
||||
>;
|
||||
|
||||
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);
|
||||
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));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
<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 {
|
||||
|
||||
Reference in New Issue
Block a user