azuremonitor: move files into grafana
Initial port of the code from the plugin - lots of small things to fix.
@ -69,6 +69,7 @@
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"mocha": "^4.0.1",
|
||||
"monaco-editor": "^0.15.6",
|
||||
"ng-annotate-loader": "^0.6.1",
|
||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||
"ngtemplate-loader": "^2.0.1",
|
||||
|
@ -23,6 +23,7 @@ const (
|
||||
DS_ACCESS_DIRECT = "direct"
|
||||
DS_ACCESS_PROXY = "proxy"
|
||||
DS_STACKDRIVER = "stackdriver"
|
||||
DS_AZURE_MONITOR = "azure-monitor"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -73,6 +74,7 @@ var knownDatasourcePlugins = map[string]bool{
|
||||
DS_MYSQL: true,
|
||||
DS_MSSQL: true,
|
||||
DS_STACKDRIVER: true,
|
||||
DS_AZURE_MONITOR: true,
|
||||
"opennms": true,
|
||||
"abhisant-druid-datasource": true,
|
||||
"dalmatinerdb-datasource": true,
|
||||
|
@ -12,6 +12,7 @@ import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
|
||||
import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
|
||||
import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
|
||||
import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
|
||||
import * as azureMonitorPlugin from 'app/plugins/datasource/grafana-azure-monitor-datasource/module';
|
||||
|
||||
import * as textPanel from 'app/plugins/panel/text/module';
|
||||
import * as text2Panel from 'app/plugins/panel/text2/module';
|
||||
@ -41,6 +42,7 @@ const builtInPlugins = {
|
||||
'app/plugins/datasource/prometheus/module': prometheusPlugin,
|
||||
'app/plugins/datasource/testdata/module': testDataDSPlugin,
|
||||
'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
|
||||
'app/plugins/datasource/grafana-azure-monitor-datasource/module': azureMonitorPlugin,
|
||||
|
||||
'app/plugins/panel/text/module': textPanel,
|
||||
'app/plugins/panel/text2/module': text2Panel,
|
||||
|
@ -0,0 +1,16 @@
|
||||
export class QueryCtrl {
|
||||
target: any;
|
||||
datasource: any;
|
||||
panelCtrl: any;
|
||||
panel: any;
|
||||
hasRawMode: boolean;
|
||||
error: string;
|
||||
|
||||
constructor(public $scope, _$injector) {
|
||||
this.panelCtrl = this.panelCtrl || { panel: {} };
|
||||
this.target = this.target || { target: '' };
|
||||
this.panel = this.panelCtrl.panel;
|
||||
}
|
||||
|
||||
refresh() {}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { QueryCtrl } from './query_ctrl';
|
||||
|
||||
export { QueryCtrl };
|
@ -0,0 +1,32 @@
|
||||
export class AzureMonitorAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
datasource: any;
|
||||
annotation: any;
|
||||
workspaces: any[];
|
||||
|
||||
defaultQuery = '<your table>\n| where $__timeFilter() \n| project TimeGenerated, Text=YourTitleColumn, Tags="tag1,tag2"';
|
||||
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
this.annotation.queryType = this.annotation.queryType || 'Azure Log Analytics';
|
||||
this.annotation.rawQuery = this.annotation.rawQuery || this.defaultQuery;
|
||||
this.getWorkspaces();
|
||||
}
|
||||
|
||||
getWorkspaces() {
|
||||
if (this.workspaces && this.workspaces.length > 0) {
|
||||
return this.workspaces;
|
||||
}
|
||||
|
||||
return this.datasource
|
||||
.getAzureLogAnalyticsWorkspaces()
|
||||
.then(list => {
|
||||
this.workspaces = list;
|
||||
if (list.length > 0 && !this.annotation.workspace) {
|
||||
this.annotation.workspace = list[0].value;
|
||||
}
|
||||
return this.workspaces;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
@ -0,0 +1,441 @@
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import Q from 'q';
|
||||
import moment from 'moment';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
describe('AppInsightsDatasource', () => {
|
||||
const ctx: any = {
|
||||
backendSrv: {},
|
||||
templateSrv: new TemplateSrv(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.$q = Q;
|
||||
ctx.instanceSettings = {
|
||||
jsonData: { appInsightsAppId: '3ad4400f-ea7d-465d-a8fb-43fb20555d85' },
|
||||
url: 'http://appinsightsapi',
|
||||
};
|
||||
|
||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
|
||||
});
|
||||
|
||||
describe('When performing testDatasource', () => {
|
||||
describe('and a list of metrics is returned', () => {
|
||||
const response = {
|
||||
metrics: {
|
||||
'requests/count': {
|
||||
displayName: 'Server requests',
|
||||
defaultAggregation: 'sum',
|
||||
},
|
||||
'requests/duration': {
|
||||
displayName: 'Server requests',
|
||||
defaultAggregation: 'sum',
|
||||
},
|
||||
},
|
||||
dimensions: {
|
||||
'request/source': {
|
||||
displayName: 'Request source',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = () => {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return success status', () => {
|
||||
return ctx.ds.testDatasource().then(results => {
|
||||
expect(results.status).toEqual('success');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and a PathNotFoundError error is returned', () => {
|
||||
const error = {
|
||||
data: {
|
||||
error: {
|
||||
code: 'PathNotFoundError',
|
||||
message: `An error message.`,
|
||||
},
|
||||
},
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
return ctx.$q.reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return error status and a detailed error message', () => {
|
||||
return ctx.ds.testDatasource().then(results => {
|
||||
expect(results.status).toEqual('error');
|
||||
expect(results.message).toEqual(
|
||||
'1. Application Insights: Not Found: Invalid Application Id for Application Insights service. '
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and an error is returned', () => {
|
||||
const error = {
|
||||
data: {
|
||||
error: {
|
||||
code: 'SomeOtherError',
|
||||
message: `An error message.`,
|
||||
},
|
||||
},
|
||||
status: 500,
|
||||
statusText: 'Error',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
return ctx.$q.reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return error status and a detailed error message', () => {
|
||||
return ctx.ds.testDatasource().then(results => {
|
||||
expect(results.status).toEqual('error');
|
||||
expect(results.message).toEqual('1. Application Insights: Error: SomeOtherError. An error message. ');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing query', () => {
|
||||
const options = {
|
||||
range: {
|
||||
from: moment.utc('2017-08-22T20:00:00Z'),
|
||||
to: moment.utc('2017-08-22T23:59:00Z'),
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
apiVersion: '2016-09-01',
|
||||
refId: 'A',
|
||||
queryType: 'Application Insights',
|
||||
appInsights: {
|
||||
metricName: 'exceptions/server',
|
||||
groupBy: '',
|
||||
timeGrainType: 'none',
|
||||
timeGrain: '',
|
||||
timeGrainUnit: '',
|
||||
alias: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('and with a single value', () => {
|
||||
const response = {
|
||||
value: {
|
||||
start: '2017-08-30T15:53:58.845Z',
|
||||
end: '2017-09-06T15:53:58.845Z',
|
||||
'exceptions/server': {
|
||||
sum: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/exceptions/server');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a single datapoint', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(1);
|
||||
expect(results.data[0].datapoints.length).toBe(1);
|
||||
expect(results.data[0].target).toEqual('exceptions/server');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1504713238845);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with an interval group and without a segment group by', () => {
|
||||
const response = {
|
||||
value: {
|
||||
start: '2017-08-30T15:53:58.845Z',
|
||||
end: '2017-09-06T15:53:58.845Z',
|
||||
interval: 'PT1H',
|
||||
segments: [
|
||||
{
|
||||
start: '2017-08-30T15:53:58.845Z',
|
||||
end: '2017-08-30T16:00:00.000Z',
|
||||
'exceptions/server': {
|
||||
sum: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
start: '2017-08-30T16:00:00.000Z',
|
||||
end: '2017-08-30T17:00:00.000Z',
|
||||
'exceptions/server': {
|
||||
sum: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
options.targets[0].appInsights.timeGrainType = 'specific';
|
||||
options.targets[0].appInsights.timeGrain = '30';
|
||||
options.targets[0].appInsights.timeGrainUnit = 'minute';
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/exceptions/server');
|
||||
expect(options.url).toContain('interval=PT30M');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(1);
|
||||
expect(results.data[0].datapoints.length).toBe(2);
|
||||
expect(results.data[0].target).toEqual('exceptions/server');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1504108800000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(3);
|
||||
expect(results.data[0].datapoints[1][1]).toEqual(1504112400000);
|
||||
expect(results.data[0].datapoints[1][0]).toEqual(66);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with a group by', () => {
|
||||
const response = {
|
||||
value: {
|
||||
start: '2017-08-30T15:53:58.845Z',
|
||||
end: '2017-09-06T15:53:58.845Z',
|
||||
interval: 'PT1H',
|
||||
segments: [
|
||||
{
|
||||
start: '2017-08-30T15:53:58.845Z',
|
||||
end: '2017-08-30T16:00:00.000Z',
|
||||
segments: [
|
||||
{
|
||||
'exceptions/server': {
|
||||
sum: 10,
|
||||
},
|
||||
'client/city': 'Miami',
|
||||
},
|
||||
{
|
||||
'exceptions/server': {
|
||||
sum: 1,
|
||||
},
|
||||
'client/city': 'San Jose',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
start: '2017-08-30T16:00:00.000Z',
|
||||
end: '2017-08-30T17:00:00.000Z',
|
||||
segments: [
|
||||
{
|
||||
'exceptions/server': {
|
||||
sum: 20,
|
||||
},
|
||||
'client/city': 'Miami',
|
||||
},
|
||||
{
|
||||
'exceptions/server': {
|
||||
sum: 2,
|
||||
},
|
||||
'client/city': 'San Antonio',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('and with no alias specified', () => {
|
||||
beforeEach(() => {
|
||||
options.targets[0].appInsights.groupBy = 'client/city';
|
||||
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/exceptions/server');
|
||||
expect(options.url).toContain('segment=client/city');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(3);
|
||||
expect(results.data[0].datapoints.length).toBe(2);
|
||||
expect(results.data[0].target).toEqual('exceptions/server{client/city="Miami"}');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1504108800000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(10);
|
||||
expect(results.data[0].datapoints[1][1]).toEqual(1504112400000);
|
||||
expect(results.data[0].datapoints[1][0]).toEqual(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with an alias specified', () => {
|
||||
beforeEach(() => {
|
||||
options.targets[0].appInsights.groupBy = 'client/city';
|
||||
options.targets[0].appInsights.alias = '{{metric}} + {{groupbyname}} + {{groupbyvalue}}';
|
||||
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/exceptions/server');
|
||||
expect(options.url).toContain('segment=client/city');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(3);
|
||||
expect(results.data[0].datapoints.length).toBe(2);
|
||||
expect(results.data[0].target).toEqual('exceptions/server + client/city + Miami');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1504108800000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(10);
|
||||
expect(results.data[0].datapoints[1][1]).toEqual(1504112400000);
|
||||
expect(results.data[0].datapoints[1][0]).toEqual(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery', () => {
|
||||
describe('with a metric names query', () => {
|
||||
const response = {
|
||||
metrics: {
|
||||
'exceptions/server': {},
|
||||
'requests/count': {},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/metadata');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of metric names', () => {
|
||||
return ctx.ds.metricFindQuery('appInsightsMetricNames()').then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].text).toBe('exceptions/server');
|
||||
expect(results[0].value).toBe('exceptions/server');
|
||||
expect(results[1].text).toBe('requests/count');
|
||||
expect(results[1].value).toBe('requests/count');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with metadata group by query', () => {
|
||||
const response = {
|
||||
metrics: {
|
||||
'exceptions/server': {
|
||||
supportedAggregations: ['sum'],
|
||||
supportedGroupBy: {
|
||||
all: ['client/os', 'client/city', 'client/browser'],
|
||||
},
|
||||
defaultAggregation: 'sum',
|
||||
},
|
||||
'requests/count': {
|
||||
supportedAggregations: ['avg', 'sum', 'total'],
|
||||
supportedGroupBy: {
|
||||
all: ['client/os', 'client/city', 'client/browser'],
|
||||
},
|
||||
defaultAggregation: 'avg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/metadata');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of group bys', () => {
|
||||
return ctx.ds.metricFindQuery('appInsightsGroupBys(requests/count)').then(results => {
|
||||
expect(results[0].text).toContain('client/os');
|
||||
expect(results[0].value).toContain('client/os');
|
||||
expect(results[1].text).toContain('client/city');
|
||||
expect(results[1].value).toContain('client/city');
|
||||
expect(results[2].text).toContain('client/browser');
|
||||
expect(results[2].value).toContain('client/browser');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When getting Metric Names', () => {
|
||||
const response = {
|
||||
metrics: {
|
||||
'exceptions/server': {},
|
||||
'requests/count': {},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/metadata');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of metric names', () => {
|
||||
return ctx.ds.getAppInsightsMetricNames().then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].text).toBe('exceptions/server');
|
||||
expect(results[0].value).toBe('exceptions/server');
|
||||
expect(results[1].text).toBe('requests/count');
|
||||
expect(results[1].value).toBe('requests/count');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When getting Metric Metadata', () => {
|
||||
const response = {
|
||||
metrics: {
|
||||
'exceptions/server': {
|
||||
supportedAggregations: ['sum'],
|
||||
supportedGroupBy: {
|
||||
all: ['client/os', 'client/city', 'client/browser'],
|
||||
},
|
||||
defaultAggregation: 'sum',
|
||||
},
|
||||
'requests/count': {
|
||||
supportedAggregations: ['avg', 'sum', 'total'],
|
||||
supportedGroupBy: {
|
||||
all: ['client/os', 'client/city', 'client/browser'],
|
||||
},
|
||||
defaultAggregation: 'avg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('/metrics/metadata');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of group bys', () => {
|
||||
return ctx.ds.getAppInsightsMetricMetadata('requests/count').then(results => {
|
||||
expect(results.primaryAggType).toEqual('avg');
|
||||
expect(results.supportedAggTypes).toContain('avg');
|
||||
expect(results.supportedAggTypes).toContain('sum');
|
||||
expect(results.supportedAggTypes).toContain('total');
|
||||
expect(results.supportedGroupBy).toContain('client/os');
|
||||
expect(results.supportedGroupBy).toContain('client/city');
|
||||
expect(results.supportedGroupBy).toContain('client/browser');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,227 @@
|
||||
import _ from 'lodash';
|
||||
import AppInsightsQuerystringBuilder from './app_insights_querystring_builder';
|
||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
||||
import ResponseParser from './response_parser';
|
||||
|
||||
export interface LogAnalyticsColumn {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
export default class AppInsightsDatasource {
|
||||
id: number;
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
version = 'beta';
|
||||
applicationId: string;
|
||||
logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private backendSrv, private templateSrv, private $q) {
|
||||
this.id = instanceSettings.id;
|
||||
this.applicationId = instanceSettings.jsonData.appInsightsAppId;
|
||||
this.baseUrl = `/appinsights/${this.version}/apps/${this.applicationId}`;
|
||||
this.url = instanceSettings.url;
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return !!this.applicationId && this.applicationId.length > 0;
|
||||
}
|
||||
|
||||
query(options) {
|
||||
const queries = _.filter(options.targets, item => {
|
||||
return item.hide !== true;
|
||||
}).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}`;
|
||||
|
||||
return {
|
||||
refId: target.refId,
|
||||
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 || ''));
|
||||
|
||||
const url = `${this.baseUrl}/metrics/${this.templateSrv.replace(
|
||||
encodeURI(item.metricName),
|
||||
options.scopedVars
|
||||
)}?${querystringBuilder.generate()}`;
|
||||
|
||||
return {
|
||||
refId: target.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: this.id,
|
||||
url: url,
|
||||
format: options.format,
|
||||
alias: item.alias,
|
||||
xaxis: '',
|
||||
yaxis: '',
|
||||
spliton: '',
|
||||
raw: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!queries || queries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = this.doQueries(queries);
|
||||
|
||||
return this.$q
|
||||
.all(promises)
|
||||
.then(results => {
|
||||
return new ResponseParser(results).parseQueryResult();
|
||||
})
|
||||
.then(results => {
|
||||
const flattened: any[] = [];
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].columnsForDropdown) {
|
||||
this.logAnalyticsColumns[results[i].refId] = results[i].columnsForDropdown;
|
||||
}
|
||||
flattened.push(results[i]);
|
||||
}
|
||||
|
||||
return flattened;
|
||||
});
|
||||
}
|
||||
|
||||
doQueries(queries) {
|
||||
return _.map(queries, query => {
|
||||
return this.doRequest(query.url)
|
||||
.then(result => {
|
||||
return {
|
||||
result: result,
|
||||
query: query,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
throw {
|
||||
error: err,
|
||||
query: query,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
annotationQuery(options) {}
|
||||
|
||||
metricFindQuery(query: string) {
|
||||
const appInsightsMetricNameQuery = query.match(/^AppInsightsMetricNames\(\)/i);
|
||||
if (appInsightsMetricNameQuery) {
|
||||
return this.getMetricNames();
|
||||
}
|
||||
|
||||
const appInsightsGroupByQuery = query.match(/^AppInsightsGroupBys\(([^\)]+?)(,\s?([^,]+?))?\)/i);
|
||||
if (appInsightsGroupByQuery) {
|
||||
const metricName = appInsightsGroupByQuery[1];
|
||||
return this.getGroupBys(this.templateSrv.replace(metricName));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
const url = `${this.baseUrl}/metrics/metadata`;
|
||||
return this.doRequest(url)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Successfully queried the Application Insights service.',
|
||||
title: 'Success',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Returned http status code ' + response.status,
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
let message = 'Application Insights: ';
|
||||
message += error.statusText ? error.statusText + ': ' : '';
|
||||
|
||||
if (error.data && error.data.error && error.data.error.code === 'PathNotFoundError') {
|
||||
message += 'Invalid Application Id for Application Insights service.';
|
||||
} else if (error.data && error.data.error) {
|
||||
message += error.data.error.code + '. ' + error.data.error.message;
|
||||
} else {
|
||||
message += 'Cannot connect to Application Insights REST API.';
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
message: message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
doRequest(url, maxRetries = 1) {
|
||||
return this.backendSrv
|
||||
.datasourceRequest({
|
||||
url: this.url + url,
|
||||
method: 'GET',
|
||||
})
|
||||
.catch(error => {
|
||||
if (maxRetries > 0) {
|
||||
return this.doRequest(url, maxRetries - 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
getMetricNames() {
|
||||
const url = `${this.baseUrl}/metrics/metadata`;
|
||||
return this.doRequest(url).then(ResponseParser.parseMetricNames);
|
||||
}
|
||||
|
||||
getMetricMetadata(metricName: string) {
|
||||
const url = `${this.baseUrl}/metrics/metadata`;
|
||||
return this.doRequest(url).then(result => {
|
||||
return new ResponseParser(result).parseMetadata(metricName);
|
||||
});
|
||||
}
|
||||
|
||||
getGroupBys(metricName: string) {
|
||||
return this.getMetricMetadata(metricName).then(result => {
|
||||
return new ResponseParser(result).parseGroupBys();
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import AppInsightsQuerystringBuilder from './app_insights_querystring_builder';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('AppInsightsQuerystringBuilder', () => {
|
||||
let builder: AppInsightsQuerystringBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = new AppInsightsQuerystringBuilder(moment.utc('2017-08-22 06:00'), moment.utc('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);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import TimeGrainConverter from '../time_grain_converter';
|
||||
|
||||
export default class AppInsightsQuerystringBuilder {
|
||||
aggregation = '';
|
||||
groupBy = '';
|
||||
timeGrainType = '';
|
||||
timeGrain = '';
|
||||
timeGrainUnit = '';
|
||||
filter = '';
|
||||
|
||||
constructor(private from, private to, public grafanaInterval) {}
|
||||
|
||||
setAggregation(aggregation) {
|
||||
this.aggregation = aggregation;
|
||||
}
|
||||
|
||||
setGroupBy(groupBy) {
|
||||
this.groupBy = groupBy;
|
||||
}
|
||||
|
||||
setInterval(timeGrainType, timeGrain, timeGrainUnit) {
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class ResponseParser {
|
||||
constructor(private results) {}
|
||||
|
||||
parseQueryResult() {
|
||||
let data: any = [];
|
||||
let columns: any = [];
|
||||
for (let i = 0; i < this.results.length; i++) {
|
||||
if (this.results[i].query.raw) {
|
||||
const xaxis = this.results[i].query.xaxis;
|
||||
const yaxises = this.results[i].query.yaxis;
|
||||
const spliton = this.results[i].query.spliton;
|
||||
columns = this.results[i].result.data.Tables[0].Columns;
|
||||
const rows = this.results[i].result.data.Tables[0].Rows;
|
||||
data = _.concat(
|
||||
data,
|
||||
this.parseRawQueryResultRow(this.results[i].query, columns, rows, xaxis, yaxises, spliton)
|
||||
);
|
||||
} else {
|
||||
const value = this.results[i].result.data.value;
|
||||
const alias = this.results[i].query.alias;
|
||||
data = _.concat(data, this.parseQueryResultRow(this.results[i].query, value, alias));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
parseRawQueryResultRow(query: any, columns, rows, xaxis: string, yaxises: string, spliton: string) {
|
||||
const data: any[] = [];
|
||||
const columnsForDropdown = _.map(columns, column => ({ text: column.ColumnName, value: column.ColumnName }));
|
||||
|
||||
const xaxisColumn = columns.findIndex(column => column.ColumnName === xaxis);
|
||||
const yaxisesSplit = yaxises.split(',');
|
||||
const yaxisColumns = {};
|
||||
_.forEach(yaxisesSplit, yaxis => {
|
||||
yaxisColumns[yaxis] = columns.findIndex(column => column.ColumnName === yaxis);
|
||||
});
|
||||
const splitonColumn = columns.findIndex(column => column.ColumnName === spliton);
|
||||
const convertTimestamp = xaxis === 'timestamp';
|
||||
|
||||
_.forEach(rows, row => {
|
||||
_.forEach(yaxisColumns, (yaxisColumn, yaxisName) => {
|
||||
const bucket =
|
||||
splitonColumn === -1
|
||||
? ResponseParser.findOrCreateBucket(data, yaxisName)
|
||||
: ResponseParser.findOrCreateBucket(data, row[splitonColumn]);
|
||||
const epoch = convertTimestamp ? ResponseParser.dateTimeToEpoch(row[xaxisColumn]) : row[xaxisColumn];
|
||||
bucket.datapoints.push([row[yaxisColumn], epoch]);
|
||||
bucket.refId = query.refId;
|
||||
bucket.query = query.query;
|
||||
bucket.columnsForDropdown = columnsForDropdown;
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
parseQueryResultRow(query: any, value, alias: string) {
|
||||
const data: any[] = [];
|
||||
|
||||
if (ResponseParser.isSingleValue(value)) {
|
||||
const metricName = ResponseParser.getMetricFieldKey(value);
|
||||
const aggField = ResponseParser.getKeyForAggregationField(value[metricName]);
|
||||
const epoch = ResponseParser.dateTimeToEpoch(value.end);
|
||||
data.push({
|
||||
target: metricName,
|
||||
datapoints: [[value[metricName][aggField], epoch]],
|
||||
refId: query.refId,
|
||||
query: query.query,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
const groupedBy = ResponseParser.hasSegmentsField(value.segments[0]);
|
||||
if (!groupedBy) {
|
||||
const metricName = ResponseParser.getMetricFieldKey(value.segments[0]);
|
||||
const dataTarget = ResponseParser.findOrCreateBucket(data, metricName);
|
||||
|
||||
for (let i = 0; i < value.segments.length; i++) {
|
||||
const epoch = ResponseParser.dateTimeToEpoch(value.segments[i].end);
|
||||
const aggField: string = ResponseParser.getKeyForAggregationField(value.segments[i][metricName]);
|
||||
|
||||
dataTarget.datapoints.push([value.segments[i][metricName][aggField], epoch]);
|
||||
}
|
||||
dataTarget.refId = query.refId;
|
||||
dataTarget.query = query.query;
|
||||
} else {
|
||||
for (let i = 0; i < value.segments.length; i++) {
|
||||
const epoch = ResponseParser.dateTimeToEpoch(value.segments[i].end);
|
||||
|
||||
for (let j = 0; j < value.segments[i].segments.length; j++) {
|
||||
const metricName = ResponseParser.getMetricFieldKey(value.segments[i].segments[j]);
|
||||
const aggField = ResponseParser.getKeyForAggregationField(value.segments[i].segments[j][metricName]);
|
||||
const target = this.getTargetName(value.segments[i].segments[j], alias);
|
||||
|
||||
const bucket = ResponseParser.findOrCreateBucket(data, target);
|
||||
bucket.datapoints.push([value.segments[i].segments[j][metricName][aggField], epoch]);
|
||||
bucket.refId = query.refId;
|
||||
bucket.query = query.query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
getTargetName(segment, alias: string) {
|
||||
let metric = '';
|
||||
let segmentName = '';
|
||||
let segmentValue = '';
|
||||
for (const prop in segment) {
|
||||
if (_.isObject(segment[prop])) {
|
||||
metric = prop;
|
||||
} else {
|
||||
segmentName = prop;
|
||||
segmentValue = segment[prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (alias) {
|
||||
const regex = /\{\{([\s\S]+?)\}\}/g;
|
||||
return alias.replace(regex, (match, g1, g2) => {
|
||||
const group = g1 || g2;
|
||||
|
||||
if (group === 'metric') {
|
||||
return metric;
|
||||
} else if (group === 'groupbyname') {
|
||||
return segmentName;
|
||||
} else if (group === 'groupbyvalue') {
|
||||
return segmentValue;
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
return metric + `{${segmentName}="${segmentValue}"}`;
|
||||
}
|
||||
|
||||
static isSingleValue(value) {
|
||||
return !ResponseParser.hasSegmentsField(value);
|
||||
}
|
||||
|
||||
static findOrCreateBucket(data, target) {
|
||||
let dataTarget = _.find(data, ['target', target]);
|
||||
if (!dataTarget) {
|
||||
dataTarget = { target: target, datapoints: [] };
|
||||
data.push(dataTarget);
|
||||
}
|
||||
|
||||
return dataTarget;
|
||||
}
|
||||
|
||||
static hasSegmentsField(obj) {
|
||||
const keys = _.keys(obj);
|
||||
return _.indexOf(keys, 'segments') > -1;
|
||||
}
|
||||
|
||||
static getMetricFieldKey(segment) {
|
||||
const keys = _.keys(segment);
|
||||
|
||||
return _.filter(_.without(keys, 'start', 'end'), key => {
|
||||
return _.isObject(segment[key]);
|
||||
})[0];
|
||||
}
|
||||
|
||||
static getKeyForAggregationField(dataObj): string {
|
||||
const keys = _.keys(dataObj);
|
||||
return _.intersection(keys, ['sum', 'avg', 'min', 'max', 'count', 'unique'])[0];
|
||||
}
|
||||
|
||||
static dateTimeToEpoch(dateTime) {
|
||||
return moment(dateTime).valueOf();
|
||||
}
|
||||
|
||||
static parseMetricNames(result) {
|
||||
const keys = _.keys(result.data.metrics);
|
||||
|
||||
return ResponseParser.toTextValueList(keys);
|
||||
}
|
||||
|
||||
parseMetadata(metricName: string) {
|
||||
const metric = this.results.data.metrics[metricName];
|
||||
|
||||
if (!metric) {
|
||||
throw Error('No data found for metric: ' + metricName);
|
||||
}
|
||||
|
||||
return {
|
||||
primaryAggType: metric.defaultAggregation,
|
||||
supportedAggTypes: metric.supportedAggregations,
|
||||
supportedGroupBy: metric.supportedGroupBy.all,
|
||||
};
|
||||
}
|
||||
|
||||
parseGroupBys() {
|
||||
return ResponseParser.toTextValueList(this.results.supportedGroupBy);
|
||||
}
|
||||
|
||||
static toTextValueList(values) {
|
||||
const list: any[] = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
list.push({
|
||||
text: values[i],
|
||||
value: values[i],
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
@ -0,0 +1,305 @@
|
||||
export default class FakeSchemaData {
|
||||
static getLogAnalyticsFakeSchema() {
|
||||
return {
|
||||
Tables: [
|
||||
{
|
||||
TableName: 'Table_0',
|
||||
Columns: [
|
||||
{
|
||||
ColumnName: 'TableName',
|
||||
DataType: 'String',
|
||||
},
|
||||
{
|
||||
ColumnName: 'ColumnName',
|
||||
DataType: 'String',
|
||||
},
|
||||
{
|
||||
ColumnName: 'ColumnType',
|
||||
DataType: 'String',
|
||||
},
|
||||
],
|
||||
Rows: [
|
||||
['AzureNetworkAnalytics_CL', 'SourceSystem', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'ManagementGroupName', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'TimeGenerated', 'System.DateTime'],
|
||||
['AzureNetworkAnalytics_CL', 'Computer', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'FASchemaVersion_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'FlowType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SrcIP_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'DestIP_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VMIP_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'L4Protocol_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'L7Protocol_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'FlowDirection_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'NSGList_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'NSGRules_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'HopNSGList_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'HopNSGRules_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Region1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Region2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'NIC_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'NIC1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'NIC2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VM_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VM1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VM2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subnet_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'ConnectionName_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'S2SConnection_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'S2SConnectionType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Country_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'AzureRegion_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subscription1_g', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subscription2_g', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'FlowStartTime_t', 'System.DateTime'],
|
||||
['AzureNetworkAnalytics_CL', 'FlowEndTime_t', 'System.DateTime'],
|
||||
['AzureNetworkAnalytics_CL', 'DestPort_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'AllowedInFlows_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'DeniedInFlows_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'AllowedOutFlows_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'DeniedOutFlows_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'DeniedInFlowsAtHops_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'DeniedOutFlowsAtHops_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'FlowCount_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'NextHopIP_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'IsVirtualAppliance_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'AddressPrefix_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'NextHopType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'RouteTable_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subnet1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subnet2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SubnetRegion1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SubnetRegion2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualAppliances_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'AllowForwardedTraffic_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'AllowGatewayTransit_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'AllowVirtualNetworkAccess_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'UseRemoteGateways_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'NSG_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'PrivateIPAddresses_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'PublicIPAddresses_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subnetwork_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualMachine_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'MACAddress_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'AddressPrefixes_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'ConnectingVirtualNetwork_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'RemoteVirtualNetworkGateway_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'IsFlowEnabled_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'GatewayType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SKU_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VIPAddress_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualSubnetwork_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'BGPEnabled_b', 'System.Boolean'],
|
||||
['AzureNetworkAnalytics_CL', 'ConnectionStatus_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'ConnectionType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'GatewayConnectionType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'LocalNetworkGateway_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualNetwork1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualNetwork2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualNetworkGateway1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualNetworkGateway2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualNetworkRegion1_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'VirtualNetworkRegion2_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'EgressBytesTransferred_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'IngressBytesTransferred_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'RoutingWeight_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'FrontendSubnet_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'LoadBalancerType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Access_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Description_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'DestinationAddressPrefix_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'DestinationPortRange_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Direction_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Protocol_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'RuleType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SourceAddressPrefix_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SourcePortRange_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Priority_d', 'System.Double'],
|
||||
['AzureNetworkAnalytics_CL', 'IPAddress', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SubnetPrefixes_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SchemaVersion_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Name_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Region_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'AppGatewayType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'BackendSubnets_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'FrontendIPs_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'GatewaySubnet_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'ComponentType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'DiscoveryRegion_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'ResourceType', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Status_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'SubType_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'TopologyVersion_s', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'Subscription_g', 'System.String'],
|
||||
['AzureNetworkAnalytics_CL', 'TimeProcessed_t', 'System.DateTime'],
|
||||
['AzureNetworkAnalytics_CL', 'Type', 'System.String'],
|
||||
],
|
||||
KqlPrimaryTimestampColumnName: 'TimeGenerated',
|
||||
},
|
||||
{
|
||||
TableName: 'Table_1',
|
||||
Columns: [
|
||||
{
|
||||
ColumnName: 'TableType',
|
||||
DataType: 'String',
|
||||
},
|
||||
{
|
||||
ColumnName: 'TableName',
|
||||
DataType: 'String',
|
||||
},
|
||||
{
|
||||
ColumnName: 'PrimaryTimestampColumnName',
|
||||
DataType: 'String',
|
||||
},
|
||||
{
|
||||
ColumnName: 'Solutions',
|
||||
DataType: 'String',
|
||||
},
|
||||
],
|
||||
Rows: [['oms', 'AzureNetworkAnalytics_CL', 'TimeGenerated', 'LogManagement']],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
static getlogAnalyticsFakeMetadata() {
|
||||
return {
|
||||
tables: [
|
||||
{
|
||||
id: 't/Alert',
|
||||
name: 'Alert',
|
||||
timespanColumn: 'TimeGenerated',
|
||||
columns: [
|
||||
{ name: 'TimeGenerated', type: 'datetime' },
|
||||
{ name: 'AlertSeverity', type: 'string' },
|
||||
{ name: 'SourceDisplayName', type: 'string' },
|
||||
{ name: 'AlertName', type: 'string' },
|
||||
{ name: 'AlertDescription', type: 'string' },
|
||||
{ name: 'SourceSystem', type: 'string' },
|
||||
{ name: 'QueryExecutionStartTime', type: 'datetime' },
|
||||
{ name: 'QueryExecutionEndTime', type: 'datetime' },
|
||||
{ name: 'Query', type: 'string' },
|
||||
{ name: 'RemediationJobId', type: 'string' },
|
||||
{ name: 'RemediationRunbookName', type: 'string' },
|
||||
{ name: 'AlertRuleId', type: 'string' },
|
||||
{ name: 'AlertRuleInstanceId', type: 'string' },
|
||||
{ name: 'ThresholdOperator', type: 'string' },
|
||||
{ name: 'ThresholdValue', type: 'int' },
|
||||
{ name: 'LinkToSearchResults', type: 'string' },
|
||||
{ name: 'ServiceDeskConnectionName', type: 'string' },
|
||||
{ name: 'ServiceDeskId', type: 'string' },
|
||||
{ name: 'ServiceDeskWorkItemLink', type: 'string' },
|
||||
{ name: 'ServiceDeskWorkItemType', type: 'string' },
|
||||
{ name: 'ResourceId', type: 'string' },
|
||||
{ name: 'ResourceType', type: 'string' },
|
||||
{ name: 'ResourceValue', type: 'string' },
|
||||
{ name: 'RootObjectName', type: 'string' },
|
||||
{ name: 'ObjectDisplayName', type: 'string' },
|
||||
{ name: 'Computer', type: 'string' },
|
||||
{ name: 'AlertPriority', type: 'string' },
|
||||
{ name: 'SourceFullName', type: 'string' },
|
||||
{ name: 'AlertId', type: 'string' },
|
||||
{ name: 'RepeatCount', type: 'int' },
|
||||
{ name: 'AlertState', type: 'string' },
|
||||
{ name: 'ResolvedBy', type: 'string' },
|
||||
{ name: 'LastModifiedBy', type: 'string' },
|
||||
{ name: 'TimeRaised', type: 'datetime' },
|
||||
{ name: 'TimeResolved', type: 'datetime' },
|
||||
{ name: 'TimeLastModified', type: 'datetime' },
|
||||
{ name: 'AlertContext', type: 'string' },
|
||||
{ name: 'TicketId', type: 'string' },
|
||||
{ name: 'Custom1', type: 'string' },
|
||||
{ name: 'Custom2', type: 'string' },
|
||||
{ name: 'Custom3', type: 'string' },
|
||||
{ name: 'Custom4', type: 'string' },
|
||||
{ name: 'Custom5', type: 'string' },
|
||||
{ name: 'Custom6', type: 'string' },
|
||||
{ name: 'Custom7', type: 'string' },
|
||||
{ name: 'Custom8', type: 'string' },
|
||||
{ name: 'Custom9', type: 'string' },
|
||||
{ name: 'Custom10', type: 'string' },
|
||||
{ name: 'ManagementGroupName', type: 'string' },
|
||||
{ name: 'PriorityNumber', type: 'int' },
|
||||
{ name: 'HostName', type: 'string' },
|
||||
{ name: 'StateType', type: 'string' },
|
||||
{ name: 'AlertTypeDescription', type: 'string' },
|
||||
{ name: 'AlertTypeNumber', type: 'int' },
|
||||
{ name: 'AlertError', type: 'string' },
|
||||
{ name: 'StatusDescription', type: 'string' },
|
||||
{ name: 'AlertStatus', type: 'int' },
|
||||
{ name: 'TriggerId', type: 'string' },
|
||||
{ name: 'Url', type: 'string' },
|
||||
{ name: 'ValueDescription', type: 'string' },
|
||||
{ name: 'AlertValue', type: 'int' },
|
||||
{ name: 'Comments', type: 'string' },
|
||||
{ name: 'TemplateId', type: 'string' },
|
||||
{ name: 'FlagsDescription', type: 'string' },
|
||||
{ name: 'Flags', type: 'int' },
|
||||
{ name: 'ValueFlagsDescription', type: 'string' },
|
||||
{ name: 'ValueFlags', type: 'int' },
|
||||
{ name: 'Expression', type: 'string' },
|
||||
{ name: 'Type', type: 'string' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 't/AzureActivity',
|
||||
name: 'AzureActivity',
|
||||
timespanColumn: 'TimeGenerated',
|
||||
columns: [
|
||||
{ name: 'OperationName', type: 'string' },
|
||||
{ name: 'Level', type: 'string' },
|
||||
{ name: 'ActivityStatus', type: 'string' },
|
||||
{ name: 'ActivitySubstatus', type: 'string' },
|
||||
{ name: 'ResourceGroup', type: 'string' },
|
||||
{ name: 'SubscriptionId', type: 'string' },
|
||||
{ name: 'CorrelationId', type: 'string' },
|
||||
{ name: 'Caller', type: 'string' },
|
||||
{ name: 'CallerIpAddress', type: 'string' },
|
||||
{ name: 'Category', type: 'string' },
|
||||
{ name: 'HTTPRequest', type: 'string' },
|
||||
{ name: 'Properties', type: 'string' },
|
||||
{ name: 'EventSubmissionTimestamp', type: 'datetime' },
|
||||
{ name: 'Authorization', type: 'string' },
|
||||
{ name: 'ResourceId', type: 'string' },
|
||||
{ name: 'OperationId', type: 'string' },
|
||||
{ name: 'ResourceProvider', type: 'string' },
|
||||
{ name: 'Resource', type: 'string' },
|
||||
{ name: 'TimeGenerated', type: 'datetime' },
|
||||
{ name: 'SourceSystem', type: 'string' },
|
||||
{ name: 'Type', type: 'string' },
|
||||
],
|
||||
},
|
||||
],
|
||||
tableGroups: [
|
||||
{
|
||||
id: 'oms/LogManagement',
|
||||
name: 'LogManagement',
|
||||
source: 'oms',
|
||||
tables: ['t/Alert', 't/AzureActivity'],
|
||||
},
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
id: 'f/Func1',
|
||||
name: 'Func1',
|
||||
displayName: 'Func1',
|
||||
body: 'AzureActivity\n| where ActivityStatus == "" \n',
|
||||
category: 'test',
|
||||
},
|
||||
],
|
||||
applications: [],
|
||||
workspaces: [
|
||||
{
|
||||
id: 'a2c1b44e-3e57-4410-b027-999999999999',
|
||||
name: 'danieltest',
|
||||
resourceId:
|
||||
'/subscriptions/44693801-6ee6-49de-9b2d-999999999999/resourceGroups/danieltest/providers/' +
|
||||
'microsoft.operationalinsights/workspaces/danieltest',
|
||||
tables: [],
|
||||
tableGroups: ['oms/LogManagement'],
|
||||
functions: ['f/Func1'],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,401 @@
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import FakeSchemaData from './__mocks__/schema';
|
||||
import Q from 'q';
|
||||
import moment from 'moment';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
describe('AzureLogAnalyticsDatasource', () => {
|
||||
const ctx: any = {
|
||||
backendSrv: {},
|
||||
templateSrv: new TemplateSrv(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.$q = Q;
|
||||
ctx.instanceSettings = {
|
||||
jsonData: { logAnalyticsSubscriptionId: 'xxx' },
|
||||
url: 'http://azureloganalyticsapi',
|
||||
};
|
||||
|
||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
|
||||
});
|
||||
|
||||
describe('When the config option "Same as Azure Monitor" has been chosen', () => {
|
||||
const tableResponseWithOneColumn = {
|
||||
tables: [
|
||||
{
|
||||
name: 'PrimaryResult',
|
||||
columns: [
|
||||
{
|
||||
name: 'Category',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
rows: [['Administrative'], ['Policy']],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workspaceResponse = {
|
||||
value: [
|
||||
{
|
||||
name: 'aworkspace',
|
||||
properties: {
|
||||
source: 'Azure',
|
||||
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let workspacesUrl;
|
||||
let azureLogAnalyticsUrl;
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx.instanceSettings.jsonData.subscriptionId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.tenantId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.clientId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.azureLogAnalyticsSameAs = true;
|
||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
|
||||
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
|
||||
workspacesUrl = options.url;
|
||||
return ctx.$q.when({ data: workspaceResponse, status: 200 });
|
||||
} else {
|
||||
azureLogAnalyticsUrl = options.url;
|
||||
return ctx.$q.when({ data: tableResponseWithOneColumn, status: 200 });
|
||||
}
|
||||
};
|
||||
|
||||
await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
|
||||
});
|
||||
|
||||
it('should use the sameasloganalyticsazure plugin route', () => {
|
||||
expect(workspacesUrl).toContain('azuremonitor');
|
||||
expect(azureLogAnalyticsUrl).toContain('sameasloganalyticsazure');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing testDatasource', () => {
|
||||
describe('and an error is returned', () => {
|
||||
const error = {
|
||||
data: {
|
||||
error: {
|
||||
code: 'InvalidApiVersionParameter',
|
||||
message: `An error message.`,
|
||||
},
|
||||
},
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.instanceSettings.jsonData.logAnalyticsSubscriptionId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.logAnalyticsTenantId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.logAnalyticsClientId = 'xxx';
|
||||
ctx.backendSrv.datasourceRequest = () => {
|
||||
return ctx.$q.reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return error status and a detailed error message', () => {
|
||||
return ctx.ds.testDatasource().then(results => {
|
||||
expect(results.status).toEqual('error');
|
||||
expect(results.message).toEqual(
|
||||
'1. Azure Log Analytics: Bad Request: InvalidApiVersionParameter. An error message. '
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing query', () => {
|
||||
const options = {
|
||||
range: {
|
||||
from: moment.utc('2017-08-22T20:00:00Z'),
|
||||
to: moment.utc('2017-08-22T23:59:00Z'),
|
||||
},
|
||||
rangeRaw: {
|
||||
from: 'now-4h',
|
||||
to: 'now',
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
apiVersion: '2016-09-01',
|
||||
refId: 'A',
|
||||
queryType: 'Azure Log Analytics',
|
||||
azureLogAnalytics: {
|
||||
resultFormat: 'time_series',
|
||||
query:
|
||||
'AzureActivity | where TimeGenerated > ago(2h) ' +
|
||||
'| summarize count() by Category, bin(TimeGenerated, 5min) ' +
|
||||
'| project TimeGenerated, Category, count_ | order by TimeGenerated asc',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = {
|
||||
tables: [
|
||||
{
|
||||
name: 'PrimaryResult',
|
||||
columns: [
|
||||
{
|
||||
name: 'TimeGenerated',
|
||||
type: 'datetime',
|
||||
},
|
||||
{
|
||||
name: 'Category',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'count_',
|
||||
type: 'long',
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
['2018-06-02T20:20:00Z', 'Administrative', 2],
|
||||
['2018-06-02T20:25:00Z', 'Administrative', 22],
|
||||
['2018-06-02T20:30:00Z', 'Policy', 20],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('in time series format', () => {
|
||||
describe('and the data is valid (has time, metric and value columns)', () => {
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('query=AzureActivity');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(2);
|
||||
expect(results.data[0].datapoints.length).toBe(2);
|
||||
expect(results.data[0].target).toEqual('Administrative');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1527970800000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(2);
|
||||
expect(results.data[0].datapoints[1][1]).toEqual(1527971100000);
|
||||
expect(results.data[0].datapoints[1][0]).toEqual(22);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the data has no time column)', () => {
|
||||
beforeEach(() => {
|
||||
const invalidResponse = {
|
||||
tables: [
|
||||
{
|
||||
name: 'PrimaryResult',
|
||||
columns: [
|
||||
{
|
||||
name: 'Category',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'count_',
|
||||
type: 'long',
|
||||
},
|
||||
],
|
||||
rows: [['Administrative', 2]],
|
||||
},
|
||||
],
|
||||
};
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('query=AzureActivity');
|
||||
return ctx.$q.when({ data: invalidResponse, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should throw an exception', () => {
|
||||
ctx.ds.query(options).catch(err => {
|
||||
expect(err.message).toContain('The Time Series format requires a time column.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('in tableformat', () => {
|
||||
beforeEach(() => {
|
||||
options.targets[0].azureLogAnalytics.resultFormat = 'table';
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('query=AzureActivity');
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of columns and rows', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data[0].type).toBe('table');
|
||||
expect(results.data[0].columns.length).toBe(3);
|
||||
expect(results.data[0].rows.length).toBe(3);
|
||||
expect(results.data[0].columns[0].text).toBe('TimeGenerated');
|
||||
expect(results.data[0].columns[0].type).toBe('datetime');
|
||||
expect(results.data[0].columns[1].text).toBe('Category');
|
||||
expect(results.data[0].columns[1].type).toBe('string');
|
||||
expect(results.data[0].columns[2].text).toBe('count_');
|
||||
expect(results.data[0].columns[2].type).toBe('long');
|
||||
expect(results.data[0].rows[0][0]).toEqual('2018-06-02T20:20:00Z');
|
||||
expect(results.data[0].rows[0][1]).toEqual('Administrative');
|
||||
expect(results.data[0].rows[0][2]).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getSchema', () => {
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain('metadata');
|
||||
return ctx.$q.when({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a schema with a table and rows', () => {
|
||||
return ctx.ds.azureLogAnalyticsDatasource.getSchema('myWorkspace').then(result => {
|
||||
expect(Object.keys(result.Databases.Default.Tables).length).toBe(2);
|
||||
expect(result.Databases.Default.Tables.Alert.Name).toBe('Alert');
|
||||
expect(result.Databases.Default.Tables.AzureActivity.Name).toBe('AzureActivity');
|
||||
expect(result.Databases.Default.Tables.Alert.OrderedColumns.length).toBe(69);
|
||||
expect(result.Databases.Default.Tables.AzureActivity.OrderedColumns.length).toBe(21);
|
||||
expect(result.Databases.Default.Tables.Alert.OrderedColumns[0].Name).toBe('TimeGenerated');
|
||||
expect(result.Databases.Default.Tables.Alert.OrderedColumns[0].Type).toBe('datetime');
|
||||
|
||||
expect(Object.keys(result.Databases.Default.Functions).length).toBe(1);
|
||||
expect(result.Databases.Default.Functions.Func1.Name).toBe('Func1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery', () => {
|
||||
const tableResponseWithOneColumn = {
|
||||
tables: [
|
||||
{
|
||||
name: 'PrimaryResult',
|
||||
columns: [
|
||||
{
|
||||
name: 'Category',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
rows: [['Administrative'], ['Policy']],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workspaceResponse = {
|
||||
value: [
|
||||
{
|
||||
name: 'aworkspace',
|
||||
properties: {
|
||||
source: 'Azure',
|
||||
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let queryResults;
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
|
||||
return ctx.$q.when({ data: workspaceResponse, status: 200 });
|
||||
} else {
|
||||
return ctx.$q.when({ data: tableResponseWithOneColumn, status: 200 });
|
||||
}
|
||||
};
|
||||
|
||||
queryResults = await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
|
||||
});
|
||||
|
||||
it('should return a list of categories in the correct format', () => {
|
||||
expect(queryResults.length).toBe(2);
|
||||
expect(queryResults[0].text).toBe('Administrative');
|
||||
expect(queryResults[0].value).toBe('Administrative');
|
||||
expect(queryResults[1].text).toBe('Policy');
|
||||
expect(queryResults[1].value).toBe('Policy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing annotationQuery', () => {
|
||||
const tableResponse = {
|
||||
tables: [
|
||||
{
|
||||
name: 'PrimaryResult',
|
||||
columns: [
|
||||
{
|
||||
name: 'TimeGenerated',
|
||||
type: 'datetime',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'Tags',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
rows: [['2018-06-02T20:20:00Z', 'Computer1', 'tag1,tag2'], ['2018-06-02T20:28:00Z', 'Computer2', 'tag2']],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workspaceResponse = {
|
||||
value: [
|
||||
{
|
||||
name: 'aworkspace',
|
||||
properties: {
|
||||
source: 'Azure',
|
||||
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let annotationResults;
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
|
||||
return ctx.$q.when({ data: workspaceResponse, status: 200 });
|
||||
} else {
|
||||
return ctx.$q.when({ data: tableResponse, status: 200 });
|
||||
}
|
||||
};
|
||||
|
||||
annotationResults = await ctx.ds.annotationQuery({
|
||||
annotation: {
|
||||
rawQuery: 'Heartbeat | where $__timeFilter()| project TimeGenerated, Text=Computer, tags="test"',
|
||||
workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
range: {
|
||||
from: moment.utc('2017-08-22T20:00:00Z'),
|
||||
to: moment.utc('2017-08-22T23:59:00Z'),
|
||||
},
|
||||
rangeRaw: {
|
||||
from: 'now-4h',
|
||||
to: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of categories in the correct format', () => {
|
||||
expect(annotationResults.length).toBe(2);
|
||||
|
||||
expect(annotationResults[0].time).toBe(1527970800000);
|
||||
expect(annotationResults[0].text).toBe('Computer1');
|
||||
expect(annotationResults[0].tags[0]).toBe('tag1');
|
||||
expect(annotationResults[0].tags[1]).toBe('tag2');
|
||||
|
||||
expect(annotationResults[1].time).toBe(1527971280000);
|
||||
expect(annotationResults[1].text).toBe('Computer2');
|
||||
expect(annotationResults[1].tags[0]).toBe('tag2');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,321 @@
|
||||
import _ from 'lodash';
|
||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
||||
import ResponseParser from './response_parser';
|
||||
|
||||
export default class AzureLogAnalyticsDatasource {
|
||||
id: number;
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
applicationId: string;
|
||||
azureMonitorUrl: string;
|
||||
defaultOrFirstWorkspace: string;
|
||||
subscriptionId: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private instanceSettings, private backendSrv, private templateSrv, private $q) {
|
||||
this.id = instanceSettings.id;
|
||||
this.baseUrl = this.instanceSettings.jsonData.azureLogAnalyticsSameAs
|
||||
? '/sameasloganalyticsazure'
|
||||
: `/loganalyticsazure`;
|
||||
this.url = instanceSettings.url;
|
||||
this.defaultOrFirstWorkspace = this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace;
|
||||
|
||||
this.setWorkspaceUrl();
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return (
|
||||
(!!this.instanceSettings.jsonData.logAnalyticsSubscriptionId &&
|
||||
this.instanceSettings.jsonData.logAnalyticsSubscriptionId.length > 0) ||
|
||||
!!this.instanceSettings.jsonData.azureLogAnalyticsSameAs
|
||||
);
|
||||
}
|
||||
|
||||
setWorkspaceUrl() {
|
||||
if (!!this.instanceSettings.jsonData.subscriptionId || !!this.instanceSettings.jsonData.azureLogAnalyticsSameAs) {
|
||||
this.subscriptionId = this.instanceSettings.jsonData.subscriptionId;
|
||||
const azureCloud = this.instanceSettings.jsonData.cloudName || 'azuremonitor';
|
||||
this.azureMonitorUrl = `/${azureCloud}/subscriptions/${this.subscriptionId}`;
|
||||
} else {
|
||||
this.subscriptionId = this.instanceSettings.jsonData.logAnalyticsSubscriptionId;
|
||||
this.azureMonitorUrl = `/workspacesloganalytics/subscriptions/${this.subscriptionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
getWorkspaces() {
|
||||
const workspaceListUrl =
|
||||
this.azureMonitorUrl + '/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview';
|
||||
return this.doRequest(workspaceListUrl).then(response => {
|
||||
return (
|
||||
_.map(response.data.value, val => {
|
||||
return { text: val.name, value: val.properties.customerId };
|
||||
}) || []
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getSchema(workspace) {
|
||||
if (!workspace) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const url = `${this.baseUrl}/${workspace}/metadata`;
|
||||
|
||||
return this.doRequest(url).then(response => {
|
||||
return new ResponseParser(response.data).parseSchemaResult();
|
||||
});
|
||||
}
|
||||
|
||||
query(options) {
|
||||
const queries = _.filter(options.targets, item => {
|
||||
return item.hide !== true;
|
||||
}).map(target => {
|
||||
const item = target.azureLogAnalytics;
|
||||
|
||||
const querystringBuilder = new LogAnalyticsQuerystringBuilder(
|
||||
this.templateSrv.replace(item.query, options.scopedVars, this.interpolateVariable),
|
||||
options,
|
||||
'TimeGenerated'
|
||||
);
|
||||
const generated = querystringBuilder.generate();
|
||||
|
||||
const url = `${this.baseUrl}/${item.workspace}/query?${generated.uriString}`;
|
||||
|
||||
return {
|
||||
refId: target.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: this.id,
|
||||
url: url,
|
||||
query: generated.rawQuery,
|
||||
format: options.format,
|
||||
resultFormat: item.resultFormat,
|
||||
};
|
||||
});
|
||||
|
||||
if (!queries || queries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = this.doQueries(queries);
|
||||
|
||||
return this.$q.all(promises).then(results => {
|
||||
return new ResponseParser(results).parseQueryResult();
|
||||
});
|
||||
}
|
||||
|
||||
metricFindQuery(query: string) {
|
||||
return this.getDefaultOrFirstWorkspace().then(workspace => {
|
||||
const queries: any[] = this.buildQuery(query, null, workspace);
|
||||
|
||||
const promises = this.doQueries(queries);
|
||||
|
||||
return this.$q
|
||||
.all(promises)
|
||||
.then(results => {
|
||||
return new ResponseParser(results).parseToVariables();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err.error &&
|
||||
err.error.data &&
|
||||
err.error.data.error &&
|
||||
err.error.data.error.innererror &&
|
||||
err.error.data.error.innererror.innererror
|
||||
) {
|
||||
throw { message: err.error.data.error.innererror.innererror.message };
|
||||
} else if (err.error && err.error.data && err.error.data.error) {
|
||||
throw { message: err.error.data.error.message };
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildQuery(query: string, options: any, workspace: any) {
|
||||
const querystringBuilder = new LogAnalyticsQuerystringBuilder(
|
||||
this.templateSrv.replace(query, {}, this.interpolateVariable),
|
||||
options,
|
||||
'TimeGenerated'
|
||||
);
|
||||
const querystring = querystringBuilder.generate().uriString;
|
||||
const url = `${this.baseUrl}/${workspace}/query?${querystring}`;
|
||||
const queries: any[] = [];
|
||||
queries.push({
|
||||
datasourceId: this.id,
|
||||
url: url,
|
||||
resultFormat: 'table',
|
||||
});
|
||||
return queries;
|
||||
}
|
||||
|
||||
interpolateVariable(value, variable) {
|
||||
if (typeof value === 'string') {
|
||||
if (variable.multi || variable.includeAll) {
|
||||
return "'" + value + "'";
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const quotedValues = _.map(value, val => {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "'" + val + "'";
|
||||
});
|
||||
return quotedValues.join(',');
|
||||
}
|
||||
|
||||
getDefaultOrFirstWorkspace() {
|
||||
if (this.defaultOrFirstWorkspace) {
|
||||
return Promise.resolve(this.defaultOrFirstWorkspace);
|
||||
}
|
||||
|
||||
return this.getWorkspaces().then(workspaces => {
|
||||
this.defaultOrFirstWorkspace = workspaces[0].value;
|
||||
return this.defaultOrFirstWorkspace;
|
||||
});
|
||||
}
|
||||
|
||||
annotationQuery(options) {
|
||||
if (!options.annotation.rawQuery) {
|
||||
return this.$q.reject({
|
||||
message: 'Query missing in annotation definition',
|
||||
});
|
||||
}
|
||||
|
||||
const queries: any[] = this.buildQuery(options.annotation.rawQuery, options, options.annotation.workspace);
|
||||
|
||||
const promises = this.doQueries(queries);
|
||||
|
||||
return this.$q.all(promises).then(results => {
|
||||
const annotations = new ResponseParser(results).transformToAnnotations(options);
|
||||
return annotations;
|
||||
});
|
||||
}
|
||||
|
||||
doQueries(queries) {
|
||||
return _.map(queries, query => {
|
||||
return this.doRequest(query.url)
|
||||
.then(result => {
|
||||
return {
|
||||
result: result,
|
||||
query: query,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
throw {
|
||||
error: err,
|
||||
query: query,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
doRequest(url, maxRetries = 1) {
|
||||
return this.backendSrv
|
||||
.datasourceRequest({
|
||||
url: this.url + url,
|
||||
method: 'GET',
|
||||
})
|
||||
.catch(error => {
|
||||
if (maxRetries > 0) {
|
||||
return this.doRequest(url, maxRetries - 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
const validationError = this.isValidConfig();
|
||||
if (validationError) {
|
||||
return validationError;
|
||||
}
|
||||
|
||||
return this.getDefaultOrFirstWorkspace()
|
||||
.then(ws => {
|
||||
const url = `${this.baseUrl}/${ws}/metadata`;
|
||||
|
||||
return this.doRequest(url);
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Successfully queried the Azure Log Analytics service.',
|
||||
title: 'Success',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Returned http status code ' + response.status,
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
let message = 'Azure Log Analytics: ';
|
||||
if (error.config && error.config.url && error.config.url.indexOf('workspacesloganalytics') > -1) {
|
||||
message = 'Azure Log Analytics requires access to Azure Monitor but had the following error: ';
|
||||
}
|
||||
|
||||
message = this.getErrorMessage(message, error);
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
message: message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getErrorMessage(message: string, error: any) {
|
||||
message += error.statusText ? error.statusText + ': ' : '';
|
||||
if (error.data && error.data.error && error.data.error.code) {
|
||||
message += error.data.error.code + '. ' + error.data.error.message;
|
||||
} else if (error.data && error.data.error) {
|
||||
message += error.data.error;
|
||||
} else if (error.data) {
|
||||
message += error.data;
|
||||
} else {
|
||||
message += 'Cannot connect to Azure Log Analytics REST API.';
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
isValidConfig() {
|
||||
if (this.instanceSettings.jsonData.azureLogAnalyticsSameAs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsSubscriptionId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Subscription Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsTenantId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Tenant Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsClientId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Client Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isValidConfigField(field: string) {
|
||||
return field && field.length > 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
export interface DataTarget {
|
||||
target: string;
|
||||
datapoints: any[];
|
||||
refId: string;
|
||||
query: any;
|
||||
}
|
||||
export interface TableResult {
|
||||
columns: TableColumn[];
|
||||
rows: any[];
|
||||
type: string;
|
||||
refId: string;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface TableColumn {
|
||||
text: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface KustoSchema {
|
||||
Databases: { [key: string]: KustoDatabase };
|
||||
Plugins: any[];
|
||||
}
|
||||
export interface KustoDatabase {
|
||||
Name: string;
|
||||
Tables: { [key: string]: KustoTable };
|
||||
Functions: { [key: string]: KustoFunction };
|
||||
}
|
||||
|
||||
export interface KustoTable {
|
||||
Name: string;
|
||||
OrderedColumns: KustoColumn[];
|
||||
}
|
||||
|
||||
export interface KustoColumn {
|
||||
Name: string;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
export interface KustoFunction {
|
||||
Name: string;
|
||||
DocString: string;
|
||||
Body: string;
|
||||
Folder: string;
|
||||
FunctionKind: string;
|
||||
InputParameters: any[];
|
||||
OutputColumns: any[];
|
||||
}
|
||||
|
||||
export interface Variable {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface AnnotationItem {
|
||||
annotation: any;
|
||||
time: number;
|
||||
text: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export default class ResponseParser {
|
||||
columns: string[];
|
||||
constructor(private results) {}
|
||||
|
||||
parseQueryResult(): any {
|
||||
let data: any[] = [];
|
||||
let columns: any[] = [];
|
||||
for (let i = 0; i < this.results.length; i++) {
|
||||
if (this.results[i].result.data.tables.length === 0) {
|
||||
continue;
|
||||
}
|
||||
columns = this.results[i].result.data.tables[0].columns;
|
||||
const rows = this.results[i].result.data.tables[0].rows;
|
||||
|
||||
if (this.results[i].query.resultFormat === 'time_series') {
|
||||
data = _.concat(data, this.parseTimeSeriesResult(this.results[i].query, columns, rows));
|
||||
} else {
|
||||
data = _.concat(data, this.parseTableResult(this.results[i].query, columns, rows));
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
parseTimeSeriesResult(query, columns, rows): DataTarget[] {
|
||||
const data: DataTarget[] = [];
|
||||
let timeIndex = -1;
|
||||
let metricIndex = -1;
|
||||
let valueIndex = -1;
|
||||
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
if (timeIndex === -1 && columns[i].type === 'datetime') {
|
||||
timeIndex = i;
|
||||
}
|
||||
|
||||
if (metricIndex === -1 && columns[i].type === 'string') {
|
||||
metricIndex = i;
|
||||
}
|
||||
|
||||
if (valueIndex === -1 && ['int', 'long', 'real', 'double'].indexOf(columns[i].type) > -1) {
|
||||
valueIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (timeIndex === -1) {
|
||||
throw new Error('No datetime column found in the result. The Time Series format requires a time column.');
|
||||
}
|
||||
|
||||
_.forEach(rows, row => {
|
||||
const epoch = ResponseParser.dateTimeToEpoch(row[timeIndex]);
|
||||
const metricName = metricIndex > -1 ? row[metricIndex] : columns[valueIndex].name;
|
||||
const bucket = ResponseParser.findOrCreateBucket(data, metricName);
|
||||
bucket.datapoints.push([row[valueIndex], epoch]);
|
||||
bucket.refId = query.refId;
|
||||
bucket.query = query.query;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
parseTableResult(query, columns, rows): TableResult {
|
||||
const tableResult: TableResult = {
|
||||
type: 'table',
|
||||
columns: _.map(columns, col => {
|
||||
return { text: col.name, type: col.type };
|
||||
}),
|
||||
rows: rows,
|
||||
refId: query.refId,
|
||||
query: query.query,
|
||||
};
|
||||
|
||||
return tableResult;
|
||||
}
|
||||
|
||||
parseToVariables(): Variable[] {
|
||||
const queryResult = this.parseQueryResult();
|
||||
|
||||
const variables: Variable[] = [];
|
||||
_.forEach(queryResult, result => {
|
||||
_.forEach(_.flattenDeep(result.rows), row => {
|
||||
variables.push({
|
||||
text: row,
|
||||
value: row,
|
||||
} as Variable);
|
||||
});
|
||||
});
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
transformToAnnotations(options: any) {
|
||||
const queryResult = this.parseQueryResult();
|
||||
|
||||
const list: AnnotationItem[] = [];
|
||||
|
||||
_.forEach(queryResult, result => {
|
||||
let timeIndex = -1;
|
||||
let textIndex = -1;
|
||||
let tagsIndex = -1;
|
||||
|
||||
for (let i = 0; i < result.columns.length; i++) {
|
||||
if (timeIndex === -1 && result.columns[i].type === 'datetime') {
|
||||
timeIndex = i;
|
||||
}
|
||||
|
||||
if (textIndex === -1 && result.columns[i].text.toLowerCase() === 'text') {
|
||||
textIndex = i;
|
||||
}
|
||||
|
||||
if (tagsIndex === -1 && result.columns[i].text.toLowerCase() === 'tags') {
|
||||
tagsIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
_.forEach(result.rows, row => {
|
||||
list.push({
|
||||
annotation: options.annotation,
|
||||
time: Math.floor(ResponseParser.dateTimeToEpoch(row[timeIndex])),
|
||||
text: row[textIndex] ? row[textIndex].toString() : '',
|
||||
tags: row[tagsIndex] ? row[tagsIndex].trim().split(/\s*,\s*/) : [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
parseSchemaResult(): KustoSchema {
|
||||
return {
|
||||
Plugins: [
|
||||
{
|
||||
Name: 'pivot',
|
||||
},
|
||||
],
|
||||
Databases: this.createSchemaDatabaseWithTables(),
|
||||
};
|
||||
}
|
||||
|
||||
createSchemaDatabaseWithTables(): { [key: string]: KustoDatabase } {
|
||||
const databases = {
|
||||
Default: {
|
||||
Name: 'Default',
|
||||
Tables: this.createSchemaTables(),
|
||||
Functions: this.createSchemaFunctions(),
|
||||
},
|
||||
};
|
||||
|
||||
return databases;
|
||||
}
|
||||
|
||||
createSchemaTables(): { [key: string]: KustoTable } {
|
||||
const tables: { [key: string]: KustoTable } = {};
|
||||
|
||||
for (const table of this.results.tables) {
|
||||
tables[table.name] = {
|
||||
Name: table.name,
|
||||
OrderedColumns: [],
|
||||
};
|
||||
for (const col of table.columns) {
|
||||
tables[table.name].OrderedColumns.push(this.convertToKustoColumn(col));
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
convertToKustoColumn(col: any): KustoColumn {
|
||||
return {
|
||||
Name: col.name,
|
||||
Type: col.type,
|
||||
};
|
||||
}
|
||||
|
||||
createSchemaFunctions(): { [key: string]: KustoFunction } {
|
||||
const functions: { [key: string]: KustoFunction } = {};
|
||||
|
||||
for (const func of this.results.functions) {
|
||||
functions[func.name] = {
|
||||
Name: func.name,
|
||||
Body: func.body,
|
||||
DocString: func.displayName,
|
||||
Folder: func.category,
|
||||
FunctionKind: 'Unknown',
|
||||
InputParameters: [],
|
||||
OutputColumns: [],
|
||||
};
|
||||
}
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
static findOrCreateBucket(data, target): DataTarget {
|
||||
let dataTarget = _.find(data, ['target', target]);
|
||||
if (!dataTarget) {
|
||||
dataTarget = { target: target, datapoints: [], refId: '', query: '' };
|
||||
data.push(dataTarget);
|
||||
}
|
||||
|
||||
return dataTarget;
|
||||
}
|
||||
|
||||
static dateTimeToEpoch(dateTime) {
|
||||
return moment(dateTime).valueOf();
|
||||
}
|
||||
}
|
@ -0,0 +1,819 @@
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import Q from 'q';
|
||||
import moment from 'moment';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
describe('AzureMonitorDatasource', () => {
|
||||
const ctx: any = {
|
||||
backendSrv: {},
|
||||
templateSrv: new TemplateSrv(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.$q = Q;
|
||||
ctx.instanceSettings = {
|
||||
url: 'http://azuremonitor.com',
|
||||
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' },
|
||||
cloudName: 'azuremonitor',
|
||||
};
|
||||
|
||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
|
||||
});
|
||||
|
||||
describe('When performing testDatasource', () => {
|
||||
describe('and an error is returned', () => {
|
||||
const error = {
|
||||
data: {
|
||||
error: {
|
||||
code: 'InvalidApiVersionParameter',
|
||||
message: `An error message.`,
|
||||
},
|
||||
},
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.instanceSettings.jsonData.tenantId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.clientId = 'xxx';
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
return ctx.$q.reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return error status and a detailed error message', () => {
|
||||
return ctx.ds.testDatasource().then(results => {
|
||||
expect(results.status).toEqual('error');
|
||||
expect(results.message).toEqual(
|
||||
'1. Azure Monitor: Bad Request: InvalidApiVersionParameter. An error message. '
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and a list of resource groups is returned', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.instanceSettings.jsonData.tenantId = 'xxx';
|
||||
ctx.instanceSettings.jsonData.clientId = 'xxx';
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return success status', () => {
|
||||
return ctx.ds.testDatasource().then(results => {
|
||||
expect(results.status).toEqual('success');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing query', () => {
|
||||
const options = {
|
||||
range: {
|
||||
from: moment.utc('2017-08-22T20:00:00Z'),
|
||||
to: moment.utc('2017-08-22T23:59:00Z'),
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
apiVersion: '2018-01-01',
|
||||
refId: 'A',
|
||||
queryType: 'Azure Monitor',
|
||||
azureMonitor: {
|
||||
resourceGroup: 'testRG',
|
||||
resourceName: 'testRN',
|
||||
metricDefinition: 'Microsoft.Compute/virtualMachines',
|
||||
metricName: 'Percentage CPU',
|
||||
timeGrain: 'PT1H',
|
||||
alias: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('and data field is average', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
timeseries: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
timeStamp: '2017-08-22T21:00:00Z',
|
||||
average: 1.0503333333333331,
|
||||
},
|
||||
{
|
||||
timeStamp: '2017-08-22T22:00:00Z',
|
||||
average: 1.045083333333333,
|
||||
},
|
||||
{
|
||||
timeStamp: '2017-08-22T23:00:00Z',
|
||||
average: 1.0457499999999995,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id:
|
||||
'/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' +
|
||||
'/testRN/providers/Microsoft.Insights/metrics/Percentage CPU',
|
||||
name: {
|
||||
value: 'Percentage CPU',
|
||||
localizedValue: 'Percentage CPU',
|
||||
},
|
||||
type: 'Microsoft.Insights/metrics',
|
||||
unit: 'Percent',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain(
|
||||
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'
|
||||
);
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(1);
|
||||
expect(results.data[0].target).toEqual('testRN.Percentage CPU');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
|
||||
expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
|
||||
expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and data field is total', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
timeseries: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
timeStamp: '2017-08-22T21:00:00Z',
|
||||
total: 1.0503333333333331,
|
||||
},
|
||||
{
|
||||
timeStamp: '2017-08-22T22:00:00Z',
|
||||
total: 1.045083333333333,
|
||||
},
|
||||
{
|
||||
timeStamp: '2017-08-22T23:00:00Z',
|
||||
total: 1.0457499999999995,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id:
|
||||
'/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' +
|
||||
'/testRN/providers/Microsoft.Insights/metrics/Percentage CPU',
|
||||
name: {
|
||||
value: 'Percentage CPU',
|
||||
localizedValue: 'Percentage CPU',
|
||||
},
|
||||
type: 'Microsoft.Insights/metrics',
|
||||
unit: 'Percent',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
expect(options.url).toContain(
|
||||
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'
|
||||
);
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(1);
|
||||
expect(results.data[0].target).toEqual('testRN.Percentage CPU');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
|
||||
expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
|
||||
expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and data has a dimension filter', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
timeseries: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
timeStamp: '2017-08-22T21:00:00Z',
|
||||
total: 1.0503333333333331,
|
||||
},
|
||||
{
|
||||
timeStamp: '2017-08-22T22:00:00Z',
|
||||
total: 1.045083333333333,
|
||||
},
|
||||
{
|
||||
timeStamp: '2017-08-22T23:00:00Z',
|
||||
total: 1.0457499999999995,
|
||||
},
|
||||
],
|
||||
metadatavalues: [
|
||||
{
|
||||
name: {
|
||||
value: 'blobtype',
|
||||
localizedValue: 'blobtype',
|
||||
},
|
||||
value: 'BlockBlob',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id:
|
||||
'/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' +
|
||||
'/testRN/providers/Microsoft.Insights/metrics/Percentage CPU',
|
||||
name: {
|
||||
value: 'Percentage CPU',
|
||||
localizedValue: 'Percentage CPU',
|
||||
},
|
||||
type: 'Microsoft.Insights/metrics',
|
||||
unit: 'Percent',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('and with no alias specified', () => {
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const expected =
|
||||
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics';
|
||||
expect(options.url).toContain(expected);
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(1);
|
||||
expect(results.data[0].target).toEqual('testRN{blobtype=BlockBlob}.Percentage CPU');
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
|
||||
expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
|
||||
expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with an alias specified', () => {
|
||||
beforeEach(() => {
|
||||
options.targets[0].azureMonitor.alias =
|
||||
'{{resourcegroup}} + {{namespace}} + {{resourcename}} + ' +
|
||||
'{{metric}} + {{dimensionname}} + {{dimensionvalue}}';
|
||||
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const expected =
|
||||
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics';
|
||||
expect(options.url).toContain(expected);
|
||||
return ctx.$q.when({ data: response, status: 200 });
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of datapoints', () => {
|
||||
return ctx.ds.query(options).then(results => {
|
||||
expect(results.data.length).toBe(1);
|
||||
const expected =
|
||||
'testRG + Microsoft.Compute/virtualMachines + testRN + Percentage CPU + blobtype + BlockBlob';
|
||||
expect(results.data[0].target).toEqual(expected);
|
||||
expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
|
||||
expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
|
||||
expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
|
||||
expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing metricFindQuery', () => {
|
||||
describe('with a metric names query', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of metric names', () => {
|
||||
return ctx.ds.metricFindQuery('ResourceGroups()').then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].text).toBe('grp1');
|
||||
expect(results[0].value).toBe('grp1');
|
||||
expect(results[1].text).toBe('grp2');
|
||||
expect(results[1].value).toBe('grp2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with metric definitions query', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: 'test',
|
||||
type: 'Microsoft.Network/networkInterfaces',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of metric definitions', () => {
|
||||
return ctx.ds.metricFindQuery('Namespaces(nodesapp)').then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].text).toEqual('Microsoft.Network/networkInterfaces');
|
||||
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with resource names query', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: 'Failure Anomalies - nodeapp',
|
||||
type: 'microsoft.insights/alertrules',
|
||||
},
|
||||
{
|
||||
name: 'nodeapp',
|
||||
type: 'microsoft.insights/components',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of resource names', () => {
|
||||
return ctx.ds.metricFindQuery('resourceNames(nodeapp, microsoft.insights/components )').then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].text).toEqual('nodeapp');
|
||||
expect(results[0].value).toEqual('nodeapp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with metric names query', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: {
|
||||
value: 'Percentage CPU',
|
||||
localizedValue: 'Percentage CPU',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
value: 'UsedCapacity',
|
||||
localizedValue: 'Used capacity',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||
expect(options.url).toBe(
|
||||
baseUrl +
|
||||
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
|
||||
'metricdefinitions?api-version=2018-01-01'
|
||||
);
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of metric names', () => {
|
||||
return ctx.ds.metricFindQuery('Metricnames(nodeapp, microsoft.insights/components, rn)').then(results => {
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0].text).toEqual('Percentage CPU');
|
||||
expect(results[0].value).toEqual('Percentage CPU');
|
||||
|
||||
expect(results[1].text).toEqual('Used capacity');
|
||||
expect(results[1].value).toEqual('UsedCapacity');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getResourceGroups', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return list of Resource Groups', () => {
|
||||
return ctx.ds.getResourceGroups().then(results => {
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0].text).toEqual('grp1');
|
||||
expect(results[0].value).toEqual('grp1');
|
||||
expect(results[1].text).toEqual('grp2');
|
||||
expect(results[1].value).toEqual('grp2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricDefinitions', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: 'test',
|
||||
type: 'Microsoft.Network/networkInterfaces',
|
||||
},
|
||||
{
|
||||
location: 'northeurope',
|
||||
name: 'northeur',
|
||||
type: 'Microsoft.Compute/virtualMachines',
|
||||
},
|
||||
{
|
||||
location: 'westcentralus',
|
||||
name: 'us',
|
||||
type: 'Microsoft.Compute/virtualMachines',
|
||||
},
|
||||
{
|
||||
name: 'IHaveNoMetrics',
|
||||
type: 'IShouldBeFilteredOut',
|
||||
},
|
||||
{
|
||||
name: 'storageTest',
|
||||
type: 'Microsoft.Storage/storageAccounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
|
||||
return ctx.ds.getMetricDefinitions('nodesapp').then(results => {
|
||||
expect(results.length).toEqual(7);
|
||||
expect(results[0].text).toEqual('Microsoft.Network/networkInterfaces');
|
||||
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
|
||||
expect(results[1].text).toEqual('Microsoft.Compute/virtualMachines');
|
||||
expect(results[1].value).toEqual('Microsoft.Compute/virtualMachines');
|
||||
expect(results[2].text).toEqual('Microsoft.Storage/storageAccounts');
|
||||
expect(results[2].value).toEqual('Microsoft.Storage/storageAccounts');
|
||||
expect(results[3].text).toEqual('Microsoft.Storage/storageAccounts/blobServices');
|
||||
expect(results[3].value).toEqual('Microsoft.Storage/storageAccounts/blobServices');
|
||||
expect(results[4].text).toEqual('Microsoft.Storage/storageAccounts/fileServices');
|
||||
expect(results[4].value).toEqual('Microsoft.Storage/storageAccounts/fileServices');
|
||||
expect(results[5].text).toEqual('Microsoft.Storage/storageAccounts/tableServices');
|
||||
expect(results[5].value).toEqual('Microsoft.Storage/storageAccounts/tableServices');
|
||||
expect(results[6].text).toEqual('Microsoft.Storage/storageAccounts/queueServices');
|
||||
expect(results[6].value).toEqual('Microsoft.Storage/storageAccounts/queueServices');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getResourceNames', () => {
|
||||
describe('and there are no special cases', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: 'Failure Anomalies - nodeapp',
|
||||
type: 'microsoft.insights/alertrules',
|
||||
},
|
||||
{
|
||||
name: 'nodeapp',
|
||||
type: 'microsoft.insights/components',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return list of Resource Names', () => {
|
||||
return ctx.ds.getResourceNames('nodeapp', 'microsoft.insights/components').then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].text).toEqual('nodeapp');
|
||||
expect(results[0].value).toEqual('nodeapp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the metric definition is blobServices', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: 'Failure Anomalies - nodeapp',
|
||||
type: 'microsoft.insights/alertrules',
|
||||
},
|
||||
{
|
||||
name: 'storagetest',
|
||||
type: 'Microsoft.Storage/storageAccounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
|
||||
expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return list of Resource Names', () => {
|
||||
return ctx.ds.getResourceNames('nodeapp', 'Microsoft.Storage/storageAccounts/blobServices').then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].text).toEqual('storagetest/default');
|
||||
expect(results[0].value).toEqual('storagetest/default');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricNames', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: {
|
||||
value: 'UsedCapacity',
|
||||
localizedValue: 'Used capacity',
|
||||
},
|
||||
unit: 'CountPerSecond',
|
||||
primaryAggregationType: 'Total',
|
||||
supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
|
||||
metricAvailabilities: [
|
||||
{ timeGrain: 'PT1H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT6H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT12H', retention: 'P93D' },
|
||||
{ timeGrain: 'P1D', retention: 'P93D' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: {
|
||||
value: 'FreeCapacity',
|
||||
localizedValue: 'Free capacity',
|
||||
},
|
||||
unit: 'CountPerSecond',
|
||||
primaryAggregationType: 'Average',
|
||||
supportedAggregationTypes: ['None', 'Average'],
|
||||
metricAvailabilities: [
|
||||
{ timeGrain: 'PT1H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT6H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT12H', retention: 'P93D' },
|
||||
{ timeGrain: 'P1D', retention: 'P93D' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||
const expected =
|
||||
baseUrl +
|
||||
'/providers/microsoft.insights/components/resource1' +
|
||||
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01';
|
||||
expect(options.url).toBe(expected);
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return list of Metric Definitions', () => {
|
||||
return ctx.ds.getMetricNames('nodeapp', 'microsoft.insights/components', 'resource1').then(results => {
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0].text).toEqual('Used capacity');
|
||||
expect(results[0].value).toEqual('UsedCapacity');
|
||||
expect(results[1].text).toEqual('Free capacity');
|
||||
expect(results[1].value).toEqual('FreeCapacity');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricMetadata', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: {
|
||||
value: 'UsedCapacity',
|
||||
localizedValue: 'Used capacity',
|
||||
},
|
||||
unit: 'CountPerSecond',
|
||||
primaryAggregationType: 'Total',
|
||||
supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
|
||||
metricAvailabilities: [
|
||||
{ timeGrain: 'PT1H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT6H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT12H', retention: 'P93D' },
|
||||
{ timeGrain: 'P1D', retention: 'P93D' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: {
|
||||
value: 'FreeCapacity',
|
||||
localizedValue: 'Free capacity',
|
||||
},
|
||||
unit: 'CountPerSecond',
|
||||
primaryAggregationType: 'Average',
|
||||
supportedAggregationTypes: ['None', 'Average'],
|
||||
metricAvailabilities: [
|
||||
{ timeGrain: 'PT1H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT6H', retention: 'P93D' },
|
||||
{ timeGrain: 'PT12H', retention: 'P93D' },
|
||||
{ timeGrain: 'P1D', retention: 'P93D' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||
const expected =
|
||||
baseUrl +
|
||||
'/providers/microsoft.insights/components/resource1' +
|
||||
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01';
|
||||
expect(options.url).toBe(expected);
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return Aggregation metadata for a Metric', () => {
|
||||
return ctx.ds
|
||||
.getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'UsedCapacity')
|
||||
.then(results => {
|
||||
expect(results.primaryAggType).toEqual('Total');
|
||||
expect(results.supportedAggTypes.length).toEqual(6);
|
||||
expect(results.supportedTimeGrains.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricMetadata on metrics with dimensions', () => {
|
||||
const response = {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
name: {
|
||||
value: 'Transactions',
|
||||
localizedValue: 'Transactions',
|
||||
},
|
||||
unit: 'Count',
|
||||
primaryAggregationType: 'Total',
|
||||
supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
|
||||
isDimensionRequired: false,
|
||||
dimensions: [
|
||||
{
|
||||
value: 'ResponseType',
|
||||
localizedValue: 'Response type',
|
||||
},
|
||||
{
|
||||
value: 'GeoType',
|
||||
localizedValue: 'Geo type',
|
||||
},
|
||||
{
|
||||
value: 'ApiName',
|
||||
localizedValue: 'API name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: {
|
||||
value: 'FreeCapacity',
|
||||
localizedValue: 'Free capacity',
|
||||
},
|
||||
unit: 'CountPerSecond',
|
||||
primaryAggregationType: 'Average',
|
||||
supportedAggregationTypes: ['None', 'Average'],
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = options => {
|
||||
const baseUrl =
|
||||
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||
const expected =
|
||||
baseUrl +
|
||||
'/providers/microsoft.insights/components/resource1' +
|
||||
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01';
|
||||
expect(options.url).toBe(expected);
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return dimensions for a Metric that has dimensions', () => {
|
||||
return ctx.ds
|
||||
.getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'Transactions')
|
||||
.then(results => {
|
||||
expect(results.dimensions.length).toEqual(4);
|
||||
expect(results.dimensions[0].text).toEqual('None');
|
||||
expect(results.dimensions[0].value).toEqual('None');
|
||||
expect(results.dimensions[1].text).toEqual('Response type');
|
||||
expect(results.dimensions[1].value).toEqual('ResponseType');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an empty array for a Metric that does not have dimensions', () => {
|
||||
return ctx.ds
|
||||
.getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'FreeCapacity')
|
||||
.then(results => {
|
||||
expect(results.dimensions.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,339 @@
|
||||
import _ from 'lodash';
|
||||
import AzureMonitorFilterBuilder from './azure_monitor_filter_builder';
|
||||
import UrlBuilder from './url_builder';
|
||||
import ResponseParser from './response_parser';
|
||||
import SupportedNamespaces from './supported_namespaces';
|
||||
import TimegrainConverter from '../time_grain_converter';
|
||||
|
||||
export default class AzureMonitorDatasource {
|
||||
apiVersion = '2018-01-01';
|
||||
id: number;
|
||||
subscriptionId: string;
|
||||
baseUrl: string;
|
||||
resourceGroup: string;
|
||||
resourceName: string;
|
||||
url: string;
|
||||
defaultDropdownValue = 'select';
|
||||
cloudName: string;
|
||||
supportedMetricNamespaces: any[] = [];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private instanceSettings, private backendSrv, private templateSrv, private $q) {
|
||||
this.id = instanceSettings.id;
|
||||
this.subscriptionId = instanceSettings.jsonData.subscriptionId;
|
||||
this.cloudName = instanceSettings.jsonData.cloudName || 'azuremonitor';
|
||||
this.baseUrl = `/${this.cloudName}/subscriptions/${this.subscriptionId}/resourceGroups`;
|
||||
this.url = instanceSettings.url;
|
||||
|
||||
this.supportedMetricNamespaces = new SupportedNamespaces(this.cloudName).get();
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return !!this.subscriptionId && this.subscriptionId.length > 0;
|
||||
}
|
||||
|
||||
query(options) {
|
||||
const queries = _.filter(options.targets, item => {
|
||||
return (
|
||||
item.hide !== true &&
|
||||
item.azureMonitor.resourceGroup &&
|
||||
item.azureMonitor.resourceGroup !== this.defaultDropdownValue &&
|
||||
item.azureMonitor.resourceName &&
|
||||
item.azureMonitor.resourceName !== this.defaultDropdownValue &&
|
||||
item.azureMonitor.metricDefinition &&
|
||||
item.azureMonitor.metricDefinition !== this.defaultDropdownValue &&
|
||||
item.azureMonitor.metricName &&
|
||||
item.azureMonitor.metricName !== this.defaultDropdownValue
|
||||
);
|
||||
}).map(target => {
|
||||
const item = target.azureMonitor;
|
||||
|
||||
if (item.timeGrainUnit && item.timeGrain !== 'auto') {
|
||||
item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
|
||||
}
|
||||
|
||||
const resourceGroup = this.templateSrv.replace(item.resourceGroup, options.scopedVars);
|
||||
const resourceName = this.templateSrv.replace(item.resourceName, options.scopedVars);
|
||||
const metricDefinition = this.templateSrv.replace(item.metricDefinition, options.scopedVars);
|
||||
const timeGrain = this.templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars);
|
||||
|
||||
const filterBuilder = new AzureMonitorFilterBuilder(
|
||||
item.metricName,
|
||||
options.range.from,
|
||||
options.range.to,
|
||||
timeGrain,
|
||||
options.interval
|
||||
);
|
||||
|
||||
if (item.timeGrains) {
|
||||
filterBuilder.setAllowedTimeGrains(item.timeGrains);
|
||||
}
|
||||
|
||||
if (item.aggregation) {
|
||||
filterBuilder.setAggregation(item.aggregation);
|
||||
}
|
||||
|
||||
if (item.dimension && item.dimension !== 'None') {
|
||||
filterBuilder.setDimensionFilter(item.dimension, item.dimensionFilter);
|
||||
}
|
||||
|
||||
const filter = this.templateSrv.replace(filterBuilder.generateFilter(), options.scopedVars);
|
||||
|
||||
const url = UrlBuilder.buildAzureMonitorQueryUrl(
|
||||
this.baseUrl,
|
||||
resourceGroup,
|
||||
metricDefinition,
|
||||
resourceName,
|
||||
this.apiVersion,
|
||||
filter
|
||||
);
|
||||
|
||||
return {
|
||||
refId: target.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: this.id,
|
||||
url: url,
|
||||
format: options.format,
|
||||
alias: item.alias,
|
||||
raw: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (!queries || queries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = this.doQueries(queries);
|
||||
|
||||
return this.$q.all(promises).then(results => {
|
||||
return new ResponseParser(results).parseQueryResult();
|
||||
});
|
||||
}
|
||||
|
||||
doQueries(queries) {
|
||||
return _.map(queries, query => {
|
||||
return this.doRequest(query.url)
|
||||
.then(result => {
|
||||
return {
|
||||
result: result,
|
||||
query: query,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
throw {
|
||||
error: err,
|
||||
query: query,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
annotationQuery(options) {}
|
||||
|
||||
metricFindQuery(query: string) {
|
||||
const resourceGroupsQuery = query.match(/^ResourceGroups\(\)/i);
|
||||
if (resourceGroupsQuery) {
|
||||
return this.getResourceGroups();
|
||||
}
|
||||
|
||||
const metricDefinitionsQuery = query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i);
|
||||
if (metricDefinitionsQuery) {
|
||||
return this.getMetricDefinitions(this.toVariable(metricDefinitionsQuery[1]));
|
||||
}
|
||||
|
||||
const resourceNamesQuery = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?)\)/i);
|
||||
if (resourceNamesQuery) {
|
||||
const resourceGroup = this.toVariable(resourceNamesQuery[1]);
|
||||
const metricDefinition = this.toVariable(resourceNamesQuery[2]);
|
||||
return this.getResourceNames(resourceGroup, metricDefinition);
|
||||
}
|
||||
|
||||
const metricNamesQuery = query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i);
|
||||
|
||||
if (metricNamesQuery) {
|
||||
const resourceGroup = this.toVariable(metricNamesQuery[1]);
|
||||
const metricDefinition = this.toVariable(metricNamesQuery[2]);
|
||||
const resourceName = this.toVariable(metricNamesQuery[3]);
|
||||
return this.getMetricNames(resourceGroup, metricDefinition, resourceName);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
toVariable(metric: string) {
|
||||
return this.templateSrv.replace((metric || '').trim());
|
||||
}
|
||||
|
||||
getResourceGroups() {
|
||||
const url = `${this.baseUrl}?api-version=${this.apiVersion}`;
|
||||
return this.doRequest(url).then(result => {
|
||||
return ResponseParser.parseResponseValues(result, 'name', 'name');
|
||||
});
|
||||
}
|
||||
|
||||
getMetricDefinitions(resourceGroup: string) {
|
||||
const url = `${this.baseUrl}/${resourceGroup}/resources?api-version=${this.apiVersion}`;
|
||||
return this.doRequest(url)
|
||||
.then(result => {
|
||||
return ResponseParser.parseResponseValues(result, 'type', 'type');
|
||||
})
|
||||
.then(result => {
|
||||
return _.filter(result, t => {
|
||||
for (let i = 0; i < this.supportedMetricNamespaces.length; i++) {
|
||||
if (t.value.toLowerCase() === this.supportedMetricNamespaces[i].toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.then(result => {
|
||||
let shouldHardcodeBlobStorage = false;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (result[i].value === 'Microsoft.Storage/storageAccounts') {
|
||||
shouldHardcodeBlobStorage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldHardcodeBlobStorage) {
|
||||
result.push({
|
||||
text: 'Microsoft.Storage/storageAccounts/blobServices',
|
||||
value: 'Microsoft.Storage/storageAccounts/blobServices',
|
||||
});
|
||||
result.push({
|
||||
text: 'Microsoft.Storage/storageAccounts/fileServices',
|
||||
value: 'Microsoft.Storage/storageAccounts/fileServices',
|
||||
});
|
||||
result.push({
|
||||
text: 'Microsoft.Storage/storageAccounts/tableServices',
|
||||
value: 'Microsoft.Storage/storageAccounts/tableServices',
|
||||
});
|
||||
result.push({
|
||||
text: 'Microsoft.Storage/storageAccounts/queueServices',
|
||||
value: 'Microsoft.Storage/storageAccounts/queueServices',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
getResourceNames(resourceGroup: string, metricDefinition: string) {
|
||||
const url = `${this.baseUrl}/${resourceGroup}/resources?api-version=${this.apiVersion}`;
|
||||
|
||||
return this.doRequest(url).then(result => {
|
||||
if (!_.startsWith(metricDefinition, 'Microsoft.Storage/storageAccounts/')) {
|
||||
return ResponseParser.parseResourceNames(result, metricDefinition);
|
||||
}
|
||||
|
||||
const list = ResponseParser.parseResourceNames(result, 'Microsoft.Storage/storageAccounts');
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
list[i].text += '/default';
|
||||
list[i].value += '/default';
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
getMetricNames(resourceGroup: string, metricDefinition: string, resourceName: string) {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
this.baseUrl,
|
||||
resourceGroup,
|
||||
metricDefinition,
|
||||
resourceName,
|
||||
this.apiVersion
|
||||
);
|
||||
|
||||
return this.doRequest(url).then(result => {
|
||||
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
|
||||
});
|
||||
}
|
||||
|
||||
getMetricMetadata(resourceGroup: string, metricDefinition: string, resourceName: string, metricName: string) {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
this.baseUrl,
|
||||
resourceGroup,
|
||||
metricDefinition,
|
||||
resourceName,
|
||||
this.apiVersion
|
||||
);
|
||||
|
||||
return this.doRequest(url).then(result => {
|
||||
return ResponseParser.parseMetadata(result, metricName);
|
||||
});
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Tenant Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Client Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}?api-version=${this.apiVersion}`;
|
||||
return this.doRequest(url)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Successfully queried the Azure Monitor service.',
|
||||
title: 'Success',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Returned http status code ' + response.status,
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
let message = 'Azure Monitor: ';
|
||||
message += error.statusText ? error.statusText + ': ' : '';
|
||||
|
||||
if (error.data && error.data.error && error.data.error.code) {
|
||||
message += error.data.error.code + '. ' + error.data.error.message;
|
||||
} else if (error.data && error.data.error) {
|
||||
message += error.data.error;
|
||||
} else if (error.data) {
|
||||
message += error.data;
|
||||
} else {
|
||||
message += 'Cannot connect to Azure Monitor REST API.';
|
||||
}
|
||||
return {
|
||||
status: 'error',
|
||||
message: message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
isValidConfigField(field: string) {
|
||||
return field && field.length > 0;
|
||||
}
|
||||
|
||||
doRequest(url, maxRetries = 1) {
|
||||
return this.backendSrv
|
||||
.datasourceRequest({
|
||||
url: this.url + url,
|
||||
method: 'GET',
|
||||
})
|
||||
.catch(error => {
|
||||
if (maxRetries > 0) {
|
||||
return this.doRequest(url, maxRetries - 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
jest.mock('app/core/utils/kbn', () => {
|
||||
return {
|
||||
interval_to_ms: interval => {
|
||||
if (interval.substring(interval.length - 1) === 's') {
|
||||
return interval.substring(0, interval.length - 1) * 1000;
|
||||
}
|
||||
|
||||
if (interval.substring(interval.length - 1) === 'm') {
|
||||
return interval.substring(0, interval.length - 1) * 1000 * 60;
|
||||
}
|
||||
|
||||
if (interval.substring(interval.length - 1) === 'd') {
|
||||
return interval.substring(0, interval.length - 1) * 1000 * 60 * 24;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import AzureMonitorFilterBuilder from './azure_monitor_filter_builder';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('AzureMonitorFilterBuilder', () => {
|
||||
let builder: AzureMonitorFilterBuilder;
|
||||
|
||||
const timefilter = 'timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z';
|
||||
const metricFilter = 'metricnames=Percentage CPU';
|
||||
|
||||
beforeEach(() => {
|
||||
builder = new AzureMonitorFilterBuilder(
|
||||
'Percentage CPU',
|
||||
moment.utc('2017-08-22 06:00'),
|
||||
moment.utc('2017-08-22 07:00'),
|
||||
'PT1H',
|
||||
'3m'
|
||||
);
|
||||
});
|
||||
|
||||
describe('with a metric name and auto time grain of 3 minutes', () => {
|
||||
beforeEach(() => {
|
||||
builder.timeGrain = 'auto';
|
||||
});
|
||||
|
||||
it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => {
|
||||
const filter = timefilter + '&interval=PT5M&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and auto time grain of 30 seconds', () => {
|
||||
beforeEach(() => {
|
||||
builder.timeGrain = 'auto';
|
||||
builder.grafanaInterval = '30s';
|
||||
});
|
||||
|
||||
it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => {
|
||||
const filter = timefilter + '&interval=PT1M&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and auto time grain of 10 minutes', () => {
|
||||
beforeEach(() => {
|
||||
builder.timeGrain = 'auto';
|
||||
builder.grafanaInterval = '10m';
|
||||
});
|
||||
|
||||
it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => {
|
||||
const filter = timefilter + '&interval=PT15M&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and auto time grain of 2 day', () => {
|
||||
beforeEach(() => {
|
||||
builder.timeGrain = 'auto';
|
||||
builder.grafanaInterval = '2d';
|
||||
});
|
||||
|
||||
it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => {
|
||||
const filter = timefilter + '&interval=P1D&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and 1 hour time grain', () => {
|
||||
it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => {
|
||||
const filter = timefilter + '&interval=PT1H&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and 1 minute time grain', () => {
|
||||
beforeEach(() => {
|
||||
builder.timeGrain = 'PT1M';
|
||||
});
|
||||
|
||||
it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => {
|
||||
const filter = timefilter + '&interval=PT1M&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and 1 day time grain and an aggregation', () => {
|
||||
beforeEach(() => {
|
||||
builder.timeGrain = 'P1D';
|
||||
builder.setAggregation('Maximum');
|
||||
});
|
||||
|
||||
it('should add time grain to the filter in ISO_8601 format', () => {
|
||||
const filter = timefilter + '&interval=P1D&aggregation=Maximum&' + metricFilter;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a metric name and 1 day time grain and an aggregation and a dimension', () => {
|
||||
beforeEach(() => {
|
||||
builder.setDimensionFilter('aDimension', 'aFilterValue');
|
||||
});
|
||||
|
||||
it('should add dimension to the filter', () => {
|
||||
const filter = timefilter + '&interval=PT1H&' + metricFilter + `&$filter=aDimension eq 'aFilterValue'`;
|
||||
expect(builder.generateFilter()).toEqual(filter);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import TimegrainConverter from '../time_grain_converter';
|
||||
|
||||
export default class AzureMonitorFilterBuilder {
|
||||
aggregation: string;
|
||||
timeGrainInterval = '';
|
||||
dimension: string;
|
||||
dimensionFilter: string;
|
||||
allowedTimeGrains = ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'];
|
||||
|
||||
constructor(
|
||||
private metricName: string,
|
||||
private from,
|
||||
private to,
|
||||
public timeGrain: string,
|
||||
public grafanaInterval: string
|
||||
) {}
|
||||
|
||||
setAllowedTimeGrains(timeGrains) {
|
||||
this.allowedTimeGrains = [];
|
||||
timeGrains.forEach(tg => {
|
||||
if (tg.value === 'auto') {
|
||||
this.allowedTimeGrains.push(tg.value);
|
||||
} else {
|
||||
this.allowedTimeGrains.push(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAggregation(agg) {
|
||||
this.aggregation = agg;
|
||||
}
|
||||
|
||||
setDimensionFilter(dimension, dimensionFilter) {
|
||||
this.dimension = dimension;
|
||||
this.dimensionFilter = dimensionFilter;
|
||||
}
|
||||
|
||||
generateFilter() {
|
||||
let filter = this.createDatetimeAndTimeGrainConditions();
|
||||
|
||||
if (this.aggregation) {
|
||||
filter += `&aggregation=${this.aggregation}`;
|
||||
}
|
||||
|
||||
if (this.metricName && this.metricName.trim().length > 0) {
|
||||
filter += `&metricnames=${this.metricName}`;
|
||||
}
|
||||
|
||||
if (this.dimension && this.dimensionFilter && this.dimensionFilter.trim().length > 0) {
|
||||
filter += `&$filter=${this.dimension} eq '${this.dimensionFilter}'`;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
createDatetimeAndTimeGrainConditions() {
|
||||
const dateTimeCondition = `timespan=${this.from.utc().format()}/${this.to.utc().format()}`;
|
||||
|
||||
if (this.timeGrain === 'auto') {
|
||||
this.timeGrain = this.calculateAutoTimeGrain();
|
||||
}
|
||||
const timeGrainCondition = `&interval=${this.timeGrain}`;
|
||||
|
||||
return dateTimeCondition + timeGrainCondition;
|
||||
}
|
||||
|
||||
calculateAutoTimeGrain() {
|
||||
const roundedInterval = TimegrainConverter.findClosestTimeGrain(this.grafanaInterval, this.allowedTimeGrains);
|
||||
|
||||
return TimegrainConverter.createISO8601DurationFromInterval(roundedInterval);
|
||||
}
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import TimeGrainConverter from '../time_grain_converter';
|
||||
|
||||
export default class ResponseParser {
|
||||
constructor(private results) {}
|
||||
|
||||
parseQueryResult() {
|
||||
const data: any[] = [];
|
||||
for (let i = 0; i < this.results.length; i++) {
|
||||
for (let j = 0; j < this.results[i].result.data.value.length; j++) {
|
||||
for (let k = 0; k < this.results[i].result.data.value[j].timeseries.length; k++) {
|
||||
const alias = this.results[i].query.alias;
|
||||
data.push({
|
||||
target: ResponseParser.createTarget(
|
||||
this.results[i].result.data.value[j],
|
||||
this.results[i].result.data.value[j].timeseries[k].metadatavalues,
|
||||
alias
|
||||
),
|
||||
datapoints: ResponseParser.convertDataToPoints(this.results[i].result.data.value[j].timeseries[k].data),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static createTarget(data, metadatavalues, alias: string) {
|
||||
const resourceGroup = ResponseParser.parseResourceGroupFromId(data.id);
|
||||
const resourceName = ResponseParser.parseResourceNameFromId(data.id);
|
||||
const namespace = ResponseParser.parseNamespaceFromId(data.id, resourceName);
|
||||
if (alias) {
|
||||
const regex = /\{\{([\s\S]+?)\}\}/g;
|
||||
return alias.replace(regex, (match, g1, g2) => {
|
||||
const group = g1 || g2;
|
||||
|
||||
if (group === 'resourcegroup') {
|
||||
return resourceGroup;
|
||||
} else if (group === 'namespace') {
|
||||
return namespace;
|
||||
} else if (group === 'resourcename') {
|
||||
return resourceName;
|
||||
} else if (group === 'metric') {
|
||||
return data.name.value;
|
||||
} else if (group === 'dimensionname') {
|
||||
return metadatavalues && metadatavalues.length > 0 ? metadatavalues[0].name.value : '';
|
||||
} else if (group === 'dimensionvalue') {
|
||||
return metadatavalues && metadatavalues.length > 0 ? metadatavalues[0].value : '';
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
if (metadatavalues && metadatavalues.length > 0) {
|
||||
return `${resourceName}{${metadatavalues[0].name.value}=${metadatavalues[0].value}}.${data.name.value}`;
|
||||
}
|
||||
|
||||
return `${resourceName}.${data.name.value}`;
|
||||
}
|
||||
|
||||
static parseResourceGroupFromId(id: string) {
|
||||
const startIndex = id.indexOf('/resourceGroups/') + 16;
|
||||
const endIndex = id.indexOf('/providers');
|
||||
|
||||
return id.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
static parseNamespaceFromId(id: string, resourceName: string) {
|
||||
const startIndex = id.indexOf('/providers/') + 11;
|
||||
const endIndex = id.indexOf('/' + resourceName);
|
||||
|
||||
return id.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
static parseResourceNameFromId(id: string) {
|
||||
const endIndex = id.lastIndexOf('/providers');
|
||||
const startIndex = id.slice(0, endIndex).lastIndexOf('/') + 1;
|
||||
|
||||
return id.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
static convertDataToPoints(timeSeriesData) {
|
||||
const dataPoints: any[] = [];
|
||||
|
||||
for (let k = 0; k < timeSeriesData.length; k++) {
|
||||
const epoch = ResponseParser.dateTimeToEpoch(timeSeriesData[k].timeStamp);
|
||||
const aggKey = ResponseParser.getKeyForAggregationField(timeSeriesData[k]);
|
||||
|
||||
if (aggKey) {
|
||||
dataPoints.push([timeSeriesData[k][aggKey], epoch]);
|
||||
}
|
||||
}
|
||||
|
||||
return dataPoints;
|
||||
}
|
||||
|
||||
static dateTimeToEpoch(dateTime) {
|
||||
return moment(dateTime).valueOf();
|
||||
}
|
||||
|
||||
static getKeyForAggregationField(dataObj): string {
|
||||
const keys = _.keys(dataObj);
|
||||
if (keys.length < 2) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return _.intersection(keys, ['total', 'average', 'maximum', 'minimum', 'count'])[0];
|
||||
}
|
||||
|
||||
static parseResponseValues(result: any, textFieldName: string, valueFieldName: string) {
|
||||
const list: any[] = [];
|
||||
for (let i = 0; i < result.data.value.length; i++) {
|
||||
if (!_.find(list, ['value', _.get(result.data.value[i], valueFieldName)])) {
|
||||
list.push({
|
||||
text: _.get(result.data.value[i], textFieldName),
|
||||
value: _.get(result.data.value[i], valueFieldName),
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseResourceNames(result: any, metricDefinition: string) {
|
||||
const list: any[] = [];
|
||||
for (let i = 0; i < result.data.value.length; i++) {
|
||||
if (result.data.value[i].type === metricDefinition) {
|
||||
list.push({
|
||||
text: result.data.value[i].name,
|
||||
value: result.data.value[i].name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseMetadata(result: any, metricName: string) {
|
||||
const metricData = _.find(result.data.value, o => {
|
||||
return _.get(o, 'name.value') === metricName;
|
||||
});
|
||||
|
||||
const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
|
||||
|
||||
return {
|
||||
primaryAggType: metricData.primaryAggregationType,
|
||||
supportedAggTypes: metricData.supportedAggregationTypes || defaultAggTypes,
|
||||
supportedTimeGrains: ResponseParser.parseTimeGrains(metricData.metricAvailabilities || []),
|
||||
dimensions: ResponseParser.parseDimensions(metricData),
|
||||
};
|
||||
}
|
||||
|
||||
static parseTimeGrains(metricAvailabilities) {
|
||||
const timeGrains: any[] = [];
|
||||
metricAvailabilities.forEach(avail => {
|
||||
if (avail.timeGrain) {
|
||||
timeGrains.push({
|
||||
text: TimeGrainConverter.createTimeGrainFromISO8601Duration(avail.timeGrain),
|
||||
value: avail.timeGrain,
|
||||
});
|
||||
}
|
||||
});
|
||||
return timeGrains;
|
||||
}
|
||||
|
||||
static parseDimensions(metricData: any) {
|
||||
const dimensions: any[] = [];
|
||||
if (!metricData.dimensions || metricData.dimensions.length === 0) {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
if (!metricData.isDimensionRequired) {
|
||||
dimensions.push({ text: 'None', value: 'None' });
|
||||
}
|
||||
|
||||
for (let i = 0; i < metricData.dimensions.length; i++) {
|
||||
dimensions.push({
|
||||
text: metricData.dimensions[i].localizedValue,
|
||||
value: metricData.dimensions[i].value,
|
||||
});
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
export default class SupportedNamespaces {
|
||||
supportedMetricNamespaces = {
|
||||
azuremonitor: [
|
||||
'Microsoft.AnalysisServices/servers',
|
||||
'Microsoft.ApiManagement/service',
|
||||
'Microsoft.Automation/automationAccounts',
|
||||
'Microsoft.Batch/batchAccounts',
|
||||
'Microsoft.Cache/redis',
|
||||
'Microsoft.ClassicCompute/virtualMachines',
|
||||
'Microsoft.ClassicCompute/domainNames/slots/roles',
|
||||
'Microsoft.CognitiveServices/accounts',
|
||||
'Microsoft.Compute/virtualMachines',
|
||||
'Microsoft.Compute/virtualMachineScaleSets',
|
||||
'Microsoft.ContainerInstance/containerGroups',
|
||||
'Microsoft.ContainerRegistry/registries',
|
||||
'Microsoft.ContainerService/managedClusters',
|
||||
'Microsoft.CustomerInsights/hubs',
|
||||
'Microsoft.DataBoxEdge/dataBoxEdgeDevices',
|
||||
'Microsoft.DataFactory/datafactories',
|
||||
'Microsoft.DataFactory/factories',
|
||||
'Microsoft.DataLakeAnalytics/accounts',
|
||||
'Microsoft.DataLakeStore/accounts',
|
||||
'Microsoft.DBforMariaDB/servers',
|
||||
'Microsoft.DBforMySQL/servers',
|
||||
'Microsoft.DBforPostgreSQL/servers',
|
||||
'Microsoft.Devices/IotHubs',
|
||||
'Microsoft.Devices/provisioningServices',
|
||||
'Microsoft.DocumentDB/databaseAccounts',
|
||||
'Microsoft.EventGrid/topics',
|
||||
'Microsoft.EventGrid/eventSubscriptions',
|
||||
'Microsoft.EventGrid/extensionTopics',
|
||||
'Microsoft.EventHub/namespaces',
|
||||
'Microsoft.EventHub/clusters',
|
||||
'Microsoft.HDInsight/clusters',
|
||||
'Microsoft.Insights/AutoscaleSettings',
|
||||
'Microsoft.Insights/components',
|
||||
'Microsoft.KeyVault/vaults',
|
||||
'Microsoft.Kusto/clusters',
|
||||
'Microsoft.LocationBasedServices/accounts',
|
||||
'Microsoft.Logic/workflows',
|
||||
'Microsoft.Logic/integrationServiceEnvironments',
|
||||
'Microsoft.NetApp/netAppAccounts/capacityPools',
|
||||
'Microsoft.NetApp/netAppAccounts/capacityPools/Volumes',
|
||||
'Microsoft.Network/networkInterfaces',
|
||||
'Microsoft.Network/loadBalancers',
|
||||
'Microsoft.Network/dnsZones',
|
||||
'Microsoft.Network/publicIPAddresses',
|
||||
'Microsoft.Network/azureFirewalls',
|
||||
'Microsoft.Network/applicationGateways',
|
||||
'Microsoft.Network/virtualNetworkGateways',
|
||||
'Microsoft.Network/expressRouteCircuits',
|
||||
'Microsoft.Network/expressRouteCircuits/Peerings',
|
||||
'Microsoft.Network/connections',
|
||||
'Microsoft.Network/trafficManagerProfiles',
|
||||
'Microsoft.Network/networkWatchers/connectionMonitors',
|
||||
'Microsoft.Network/frontdoors',
|
||||
'Microsoft.NotificationHubs/namespaces/notificationHubs',
|
||||
'Microsoft.PowerBIDedicated/capacities',
|
||||
'Microsoft.Relay/namespaces',
|
||||
'Microsoft.Search/searchServices',
|
||||
'Microsoft.ServiceBus/namespaces',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'Microsoft.Sql/servers/elasticPools',
|
||||
'Microsoft.Sql/managedInstances',
|
||||
'Microsoft.Storage/storageAccounts',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'Microsoft.Storage/storageAccounts/fileServices',
|
||||
'Microsoft.Storage/storageAccounts/queueServices',
|
||||
'Microsoft.Storage/storageAccounts/tableServices',
|
||||
'Microsoft.StorageSync/storageSyncServices',
|
||||
'Microsoft.StorageSync/storageSyncServices/syncGroups',
|
||||
'Microsoft.StorageSync/storageSyncServices/syncGroups/serverEndpoints',
|
||||
'Microsoft.StorageSync/storageSyncServices/registeredServers',
|
||||
'Microsoft.StreamAnalytics/streamingJobs',
|
||||
'Microsoft.Web/serverfarms',
|
||||
'Microsoft.Web/sites',
|
||||
'Microsoft.Web/sites/slots',
|
||||
'Microsoft.Web/hostingEnvironments/multiRolePools',
|
||||
'Microsoft.Web/hostingEnvironments/workerPools',
|
||||
],
|
||||
govazuremonitor: [
|
||||
'Microsoft.AnalysisServices/servers',
|
||||
'Microsoft.ApiManagement/service',
|
||||
'Microsoft.Batch/batchAccounts',
|
||||
'Microsoft.Cache/redis',
|
||||
'Microsoft.ClassicCompute/virtualMachines',
|
||||
'Microsoft.ClassicCompute/domainNames/slots/roles',
|
||||
'Microsoft.CognitiveServices/accounts',
|
||||
'Microsoft.Compute/virtualMachines',
|
||||
'Microsoft.Compute/virtualMachineScaleSets',
|
||||
'Microsoft.ContainerRegistry/registries',
|
||||
'Microsoft.DBforMySQL/servers',
|
||||
'Microsoft.DBforPostgreSQL/servers',
|
||||
'Microsoft.Devices/IotHubs',
|
||||
'Microsoft.Devices/provisioningServices',
|
||||
'Microsoft.EventGrid/topics',
|
||||
'Microsoft.EventGrid/eventSubscriptions',
|
||||
'Microsoft.EventGrid/extensionTopics',
|
||||
'Microsoft.EventHub/namespaces',
|
||||
'Microsoft.EventHub/clusters',
|
||||
'Microsoft.Insights/AutoscaleSettings',
|
||||
'Microsoft.KeyVault/vaults',
|
||||
'Microsoft.Logic/workflows',
|
||||
'Microsoft.Network/networkInterfaces',
|
||||
'Microsoft.Network/loadBalancers',
|
||||
'Microsoft.Network/dnsZones',
|
||||
'Microsoft.Network/publicIPAddresses',
|
||||
'Microsoft.Network/azureFirewalls',
|
||||
'Microsoft.Network/applicationGateways',
|
||||
'Microsoft.Network/virtualNetworkGateways',
|
||||
'Microsoft.Network/expressRouteCircuits',
|
||||
'Microsoft.Network/expressRouteCircuits/Peerings',
|
||||
'Microsoft.Network/connections',
|
||||
'Microsoft.Network/trafficManagerProfiles',
|
||||
'Microsoft.Network/networkWatchers/connectionMonitors',
|
||||
'Microsoft.Network/frontdoors',
|
||||
'Microsoft.NotificationHubs/namespaces/notificationHubs',
|
||||
'Microsoft.OperationalInsights/workspaces',
|
||||
'Microsoft.PowerBIDedicated/capacities',
|
||||
'Microsoft.Relay/namespaces',
|
||||
'Microsoft.ServiceBus/namespaces',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'Microsoft.Sql/servers/elasticPools',
|
||||
'Microsoft.Sql/managedInstances',
|
||||
'Microsoft.Storage/storageAccounts',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'Microsoft.Storage/storageAccounts/fileServices',
|
||||
'Microsoft.Storage/storageAccounts/queueServices',
|
||||
'Microsoft.Storage/storageAccounts/tableServices',
|
||||
'Microsoft.Web/serverfarms',
|
||||
'Microsoft.Web/sites',
|
||||
'Microsoft.Web/sites/slots',
|
||||
'Microsoft.Web/hostingEnvironments/multiRolePools',
|
||||
'Microsoft.Web/hostingEnvironments/workerPools',
|
||||
],
|
||||
germanyazuremonitor: [
|
||||
'Microsoft.AnalysisServices/servers',
|
||||
'Microsoft.Batch/batchAccounts',
|
||||
'Microsoft.Cache/redis',
|
||||
'Microsoft.ClassicCompute/virtualMachines',
|
||||
'Microsoft.ClassicCompute/domainNames/slots/roles',
|
||||
'Microsoft.Compute/virtualMachines',
|
||||
'Microsoft.Compute/virtualMachineScaleSets',
|
||||
'Microsoft.DBforMySQL/servers',
|
||||
'Microsoft.DBforPostgreSQL/servers',
|
||||
'Microsoft.Devices/IotHubs',
|
||||
'Microsoft.Devices/provisioningServices',
|
||||
'Microsoft.EventHub/namespaces',
|
||||
'Microsoft.EventHub/clusters',
|
||||
'Microsoft.Insights/AutoscaleSettings',
|
||||
'Microsoft.KeyVault/vaults',
|
||||
'Microsoft.Network/networkInterfaces',
|
||||
'Microsoft.Network/loadBalancers',
|
||||
'Microsoft.Network/dnsZones',
|
||||
'Microsoft.Network/publicIPAddresses',
|
||||
'Microsoft.Network/azureFirewalls',
|
||||
'Microsoft.Network/applicationGateways',
|
||||
'Microsoft.Network/virtualNetworkGateways',
|
||||
'Microsoft.Network/expressRouteCircuits',
|
||||
'Microsoft.Network/expressRouteCircuits/Peerings',
|
||||
'Microsoft.Network/connections',
|
||||
'Microsoft.Network/trafficManagerProfiles',
|
||||
'Microsoft.Network/networkWatchers/connectionMonitors',
|
||||
'Microsoft.Network/frontdoors',
|
||||
'Microsoft.NotificationHubs/namespaces/notificationHubs',
|
||||
'Microsoft.PowerBIDedicated/capacities',
|
||||
'Microsoft.Relay/namespaces',
|
||||
'Microsoft.ServiceBus/namespaces',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'Microsoft.Sql/servers/elasticPools',
|
||||
'Microsoft.Sql/managedInstances',
|
||||
'Microsoft.Storage/storageAccounts',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'Microsoft.Storage/storageAccounts/fileServices',
|
||||
'Microsoft.Storage/storageAccounts/queueServices',
|
||||
'Microsoft.Storage/storageAccounts/tableServices',
|
||||
'Microsoft.StreamAnalytics/streamingJobs',
|
||||
'Microsoft.Web/serverfarms',
|
||||
'Microsoft.Web/sites',
|
||||
'Microsoft.Web/sites/slots',
|
||||
'Microsoft.Web/hostingEnvironments/multiRolePools',
|
||||
'Microsoft.Web/hostingEnvironments/workerPools',
|
||||
],
|
||||
chinaazuremonitor: [
|
||||
'Microsoft.AnalysisServices/servers',
|
||||
'Microsoft.Batch/batchAccounts',
|
||||
'Microsoft.Cache/redis',
|
||||
'Microsoft.ClassicCompute/virtualMachines',
|
||||
'Microsoft.ClassicCompute/domainNames/slots/roles',
|
||||
'Microsoft.CognitiveServices/accounts',
|
||||
'Microsoft.Compute/virtualMachines',
|
||||
'Microsoft.Compute/virtualMachineScaleSets',
|
||||
'Microsoft.ContainerRegistry/registries',
|
||||
'Microsoft.DBforMySQL/servers',
|
||||
'Microsoft.DBforPostgreSQL/servers',
|
||||
'Microsoft.Devices/IotHubs',
|
||||
'Microsoft.Devices/provisioningServices',
|
||||
'Microsoft.EventHub/namespaces',
|
||||
'Microsoft.Insights/AutoscaleSettings',
|
||||
'Microsoft.KeyVault/vaults',
|
||||
'Microsoft.Logic/workflows',
|
||||
'Microsoft.Network/networkInterfaces',
|
||||
'Microsoft.Network/loadBalancers',
|
||||
'Microsoft.Network/dnsZones',
|
||||
'Microsoft.Network/publicIPAddresses',
|
||||
'Microsoft.Network/azureFirewalls',
|
||||
'Microsoft.Network/applicationGateways',
|
||||
'Microsoft.Network/virtualNetworkGateways',
|
||||
'Microsoft.Network/expressRouteCircuits',
|
||||
'Microsoft.Network/expressRouteCircuits/Peerings',
|
||||
'Microsoft.Network/connections',
|
||||
'Microsoft.Network/trafficManagerProfiles',
|
||||
'Microsoft.Network/networkWatchers/connectionMonitors',
|
||||
'Microsoft.Network/frontdoors',
|
||||
'Microsoft.NotificationHubs/namespaces/notificationHubs',
|
||||
'Microsoft.PowerBIDedicated/capacities',
|
||||
'Microsoft.Relay/namespaces',
|
||||
'Microsoft.ServiceBus/namespaces',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'Microsoft.Sql/servers/elasticPools',
|
||||
'Microsoft.Sql/managedInstances',
|
||||
'Microsoft.Storage/storageAccounts',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'Microsoft.Storage/storageAccounts/fileServices',
|
||||
'Microsoft.Storage/storageAccounts/queueServices',
|
||||
'Microsoft.Storage/storageAccounts/tableServices',
|
||||
'Microsoft.StreamAnalytics/streamingJobs',
|
||||
'Microsoft.Web/serverfarms',
|
||||
'Microsoft.Web/sites',
|
||||
'Microsoft.Web/sites/slots',
|
||||
'Microsoft.Web/hostingEnvironments/multiRolePools',
|
||||
'Microsoft.Web/hostingEnvironments/workerPools',
|
||||
],
|
||||
};
|
||||
|
||||
constructor(private cloudName: string) {}
|
||||
|
||||
get() {
|
||||
return this.supportedMetricNamespaces[this.cloudName];
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
import UrlBuilder from './url_builder';
|
||||
|
||||
describe('AzureMonitorUrlBuilder', () => {
|
||||
describe('when metric definition is Microsoft.Sql/servers/databases', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'rn1/rn2',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Sql/servers', () => {
|
||||
it('should build the getMetricNames url in the shorter format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers',
|
||||
'rn',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Sql/servers/rn/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices', () => {
|
||||
it('should build the query url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorQueryUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview',
|
||||
'metricnames=aMetric'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
|
||||
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/fileServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices', () => {
|
||||
it('should build the query url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorQueryUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/fileServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview',
|
||||
'metricnames=aMetric'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
|
||||
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/tableServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices', () => {
|
||||
it('should build the query url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorQueryUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/tableServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview',
|
||||
'metricnames=aMetric'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
|
||||
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/queueServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices', () => {
|
||||
it('should build the query url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorQueryUrl(
|
||||
'',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/queueServices',
|
||||
'rn1/default',
|
||||
'2017-05-01-preview',
|
||||
'metricnames=aMetric'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
|
||||
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
export default class UrlBuilder {
|
||||
static buildAzureMonitorQueryUrl(
|
||||
baseUrl: string,
|
||||
resourceGroup: string,
|
||||
metricDefinition: string,
|
||||
resourceName: string,
|
||||
apiVersion: string,
|
||||
filter: string
|
||||
) {
|
||||
if ((metricDefinition.match(/\//g) || []).length > 1) {
|
||||
const rn = resourceName.split('/');
|
||||
const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
|
||||
const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
|
||||
return (
|
||||
`${baseUrl}/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
|
||||
`/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
`${baseUrl}/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
|
||||
`/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}`
|
||||
);
|
||||
}
|
||||
|
||||
static buildAzureMonitorGetMetricNamesUrl(
|
||||
baseUrl: string,
|
||||
resourceGroup: string,
|
||||
metricDefinition: string,
|
||||
resourceName: string,
|
||||
apiVersion: string
|
||||
) {
|
||||
if ((metricDefinition.match(/\//g) || []).length > 1) {
|
||||
const rn = resourceName.split('/');
|
||||
const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
|
||||
const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
|
||||
return (
|
||||
`${baseUrl}/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
|
||||
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
`${baseUrl}/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
|
||||
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
|
||||
import config from 'app/core/config';
|
||||
import { isVersionGtOrEq } from './version';
|
||||
|
||||
export class AzureMonitorConfigCtrl {
|
||||
static templateUrl = 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html';
|
||||
current: any;
|
||||
azureLogAnalyticsDatasource: any;
|
||||
workspaces: any[];
|
||||
hasRequiredGrafanaVersion: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, backendSrv, $q) {
|
||||
this.hasRequiredGrafanaVersion = this.hasMinVersion();
|
||||
this.current.jsonData.cloudName = this.current.jsonData.cloudName || 'azuremonitor';
|
||||
this.current.jsonData.azureLogAnalyticsSameAs = this.current.jsonData.azureLogAnalyticsSameAs || false;
|
||||
|
||||
if (this.current.id) {
|
||||
this.current.url = '/api/datasources/proxy/' + this.current.id;
|
||||
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(this.current, backendSrv, null, $q);
|
||||
this.getWorkspaces();
|
||||
}
|
||||
}
|
||||
|
||||
hasMinVersion(): boolean {
|
||||
return isVersionGtOrEq(config.buildInfo.version, '5.2');
|
||||
}
|
||||
|
||||
showMinVersionWarning() {
|
||||
return !this.hasRequiredGrafanaVersion && this.current.secureJsonFields.logAnalyticsClientSecret;
|
||||
}
|
||||
|
||||
getWorkspaces() {
|
||||
if (!this.azureLogAnalyticsDatasource.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.azureLogAnalyticsDatasource.getWorkspaces().then(workspaces => {
|
||||
this.workspaces = workspaces;
|
||||
if (this.workspaces.length > 0) {
|
||||
this.current.jsonData.logAnalyticsDefaultWorkspace =
|
||||
this.current.jsonData.logAnalyticsDefaultWorkspace || this.workspaces[0].value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
.min-width-10 {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.min-width-12 {
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.min-width-20 {
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
.gf-form-select-wrapper select.gf-form-input {
|
||||
height: 2.64rem;
|
||||
}
|
||||
|
||||
.gf-form-select-wrapper--caret-indent.gf-form-select-wrapper::after {
|
||||
right: 0.775rem
|
||||
}
|
||||
|
||||
.service-dropdown {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.aggregation-dropdown-wrapper {
|
||||
max-width: 29.1rem;
|
||||
}
|
||||
|
||||
.timegrainunit-dropdown-wrapper {
|
||||
width: 8rem;
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
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';
|
||||
|
||||
export default class Datasource {
|
||||
id: number;
|
||||
name: string;
|
||||
azureMonitorDatasource: AzureMonitorDatasource;
|
||||
appInsightsDatasource: AppInsightsDatasource;
|
||||
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private backendSrv, private templateSrv, private $q) {
|
||||
this.name = instanceSettings.name;
|
||||
this.id = instanceSettings.id;
|
||||
this.azureMonitorDatasource = new AzureMonitorDatasource(
|
||||
instanceSettings,
|
||||
this.backendSrv,
|
||||
this.templateSrv,
|
||||
this.$q
|
||||
);
|
||||
this.appInsightsDatasource = new AppInsightsDatasource(
|
||||
instanceSettings,
|
||||
this.backendSrv,
|
||||
this.templateSrv,
|
||||
this.$q
|
||||
);
|
||||
|
||||
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(
|
||||
instanceSettings,
|
||||
this.backendSrv,
|
||||
this.templateSrv,
|
||||
this.$q
|
||||
);
|
||||
}
|
||||
|
||||
query(options) {
|
||||
const promises: any[] = [];
|
||||
const azureMonitorOptions = _.cloneDeep(options);
|
||||
const appInsightsTargets = _.cloneDeep(options);
|
||||
const azureLogAnalyticsTargets = _.cloneDeep(options);
|
||||
|
||||
azureMonitorOptions.targets = _.filter(azureMonitorOptions.targets, ['queryType', 'Azure Monitor']);
|
||||
appInsightsTargets.targets = _.filter(appInsightsTargets.targets, ['queryType', 'Application Insights']);
|
||||
azureLogAnalyticsTargets.targets = _.filter(azureLogAnalyticsTargets.targets, ['queryType', 'Azure Log Analytics']);
|
||||
|
||||
if (azureMonitorOptions.targets.length > 0) {
|
||||
const amPromise = this.azureMonitorDatasource.query(azureMonitorOptions);
|
||||
if (amPromise) {
|
||||
promises.push(amPromise);
|
||||
}
|
||||
}
|
||||
|
||||
if (appInsightsTargets.targets.length > 0) {
|
||||
const aiPromise = this.appInsightsDatasource.query(appInsightsTargets);
|
||||
if (aiPromise) {
|
||||
promises.push(aiPromise);
|
||||
}
|
||||
}
|
||||
|
||||
if (azureLogAnalyticsTargets.targets.length > 0) {
|
||||
const alaPromise = this.azureLogAnalyticsDatasource.query(azureLogAnalyticsTargets);
|
||||
if (alaPromise) {
|
||||
promises.push(alaPromise);
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
return this.$q.when({ data: [] });
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(results => {
|
||||
return { data: _.flatten(results) };
|
||||
});
|
||||
}
|
||||
|
||||
annotationQuery(options) {
|
||||
return this.azureLogAnalyticsDatasource.annotationQuery(options);
|
||||
}
|
||||
|
||||
metricFindQuery(query: string) {
|
||||
if (!query) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const aiResult = this.appInsightsDatasource.metricFindQuery(query);
|
||||
if (aiResult) {
|
||||
return aiResult;
|
||||
}
|
||||
|
||||
const amResult = this.azureMonitorDatasource.metricFindQuery(query);
|
||||
if (amResult) {
|
||||
return amResult;
|
||||
}
|
||||
|
||||
const alaResult = this.azureLogAnalyticsDatasource.metricFindQuery(query);
|
||||
if (alaResult) {
|
||||
return alaResult;
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
const promises: any[] = [];
|
||||
|
||||
if (this.azureMonitorDatasource.isConfigured()) {
|
||||
promises.push(this.azureMonitorDatasource.testDatasource());
|
||||
}
|
||||
|
||||
if (this.appInsightsDatasource.isConfigured()) {
|
||||
promises.push(this.appInsightsDatasource.testDatasource());
|
||||
}
|
||||
|
||||
if (this.azureLogAnalyticsDatasource.isConfigured()) {
|
||||
promises.push(this.azureLogAnalyticsDatasource.testDatasource());
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Nothing configured. At least one of the API's must be configured.`,
|
||||
title: 'Error',
|
||||
};
|
||||
}
|
||||
|
||||
return this.$q.all(promises).then(results => {
|
||||
let status = 'success';
|
||||
let message = '';
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].status !== 'success') {
|
||||
status = results[i].status;
|
||||
}
|
||||
message += `${i + 1}. ${results[i].message} `;
|
||||
}
|
||||
|
||||
return {
|
||||
status: status,
|
||||
message: message,
|
||||
title: _.upperFirst(status),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* Azure Monitor REST API methods */
|
||||
getResourceGroups() {
|
||||
return this.azureMonitorDatasource.getResourceGroups();
|
||||
}
|
||||
|
||||
getMetricDefinitions(resourceGroup: string) {
|
||||
return this.azureMonitorDatasource.getMetricDefinitions(resourceGroup);
|
||||
}
|
||||
|
||||
getResourceNames(resourceGroup: string, metricDefinition: string) {
|
||||
return this.azureMonitorDatasource.getResourceNames(resourceGroup, metricDefinition);
|
||||
}
|
||||
|
||||
getMetricNames(resourceGroup: string, metricDefinition: string, resourceName: string) {
|
||||
return this.azureMonitorDatasource.getMetricNames(resourceGroup, metricDefinition, resourceName);
|
||||
}
|
||||
|
||||
getMetricMetadata(resourceGroup: string, metricDefinition: string, resourceName: string, metricName: string) {
|
||||
return this.azureMonitorDatasource.getMetricMetadata(resourceGroup, metricDefinition, resourceName, metricName);
|
||||
}
|
||||
|
||||
/* Application Insights API method */
|
||||
getAppInsightsMetricNames() {
|
||||
return this.appInsightsDatasource.getMetricNames();
|
||||
}
|
||||
|
||||
getAppInsightsMetricMetadata(metricName) {
|
||||
return this.appInsightsDatasource.getMetricMetadata(metricName);
|
||||
}
|
||||
|
||||
getAppInsightsColumns(refId) {
|
||||
return this.appInsightsDatasource.logAnalyticsColumns[refId];
|
||||
}
|
||||
|
||||
/*Azure Log Analytics */
|
||||
getAzureLogAnalyticsWorkspaces() {
|
||||
return this.azureLogAnalyticsDatasource.getWorkspaces();
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 253 KiB |
After Width: | Height: | Size: 166 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 251 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 9.4 KiB |
113
public/app/plugins/datasource/grafana-azure-monitor-datasource/lib/monaco.min.js
vendored
Normal file
@ -0,0 +1,166 @@
|
||||
import LogAnalyticsQuerystringBuilder from './querystring_builder';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('LogAnalyticsDatasource', () => {
|
||||
let builder: LogAnalyticsQuerystringBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = new LogAnalyticsQuerystringBuilder(
|
||||
'query=Tablename | where $__timeFilter()',
|
||||
{
|
||||
interval: '5m',
|
||||
range: {
|
||||
from: moment().subtract(24, 'hours'),
|
||||
to: moment(),
|
||||
},
|
||||
rangeRaw: {
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
'TimeGenerated'
|
||||
);
|
||||
});
|
||||
|
||||
describe('when $__timeFilter has no column parameter', () => {
|
||||
it('should generate a time filter condition with TimeGenerated as the datetime field', () => {
|
||||
const query = builder.generate().uriString;
|
||||
|
||||
expect(query).toContain('where%20TimeGenerated%20%3E%3D%20datetime(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $__timeFilter has a column parameter', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | where $__timeFilter(myTime)';
|
||||
});
|
||||
|
||||
it('should generate a time filter condition with myTime as the datetime field', () => {
|
||||
const query = builder.generate().uriString;
|
||||
|
||||
expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $__contains and multi template variable has custom All value', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | where $__contains(col, all)';
|
||||
});
|
||||
|
||||
it('should generate a where..in clause', () => {
|
||||
const query = builder.generate().rawQuery;
|
||||
|
||||
expect(query).toContain(`where 1 == 1`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $__contains and multi template variable has one selected value', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = `query=Tablename | where $__contains(col, 'val1')`;
|
||||
});
|
||||
|
||||
it('should generate a where..in clause', () => {
|
||||
const query = builder.generate().rawQuery;
|
||||
|
||||
expect(query).toContain(`where col in ('val1')`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $__contains and multi template variable has multiple selected values', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = `query=Tablename | where $__contains(col, 'val1','val2')`;
|
||||
});
|
||||
|
||||
it('should generate a where..in clause', () => {
|
||||
const query = builder.generate().rawQuery;
|
||||
|
||||
expect(query).toContain(`where col in ('val1','val2')`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $__interval is in the query', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | summarize count() by Category, bin(TimeGenerated, $__interval)';
|
||||
});
|
||||
|
||||
it('should replace $__interval with the inbuilt interval option', () => {
|
||||
const query = builder.generate().uriString;
|
||||
|
||||
expect(query).toContain('bin(TimeGenerated%2C%205m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using $__from and $__to is in the query and range is until now', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | where myTime >= $__from and myTime <= $__to';
|
||||
});
|
||||
|
||||
it('should replace $__from and $__to with a datetime and the now() function', () => {
|
||||
const query = builder.generate().uriString;
|
||||
|
||||
expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
|
||||
expect(query).toContain('myTime%20%3C%3D%20now()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using $__from and $__to is in the query and range is a specific interval', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | where myTime >= $__from and myTime <= $__to';
|
||||
builder.options.range.to = moment().subtract(1, 'hour');
|
||||
builder.options.rangeRaw.to = 'now-1h';
|
||||
});
|
||||
|
||||
it('should replace $__from and $__to with datetimes', () => {
|
||||
const query = builder.generate().uriString;
|
||||
|
||||
expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
|
||||
expect(query).toContain('myTime%20%3C%3D%20datetime(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using $__escape and multi template variable has one selected value', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = `$__escapeMulti('\\grafana-vm\Network(eth0)\Total Bytes Received')`;
|
||||
});
|
||||
|
||||
it('should replace $__escape(val) with KQL style escaped string', () => {
|
||||
const query = builder.generate().uriString;
|
||||
expect(query).toContain(`%40'%5Cgrafana-vmNetwork(eth0)Total%20Bytes%20Received'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using $__escape and multi template variable has multiple selected values', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = `CounterPath in ($__escapeMulti('\\grafana-vm\Network(eth0)\Total','\\grafana-vm\Network(eth0)\Total'))`;
|
||||
});
|
||||
|
||||
it('should replace $__escape(val) with multiple KQL style escaped string', () => {
|
||||
const query = builder.generate().uriString;
|
||||
expect(query).toContain(
|
||||
`CounterPath%20in%20(%40'%5Cgrafana-vmNetwork(eth0)Total'%2C%20%40'%5Cgrafana-vmNetwork(eth0)Total')`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using $__escape and multi template variable has one selected value that contains comma', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = `$__escapeMulti('\\grafana-vm,\Network(eth0)\Total Bytes Received')`;
|
||||
});
|
||||
|
||||
it('should replace $__escape(val) with KQL style escaped string', () => {
|
||||
const query = builder.generate().uriString;
|
||||
expect(query).toContain(`%40'%5Cgrafana-vm%2CNetwork(eth0)Total%20Bytes%20Received'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when using $__escape and multi template variable value is not wrapped in single '`, () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = `$__escapeMulti(\\grafana-vm,\Network(eth0)\Total Bytes Received)`;
|
||||
});
|
||||
|
||||
it('should not replace macro', () => {
|
||||
const query = builder.generate().uriString;
|
||||
expect(query).toContain(`%24__escapeMulti(%5Cgrafana-vm%2CNetwork(eth0)Total%20Bytes%20Received)`);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default class LogAnalyticsQuerystringBuilder {
|
||||
constructor(public rawQueryString, public options, public defaultTimeField) {}
|
||||
|
||||
generate() {
|
||||
let queryString = this.rawQueryString;
|
||||
const macroRegexp = /\$__([_a-zA-Z0-9]+)\(([^\)]*)\)/gi;
|
||||
queryString = queryString.replace(macroRegexp, (match, p1, p2) => {
|
||||
if (p1 === 'contains') {
|
||||
return this.getMultiContains(p2);
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
queryString = queryString.replace(/\$__escapeMulti\(('[^]*')\)/gi, (match, p1) => this.escape(p1));
|
||||
|
||||
if (this.options) {
|
||||
queryString = queryString.replace(macroRegexp, (match, p1, p2) => {
|
||||
if (p1 === 'timeFilter') {
|
||||
return this.getTimeFilter(p2, this.options);
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
queryString = queryString.replace(/\$__interval/gi, this.options.interval);
|
||||
queryString = queryString.replace(/\$__from/gi, this.getFrom(this.options));
|
||||
queryString = queryString.replace(/\$__to/gi, this.getUntil(this.options));
|
||||
}
|
||||
const rawQuery = queryString;
|
||||
queryString = encodeURIComponent(queryString);
|
||||
const uriString = `query=${queryString}`;
|
||||
|
||||
return { uriString, rawQuery };
|
||||
}
|
||||
|
||||
getFrom(options) {
|
||||
const from = options.range.from;
|
||||
return `datetime(${moment(from)
|
||||
.startOf('minute')
|
||||
.toISOString()})`;
|
||||
}
|
||||
|
||||
getUntil(options) {
|
||||
if (options.rangeRaw.to === 'now') {
|
||||
return 'now()';
|
||||
} else {
|
||||
const until = options.range.to;
|
||||
return `datetime(${moment(until)
|
||||
.startOf('minute')
|
||||
.toISOString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeFilter(timeFieldArg, options) {
|
||||
const timeField = timeFieldArg || this.defaultTimeField;
|
||||
if (options.rangeRaw.to === 'now') {
|
||||
return `${timeField} >= ${this.getFrom(options)}`;
|
||||
} else {
|
||||
return `${timeField} >= ${this.getFrom(options)} and ${timeField} <= ${this.getUntil(options)}`;
|
||||
}
|
||||
}
|
||||
|
||||
getMultiContains(inputs: string) {
|
||||
const firstCommaIndex = inputs.indexOf(',');
|
||||
const field = inputs.substring(0, firstCommaIndex);
|
||||
const templateVar = inputs.substring(inputs.indexOf(',') + 1);
|
||||
|
||||
if (templateVar && templateVar.toLowerCase().trim() === 'all') {
|
||||
return '1 == 1';
|
||||
}
|
||||
|
||||
return `${field.trim()} in (${templateVar.trim()})`;
|
||||
}
|
||||
|
||||
escape(inputs: string) {
|
||||
return inputs
|
||||
.substring(1, inputs.length - 1)
|
||||
.split(`','`)
|
||||
.map(v => `@'${v}'`)
|
||||
.join(', ');
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import Datasource from './datasource';
|
||||
import { AzureMonitorQueryCtrl } from './query_ctrl';
|
||||
import { AzureMonitorAnnotationsQueryCtrl } from './annotations_query_ctrl';
|
||||
import { AzureMonitorConfigCtrl } from './config_ctrl';
|
||||
|
||||
export {
|
||||
Datasource,
|
||||
AzureMonitorQueryCtrl as QueryCtrl,
|
||||
AzureMonitorConfigCtrl as ConfigCtrl,
|
||||
AzureMonitorAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
@ -0,0 +1 @@
|
||||
Object.assign({});
|
@ -0,0 +1,219 @@
|
||||
// tslint:disable-next-line:no-reference
|
||||
///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
|
||||
|
||||
import KustoCodeEditor from './kusto_code_editor';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('KustoCodeEditor', () => {
|
||||
let editor;
|
||||
|
||||
describe('getCompletionItems', () => {
|
||||
let completionItems;
|
||||
let lineContent;
|
||||
let model;
|
||||
|
||||
beforeEach(() => {
|
||||
(global as any).monaco = {
|
||||
languages: {
|
||||
CompletionItemKind: {
|
||||
Keyword: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
model = {
|
||||
getLineCount: () => 3,
|
||||
getValueInRange: () => 'atable/n' + lineContent,
|
||||
getLineContent: () => lineContent,
|
||||
};
|
||||
|
||||
const StandaloneMock = jest.fn<monaco.editor.ICodeEditor>();
|
||||
editor = new KustoCodeEditor(null, 'TimeGenerated', () => {}, {});
|
||||
editor.codeEditor = new StandaloneMock();
|
||||
});
|
||||
|
||||
describe('when no where clause and no | in model text', () => {
|
||||
beforeEach(() => {
|
||||
lineContent = ' ';
|
||||
const position = { lineNumber: 2, column: 2 };
|
||||
completionItems = editor.getCompletionItems(model, position);
|
||||
});
|
||||
|
||||
it('should not return any grafana macros', () => {
|
||||
expect(completionItems.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no where clause in model text', () => {
|
||||
beforeEach(() => {
|
||||
lineContent = '| ';
|
||||
const position = { lineNumber: 2, column: 3 };
|
||||
completionItems = editor.getCompletionItems(model, position);
|
||||
});
|
||||
|
||||
it('should return grafana macros for where and timefilter', () => {
|
||||
expect(completionItems.length).toBe(1);
|
||||
|
||||
expect(completionItems[0].label).toBe('where $__timeFilter(timeColumn)');
|
||||
expect(completionItems[0].insertText.value).toBe('where \\$__timeFilter(${0:TimeGenerated})');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on line with where clause', () => {
|
||||
beforeEach(() => {
|
||||
lineContent = '| where Test == 2 and ';
|
||||
const position = { lineNumber: 2, column: 23 };
|
||||
completionItems = editor.getCompletionItems(model, position);
|
||||
});
|
||||
|
||||
it('should return grafana macros and variables', () => {
|
||||
expect(completionItems.length).toBe(4);
|
||||
|
||||
expect(completionItems[0].label).toBe('$__timeFilter(timeColumn)');
|
||||
expect(completionItems[0].insertText.value).toBe('\\$__timeFilter(${0:TimeGenerated})');
|
||||
|
||||
expect(completionItems[1].label).toBe('$__from');
|
||||
expect(completionItems[1].insertText.value).toBe('\\$__from');
|
||||
|
||||
expect(completionItems[2].label).toBe('$__to');
|
||||
expect(completionItems[2].insertText.value).toBe('\\$__to');
|
||||
|
||||
expect(completionItems[3].label).toBe('$__interval');
|
||||
expect(completionItems[3].insertText.value).toBe('\\$__interval');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDidChangeCursorSelection', () => {
|
||||
const keyboardEvent = {
|
||||
selection: {
|
||||
startLineNumber: 4,
|
||||
startColumn: 26,
|
||||
endLineNumber: 4,
|
||||
endColumn: 31,
|
||||
selectionStartLineNumber: 4,
|
||||
selectionStartColumn: 26,
|
||||
positionLineNumber: 4,
|
||||
positionColumn: 31,
|
||||
},
|
||||
secondarySelections: [],
|
||||
source: 'keyboard',
|
||||
reason: 3,
|
||||
};
|
||||
|
||||
const modelChangedEvent = {
|
||||
selection: {
|
||||
startLineNumber: 2,
|
||||
startColumn: 1,
|
||||
endLineNumber: 3,
|
||||
endColumn: 3,
|
||||
selectionStartLineNumber: 2,
|
||||
selectionStartColumn: 1,
|
||||
positionLineNumber: 3,
|
||||
positionColumn: 3,
|
||||
},
|
||||
secondarySelections: [],
|
||||
source: 'modelChange',
|
||||
reason: 2,
|
||||
};
|
||||
|
||||
describe('suggestion trigger', () => {
|
||||
let suggestionTriggered;
|
||||
let lineContent = '';
|
||||
|
||||
beforeEach(() => {
|
||||
(global as any).monaco = {
|
||||
languages: {
|
||||
CompletionItemKind: {
|
||||
Keyword: '',
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
CursorChangeReason: {
|
||||
NotSet: 0,
|
||||
ContentFlush: 1,
|
||||
RecoverFromMarkers: 2,
|
||||
Explicit: 3,
|
||||
Paste: 4,
|
||||
Undo: 5,
|
||||
Redo: 6,
|
||||
},
|
||||
},
|
||||
};
|
||||
const StandaloneMock = jest.fn<monaco.editor.ICodeEditor>(() => ({
|
||||
getModel: () => {
|
||||
return {
|
||||
getLineCount: () => 3,
|
||||
getLineContent: () => lineContent,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
editor = new KustoCodeEditor(null, 'TimeGenerated', () => {}, {});
|
||||
editor.codeEditor = new StandaloneMock();
|
||||
editor.triggerSuggestions = () => {
|
||||
suggestionTriggered = true;
|
||||
};
|
||||
});
|
||||
|
||||
describe('when model change event, reason is RecoverFromMarkers and there is a space after', () => {
|
||||
beforeEach(() => {
|
||||
suggestionTriggered = false;
|
||||
lineContent = '| ';
|
||||
editor.onDidChangeCursorSelection(modelChangedEvent);
|
||||
});
|
||||
|
||||
it('should trigger suggestion', () => {
|
||||
expect(suggestionTriggered).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not model change event', () => {
|
||||
beforeEach(() => {
|
||||
suggestionTriggered = false;
|
||||
editor.onDidChangeCursorSelection(keyboardEvent);
|
||||
});
|
||||
|
||||
it('should not trigger suggestion', () => {
|
||||
expect(suggestionTriggered).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when model change event but with incorrect reason', () => {
|
||||
beforeEach(() => {
|
||||
suggestionTriggered = false;
|
||||
const modelChangedWithInvalidReason = _.cloneDeep(modelChangedEvent);
|
||||
modelChangedWithInvalidReason.reason = 5;
|
||||
editor.onDidChangeCursorSelection(modelChangedWithInvalidReason);
|
||||
});
|
||||
|
||||
it('should not trigger suggestion', () => {
|
||||
expect(suggestionTriggered).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when model change event but with no space after', () => {
|
||||
beforeEach(() => {
|
||||
suggestionTriggered = false;
|
||||
lineContent = '|';
|
||||
editor.onDidChangeCursorSelection(modelChangedEvent);
|
||||
});
|
||||
|
||||
it('should not trigger suggestion', () => {
|
||||
expect(suggestionTriggered).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when model change event but with no space after', () => {
|
||||
beforeEach(() => {
|
||||
suggestionTriggered = false;
|
||||
lineContent = '|';
|
||||
editor.onDidChangeCursorSelection(modelChangedEvent);
|
||||
});
|
||||
|
||||
it('should not trigger suggestion', () => {
|
||||
expect(suggestionTriggered).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,326 @@
|
||||
// tslint:disable-next-line:no-reference
|
||||
///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface SuggestionController {
|
||||
_model: any;
|
||||
}
|
||||
|
||||
export default class KustoCodeEditor {
|
||||
codeEditor: monaco.editor.IStandaloneCodeEditor;
|
||||
completionItemProvider: monaco.IDisposable;
|
||||
signatureHelpProvider: monaco.IDisposable;
|
||||
|
||||
splitWithNewLineRegex = /[^\n]+\n?|\n/g;
|
||||
newLineRegex = /\r?\n/;
|
||||
startsWithKustoPipeRegex = /^\|\s*/g;
|
||||
kustoPipeRegexStrict = /^\|\s*$/g;
|
||||
|
||||
constructor(
|
||||
private containerDiv: any,
|
||||
private defaultTimeField: string,
|
||||
private getSchema: () => any,
|
||||
private config: any
|
||||
) {}
|
||||
|
||||
initMonaco(scope) {
|
||||
const themeName = this.config.bootData.user.lightTheme ? 'grafana-light' : 'vs-dark';
|
||||
|
||||
monaco.editor.defineTheme('grafana-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'comment', foreground: '008000' },
|
||||
{ token: 'variable.predefined', foreground: '800080' },
|
||||
{ token: 'function', foreground: '0000FF' },
|
||||
{ token: 'operator.sql', foreground: 'FF4500' },
|
||||
{ token: 'string', foreground: 'B22222' },
|
||||
{ token: 'operator.scss', foreground: '0000FF' },
|
||||
{ token: 'variable', foreground: 'C71585' },
|
||||
{ token: 'variable.parameter', foreground: '9932CC' },
|
||||
{ token: '', foreground: '000000' },
|
||||
{ token: 'type', foreground: '0000FF' },
|
||||
{ token: 'tag', foreground: '0000FF' },
|
||||
{ token: 'annotation', foreground: '2B91AF' },
|
||||
{ token: 'keyword', foreground: '0000FF' },
|
||||
{ token: 'number', foreground: '191970' },
|
||||
{ token: 'annotation', foreground: '9400D3' },
|
||||
{ token: 'invalid', background: 'cd3131' },
|
||||
],
|
||||
colors: {
|
||||
'textCodeBlock.background': '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
monaco.languages['kusto'].kustoDefaults.setLanguageSettings({
|
||||
includeControlCommands: true,
|
||||
newlineAfterPipe: true,
|
||||
useIntellisenseV2: false,
|
||||
});
|
||||
|
||||
this.codeEditor = monaco.editor.create(this.containerDiv, {
|
||||
value: scope.content || 'Write your query here',
|
||||
language: 'kusto',
|
||||
selectionHighlight: false,
|
||||
theme: themeName,
|
||||
folding: true,
|
||||
lineNumbers: 'off',
|
||||
lineHeight: 16,
|
||||
suggestFontSize: 13,
|
||||
dragAndDrop: false,
|
||||
occurrencesHighlight: false,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
renderIndentGuides: false,
|
||||
wordWrap: 'on',
|
||||
});
|
||||
this.codeEditor.layout();
|
||||
|
||||
if (monaco.editor.getModels().length === 1) {
|
||||
this.completionItemProvider = monaco.languages.registerCompletionItemProvider('kusto', {
|
||||
triggerCharacters: ['.', ' '],
|
||||
provideCompletionItems: this.getCompletionItems.bind(this),
|
||||
});
|
||||
|
||||
this.signatureHelpProvider = monaco.languages.registerSignatureHelpProvider('kusto', {
|
||||
signatureHelpTriggerCharacters: ['(', ')'],
|
||||
provideSignatureHelp: this.getSignatureHelp.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
this.codeEditor.createContextKey('readyToExecute', true);
|
||||
|
||||
this.codeEditor.onDidChangeCursorSelection(event => {
|
||||
this.onDidChangeCursorSelection(event);
|
||||
});
|
||||
|
||||
this.getSchema().then(schema => {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
monaco.languages['kusto'].getKustoWorker().then(workerAccessor => {
|
||||
const model = this.codeEditor.getModel();
|
||||
workerAccessor(model.uri).then(worker => {
|
||||
const dbName = Object.keys(schema.Databases).length > 0 ? Object.keys(schema.Databases)[0] : '';
|
||||
worker.setSchemaFromShowSchema(schema, 'https://help.kusto.windows.net', dbName);
|
||||
this.codeEditor.layout();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setOnDidChangeModelContent(listener) {
|
||||
this.codeEditor.onDidChangeModelContent(listener);
|
||||
}
|
||||
|
||||
disposeMonaco() {
|
||||
if (this.completionItemProvider) {
|
||||
try {
|
||||
this.completionItemProvider.dispose();
|
||||
} catch (e) {
|
||||
console.error('Failed to dispose the completion item provider.', e);
|
||||
}
|
||||
}
|
||||
if (this.signatureHelpProvider) {
|
||||
try {
|
||||
this.signatureHelpProvider.dispose();
|
||||
} catch (e) {
|
||||
console.error('Failed to dispose the signature help provider.', e);
|
||||
}
|
||||
}
|
||||
if (this.codeEditor) {
|
||||
try {
|
||||
this.codeEditor.dispose();
|
||||
} catch (e) {
|
||||
console.error('Failed to dispose the editor component.', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addCommand(keybinding: number, commandFunc: monaco.editor.ICommandHandler) {
|
||||
this.codeEditor.addCommand(keybinding, commandFunc, 'readyToExecute');
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.codeEditor.getValue();
|
||||
}
|
||||
|
||||
toSuggestionController(srv: monaco.editor.IEditorContribution): SuggestionController {
|
||||
return srv as any;
|
||||
}
|
||||
|
||||
setEditorContent(value) {
|
||||
this.codeEditor.setValue(value);
|
||||
}
|
||||
|
||||
getCompletionItems(model: monaco.editor.IReadOnlyModel, position: monaco.Position) {
|
||||
const timeFilterDocs =
|
||||
'##### Macro that uses the selected timerange in Grafana to filter the query.\n\n' +
|
||||
'- `$__timeFilter()` -> Uses the ' +
|
||||
this.defaultTimeField +
|
||||
' column\n\n' +
|
||||
'- `$__timeFilter(datetimeColumn)` -> Uses the specified datetime column to build the query.';
|
||||
|
||||
const textUntilPosition = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column,
|
||||
});
|
||||
|
||||
if (!_.includes(textUntilPosition, '|')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!_.includes(textUntilPosition.toLowerCase(), 'where')) {
|
||||
return [
|
||||
{
|
||||
label: 'where $__timeFilter(timeColumn)',
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: {
|
||||
value: 'where \\$__timeFilter(${0:' + this.defaultTimeField + '})',
|
||||
},
|
||||
documentation: {
|
||||
value: timeFilterDocs,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (_.includes(model.getLineContent(position.lineNumber).toLowerCase(), 'where')) {
|
||||
return [
|
||||
{
|
||||
label: '$__timeFilter(timeColumn)',
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: {
|
||||
value: '\\$__timeFilter(${0:' + this.defaultTimeField + '})',
|
||||
},
|
||||
documentation: {
|
||||
value: timeFilterDocs,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '$__from',
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: {
|
||||
value: `\\$__from`,
|
||||
},
|
||||
documentation: {
|
||||
value:
|
||||
'Built-in variable that returns the from value of the selected timerange in Grafana.\n\n' +
|
||||
'Example: `where ' +
|
||||
this.defaultTimeField +
|
||||
' > $__from` ',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '$__to',
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: {
|
||||
value: `\\$__to`,
|
||||
},
|
||||
documentation: {
|
||||
value:
|
||||
'Built-in variable that returns the to value of the selected timerange in Grafana.\n\n' +
|
||||
'Example: `where ' +
|
||||
this.defaultTimeField +
|
||||
' < $__to` ',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '$__interval',
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: {
|
||||
value: `\\$__interval`,
|
||||
},
|
||||
documentation: {
|
||||
value:
|
||||
'##### Built-in variable that returns an automatic time grain suitable for the current timerange.\n\n' +
|
||||
'Used with the bin() function - `bin(' +
|
||||
this.defaultTimeField +
|
||||
', $__interval)` \n\n' +
|
||||
'[Grafana docs](http://docs.grafana.org/reference/templating/#the-interval-variable)',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
getSignatureHelp(model: monaco.editor.IReadOnlyModel, position: monaco.Position, token: monaco.CancellationToken) {
|
||||
const textUntilPosition = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column - 14,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column,
|
||||
});
|
||||
|
||||
if (textUntilPosition !== '$__timeFilter(') {
|
||||
return {} as monaco.languages.SignatureHelp;
|
||||
}
|
||||
|
||||
const signature: monaco.languages.SignatureHelp = {
|
||||
activeParameter: 0,
|
||||
activeSignature: 0,
|
||||
signatures: [
|
||||
{
|
||||
label: '$__timeFilter(timeColumn)',
|
||||
parameters: [
|
||||
{
|
||||
label: 'timeColumn',
|
||||
documentation:
|
||||
'Default is ' +
|
||||
this.defaultTimeField +
|
||||
' column. Datetime column to filter data using the selected date range. ',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
onDidChangeCursorSelection(event) {
|
||||
if (event.source !== 'modelChange' || event.reason !== monaco.editor.CursorChangeReason.RecoverFromMarkers) {
|
||||
return;
|
||||
}
|
||||
const lastChar = this.getCharAt(event.selection.positionLineNumber, event.selection.positionColumn - 1);
|
||||
|
||||
if (lastChar !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.triggerSuggestions();
|
||||
}
|
||||
|
||||
triggerSuggestions() {
|
||||
const suggestController = this.codeEditor.getContribution('editor.contrib.suggestController');
|
||||
if (!suggestController) {
|
||||
return;
|
||||
}
|
||||
|
||||
const convertedController = this.toSuggestionController(suggestController);
|
||||
|
||||
convertedController._model.cancel();
|
||||
setTimeout(() => {
|
||||
convertedController._model.trigger(true);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
getCharAt(lineNumber: number, column: number) {
|
||||
const model = this.codeEditor.getModel();
|
||||
if (model.getLineCount() === 0 || model.getLineCount() < lineNumber) {
|
||||
return '';
|
||||
}
|
||||
const line = model.getLineContent(lineNumber);
|
||||
if (line.length < column || column < 1) {
|
||||
return '';
|
||||
}
|
||||
return line[column - 1];
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
// tslint:disable-next-line:no-reference
|
||||
///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import KustoCodeEditor from './kusto_code_editor';
|
||||
import config from 'app/core/config';
|
||||
|
||||
const editorTemplate = `<div id="content" tabindex="0" style="width: 100%; height: 120px"></div>`;
|
||||
|
||||
function link(scope, elem, attrs) {
|
||||
const containerDiv = elem.find('#content')[0];
|
||||
|
||||
if (!(global as any).monaco) {
|
||||
(global as any).System.import(`./${scope.pluginBaseUrl}/lib/monaco.min.js`).then(() => {
|
||||
setTimeout(() => {
|
||||
initMonaco(containerDiv, scope);
|
||||
}, 1);
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
initMonaco(containerDiv, scope);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
containerDiv.onblur = () => {
|
||||
scope.onChange();
|
||||
};
|
||||
|
||||
containerDiv.onkeydown = evt => {
|
||||
if (evt.key === 'Escape') {
|
||||
evt.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function initMonaco(containerDiv, scope) {
|
||||
const kustoCodeEditor = new KustoCodeEditor(containerDiv, scope.defaultTimeField, scope.getSchema, config);
|
||||
|
||||
kustoCodeEditor.initMonaco(scope);
|
||||
|
||||
/* tslint:disable:no-bitwise */
|
||||
kustoCodeEditor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
|
||||
const newValue = kustoCodeEditor.getValue();
|
||||
scope.content = newValue;
|
||||
scope.onChange();
|
||||
});
|
||||
/* tslint:enable:no-bitwise */
|
||||
|
||||
// Sync with outer scope - update editor content if model has been changed from outside of directive.
|
||||
scope.$watch('content', (newValue, oldValue) => {
|
||||
const editorValue = kustoCodeEditor.getValue();
|
||||
if (newValue !== editorValue && newValue !== oldValue) {
|
||||
scope.$$postDigest(() => {
|
||||
kustoCodeEditor.setEditorContent(newValue);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
kustoCodeEditor.setOnDidChangeModelContent(() => {
|
||||
scope.$apply(() => {
|
||||
const newValue = kustoCodeEditor.getValue();
|
||||
scope.content = newValue;
|
||||
});
|
||||
});
|
||||
|
||||
scope.$on('$destroy', () => {
|
||||
kustoCodeEditor.disposeMonaco();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function kustoMonacoEditorDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: editorTemplate,
|
||||
scope: {
|
||||
content: '=',
|
||||
onChange: '&',
|
||||
getSchema: '&',
|
||||
defaultTimeField: '@',
|
||||
pluginBaseUrl: '@',
|
||||
},
|
||||
link: link,
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').directive('kustoMonacoEditor', kustoMonacoEditorDirective);
|
@ -0,0 +1,64 @@
|
||||
<div class="gf-form-group">
|
||||
<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" ng-model="ctrl.annotation.queryType" ng-options="f as f for f in ['Application Insights', 'Azure Monitor', 'Azure Log Analytics']"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="ctrl.annotation.queryType === 'Azure Log Analytics'">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Workspace</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input min-width-12" ng-model="ctrl.annotation.workspace" ng-options="f.value as f.text for f in ctrl.workspaces"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<div class="width-1"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-primary width-10" ng-click="ctrl.panelCtrl.refresh()">Run</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">(Run Query: Shift+Enter, Trigger Suggestion: Ctrl+Space)</label>
|
||||
</div>
|
||||
</div>
|
||||
<kusto-monaco-editor content="ctrl.annotation.rawQuery" get-schema="ctrl.datasource.azureLogAnalyticsDatasource.getSchema(ctrl.annotation.workspace)"
|
||||
default-time-field="TimeGenerated" plugin-base-url={{ctrl.datasource.meta.baseUrl}}></kusto-monaco-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-show="ctrl.annotation.queryType !== 'Azure Log Analytics'">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label">No annotations support for {{ctrl.annotation.queryType}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
Show Help
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info" ng-show="ctrl.annotation.queryType === 'Azure Log Analytics'"><h6>Annotation Query Format</h6>
|
||||
An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the datetime column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
|
||||
|
||||
- column with the datetime type.
|
||||
- column with alias: <b>Text</b> or <b>text</b> for the annotation text
|
||||
- column with alias: <b>Tags</b> or <b>tags</b> for annotation tags. This is should return a comma separated string of tags e.g. 'tag1,tag2'
|
||||
|
||||
Macros:
|
||||
- $__timeFilter() -> TimeGenerated ≥ datetime(2018-06-05T18:09:58.907Z) and TimeGenerated ≤ datetime(2018-06-05T20:09:58.907Z)
|
||||
- $__timeFilter(datetimeColumn) -> datetimeColumn ≥ datetime(2018-06-05T18:09:58.907Z) and datetimeColumn ≤ datetime(2018-06-05T20:09:58.907Z)
|
||||
|
||||
Or build your own conditionals using these built-in variables which just return the values:
|
||||
- $__from -> datetime(2018-06-05T18:09:58.907Z)
|
||||
- $__to -> datetime(2018-06-05T20:09:58.907Z)
|
||||
- $__interval -> 5m
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,196 @@
|
||||
<h3 class="page-heading">Azure Monitor API Details</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Azure Cloud</span>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.cloudName" ng-options="f.value as f.text for f in [{value: 'azuremonitor', text: 'Azure'}, {value: 'govazuremonitor', text: 'Azure US Government'}, {value: 'germanyazuremonitor', text: 'Azure Germany'}, {value: 'chinaazuremonitor', text: 'Azure China'}]"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
<info-popover mode="right-normal">
|
||||
<p>Choose an Azure Cloud.</p>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Subscription Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.subscriptionId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Tenant Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.tenantId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>In the Azure Portal, navigate to Azure Active Directory -> Properties -> Directory ID.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Client Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.clientId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>In the Azure Portal, navigate to Azure Active Directory -> App Registrations -> Choose your app ->
|
||||
Application ID.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-if="!ctrl.current.secureJsonFields.clientSecret">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Client Secret</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.clientSecret" placeholder=""></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>To create a new key, log in to Azure Portal, navigate to Azure Active Directory -> App Registrations ->
|
||||
Choose your
|
||||
app -> Keys.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.clientSecret">
|
||||
<span class="gf-form-label width-9">Client Secret</span>
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.clientSecret = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">Azure Log Analytics API Details</h3>
|
||||
|
||||
<div class="grafana-info-box ng-scope">
|
||||
The Azure Log Analytics support is marked as being in a preview development state. This means it is in currently in active development and major changes might be made - depending on feedback from users.
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<gf-form-switch class="gf-form" label="Same details as Azure Monitor API" label-class="width-19" switch-class="max-width-6"
|
||||
checked="ctrl.current.jsonData.azureLogAnalyticsSameAs" on-change="ctrl.onSameAsToggle()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-show="!ctrl.current.jsonData.azureLogAnalyticsSameAs">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Subscription Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.logAnalyticsSubscriptionId"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" />
|
||||
<info-popover mode="right-absolute">
|
||||
<p>In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Tenant Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.logAnalyticsTenantId"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" />
|
||||
<info-popover mode="right-absolute">
|
||||
<p>In the Azure Portal, navigate to Azure Active Directory -> Properties -> Directory ID.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Client Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.logAnalyticsClientId"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>In the Azure Portal, navigate to Azure Active Directory -> App Registrations -> Choose your app ->
|
||||
Application ID.
|
||||
</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-if="!ctrl.current.secureJsonFields.logAnalyticsClientSecret">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Client Secret</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.logAnalyticsClientSecret"
|
||||
placeholder="" />
|
||||
<info-popover mode="right-absolute">
|
||||
<p>To create a new key, log in to Azure Portal, navigate to Azure Active Directory -> App Registrations ->
|
||||
Choose your
|
||||
app -> Keys.</p>
|
||||
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
|
||||
here for detailed instructions on setting up an Azure Active Directory (AD) application.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.logAnalyticsClientSecret">
|
||||
<span class="gf-form-label width-9">Client Secret</span>
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.logAnalyticsClientSecret = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Default Workspace</span>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.logAnalyticsDefaultWorkspace" ng-options="f.value as f.text for f in ctrl.workspaces"
|
||||
ng-disabled="!ctrl.workspaces"></select>
|
||||
</div>
|
||||
<info-popover mode="right-normal">
|
||||
<p>Choose the default/preferred Workspace for Azure Log Analytics queries.</p>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.showMinVersionWarning()">
|
||||
<div class=" alert alert-error">
|
||||
<p>
|
||||
The Azure Log Analytics feature requires Grafana 5.2.0 or greater. Download a new version of
|
||||
Grafana
|
||||
<a class="external-link" target="_blank" href="https://grafana.com/get">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">Application Insights Details</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline" ng-if="!ctrl.current.secureJsonFields.appInsightsApiKey">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">API Key</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.appInsightsApiKey"
|
||||
placeholder="" />
|
||||
<info-popover mode="right-absolute">
|
||||
<p>Section 2 of the Quickstart guide shows where to find/create the API Key:</p>
|
||||
<a target="_blank" href="https://dev.applicationinsights.io/quickstart/">**Click here to open the Application
|
||||
Insights Quickstart.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.appInsightsApiKey">
|
||||
<span class="gf-form-label width-9">API Key</span>
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.appInsightsApiKey = false">reset</a>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Application Id</span>
|
||||
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.appInsightsAppId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>Section 2 of the Quickstart guide shows where to find the Application ID:</p>
|
||||
<a target="_blank" href="https://dev.applicationinsights.io/quickstart/">**Click here to open the Application
|
||||
Insights Quickstart.**</a>
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,313 @@
|
||||
<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" ng-model="ctrl.target.queryType" ng-options="f as f for f in ['Application Insights', 'Azure Monitor', 'Azure Log Analytics']"
|
||||
ng-change="ctrl.onQueryTypeChange()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="ctrl.target.queryType === 'Azure Monitor'">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Resource Group</label>
|
||||
<gf-form-dropdown model="ctrl.target.azureMonitor.resourceGroup" allow-custom="true" lookup-text="true"
|
||||
get-options="ctrl.getResourceGroups($query)" on-change="ctrl.onResourceGroupChange()" css-class="min-width-12">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Namespace</label>
|
||||
<gf-form-dropdown model="ctrl.target.azureMonitor.metricDefinition" allow-custom="true" lookup-text="true"
|
||||
get-options="ctrl.getMetricDefinitions($query)" on-change="ctrl.onMetricDefinitionChange()" css-class="min-width-20">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Resource Name</label>
|
||||
<gf-form-dropdown model="ctrl.target.azureMonitor.resourceName" allow-custom="true" lookup-text="true"
|
||||
get-options="ctrl.getResourceNames($query)" on-change="ctrl.onResourceNameChange()" css-class="min-width-12">
|
||||
</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">Metric</label>
|
||||
<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">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow aggregation-dropdown-wrapper">
|
||||
<label class="gf-form-label query-keyword width-9">Aggregation</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input width-11" ng-model="ctrl.target.azureMonitor.aggregation" ng-options="f as f for f in ctrl.target.azureMonitor.aggOptions"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<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">
|
||||
<select class="gf-form-input" ng-model="ctrl.target.azureMonitor.timeGrain" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.timeGrains"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<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">{{ctrl.getAutoInterval()}}</label>
|
||||
</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" ng-show="ctrl.target.azureMonitor.dimensions.length > 0">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Dimension</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input min-width-12" ng-model="ctrl.target.azureMonitor.dimension" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.dimensions"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-3">eq</label>
|
||||
<input type="text" class="gf-form-input width-17" ng-model="ctrl.target.azureMonitor.dimensionFilter"
|
||||
spellcheck="false" placeholder="auto" ng-blur="ctrl.refresh()">
|
||||
</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">Legend Format</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model="ctrl.target.azureMonitor.alias" spellcheck="false"
|
||||
placeholder="alias patterns (see help for more info)" ng-blur="ctrl.refresh()">
|
||||
</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 === 'Azure Log Analytics'">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Workspace</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input min-width-12" ng-model="ctrl.target.azureLogAnalytics.workspace" ng-options="f.value as f.text for f in ctrl.workspaces"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<div class="width-1"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">(Run Query: Shift+Enter, Trigger Suggestion: Ctrl+Space)</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<kusto-monaco-editor content="ctrl.target.azureLogAnalytics.query" on-change="ctrl.refresh()" get-schema="ctrl.getAzureLogAnalyticsSchema()"
|
||||
default-time-field="TimeGenerated" plugin-base-url={{ctrl.datasource.meta.baseUrl}}></kusto-monaco-editor>
|
||||
|
||||
<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.azureLogAnalytics.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats"
|
||||
ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
Show Help
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQuery">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuery = !ctrl.showLastQuery">
|
||||
Raw Query
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showLastQuery"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showLastQuery"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuery">
|
||||
<pre class="gf-form-pre">{{ctrl.lastQuery}}</pre>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info">
|
||||
Format as Table:
|
||||
- return any set of columns
|
||||
|
||||
Format as Time series:
|
||||
- Requires a column of type datetime
|
||||
- returns the first column with a numeric datatype as the value
|
||||
- (Optional: returns the first column with type string to represent the series name. If no column is found the column name of the value column is used as series name)
|
||||
|
||||
Example Time Series Query:
|
||||
|
||||
AzureActivity
|
||||
| where $__timeFilter()
|
||||
| summarize count() by Category, bin(TimeGenerated, 60min)
|
||||
| order by TimeGenerated asc
|
||||
|
||||
Macros:
|
||||
- $__timeFilter() -> TimeGenerated ≥ datetime(2018-06-05T18:09:58.907Z) and TimeGenerated ≤ datetime(2018-06-05T20:09:58.907Z)
|
||||
- $__timeFilter(datetimeColumn) -> datetimeColumn ≥ datetime(2018-06-05T18:09:58.907Z) and datetimeColumn ≤ datetime(2018-06-05T20:09:58.907Z)
|
||||
- $__escapeMulti($myTemplateVar) -> $myTemplateVar should be a multi-value template variables that contains illegal characters
|
||||
- $__contains(aColumn, $myTemplateVar) -> aColumn in ($myTemplateVar)
|
||||
If using the All option, then check the Include All Option checkbox and in the Custom all value field type in: all. If All is chosen -> 1 == 1
|
||||
|
||||
Or build your own conditionals using these built-in variables which just return the values:
|
||||
- $__from -> datetime(2018-06-05T18:09:58.907Z)
|
||||
- $__to -> datetime(2018-06-05T20:09:58.907Z)
|
||||
- $__interval -> 5m
|
||||
|
||||
Examples:
|
||||
- ¡ where $__timeFilter
|
||||
- | where TimeGenerated ≥ $__from and TimeGenerated ≤ $__to
|
||||
- | summarize count() by Category, bin(TimeGenerated, $__interval)
|
||||
</pre>
|
||||
</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>
|
||||
<gf-form-dropdown model="ctrl.target.appInsights.metricName" allow-custom="true" lookup-text="true"
|
||||
get-options="ctrl.getAppInsightsMetricNames($query)" on-change="ctrl.onAppInsightsMetricNameChange()"
|
||||
css-class="min-width-20">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Aggregation</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input" ng-model="ctrl.target.appInsights.aggregation" ng-options="f as f for f in ctrl.target.appInsights.aggOptions"
|
||||
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 class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<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"
|
||||
lookup-text="true" get-options="ctrl.getAppInsightsGroupBySegments($query)" on-change="ctrl.refresh()"
|
||||
css-class="min-width-20">
|
||||
</gf-form-dropdown>
|
||||
<label class="gf-form-label min-width-20 pointer" ng-hide="ctrl.target.appInsights.groupBy === 'none'"
|
||||
ng-click="ctrl.resetAppInsightsGroupBy()">{{ctrl.target.appInsights.groupBy}}
|
||||
<i class="fa fa-remove"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<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"
|
||||
placeholder="your/groupby eq 'a_value'" 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 class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Time Grain</label>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
|
||||
<select class="gf-form-input" ng-model="ctrl.target.appInsights.timeGrainType" ng-options="f as f for f in ['auto', 'none', 'specific']"
|
||||
ng-change="ctrl.updateTimeGrainType()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
placeholder="" ng-blur="ctrl.refresh()">
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType !== 'auto'">
|
||||
<label class="gf-form-label">Auto Interval</label>
|
||||
<label class="gf-form-label">{{ctrl.getAppInsightsAutoInterval()}}</label>
|
||||
</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">Legend Format</label>
|
||||
<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()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</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-inline">
|
||||
<div class="gf-form">
|
||||
<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'"
|
||||
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(es)</label>
|
||||
<gf-form-dropdown model="ctrl.target.appInsights.yaxis" 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.spliton" 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>
|
||||
</query-editor-row>
|
@ -0,0 +1,162 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Azure Monitor",
|
||||
"id": "grafana-azure-monitor-datasource",
|
||||
|
||||
"info": {
|
||||
"description": "Grafana data source for Azure Monitor/Application Insights",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"keywords": ["azure", "monitor", "Application Insights", "Log Analytics", "App Insights"],
|
||||
"logos": {
|
||||
"small": "img/logo.jpg",
|
||||
"large": "img/logo.jpg"
|
||||
},
|
||||
"links": [
|
||||
{ "name": "Project site", "url": "https://github.com/grafana/azure-monitor-datasource" },
|
||||
{ "name": "Apache License", "url": "https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE" }
|
||||
],
|
||||
"screenshots": [
|
||||
{ "name": "Azure Contoso Loans", "path": "img/contoso_loans_grafana_dashboard.png" },
|
||||
{ "name": "Azure Monitor Network", "path": "img/azure_monitor_network.png" },
|
||||
{ "name": "Azure Monitor CPU", "path": "img/azure_monitor_cpu.png" }
|
||||
],
|
||||
"version": "0.3.0",
|
||||
"updated": "2018-12-06"
|
||||
},
|
||||
|
||||
"routes": [
|
||||
{
|
||||
"path": "azuremonitor",
|
||||
"method": "GET",
|
||||
"url": "https://management.azure.com",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://management.azure.com/"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "govazuremonitor",
|
||||
"method": "GET",
|
||||
"url": "https://management.usgovcloudapi.net",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.us/{{.JsonData.tenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://management.usgovcloudapi.net/"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "germanyazuremonitor",
|
||||
"method": "GET",
|
||||
"url": "https://management.microsoftazure.de",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.de/{{.JsonData.tenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://management.microsoftazure.de/"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "chinaazuremonitor",
|
||||
"method": "GET",
|
||||
"url": "https://management.chinacloudapi.cn",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.chinacloudapi.cn/{{.JsonData.tenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://management.chinacloudapi.cn/"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "appinsights",
|
||||
"method": "GET",
|
||||
"url": "https://api.applicationinsights.io",
|
||||
"headers": [
|
||||
{ "name": "X-API-Key", "content": "{{.SecureJsonData.appInsightsApiKey}}" },
|
||||
{ "name": "x-ms-app", "content": "Grafana" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "workspacesloganalytics",
|
||||
"method": "GET",
|
||||
"url": "https://management.azure.com",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.com/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.logAnalyticsClientId}}",
|
||||
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
|
||||
"resource": "https://management.azure.com/"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "loganalyticsazure",
|
||||
"method": "GET",
|
||||
"url": "https://api.loganalytics.io/v1/workspaces",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.com/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.logAnalyticsClientId}}",
|
||||
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
|
||||
"resource": "https://api.loganalytics.io"
|
||||
}
|
||||
},
|
||||
"headers": [
|
||||
{ "name": "x-ms-app", "content": "Grafana" },
|
||||
{ "name": "Cache-Control", "content": "public, max-age=60" },
|
||||
{ "name": "Accept-Encoding", "content": "gzip" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "sameasloganalyticsazure",
|
||||
"method": "GET",
|
||||
"url": "https://api.loganalytics.io/v1/workspaces",
|
||||
"tokenAuth": {
|
||||
"url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://api.loganalytics.io"
|
||||
}
|
||||
},
|
||||
"headers": [
|
||||
{ "name": "x-ms-app", "content": "Grafana" },
|
||||
{ "name": "Cache-Control", "content": "public, max-age=60" },
|
||||
{ "name": "Accept-Encoding", "content": "gzip" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"dependencies": {
|
||||
"grafanaVersion": "5.2.x",
|
||||
"plugins": []
|
||||
},
|
||||
|
||||
"metrics": true,
|
||||
"annotations": true
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
jest.mock('./css/query_editor.css', () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
jest.mock('./monaco/kusto_monaco_editor');
|
||||
|
||||
import { AzureMonitorQueryCtrl } from './query_ctrl';
|
||||
import Q from 'q';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
describe('AzureMonitorQueryCtrl', () => {
|
||||
let queryCtrl: any;
|
||||
|
||||
beforeEach(() => {
|
||||
AzureMonitorQueryCtrl.prototype.panelCtrl = {
|
||||
events: { on: () => {} },
|
||||
panel: { scopedVars: [], targets: [] },
|
||||
};
|
||||
AzureMonitorQueryCtrl.prototype.target = {} as any;
|
||||
|
||||
queryCtrl = new AzureMonitorQueryCtrl({}, {}, new TemplateSrv());
|
||||
queryCtrl.datasource = { $q: Q, appInsightsDatasource: { isConfigured: () => false } };
|
||||
});
|
||||
|
||||
describe('init query_ctrl variables', () => {
|
||||
it('should set default query type to Azure Monitor', () => {
|
||||
expect(queryCtrl.target.queryType).toBe('Azure Monitor');
|
||||
});
|
||||
|
||||
it('should set default App Insights editor to be builder', () => {
|
||||
expect(queryCtrl.target.appInsights.rawQuery).toBe(false);
|
||||
});
|
||||
|
||||
it('should set query parts to select', () => {
|
||||
expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select');
|
||||
expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select');
|
||||
expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
|
||||
expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
|
||||
expect(queryCtrl.target.appInsights.groupBy).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the query type is Azure Monitor', () => {
|
||||
describe('and getOptions for the Resource Group dropdown is called', () => {
|
||||
const response = [{ text: 'nodeapp', value: 'nodeapp' }, { text: 'otherapp', value: 'otherapp' }];
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.datasource.getResourceGroups = () => {
|
||||
return queryCtrl.datasource.$q.when(response);
|
||||
};
|
||||
queryCtrl.datasource.azureMonitorDatasource = {
|
||||
isConfigured: () => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of Resource Groups', () => {
|
||||
return queryCtrl.getResourceGroups('').then(result => {
|
||||
expect(result[0].text).toBe('nodeapp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getOptions for the Metric Definition dropdown is called', () => {
|
||||
describe('and resource group has a value', () => {
|
||||
const response = [
|
||||
{ text: 'Microsoft.Compute/virtualMachines', value: 'Microsoft.Compute/virtualMachines' },
|
||||
{ text: 'Microsoft.Network/publicIPAddresses', value: 'Microsoft.Network/publicIPAddresses' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
||||
queryCtrl.datasource.getMetricDefinitions = function(query) {
|
||||
expect(query).toBe('test');
|
||||
return this.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of Metric Definitions', () => {
|
||||
return queryCtrl.getMetricDefinitions('').then(result => {
|
||||
expect(result[0].text).toBe('Microsoft.Compute/virtualMachines');
|
||||
expect(result[1].text).toBe('Microsoft.Network/publicIPAddresses');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and resource group has no value', () => {
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'select';
|
||||
});
|
||||
|
||||
it('should return without making a call to datasource', () => {
|
||||
expect(queryCtrl.getMetricDefinitions('')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getOptions for the ResourceNames dropdown is called', () => {
|
||||
describe('and resourceGroup and metricDefinition have values', () => {
|
||||
const response = [{ text: 'test1', value: 'test1' }, { text: 'test2', value: 'test2' }];
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
||||
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
|
||||
queryCtrl.datasource.getResourceNames = function(resourceGroup, metricDefinition) {
|
||||
expect(resourceGroup).toBe('test');
|
||||
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
|
||||
return this.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of Resource Names', () => {
|
||||
return queryCtrl.getResourceNames('').then(result => {
|
||||
expect(result[0].text).toBe('test1');
|
||||
expect(result[1].text).toBe('test2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and resourceGroup and metricDefinition do not have values', () => {
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'select';
|
||||
queryCtrl.target.azureMonitor.metricDefinition = 'select';
|
||||
});
|
||||
|
||||
it('should return without making a call to datasource', () => {
|
||||
expect(queryCtrl.getResourceNames('')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getOptions for the Metric Names dropdown is called', () => {
|
||||
describe('and resourceGroup, metricDefinition and resourceName have values', () => {
|
||||
const response = [{ text: 'metric1', value: 'metric1' }, { text: 'metric2', value: 'metric2' }];
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
||||
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
|
||||
queryCtrl.target.azureMonitor.resourceName = 'test';
|
||||
queryCtrl.datasource.getMetricNames = function(resourceGroup, metricDefinition, resourceName) {
|
||||
expect(resourceGroup).toBe('test');
|
||||
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
|
||||
expect(resourceName).toBe('test');
|
||||
return this.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of Metric Names', () => {
|
||||
return queryCtrl.getMetricNames('').then(result => {
|
||||
expect(result[0].text).toBe('metric1');
|
||||
expect(result[1].text).toBe('metric2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and resourceGroup, metricDefinition and resourceName do not have values', () => {
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'select';
|
||||
queryCtrl.target.azureMonitor.metricDefinition = 'select';
|
||||
queryCtrl.target.azureMonitor.resourceName = 'select';
|
||||
});
|
||||
|
||||
it('should return without making a call to datasource', () => {
|
||||
expect(queryCtrl.getMetricNames('')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when onMetricNameChange is triggered for the Metric Names dropdown', () => {
|
||||
const response = {
|
||||
primaryAggType: 'Average',
|
||||
supportAggOptions: ['Average', 'Total'],
|
||||
supportedTimeGrains: ['PT1M', 'P1D'],
|
||||
dimensions: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.azureMonitor.resourceGroup = 'test';
|
||||
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
|
||||
queryCtrl.target.azureMonitor.resourceName = 'test';
|
||||
queryCtrl.target.azureMonitor.metricName = 'Percentage CPU';
|
||||
queryCtrl.datasource.getMetricMetadata = function(resourceGroup, metricDefinition, resourceName, metricName) {
|
||||
expect(resourceGroup).toBe('test');
|
||||
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
|
||||
expect(resourceName).toBe('test');
|
||||
expect(metricName).toBe('Percentage CPU');
|
||||
return this.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should set the options and default selected value for the Aggregations dropdown', () => {
|
||||
queryCtrl.onMetricNameChange().then(() => {
|
||||
expect(queryCtrl.target.azureMonitor.aggregation).toBe('Average');
|
||||
expect(queryCtrl.target.azureMonitor.aggOptions).toBe(['Average', 'Total']);
|
||||
expect(queryCtrl.target.azureMonitor.timeGrains).toBe(['PT1M', 'P1D']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and query type is Application Insights', () => {
|
||||
describe('when getOptions for the Metric Names dropdown is called', () => {
|
||||
const response = [{ text: 'metric1', value: 'metric1' }, { text: 'metric2', value: 'metric2' }];
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.datasource.appInsightsDatasource.isConfigured = () => true;
|
||||
queryCtrl.datasource.getAppInsightsMetricNames = () => {
|
||||
return queryCtrl.datasource.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return a list of Metric Names', () => {
|
||||
return queryCtrl.getAppInsightsMetricNames().then(result => {
|
||||
expect(result[0].text).toBe('metric1');
|
||||
expect(result[1].text).toBe('metric2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getOptions for the GroupBy segments dropdown is called', () => {
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.appInsights.groupByOptions = ['opt1', 'opt2'];
|
||||
});
|
||||
|
||||
it('should return a list of GroupBy segments', () => {
|
||||
const result = queryCtrl.getAppInsightsGroupBySegments('');
|
||||
expect(result[0].text).toBe('opt1');
|
||||
expect(result[0].value).toBe('opt1');
|
||||
expect(result[1].text).toBe('opt2');
|
||||
expect(result[1].value).toBe('opt2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when onAppInsightsMetricNameChange is triggered for the Metric Names dropdown', () => {
|
||||
const response = {
|
||||
primaryAggType: 'avg',
|
||||
supportedAggTypes: ['avg', 'sum'],
|
||||
supportedGroupBy: ['client/os', 'client/city'],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
queryCtrl.target.appInsights.metricName = 'requests/failed';
|
||||
queryCtrl.datasource.getAppInsightsMetricMetadata = function(metricName) {
|
||||
expect(metricName).toBe('requests/failed');
|
||||
return this.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should set the options and default selected value for the Aggregations dropdown', () => {
|
||||
return queryCtrl.onAppInsightsMetricNameChange().then(() => {
|
||||
expect(queryCtrl.target.appInsights.aggregation).toBe('avg');
|
||||
expect(queryCtrl.target.appInsights.aggOptions).toContain('avg');
|
||||
expect(queryCtrl.target.appInsights.aggOptions).toContain('sum');
|
||||
expect(queryCtrl.target.appInsights.groupByOptions).toContain('client/os');
|
||||
expect(queryCtrl.target.appInsights.groupByOptions).toContain('client/city');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,391 @@
|
||||
import _ from 'lodash';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
// import './css/query_editor.css';
|
||||
import TimegrainConverter from './time_grain_converter';
|
||||
import './monaco/kusto_monaco_editor';
|
||||
|
||||
export interface ResultFormat {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
defaultDropdownValue = 'select';
|
||||
|
||||
target: {
|
||||
refId: string;
|
||||
queryType: string;
|
||||
azureMonitor: {
|
||||
resourceGroup: string;
|
||||
resourceName: string;
|
||||
metricDefinition: string;
|
||||
metricName: string;
|
||||
dimensionFilter: string;
|
||||
timeGrain: string;
|
||||
timeGrainUnit: string;
|
||||
timeGrains: any[];
|
||||
dimensions: any[];
|
||||
dimension: any;
|
||||
aggregation: string;
|
||||
aggOptions: string[];
|
||||
};
|
||||
azureLogAnalytics: {
|
||||
query: string;
|
||||
resultFormat: string;
|
||||
workspace: string;
|
||||
};
|
||||
appInsights: {
|
||||
metricName: string;
|
||||
rawQuery: boolean;
|
||||
rawQueryString: string;
|
||||
groupBy: string;
|
||||
timeGrainType: string;
|
||||
xaxis: string;
|
||||
yaxis: string;
|
||||
spliton: string;
|
||||
aggOptions: string[];
|
||||
aggregation: string;
|
||||
groupByOptions: string[];
|
||||
timeGrainUnit: string;
|
||||
timeGrain: string;
|
||||
};
|
||||
};
|
||||
|
||||
defaults = {
|
||||
queryType: 'Azure Monitor',
|
||||
azureMonitor: {
|
||||
resourceGroup: this.defaultDropdownValue,
|
||||
metricDefinition: this.defaultDropdownValue,
|
||||
resourceName: this.defaultDropdownValue,
|
||||
metricName: this.defaultDropdownValue,
|
||||
dimensionFilter: '*',
|
||||
timeGrain: 'auto',
|
||||
},
|
||||
azureLogAnalytics: {
|
||||
query: [
|
||||
'//change this example to create your own time series query',
|
||||
'<table name> ' +
|
||||
'//the table to query (e.g. Usage, Heartbeat, Perf)',
|
||||
'| where $__timeFilter(TimeGenerated) ' +
|
||||
'//this is a macro used to show the full chart’s time range, choose the datetime column here',
|
||||
'| summarize count() by <group by column>, bin(TimeGenerated, $__interval) ' +
|
||||
'//change “group by column” to a column in your table, such as “Computer”. ' +
|
||||
'The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.',
|
||||
'| order by TimeGenerated asc',
|
||||
].join('\n'),
|
||||
resultFormat: 'time_series',
|
||||
workspace:
|
||||
this.datasource && this.datasource.azureLogAnalyticsDatasource
|
||||
? this.datasource.azureLogAnalyticsDatasource.defaultOrFirstWorkspace
|
||||
: '',
|
||||
},
|
||||
appInsights: {
|
||||
metricName: this.defaultDropdownValue,
|
||||
rawQuery: false,
|
||||
rawQueryString: '',
|
||||
groupBy: 'none',
|
||||
timeGrainType: 'auto',
|
||||
xaxis: 'timestamp',
|
||||
yaxis: '',
|
||||
spliton: '',
|
||||
},
|
||||
};
|
||||
|
||||
resultFormats: ResultFormat[];
|
||||
workspaces: any[];
|
||||
showHelp: boolean;
|
||||
showLastQuery: boolean;
|
||||
lastQuery: string;
|
||||
lastQueryError?: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private templateSrv) {
|
||||
super($scope, $injector);
|
||||
|
||||
_.defaultsDeep(this.target, this.defaults);
|
||||
|
||||
this.migrateTimeGrains();
|
||||
|
||||
this.panelCtrl.events.on('data-received', this.onDataReceived.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' }];
|
||||
if (this.target.queryType === 'Azure Log Analytics') {
|
||||
this.getWorkspaces();
|
||||
}
|
||||
}
|
||||
|
||||
onDataReceived(dataList) {
|
||||
this.lastQueryError = undefined;
|
||||
this.lastQuery = '';
|
||||
|
||||
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
|
||||
if (anySeriesFromQuery) {
|
||||
this.lastQuery = anySeriesFromQuery.query;
|
||||
}
|
||||
}
|
||||
|
||||
onDataError(err) {
|
||||
this.handleQueryCtrlError(err);
|
||||
}
|
||||
|
||||
handleQueryCtrlError(err) {
|
||||
if (err.query && err.query.refId && err.query.refId !== this.target.refId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.error && err.error.data && err.error.data.error && err.error.data.error.innererror) {
|
||||
if (err.error.data.error.innererror.innererror) {
|
||||
this.lastQueryError = err.error.data.error.innererror.innererror.message;
|
||||
} else {
|
||||
this.lastQueryError = err.error.data.error.innererror.message;
|
||||
}
|
||||
} else if (err.error && err.error.data && err.error.data.error) {
|
||||
this.lastQueryError = err.error.data.error.message;
|
||||
} else if (err.error && err.error.data) {
|
||||
this.lastQueryError = err.error.data.message;
|
||||
} else if (err.data && err.data.error) {
|
||||
this.lastQueryError = err.data.error.message;
|
||||
} else if (err.data && err.data.message) {
|
||||
this.lastQueryError = err.data.message;
|
||||
} else {
|
||||
this.lastQueryError = err;
|
||||
}
|
||||
}
|
||||
|
||||
migrateTimeGrains() {
|
||||
if (this.target.azureMonitor.timeGrainUnit) {
|
||||
if (this.target.azureMonitor.timeGrain !== 'auto') {
|
||||
this.target.azureMonitor.timeGrain = TimegrainConverter.createISO8601Duration(
|
||||
this.target.azureMonitor.timeGrain,
|
||||
this.target.azureMonitor.timeGrainUnit
|
||||
);
|
||||
}
|
||||
|
||||
delete this.target.azureMonitor.timeGrainUnit;
|
||||
this.onMetricNameChange();
|
||||
}
|
||||
}
|
||||
|
||||
replace(variable: string) {
|
||||
return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars);
|
||||
}
|
||||
|
||||
onQueryTypeChange() {
|
||||
if (this.target.queryType === 'Azure Log Analytics') {
|
||||
return this.getWorkspaces();
|
||||
}
|
||||
}
|
||||
|
||||
/* Azure Monitor Section */
|
||||
getResourceGroups(query) {
|
||||
if (this.target.queryType !== 'Azure Monitor' || !this.datasource.azureMonitorDatasource.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource.getResourceGroups().catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getMetricDefinitions(query) {
|
||||
if (
|
||||
this.target.queryType !== 'Azure Monitor' ||
|
||||
!this.target.azureMonitor.resourceGroup ||
|
||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return this.datasource
|
||||
.getMetricDefinitions(this.replace(this.target.azureMonitor.resourceGroup))
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getResourceNames(query) {
|
||||
if (
|
||||
this.target.queryType !== 'Azure Monitor' ||
|
||||
!this.target.azureMonitor.resourceGroup ||
|
||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
|
||||
!this.target.azureMonitor.metricDefinition ||
|
||||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource
|
||||
.getResourceNames(
|
||||
this.replace(this.target.azureMonitor.resourceGroup),
|
||||
this.replace(this.target.azureMonitor.metricDefinition)
|
||||
)
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getMetricNames(query) {
|
||||
if (
|
||||
this.target.queryType !== 'Azure Monitor' ||
|
||||
!this.target.azureMonitor.resourceGroup ||
|
||||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
|
||||
!this.target.azureMonitor.metricDefinition ||
|
||||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
|
||||
!this.target.azureMonitor.resourceName ||
|
||||
this.target.azureMonitor.resourceName === this.defaultDropdownValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource
|
||||
.getMetricNames(
|
||||
this.replace(this.target.azureMonitor.resourceGroup),
|
||||
this.replace(this.target.azureMonitor.metricDefinition),
|
||||
this.replace(this.target.azureMonitor.resourceName)
|
||||
)
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
onResourceGroupChange() {
|
||||
this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
|
||||
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
|
||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
||||
this.target.azureMonitor.dimensions = [];
|
||||
this.target.azureMonitor.dimension = '';
|
||||
}
|
||||
|
||||
onMetricDefinitionChange() {
|
||||
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
|
||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
||||
this.target.azureMonitor.dimensions = [];
|
||||
this.target.azureMonitor.dimension = '';
|
||||
}
|
||||
|
||||
onResourceNameChange() {
|
||||
this.target.azureMonitor.metricName = this.defaultDropdownValue;
|
||||
this.target.azureMonitor.dimensions = [];
|
||||
this.target.azureMonitor.dimension = '';
|
||||
}
|
||||
|
||||
onMetricNameChange() {
|
||||
if (!this.target.azureMonitor.metricName || this.target.azureMonitor.metricName === this.defaultDropdownValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource
|
||||
.getMetricMetadata(
|
||||
this.replace(this.target.azureMonitor.resourceGroup),
|
||||
this.replace(this.target.azureMonitor.metricDefinition),
|
||||
this.replace(this.target.azureMonitor.resourceName),
|
||||
this.replace(this.target.azureMonitor.metricName)
|
||||
)
|
||||
.then(metadata => {
|
||||
this.target.azureMonitor.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
|
||||
this.target.azureMonitor.aggregation = metadata.primaryAggType;
|
||||
this.target.azureMonitor.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
|
||||
|
||||
this.target.azureMonitor.dimensions = metadata.dimensions;
|
||||
if (metadata.dimensions.length > 0) {
|
||||
this.target.azureMonitor.dimension = metadata.dimensions[0].value;
|
||||
}
|
||||
return this.refresh();
|
||||
})
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getAutoInterval() {
|
||||
if (this.target.azureMonitor.timeGrain === 'auto') {
|
||||
return TimegrainConverter.findClosestTimeGrain(
|
||||
this.templateSrv.builtIns.__interval.value,
|
||||
_.map(this.target.azureMonitor.timeGrains, o =>
|
||||
TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)
|
||||
) || ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d']
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/* Azure Log Analytics */
|
||||
|
||||
getWorkspaces() {
|
||||
return this.datasource.azureLogAnalyticsDatasource
|
||||
.getWorkspaces()
|
||||
.then(list => {
|
||||
this.workspaces = list;
|
||||
if (list.length > 0 && !this.target.azureLogAnalytics.workspace) {
|
||||
this.target.azureLogAnalytics.workspace = list[0].value;
|
||||
}
|
||||
})
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getAzureLogAnalyticsSchema() {
|
||||
return this.getWorkspaces()
|
||||
.then(() => {
|
||||
return this.datasource.azureLogAnalyticsDatasource.getSchema(this.target.azureLogAnalytics.workspace);
|
||||
})
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
/* Application Insights Section */
|
||||
|
||||
getAppInsightsAutoInterval() {
|
||||
const interval = this.templateSrv.builtIns.__interval.value;
|
||||
if (interval[interval.length - 1] === 's') {
|
||||
return '1m';
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
getAppInsightsMetricNames() {
|
||||
if (!this.datasource.appInsightsDatasource.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource.getAppInsightsMetricNames().catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getAppInsightsColumns() {
|
||||
return this.datasource.getAppInsightsColumns(this.target.refId);
|
||||
}
|
||||
|
||||
onAppInsightsColumnChange() {
|
||||
return this.refresh();
|
||||
}
|
||||
|
||||
onAppInsightsMetricNameChange() {
|
||||
if (!this.target.appInsights.metricName || this.target.appInsights.metricName === this.defaultDropdownValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource
|
||||
.getAppInsightsMetricMetadata(this.replace(this.target.appInsights.metricName))
|
||||
.then(aggData => {
|
||||
this.target.appInsights.aggOptions = aggData.supportedAggTypes;
|
||||
this.target.appInsights.groupByOptions = aggData.supportedGroupBy;
|
||||
this.target.appInsights.aggregation = aggData.primaryAggType;
|
||||
return this.refresh();
|
||||
})
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
getAppInsightsGroupBySegments(query) {
|
||||
return _.map(this.target.appInsights.groupByOptions, option => {
|
||||
return { text: option, value: option };
|
||||
});
|
||||
}
|
||||
|
||||
resetAppInsightsGroupBy() {
|
||||
this.target.appInsights.groupBy = 'none';
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
updateTimeGrainType() {
|
||||
if (this.target.appInsights.timeGrainType === 'specific') {
|
||||
this.target.appInsights.timeGrain = '1';
|
||||
this.target.appInsights.timeGrainUnit = 'minute';
|
||||
} else {
|
||||
this.target.appInsights.timeGrain = '';
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
toggleEditorMode() {
|
||||
this.target.appInsights.rawQuery = !this.target.appInsights.rawQuery;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
Format the legend keys any way you want by using alias patterns.
|
||||
|
||||
- Example for Azure Monitor: `dimension: {{dimensionvalue}}`
|
||||
- Example for Application Insights: `server: {{groupbyvalue}}`
|
||||
|
||||
#### Alias Patterns for Application Insights
|
||||
|
||||
- {{groupbyvalue}} = replaced with the value of the group by
|
||||
- {{groupbyname}} = replaced with the name/label of the group by
|
||||
- {{metric}} = replaced with metric name (e.g. requests/count)
|
||||
|
||||
#### Alias Patterns for Azure Monitor
|
||||
|
||||
- {{resourcegroup}} = replaced with the value of the Resource Group
|
||||
- {{namespace}} = replaced with the value of the Namespace (e.g. Microsoft.Compute/virtualMachines)
|
||||
- {{resourcename}} = replaced with the value of the Resource Name
|
||||
- {{metric}} = replaced with metric name (e.g. Percentage CPU)
|
||||
- {{dimensionname}} = replaced with dimension key/label (e.g. blobtype)
|
||||
- {{dimensionvalue}} = replaced with dimension value (e.g. BlockBlob)
|
||||
|
||||
#### Filter Expressions for Application Insights
|
||||
|
||||
The filter field takes an OData filter expression.
|
||||
|
||||
Examples:
|
||||
|
||||
- `client/city eq 'Boydton'`
|
||||
- `client/city ne 'Boydton'`
|
||||
- `client/city ne 'Boydton' and client/city ne 'Dublin'`
|
||||
- `client/city eq 'Boydton' or client/city eq 'Dublin'`
|
||||
|
||||
#### Writing Queries for Template Variables
|
||||
|
||||
See the [docs](https://github.com/grafana/azure-monitor-datasource#templating-with-variables) for details and examples.
|
@ -0,0 +1,23 @@
|
||||
import TimeGrainConverter from './time_grain_converter';
|
||||
|
||||
describe('TimeGrainConverter', () => {
|
||||
describe('with duration of PT1H', () => {
|
||||
it('should convert it to text', () => {
|
||||
expect(TimeGrainConverter.createTimeGrainFromISO8601Duration('PT1H')).toEqual('1 hour');
|
||||
});
|
||||
|
||||
it('should convert it to kbn', () => {
|
||||
expect(TimeGrainConverter.createKbnUnitFromISO8601Duration('PT1H')).toEqual('1h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with duration of P1D', () => {
|
||||
it('should convert it to text', () => {
|
||||
expect(TimeGrainConverter.createTimeGrainFromISO8601Duration('P1D')).toEqual('1 day');
|
||||
});
|
||||
|
||||
it('should convert it to kbn', () => {
|
||||
expect(TimeGrainConverter.createKbnUnitFromISO8601Duration('P1D')).toEqual('1d');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,122 @@
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export default class TimeGrainConverter {
|
||||
static createISO8601Duration(timeGrain, timeGrainUnit) {
|
||||
const timeIntervals = ['hour', 'minute', 'h', 'm'];
|
||||
if (_.includes(timeIntervals, timeGrainUnit)) {
|
||||
return `PT${timeGrain}${timeGrainUnit[0].toUpperCase()}`;
|
||||
}
|
||||
|
||||
return `P${timeGrain}${timeGrainUnit[0].toUpperCase()}`;
|
||||
}
|
||||
|
||||
static createISO8601DurationFromInterval(interval: string) {
|
||||
const timeGrain = +interval.slice(0, interval.length - 1);
|
||||
const unit = interval[interval.length - 1];
|
||||
|
||||
if (interval.indexOf('ms') > -1) {
|
||||
return TimeGrainConverter.createISO8601Duration(1, 'm');
|
||||
}
|
||||
|
||||
if (interval[interval.length - 1] === 's') {
|
||||
let toMinutes = (timeGrain * 60) % 60;
|
||||
|
||||
if (toMinutes < 1) {
|
||||
toMinutes = 1;
|
||||
}
|
||||
|
||||
return TimeGrainConverter.createISO8601Duration(toMinutes, 'm');
|
||||
}
|
||||
|
||||
return TimeGrainConverter.createISO8601Duration(timeGrain, unit);
|
||||
}
|
||||
|
||||
static findClosestTimeGrain(interval, allowedTimeGrains) {
|
||||
const timeGrains = _.filter(allowedTimeGrains, o => o !== 'auto');
|
||||
|
||||
let closest = timeGrains[0];
|
||||
const intervalMs = kbn.interval_to_ms(interval);
|
||||
|
||||
for (let i = 0; i < timeGrains.length; i++) {
|
||||
// abs (num - val) < abs (num - curr):
|
||||
if (intervalMs > kbn.interval_to_ms(timeGrains[i])) {
|
||||
if (i + 1 < timeGrains.length) {
|
||||
closest = timeGrains[i + 1];
|
||||
} else {
|
||||
closest = timeGrains[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
static createTimeGrainFromISO8601Duration(duration: string) {
|
||||
let offset = 1;
|
||||
if (duration.substring(0, 2) === 'PT') {
|
||||
offset = 2;
|
||||
}
|
||||
|
||||
const value = duration.substring(offset, duration.length - 1);
|
||||
const unit = duration.substring(duration.length - 1);
|
||||
|
||||
return value + ' ' + TimeGrainConverter.timeUnitToText(+value, unit);
|
||||
}
|
||||
|
||||
static timeUnitToText(value: number, unit: string) {
|
||||
let text = '';
|
||||
|
||||
if (unit === 'S') {
|
||||
text = 'second';
|
||||
}
|
||||
if (unit === 'M') {
|
||||
text = 'minute';
|
||||
}
|
||||
if (unit === 'H') {
|
||||
text = 'hour';
|
||||
}
|
||||
if (unit === 'D') {
|
||||
text = 'day';
|
||||
}
|
||||
|
||||
if (value > 1) {
|
||||
return text + 's';
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static createKbnUnitFromISO8601Duration(duration: string) {
|
||||
if (duration === 'auto') {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
let offset = 1;
|
||||
if (duration.substring(0, 2) === 'PT') {
|
||||
offset = 2;
|
||||
}
|
||||
|
||||
const value = duration.substring(offset, duration.length - 1);
|
||||
const unit = duration.substring(duration.length - 1);
|
||||
|
||||
return value + TimeGrainConverter.timeUnitToKbn(+value, unit);
|
||||
}
|
||||
|
||||
static timeUnitToKbn(value: number, unit: string) {
|
||||
if (unit === 'S') {
|
||||
return 's';
|
||||
}
|
||||
if (unit === 'M') {
|
||||
return 'm';
|
||||
}
|
||||
if (unit === 'H') {
|
||||
return 'h';
|
||||
}
|
||||
if (unit === 'D') {
|
||||
return 'd';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import { SemVersion, isVersionGtOrEq } from './version';
|
||||
|
||||
describe('SemVersion', () => {
|
||||
let version = '1.0.0-alpha.1';
|
||||
|
||||
describe('parsing', () => {
|
||||
it('should parse version properly', () => {
|
||||
const semver = new SemVersion(version);
|
||||
expect(semver.major).toBe(1);
|
||||
expect(semver.minor).toBe(0);
|
||||
expect(semver.patch).toBe(0);
|
||||
expect(semver.meta).toBe('alpha.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('comparing', () => {
|
||||
beforeEach(() => {
|
||||
version = '3.4.5';
|
||||
});
|
||||
|
||||
it('should detect greater version properly', () => {
|
||||
const semver = new SemVersion(version);
|
||||
const cases = [
|
||||
{ value: '3.4.5', expected: true },
|
||||
{ value: '3.4.4', expected: true },
|
||||
{ value: '3.4.6', expected: false },
|
||||
{ value: '4', expected: false },
|
||||
{ value: '3.5', expected: false },
|
||||
];
|
||||
cases.forEach(testCase => {
|
||||
expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVersionGtOrEq', () => {
|
||||
it('should compare versions properly (a >= b)', () => {
|
||||
const cases = [
|
||||
{ values: ['3.4.5', '3.4.5'], expected: true },
|
||||
{ values: ['3.4.5', '3.4.4'], expected: true },
|
||||
{ values: ['3.4.5', '3.4.6'], expected: false },
|
||||
{ values: ['3.4', '3.4.0'], expected: true },
|
||||
{ values: ['3', '3.0.0'], expected: true },
|
||||
{ values: ['3.1.1-beta1', '3.1'], expected: true },
|
||||
{ values: ['3.4.5', '4'], expected: false },
|
||||
{ values: ['3.4.5', '3.5'], expected: false },
|
||||
];
|
||||
cases.forEach(testCase => {
|
||||
expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,34 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/;
|
||||
|
||||
export class SemVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
meta: string;
|
||||
|
||||
constructor(version: string) {
|
||||
const match = versionPattern.exec(version);
|
||||
if (match) {
|
||||
this.major = Number(match[1]);
|
||||
this.minor = Number(match[2] || 0);
|
||||
this.patch = Number(match[3] || 0);
|
||||
this.meta = match[4];
|
||||
}
|
||||
}
|
||||
|
||||
isGtOrEq(version: string): boolean {
|
||||
const compared = new SemVersion(version);
|
||||
return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch);
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return _.isNumber(this.major);
|
||||
}
|
||||
}
|
||||
|
||||
export function isVersionGtOrEq(a: string, b: string): boolean {
|
||||
const aSemver = new SemVersion(a);
|
||||
return aSemver.isGtOrEq(b);
|
||||
}
|
@ -8463,6 +8463,11 @@ moment@^2.22.2:
|
||||
version "2.23.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225"
|
||||
|
||||
monaco-editor@^0.15.6:
|
||||
version "0.15.6"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.15.6.tgz#d63b3b06f86f803464f003b252627c3eb4a09483"
|
||||
integrity sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg==
|
||||
|
||||
moo@^0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
|
||||
|