Commit dd96f2a5 by Daniel Lee

azuremonitor: move files into grafana

Initial port of the code from the plugin - lots of small things to fix.
parent 56813706
......@@ -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,
......
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() {}
}
import { QueryCtrl } from './query_ctrl';
export { QueryCtrl };
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(() => {});
}
}
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();
});
}
}
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);
});
});
});
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;
}
}
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;
}
}
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;
}
}
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();
}
}
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);
});
});
});
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);
}
}
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;
}
}
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'
);
});
});
});
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}`
);
}
}
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;
}
});
}
}
.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;
}
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();
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
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)`);
});
});
});
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(', ');
}
}
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,
};
// 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();
});
});
});
});
});
// 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);
<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() -&gt; TimeGenerated &ge; datetime(2018-06-05T18:09:58.907Z) and TimeGenerated &le; datetime(2018-06-05T20:09:58.907Z)
- $__timeFilter(datetimeColumn) -&gt; datetimeColumn &ge; datetime(2018-06-05T18:09:58.907Z) and datetimeColumn &le; datetime(2018-06-05T20:09:58.907Z)
Or build your own conditionals using these built-in variables which just return the values:
- $__from -&gt; datetime(2018-06-05T18:09:58.907Z)
- $__to -&gt; datetime(2018-06-05T20:09:58.907Z)
- $__interval -&gt; 5m
</pre>
</div>
</div>
{
"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
}
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.
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');
});
});
});
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 '';
}
}
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);
});
});
});
});
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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment