Commit befa40ef by Daniel Lee Committed by GitHub

AzureMonitor: adds support for multiple subscriptions per datasource (#16922)

* chore: AzureMonitor typescript typings

Removes some types and using @grafana/ui types instead.
Adds some typing for the AzureMonitor query.

Also adds a getSubscriptions function that will used in the query
editor.

* fix: AzureMonitor adds back editor for annotation queries

This must have been broken for a month or more. Now possible to
edit annotation queries again.

* feat: Azure Monitor - support for multiple subscriptions

Adds a new dropdown for subscriptions in the query editor.

Defaults to the subscription id in jsonData for queries
that have no subscription id.

* feat: adds Azure Logs multi subscriptions support

The subscription id is needed for fetching the list of
workspaces. Adds support to the Log Analytics datasource
and to the annotations for Log Analytics to be able to
choose between multiple subscriptions.

* feat: AzureMonitor config page with multiple subs

Adds support for multiple subscriptions for the different
variations of configuring Azure Monitor and Azure Logs.

To be able to show a list of subscriptions, the config
has to be saved first - the plugin route fetches the
tenant id, client id and client secret from the database
so a call to get subscriptions requires that those
fields are saved first. If the page has not saved then
the use can manually paste in a subscription id.

* feat: support for multi subs in Azure Monitor variables

Adds an optional subscription parameter to the template
variable macros. Also adds a Subscriptions macro.

* fix: remove some implicit anys from tests
parent 70abb195
......@@ -3,24 +3,48 @@ export class AzureMonitorAnnotationsQueryCtrl {
datasource: any;
annotation: any;
workspaces: any[];
subscriptions: Array<{ text: string; value: string }>;
defaultQuery =
'<your table>\n| where $__timeFilter() \n| project TimeGenerated, Text=YourTitleColumn, Tags="tag1,tag2"';
/** @ngInject */
constructor() {
constructor(private templateSrv) {
this.annotation.queryType = this.annotation.queryType || 'Azure Log Analytics';
this.annotation.rawQuery = this.annotation.rawQuery || this.defaultQuery;
this.getWorkspaces();
this.initDropdowns();
}
getWorkspaces() {
if (this.workspaces && this.workspaces.length > 0) {
async initDropdowns() {
await this.getSubscriptions();
await this.getWorkspaces();
}
async getSubscriptions() {
if (!this.datasource.azureMonitorDatasource.isConfigured()) {
return;
}
return this.datasource.azureMonitorDatasource.getSubscriptions().then(subs => {
this.subscriptions = subs;
if (!this.annotation.subscription && this.annotation.queryType === 'Azure Log Analytics') {
this.annotation.subscription = this.datasource.azureLogAnalyticsDatasource.subscriptionId;
}
if (!this.annotation.subscription && this.subscriptions.length > 0) {
this.annotation.subscription = this.subscriptions[0].value;
}
});
}
async getWorkspaces(bustCache?: boolean) {
if (!bustCache && this.workspaces && this.workspaces.length > 0) {
return this.workspaces;
}
return this.datasource
.getAzureLogAnalyticsWorkspaces()
.getAzureLogAnalyticsWorkspaces(this.annotation.subscription)
.then(list => {
this.workspaces = list;
if (list.length > 0 && !this.annotation.workspace) {
......@@ -30,4 +54,24 @@ export class AzureMonitorAnnotationsQueryCtrl {
})
.catch(() => {});
}
getAzureLogAnalyticsSchema = () => {
return this.getWorkspaces()
.then(() => {
return this.datasource.azureLogAnalyticsDatasource.getSchema(this.annotation.workspace);
})
.catch(() => {});
};
onSubscriptionChange = () => {
this.getWorkspaces(true);
};
onLogAnalyticsQueryChange = (nextQuery: string) => {
this.annotation.rawQuery = nextQuery;
};
get templateVariables() {
return this.templateSrv.variables.map(t => '$' + t.name);
}
}
......@@ -65,7 +65,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.reject(error);
};
});
......@@ -93,7 +93,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.reject(error);
};
});
......@@ -142,7 +142,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/exceptions/server');
return ctx.$q.when({ data: response, status: 200 });
};
......@@ -188,7 +188,7 @@ describe('AppInsightsDatasource', () => {
options.targets[0].appInsights.timeGrainType = 'specific';
options.targets[0].appInsights.timeGrain = '30';
options.targets[0].appInsights.timeGrainUnit = 'minute';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/exceptions/server');
expect(options.url).toContain('interval=PT30M');
return ctx.$q.when({ data: response, status: 200 });
......@@ -259,7 +259,7 @@ describe('AppInsightsDatasource', () => {
beforeEach(() => {
options.targets[0].appInsights.groupBy = 'client/city';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/exceptions/server');
expect(options.url).toContain('segment=client/city');
return ctx.$q.when({ data: response, status: 200 });
......@@ -284,7 +284,7 @@ describe('AppInsightsDatasource', () => {
options.targets[0].appInsights.groupBy = 'client/city';
options.targets[0].appInsights.alias = '{{metric}} + {{groupbyname}} + {{groupbyvalue}}';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/exceptions/server');
expect(options.url).toContain('segment=client/city');
return ctx.$q.when({ data: response, status: 200 });
......@@ -316,7 +316,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return ctx.$q.when({ data: response, status: 200 });
};
......@@ -354,7 +354,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return ctx.$q.when({ data: response, status: 200 });
};
......@@ -382,7 +382,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return ctx.$q.when({ data: response, status: 200 });
};
......@@ -420,7 +420,7 @@ describe('AppInsightsDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('/metrics/metadata');
return ctx.$q.when({ data: response, status: 200 });
};
......
......@@ -98,7 +98,9 @@ export default class ResponseParser {
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;
bucket.meta = {
query: query.query,
};
}
}
}
......
......@@ -3,6 +3,7 @@ import FakeSchemaData from './__mocks__/schema';
import Q from 'q';
import moment from 'moment';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { KustoSchema } from '../types';
describe('AzureLogAnalyticsDatasource', () => {
const ctx: any = {
......@@ -58,7 +59,7 @@ describe('AzureLogAnalyticsDatasource', () => {
ctx.instanceSettings.jsonData.azureLogAnalyticsSameAs = true;
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
workspacesUrl = options.url;
return ctx.$q.when({ data: workspaceResponse, status: 200 });
......@@ -166,7 +167,7 @@ describe('AzureLogAnalyticsDatasource', () => {
describe('in time series format', () => {
describe('and the data is valid (has time, metric and value columns)', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('query=AzureActivity');
return ctx.$q.when({ data: response, status: 200 });
};
......@@ -205,7 +206,7 @@ describe('AzureLogAnalyticsDatasource', () => {
},
],
};
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('query=AzureActivity');
return ctx.$q.when({ data: invalidResponse, status: 200 });
};
......@@ -222,7 +223,7 @@ describe('AzureLogAnalyticsDatasource', () => {
describe('in tableformat', () => {
beforeEach(() => {
options.targets[0].azureLogAnalytics.resultFormat = 'table';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('query=AzureActivity');
return ctx.$q.when({ data: response, status: 200 });
};
......@@ -249,14 +250,14 @@ describe('AzureLogAnalyticsDatasource', () => {
describe('When performing getSchema', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
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 => {
return ctx.ds.azureLogAnalyticsDatasource.getSchema('myWorkspace').then((result: KustoSchema) => {
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');
......@@ -302,7 +303,7 @@ describe('AzureLogAnalyticsDatasource', () => {
let queryResults;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
return ctx.$q.when({ data: workspaceResponse, status: 200 });
} else {
......@@ -361,7 +362,7 @@ describe('AzureLogAnalyticsDatasource', () => {
let annotationResults;
beforeEach(async () => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
return ctx.$q.when({ data: workspaceResponse, status: 200 });
} else {
......
import _ from 'lodash';
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
import ResponseParser from './response_parser';
import { AzureMonitorQuery } from '../types';
import { DataQueryRequest } from '@grafana/ui/src/types';
export default class AzureLogAnalyticsDatasource {
id: number;
......@@ -35,16 +37,19 @@ export default class AzureLogAnalyticsDatasource {
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}`;
this.azureMonitorUrl = `/${azureCloud}/subscriptions`;
} else {
this.subscriptionId = this.instanceSettings.jsonData.logAnalyticsSubscriptionId;
this.azureMonitorUrl = `/workspacesloganalytics/subscriptions/${this.subscriptionId}`;
this.azureMonitorUrl = `/workspacesloganalytics/subscriptions`;
}
}
getWorkspaces() {
getWorkspaces(subscription: string) {
const subscriptionId = this.templateSrv.replace(subscription || this.subscriptionId);
const workspaceListUrl =
this.azureMonitorUrl + '/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview';
this.azureMonitorUrl +
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
return this.doRequest(workspaceListUrl).then(response => {
return (
_.map(response.data.value, val => {
......@@ -54,7 +59,7 @@ export default class AzureLogAnalyticsDatasource {
});
}
getSchema(workspace) {
getSchema(workspace: string) {
if (!workspace) {
return Promise.resolve();
}
......@@ -65,7 +70,7 @@ export default class AzureLogAnalyticsDatasource {
});
}
query(options) {
async query(options: DataQueryRequest<AzureMonitorQuery>) {
const queries = _.filter(options.targets, item => {
return item.hide !== true;
}).map(target => {
......@@ -78,7 +83,9 @@ export default class AzureLogAnalyticsDatasource {
);
const generated = querystringBuilder.generate();
const url = `${this.baseUrl}/${item.workspace}/query?${generated.uriString}`;
const workspace = this.templateSrv.replace(item.workspace, options.scopedVars);
const url = `${this.baseUrl}/${workspace}/query?${generated.uriString}`;
return {
refId: target.refId,
......@@ -87,7 +94,7 @@ export default class AzureLogAnalyticsDatasource {
datasourceId: this.id,
url: url,
query: generated.rawQuery,
format: options.format,
format: target.format,
resultFormat: item.resultFormat,
};
});
......@@ -175,7 +182,7 @@ export default class AzureLogAnalyticsDatasource {
return Promise.resolve(this.defaultOrFirstWorkspace);
}
return this.getWorkspaces().then(workspaces => {
return this.getWorkspaces(this.subscriptionId).then(workspaces => {
this.defaultOrFirstWorkspace = workspaces[0].value;
return this.defaultOrFirstWorkspace;
});
......
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[];
}
import {
AzureLogsVariable,
AzureLogsTableData,
KustoDatabase,
KustoFunction,
KustoTable,
KustoSchema,
KustoColumn,
} from '../types';
import { TimeSeries, AnnotationEvent } from '@grafana/ui/src/types';
export default class ResponseParser {
columns: string[];
......@@ -86,8 +35,8 @@ export default class ResponseParser {
return data;
}
parseTimeSeriesResult(query, columns, rows): DataTarget[] {
const data: DataTarget[] = [];
parseTimeSeriesResult(query, columns, rows): TimeSeries[] {
const data: TimeSeries[] = [];
let timeIndex = -1;
let metricIndex = -1;
let valueIndex = -1;
......@@ -116,36 +65,40 @@ export default class ResponseParser {
const bucket = ResponseParser.findOrCreateBucket(data, metricName);
bucket.datapoints.push([row[valueIndex], epoch]);
bucket.refId = query.refId;
bucket.query = query.query;
bucket.meta = {
query: query.query,
};
});
return data;
}
parseTableResult(query, columns, rows): TableResult {
const tableResult: TableResult = {
parseTableResult(query, columns, rows): AzureLogsTableData {
const tableResult: AzureLogsTableData = {
type: 'table',
columns: _.map(columns, col => {
return { text: col.name, type: col.type };
}),
rows: rows,
refId: query.refId,
query: query.query,
meta: {
query: query.query,
},
};
return tableResult;
}
parseToVariables(): Variable[] {
parseToVariables(): AzureLogsVariable[] {
const queryResult = this.parseQueryResult();
const variables: Variable[] = [];
const variables: AzureLogsVariable[] = [];
_.forEach(queryResult, result => {
_.forEach(_.flattenDeep(result.rows), row => {
variables.push({
text: row,
value: row,
} as Variable);
} as AzureLogsVariable);
});
});
......@@ -155,7 +108,7 @@ export default class ResponseParser {
transformToAnnotations(options: any) {
const queryResult = this.parseQueryResult();
const list: AnnotationItem[] = [];
const list: AnnotationEvent[] = [];
_.forEach(queryResult, result => {
let timeIndex = -1;
......@@ -253,7 +206,7 @@ export default class ResponseParser {
return functions;
}
static findOrCreateBucket(data, target): DataTarget {
static findOrCreateBucket(data, target): TimeSeries {
let dataTarget: any = _.find(data, ['target', target]);
if (!dataTarget) {
dataTarget = { target: target, datapoints: [], refId: '', query: '' };
......
......@@ -36,7 +36,7 @@ describe('AzureMonitorDatasource', () => {
beforeEach(() => {
ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.reject(error);
};
});
......@@ -63,7 +63,7 @@ describe('AzureMonitorDatasource', () => {
beforeEach(() => {
ctx.instanceSettings.jsonData.tenantId = 'xxx';
ctx.instanceSettings.jsonData.clientId = 'xxx';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.when({ data: response, status: 200 });
};
});
......@@ -135,7 +135,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain(
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'
);
......@@ -191,7 +191,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain(
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'
);
......@@ -257,7 +257,7 @@ describe('AzureMonitorDatasource', () => {
describe('and with no alias specified', () => {
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const expected =
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics';
expect(options.url).toContain(expected);
......@@ -283,7 +283,7 @@ describe('AzureMonitorDatasource', () => {
'{{resourcegroup}} + {{namespace}} + {{resourcename}} + ' +
'{{metric}} + {{dimensionname}} + {{dimensionvalue}}';
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const expected =
'/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics';
expect(options.url).toContain(expected);
......@@ -308,7 +308,36 @@ describe('AzureMonitorDatasource', () => {
});
describe('When performing metricFindQuery', () => {
describe('with a metric names query', () => {
describe('with a subscriptions query', () => {
const response = {
data: {
value: [
{ displayName: 'Primary', subscriptionId: 'sub1' },
{ displayName: 'Secondary', subscriptionId: 'sub2' },
],
},
status: 200,
statusText: 'OK',
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.when(response);
};
});
it('should return a list of subscriptions', () => {
return ctx.ds.metricFindQuery('subscriptions()').then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toBe(2);
expect(results[0].text).toBe('Primary - sub1');
expect(results[0].value).toBe('sub1');
expect(results[1].text).toBe('Secondary - sub2');
expect(results[1].value).toBe('sub2');
});
});
});
describe('with a resource groups query', () => {
const response = {
data: {
value: [{ name: 'grp1' }, { name: 'grp2' }],
......@@ -318,13 +347,13 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.when(response);
};
});
it('should return a list of metric names', () => {
return ctx.ds.metricFindQuery('ResourceGroups()').then(results => {
it('should return a list of resource groups', () => {
return ctx.ds.metricFindQuery('ResourceGroups()').then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toBe(2);
expect(results[0].text).toBe('grp1');
expect(results[0].value).toBe('grp1');
......@@ -334,7 +363,36 @@ describe('AzureMonitorDatasource', () => {
});
});
describe('with metric definitions query', () => {
describe('with a resource groups query that specifies a subscription id', () => {
const response = {
data: {
value: [{ name: 'grp1' }, { name: 'grp2' }],
},
status: 200,
statusText: 'OK',
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123');
return ctx.$q.when(response);
};
});
it('should return a list of resource groups', () => {
return ctx.ds
.metricFindQuery('ResourceGroups(11112222-eeee-4949-9b2d-9106972f9123)')
.then((results: Array<{ text: string; value: string }>) => {
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 namespaces query', () => {
const response = {
data: {
value: [
......@@ -349,7 +407,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
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');
......@@ -357,12 +415,48 @@ describe('AzureMonitorDatasource', () => {
};
});
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');
});
it('should return a list of namespaces', () => {
return ctx.ds
.metricFindQuery('Namespaces(nodesapp)')
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Microsoft.Network/networkInterfaces');
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
});
});
});
describe('with namespaces query that specifies a subscription id', () => {
const response = {
data: {
value: [
{
name: 'test',
type: 'Microsoft.Network/networkInterfaces',
},
],
},
status: 200,
statusText: 'OK',
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
return ctx.$q.when(response);
};
});
it('should return a list of namespaces', () => {
return ctx.ds
.metricFindQuery('namespaces(11112222-eeee-4949-9b2d-9106972f9123, nodesapp)')
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Microsoft.Network/networkInterfaces');
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
});
});
});
......@@ -385,7 +479,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
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');
......@@ -394,11 +488,53 @@ describe('AzureMonitorDatasource', () => {
});
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');
});
return ctx.ds
.metricFindQuery('resourceNames(nodeapp, microsoft.insights/components )')
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
});
describe('with resource names query and that specifies a subscription id', () => {
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: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/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(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, microsoft.insights/components )'
)
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
});
......@@ -425,7 +561,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(options.url).toBe(
......@@ -438,14 +574,109 @@ describe('AzureMonitorDatasource', () => {
});
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');
return ctx.ds
.metricFindQuery('Metricnames(nodeapp, microsoft.insights/components, rn)')
.then((results: Array<{ text: string; value: string }>) => {
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');
});
expect(results[1].text).toEqual('Used capacity');
expect(results[1].value).toEqual('UsedCapacity');
});
});
});
describe('with metric names query and specifies a subscription id', () => {
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: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/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(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, microsoft.insights/components, rn)'
)
.then((results: Array<{ text: string; value: string }>) => {
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 getSubscriptions', () => {
const response = {
data: {
value: [
{
id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
subscriptionId: '99999999-cccc-bbbb-aaaa-9106972f9572',
tenantId: '99999999-aaaa-bbbb-cccc-51c4f982ec48',
displayName: 'Primary Subscription',
state: 'Enabled',
subscriptionPolicies: {
locationPlacementId: 'Public_2014-09-01',
quotaId: 'PayAsYouGo_2014-09-01',
spendingLimit: 'Off',
},
authorizationSource: 'RoleBased',
},
],
count: {
type: 'Total',
value: 1,
},
},
status: 200,
statusText: 'OK',
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.when(response);
};
});
it('should return list of Resource Groups', () => {
return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Primary Subscription - 99999999-cccc-bbbb-aaaa-9106972f9572');
expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
});
});
});
......@@ -460,13 +691,13 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = () => {
return ctx.$q.when(response);
};
});
it('should return list of Resource Groups', () => {
return ctx.ds.getResourceGroups().then(results => {
return ctx.ds.getResourceGroups().then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('grp1');
expect(results[0].value).toEqual('grp1');
......@@ -509,7 +740,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
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');
......@@ -518,23 +749,25 @@ describe('AzureMonitorDatasource', () => {
});
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');
});
return ctx.ds
.getMetricDefinitions('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodesapp')
.then((results: Array<{ text: string; value: string }>) => {
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');
});
});
});
......@@ -558,7 +791,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
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');
......@@ -567,11 +800,13 @@ describe('AzureMonitorDatasource', () => {
});
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');
});
return ctx.ds
.getResourceNames('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodeapp', 'microsoft.insights/components')
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
});
......@@ -594,7 +829,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
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');
......@@ -603,11 +838,17 @@ describe('AzureMonitorDatasource', () => {
});
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');
});
return ctx.ds
.getResourceNames(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'Microsoft.Storage/storageAccounts/blobServices'
)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('storagetest/default');
expect(results[0].value).toEqual('storagetest/default');
});
});
});
});
......@@ -653,7 +894,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
......@@ -666,13 +907,15 @@ describe('AzureMonitorDatasource', () => {
});
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');
});
return ctx.ds
.getMetricNames('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodeapp', 'microsoft.insights/components', 'resource1')
.then((results: Array<{ text: string; value: string }>) => {
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');
});
});
});
......@@ -717,7 +960,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
......@@ -731,8 +974,14 @@ describe('AzureMonitorDatasource', () => {
it('should return Aggregation metadata for a Metric', () => {
return ctx.ds
.getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'UsedCapacity')
.then(results => {
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'UsedCapacity'
)
.then((results: any) => {
expect(results.primaryAggType).toEqual('Total');
expect(results.supportedAggTypes.length).toEqual(6);
expect(results.supportedTimeGrains.length).toEqual(4);
......@@ -784,7 +1033,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.backendSrv.datasourceRequest = options => {
ctx.backendSrv.datasourceRequest = (options: { url: string }) => {
const baseUrl =
'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
......@@ -798,7 +1047,13 @@ describe('AzureMonitorDatasource', () => {
it('should return dimensions for a Metric that has dimensions', () => {
return ctx.ds
.getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'Transactions')
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'Transactions'
)
.then(results => {
expect(results.dimensions.length).toEqual(4);
expect(results.dimensions[0].text).toEqual('None');
......@@ -810,7 +1065,13 @@ describe('AzureMonitorDatasource', () => {
it('should return an empty array for a Metric that does not have dimensions', () => {
return ctx.ds
.getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'FreeCapacity')
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'FreeCapacity'
)
.then(results => {
expect(results.dimensions.length).toEqual(0);
});
......
......@@ -4,6 +4,8 @@ import UrlBuilder from './url_builder';
import ResponseParser from './response_parser';
import SupportedNamespaces from './supported_namespaces';
import TimegrainConverter from '../time_grain_converter';
import { AzureMonitorQuery } from '../types';
import { DataQueryRequest } from '@grafana/ui/src/types';
export default class AzureMonitorDatasource {
apiVersion = '2018-01-01';
......@@ -18,11 +20,11 @@ export default class AzureMonitorDatasource {
supportedMetricNamespaces: any[] = [];
/** @ngInject */
constructor(private instanceSettings, private backendSrv, private templateSrv, private $q) {
constructor(private instanceSettings, private backendSrv, private templateSrv) {
this.id = instanceSettings.id;
this.subscriptionId = instanceSettings.jsonData.subscriptionId;
this.cloudName = instanceSettings.jsonData.cloudName || 'azuremonitor';
this.baseUrl = `/${this.cloudName}/subscriptions/${this.subscriptionId}/resourceGroups`;
this.baseUrl = `/${this.cloudName}/subscriptions`;
this.url = instanceSettings.url;
this.supportedMetricNamespaces = new SupportedNamespaces(this.cloudName).get();
......@@ -32,7 +34,7 @@ export default class AzureMonitorDatasource {
return !!this.subscriptionId && this.subscriptionId.length > 0;
}
query(options) {
async query(options: DataQueryRequest<AzureMonitorQuery>) {
const queries = _.filter(options.targets, item => {
return (
item.hide !== true &&
......@@ -52,6 +54,7 @@ export default class AzureMonitorDatasource {
item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
}
const subscriptionId = this.templateSrv.replace(target.subscription || this.subscriptionId, options.scopedVars);
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);
......@@ -81,6 +84,7 @@ export default class AzureMonitorDatasource {
const url = UrlBuilder.buildAzureMonitorQueryUrl(
this.baseUrl,
subscriptionId,
resourceGroup,
metricDefinition,
resourceName,
......@@ -94,19 +98,19 @@ export default class AzureMonitorDatasource {
maxDataPoints: options.maxDataPoints,
datasourceId: this.id,
url: url,
format: options.format,
format: target.format,
alias: item.alias,
raw: false,
};
});
if (!queries || queries.length === 0) {
return;
return [];
}
const promises = this.doQueries(queries);
return this.$q.all(promises).then(results => {
return Promise.all(promises).then(results => {
return new ResponseParser(results).parseQueryResult();
});
}
......@@ -132,30 +136,70 @@ export default class AzureMonitorDatasource {
annotationQuery(options) {}
metricFindQuery(query: string) {
const subscriptionsQuery = query.match(/^Subscriptions\(\)/i);
if (subscriptionsQuery) {
return this.getSubscriptions();
}
const resourceGroupsQuery = query.match(/^ResourceGroups\(\)/i);
if (resourceGroupsQuery) {
return this.getResourceGroups();
return this.getResourceGroups(this.subscriptionId);
}
const resourceGroupsQueryWithSub = query.match(/^ResourceGroups\(([^\)]+?)(,\s?([^,]+?))?\)/i);
if (resourceGroupsQueryWithSub) {
return this.getResourceGroups(this.toVariable(resourceGroupsQueryWithSub[1]));
}
const metricDefinitionsQuery = query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i);
if (metricDefinitionsQuery) {
return this.getMetricDefinitions(this.toVariable(metricDefinitionsQuery[1]));
if (!metricDefinitionsQuery[3]) {
return this.getMetricDefinitions(this.subscriptionId, this.toVariable(metricDefinitionsQuery[1]));
}
}
const metricDefinitionsQueryWithSub = query.match(/^Namespaces\(([^,]+?),\s?([^,]+?)\)/i);
if (metricDefinitionsQueryWithSub) {
return this.getMetricDefinitions(
this.toVariable(metricDefinitionsQueryWithSub[1]),
this.toVariable(metricDefinitionsQueryWithSub[2])
);
}
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);
return this.getResourceNames(this.subscriptionId, resourceGroup, metricDefinition);
}
const resourceNamesQueryWithSub = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i);
if (resourceNamesQueryWithSub) {
const subscription = this.toVariable(resourceNamesQueryWithSub[1]);
const resourceGroup = this.toVariable(resourceNamesQueryWithSub[2]);
const metricDefinition = this.toVariable(resourceNamesQueryWithSub[3]);
return this.getResourceNames(subscription, 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);
if (metricNamesQuery[3].indexOf(',') === -1) {
const resourceGroup = this.toVariable(metricNamesQuery[1]);
const metricDefinition = this.toVariable(metricNamesQuery[2]);
const resourceName = this.toVariable(metricNamesQuery[3]);
return this.getMetricNames(this.subscriptionId, resourceGroup, metricDefinition, resourceName);
}
}
const metricNamesQueryWithSub = query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?(.+?)\)/i);
if (metricNamesQueryWithSub) {
const subscription = this.toVariable(metricNamesQueryWithSub[1]);
const resourceGroup = this.toVariable(metricNamesQueryWithSub[2]);
const metricDefinition = this.toVariable(metricNamesQueryWithSub[3]);
const resourceName = this.toVariable(metricNamesQueryWithSub[4]);
return this.getMetricNames(subscription, resourceGroup, metricDefinition, resourceName);
}
return undefined;
......@@ -165,15 +209,24 @@ export default class AzureMonitorDatasource {
return this.templateSrv.replace((metric || '').trim());
}
getResourceGroups() {
const url = `${this.baseUrl}?api-version=${this.apiVersion}`;
getSubscriptions(route?: string) {
const url = `/${route || this.cloudName}/subscriptions?api-version=2019-03-01`;
return this.doRequest(url).then(result => {
return ResponseParser.parseSubscriptions(result);
});
}
getResourceGroups(subscriptionId: string) {
const url = `${this.baseUrl}/${subscriptionId}/resourceGroups?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}`;
getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
const url = `${this.baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${
this.apiVersion
}`;
return this.doRequest(url)
.then(result => {
return ResponseParser.parseResponseValues(result, 'type', 'type');
......@@ -221,8 +274,10 @@ export default class AzureMonitorDatasource {
});
}
getResourceNames(resourceGroup: string, metricDefinition: string) {
const url = `${this.baseUrl}/${resourceGroup}/resources?api-version=${this.apiVersion}`;
getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) {
const url = `${this.baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${
this.apiVersion
}`;
return this.doRequest(url).then(result => {
if (!_.startsWith(metricDefinition, 'Microsoft.Storage/storageAccounts/')) {
......@@ -239,9 +294,10 @@ export default class AzureMonitorDatasource {
});
}
getMetricNames(resourceGroup: string, metricDefinition: string, resourceName: string) {
getMetricNames(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.baseUrl,
subscriptionId,
resourceGroup,
metricDefinition,
resourceName,
......@@ -253,9 +309,16 @@ export default class AzureMonitorDatasource {
});
}
getMetricMetadata(resourceGroup: string, metricDefinition: string, resourceName: string, metricName: string) {
getMetricMetadata(
subscriptionId: string,
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricName: string
) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.baseUrl,
subscriptionId,
resourceGroup,
metricDefinition,
resourceName,
......@@ -282,7 +345,7 @@ export default class AzureMonitorDatasource {
};
}
const url = `${this.baseUrl}?api-version=${this.apiVersion}`;
const url = `/${this.cloudName}/subscriptions?api-version=2019-03-01`;
return this.doRequest(url)
.then(response => {
if (response.status === 200) {
......
......@@ -181,4 +181,20 @@ export default class ResponseParser {
}
return dimensions;
}
static parseSubscriptions(result: any) {
const valueFieldName = 'subscriptionId';
const textFieldName = 'displayName';
const list: Array<{ text: string; value: string }> = [];
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)} - ${_.get(result.data.value[i], valueFieldName)}`,
value: _.get(result.data.value[i], valueFieldName),
});
}
}
return list;
}
}
......@@ -5,13 +5,14 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Sql/servers/databases',
'rn1/rn2',
'2017-05-01-preview'
);
expect(url).toBe(
'/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
......@@ -21,13 +22,14 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the getMetricNames url in the shorter format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Sql/servers',
'rn',
'2017-05-01-preview'
);
expect(url).toBe(
'/rg/providers/Microsoft.Sql/servers/rn/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
......@@ -37,13 +39,14 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/blobServices',
'rn1/default',
'2017-05-01-preview'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
......@@ -53,6 +56,7 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the query url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorQueryUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/blobServices',
'rn1/default',
......@@ -60,7 +64,7 @@ describe('AzureMonitorUrlBuilder', () => {
'metricnames=aMetric'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
);
});
......@@ -70,13 +74,14 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/fileServices',
'rn1/default',
'2017-05-01-preview'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
......@@ -86,6 +91,7 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the query url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorQueryUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/fileServices',
'rn1/default',
......@@ -93,7 +99,7 @@ describe('AzureMonitorUrlBuilder', () => {
'metricnames=aMetric'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
);
});
......@@ -103,13 +109,14 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/tableServices',
'rn1/default',
'2017-05-01-preview'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
......@@ -119,6 +126,7 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the query url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorQueryUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/tableServices',
'rn1/default',
......@@ -126,7 +134,7 @@ describe('AzureMonitorUrlBuilder', () => {
'metricnames=aMetric'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
);
});
......@@ -136,13 +144,14 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/queueServices',
'rn1/default',
'2017-05-01-preview'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
......@@ -152,6 +161,7 @@ describe('AzureMonitorUrlBuilder', () => {
it('should build the query url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorQueryUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/queueServices',
'rn1/default',
......@@ -159,7 +169,7 @@ describe('AzureMonitorUrlBuilder', () => {
'metricnames=aMetric'
);
expect(url).toBe(
'/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
'/sub1/resourceGroups/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,
subscriptionId: string,
resourceGroup: string,
metricDefinition: string,
resourceName: string,
......@@ -12,19 +13,20 @@ export default class UrlBuilder {
const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
return (
`${baseUrl}/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
`/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}`
);
}
return (
`${baseUrl}/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
`/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}`
);
}
static buildAzureMonitorGetMetricNamesUrl(
baseUrl: string,
subscriptionId: string,
resourceGroup: string,
metricDefinition: string,
resourceName: string,
......@@ -35,13 +37,13 @@ export default class UrlBuilder {
const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
return (
`${baseUrl}/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`
);
}
return (
`${baseUrl}/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
`${baseUrl}/${subscriptionId}/resourceGroups/${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 'app/core/utils/version';
import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
interface AzureCloud {
key: string;
url: string;
loginUrl: string;
}
export class AzureMonitorConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html';
current: any;
azureLogAnalyticsDatasource: any;
azureMonitorDatasource: any;
workspaces: any[];
subscriptions: Array<{ text: string; value: string }>;
subscriptionsForLogAnalytics: Array<{ text: string; value: string }>;
hasRequiredGrafanaVersion: boolean;
azureClouds: AzureCloud[];
token: string;
/** @ngInject */
constructor($scope, backendSrv, $q) {
constructor(private backendSrv, private $q, private templateSrv) {
this.hasRequiredGrafanaVersion = this.hasMinVersion();
this.current.jsonData.cloudName = this.current.jsonData.cloudName || 'azuremonitor';
this.current.jsonData.azureLogAnalyticsSameAs = this.current.jsonData.azureLogAnalyticsSameAs || false;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonFields = this.current.secureJsonFields || {};
this.subscriptions = [];
this.subscriptionsForLogAnalytics = [];
this.azureClouds = [
{
key: 'azuremonitor',
url: 'https://management.azure.com/',
loginUrl: 'https://login.microsoftonline.com/',
},
{
key: 'govazuremonitor',
url: 'https://management.usgovcloudapi.net/',
loginUrl: 'https://login.microsoftonline.us/',
},
{
key: 'germanyazuremonitor',
url: 'https://management.microsoftazure.de',
loginUrl: 'https://management.microsoftazure.de/',
},
{
key: 'chinaazuremonitor',
url: 'https://management.chinacloudapi.cn',
loginUrl: 'https://login.chinacloudapi.cn',
},
];
if (this.current.id) {
this.current.url = '/api/datasources/proxy/' + this.current.id;
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(this.current, backendSrv, null, $q);
this.getWorkspaces();
this.init();
}
}
async init() {
this.azureMonitorDatasource = new AzureMonitorDatasource(this.current, this.backendSrv, this.templateSrv);
await this.getSubscriptions();
await this.getSubscriptionsForLogsAnalytics();
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(
this.current,
this.backendSrv,
this.templateSrv,
this.$q
);
await this.getWorkspaces();
}
hasMinVersion(): boolean {
return isVersionGtOrEq(config.buildInfo.version, '5.2');
}
......@@ -30,17 +81,44 @@ export class AzureMonitorConfigCtrl {
return !this.hasRequiredGrafanaVersion && this.current.secureJsonFields.logAnalyticsClientSecret;
}
getWorkspaces() {
if (!this.azureLogAnalyticsDatasource.isConfigured()) {
async getWorkspaces() {
const sameAs = this.current.jsonData.azureLogAnalyticsSameAs && this.subscriptions.length > 0;
if (!sameAs && this.subscriptionsForLogAnalytics.length === 0) {
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;
}
});
this.workspaces = await this.azureLogAnalyticsDatasource.getWorkspaces();
if (this.workspaces.length > 0) {
this.current.jsonData.logAnalyticsDefaultWorkspace =
this.current.jsonData.logAnalyticsDefaultWorkspace || this.workspaces[0].value;
}
}
async getSubscriptions() {
if (!this.current.secureJsonFields.clientSecret && !this.current.secureJsonData.clientSecret) {
return;
}
this.subscriptions = (await this.azureMonitorDatasource.getSubscriptions()) || [];
if (this.subscriptions && this.subscriptions.length > 0) {
this.current.jsonData.subscriptionId = this.current.jsonData.subscriptionId || this.subscriptions[0].value;
}
}
async getSubscriptionsForLogsAnalytics() {
if (
!this.current.secureJsonFields.logAnalyticsClientSecret &&
!this.current.secureJsonData.logAnalyticsClientSecret
) {
return;
}
this.subscriptionsForLogAnalytics =
(await this.azureMonitorDatasource.getSubscriptions('workspacesloganalytics')) || [];
if (this.subscriptionsForLogAnalytics && this.subscriptionsForLogAnalytics.length > 0) {
this.current.jsonData.logAnalyticsSubscriptionId =
this.current.jsonData.logAnalyticsSubscriptionId || this.subscriptionsForLogAnalytics[0].value;
}
}
}
......@@ -15,7 +15,7 @@
}
.gf-form-select-wrapper--caret-indent.gf-form-select-wrapper::after {
right: 0.775rem
right: 0.775rem;
}
.service-dropdown {
......
......@@ -2,8 +2,10 @@ import _ from 'lodash';
import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
import AppInsightsDatasource from './app_insights/app_insights_datasource';
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
import { AzureMonitorQuery } from './types';
import { DataSourceApi, DataQueryRequest } from '@grafana/ui/src/types';
export default class Datasource {
export default class Datasource implements DataSourceApi<AzureMonitorQuery> {
id: number;
name: string;
azureMonitorDatasource: AzureMonitorDatasource;
......@@ -14,12 +16,7 @@ export default class Datasource {
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.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.backendSrv, this.templateSrv);
this.appInsightsDatasource = new AppInsightsDatasource(
instanceSettings,
this.backendSrv,
......@@ -35,15 +32,15 @@ export default class Datasource {
);
}
query(options) {
async query(options: DataQueryRequest<AzureMonitorQuery>) {
const promises: any[] = [];
const azureMonitorOptions = _.cloneDeep(options);
const appInsightsTargets = _.cloneDeep(options);
const azureLogAnalyticsTargets = _.cloneDeep(options);
const appInsightsOptions = _.cloneDeep(options);
const azureLogAnalyticsOptions = _.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']);
appInsightsOptions.targets = _.filter(appInsightsOptions.targets, ['queryType', 'Application Insights']);
azureLogAnalyticsOptions.targets = _.filter(azureLogAnalyticsOptions.targets, ['queryType', 'Azure Log Analytics']);
if (azureMonitorOptions.targets.length > 0) {
const amPromise = this.azureMonitorDatasource.query(azureMonitorOptions);
......@@ -52,15 +49,15 @@ export default class Datasource {
}
}
if (appInsightsTargets.targets.length > 0) {
const aiPromise = this.appInsightsDatasource.query(appInsightsTargets);
if (appInsightsOptions.targets.length > 0) {
const aiPromise = this.appInsightsDatasource.query(appInsightsOptions);
if (aiPromise) {
promises.push(aiPromise);
}
}
if (azureLogAnalyticsTargets.targets.length > 0) {
const alaPromise = this.azureLogAnalyticsDatasource.query(azureLogAnalyticsTargets);
if (azureLogAnalyticsOptions.targets.length > 0) {
const alaPromise = this.azureLogAnalyticsDatasource.query(azureLogAnalyticsOptions);
if (alaPromise) {
promises.push(alaPromise);
}
......@@ -75,11 +72,11 @@ export default class Datasource {
});
}
annotationQuery(options) {
async annotationQuery(options) {
return this.azureLogAnalyticsDatasource.annotationQuery(options);
}
metricFindQuery(query: string) {
async metricFindQuery(query: string) {
if (!query) {
return Promise.resolve([]);
}
......@@ -102,7 +99,7 @@ export default class Datasource {
return Promise.resolve([]);
}
testDatasource() {
async testDatasource() {
const promises: any[] = [];
if (this.azureMonitorDatasource.isConfigured()) {
......@@ -125,7 +122,7 @@ export default class Datasource {
};
}
return this.$q.all(promises).then(results => {
return Promise.all(promises).then(results => {
let status = 'success';
let message = '';
......@@ -145,24 +142,36 @@ export default class Datasource {
}
/* Azure Monitor REST API methods */
getResourceGroups() {
return this.azureMonitorDatasource.getResourceGroups();
getResourceGroups(subscriptionId: string) {
return this.azureMonitorDatasource.getResourceGroups(subscriptionId);
}
getMetricDefinitions(resourceGroup: string) {
return this.azureMonitorDatasource.getMetricDefinitions(resourceGroup);
getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
return this.azureMonitorDatasource.getMetricDefinitions(subscriptionId, resourceGroup);
}
getResourceNames(resourceGroup: string, metricDefinition: string) {
return this.azureMonitorDatasource.getResourceNames(resourceGroup, metricDefinition);
getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) {
return this.azureMonitorDatasource.getResourceNames(subscriptionId, resourceGroup, metricDefinition);
}
getMetricNames(resourceGroup: string, metricDefinition: string, resourceName: string) {
return this.azureMonitorDatasource.getMetricNames(resourceGroup, metricDefinition, resourceName);
getMetricNames(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
return this.azureMonitorDatasource.getMetricNames(subscriptionId, resourceGroup, metricDefinition, resourceName);
}
getMetricMetadata(resourceGroup: string, metricDefinition: string, resourceName: string, metricName: string) {
return this.azureMonitorDatasource.getMetricMetadata(resourceGroup, metricDefinition, resourceName, metricName);
getMetricMetadata(
subscriptionId: string,
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricName: string
) {
return this.azureMonitorDatasource.getMetricMetadata(
subscriptionId,
resourceGroup,
metricDefinition,
resourceName,
metricName
);
}
/* Application Insights API method */
......@@ -179,7 +188,11 @@ export default class Datasource {
}
/*Azure Log Analytics */
getAzureLogAnalyticsWorkspaces() {
return this.azureLogAnalyticsDatasource.getWorkspaces();
getAzureLogAnalyticsWorkspaces(subscriptionId: string) {
return this.azureLogAnalyticsDatasource.getWorkspaces(subscriptionId);
}
getSubscriptions() {
return this.azureMonitorDatasource.getSubscriptions();
}
}
......@@ -5,6 +5,12 @@
<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 class="gf-form" ng-show="ctrl.annotation.queryType === 'Azure Log Analytics'">
<label class="gf-form-label query-keyword width-9">Subscription</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select class="gf-form-input service-dropdown" ng-model="ctrl.annotation.subscription" ng-options="f.value as f.text for f in ctrl.subscriptions" ng-change="ctrl.onSubscriptionChange()"></select>
</div>
</div>
<div ng-show="ctrl.annotation.queryType === 'Azure Log Analytics'">
<div class="gf-form-inline">
<div class="gf-form">
......@@ -23,8 +29,13 @@
<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>
<kusto-editor
class="gf-form gf-form--grow"
query="ctrl.annotation.rawQuery"
variables="ctrl.templateVariables"
change="ctrl.onLogAnalyticsQueryChange"
getSchema="ctrl.getAzureLogAnalyticsSchema"
/>
</div>
<div class="gf-form-inline" ng-show="ctrl.annotation.queryType !== 'Azure Log Analytics'">
......
<h3 class="page-heading">Azure Monitor API Details</h3>
<h3 class="page-heading">Azure Monitor 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">
<span class="gf-form-label width-10">Azure Cloud</span>
<div class="gf-form-select-wrapper width-15 gf-form-select-wrapper--has-help-icon">
<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 gf-form-input--has-help-icon" 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>
<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">Tenant Id</span>
<span class="gf-form-label width-10">Tenant Id</span>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" 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>
......@@ -37,7 +26,7 @@
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Client Id</span>
<span class="gf-form-label width-10">Client Id</span>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" 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 ->
......@@ -49,7 +38,7 @@
</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>
<span class="gf-form-label width-10">Client Secret</span>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" 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 ->
......@@ -61,10 +50,26 @@
</div>
</div>
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.clientSecret">
<span class="gf-form-label width-9">Client Secret</span>
<span class="gf-form-label width-10">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 class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Default Subscription</span>
<gf-form-dropdown model="ctrl.current.jsonData.subscriptionId" allow-custom="true" lookup-text="false"
get-options="ctrl.subscriptions" css-class="width-30 gf-form-input">
</gf-form-dropdown>
<info-popover mode="right-absolute">
<p>Choose the default/preferred Subscription for Azure Metrics.</p>
<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>
</div>
<h3 class="page-heading">Azure Log Analytics API Details</h3>
......@@ -82,19 +87,7 @@
<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 gf-form-input--has-help-icon" 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>
<span class="gf-form-label width-10">Tenant Id</span>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.logAnalyticsTenantId"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" />
<info-popover mode="right-absolute">
......@@ -106,7 +99,7 @@
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Client Id</span>
<span class="gf-form-label width-10">Client Id</span>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.logAnalyticsClientId"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<info-popover mode="right-absolute">
......@@ -120,7 +113,7 @@
</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>
<span class="gf-form-label width-10">Client Secret</span>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.secureJsonData.logAnalyticsClientSecret"
placeholder="" />
<info-popover mode="right-absolute">
......@@ -133,20 +126,34 @@
</div>
</div>
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.logAnalyticsClientSecret">
<span class="gf-form-label width-9">Client Secret</span>
<span class="gf-form-label width-10">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 class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Default Subscription</span>
<gf-form-dropdown model="ctrl.current.jsonData.logAnalyticsSubscriptionId" allow-custom="true" lookup-text="true"
get-options="ctrl.subscriptionsForLogAnalytics" css-class="min-width-30">
</gf-form-dropdown>
<info-popover mode="right-absolute">
<p>Choose the default/preferred Subscription for Azure Metrics.</p>
<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>
<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">
<span class="gf-form-label width-10">Default Workspace</span>
<div class="gf-form-select-wrapper min-width-30 gf-form-select-wrapper--has-help-icon">
<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">
<info-popover mode="right-absolute">
<p>Choose the default/preferred Workspace for Azure Log Analytics queries.</p>
</info-popover>
</div>
......
......@@ -7,6 +7,12 @@
ng-change="ctrl.onQueryTypeChange()"></select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.target.queryType === 'Azure Monitor' || ctrl.target.queryType === 'Azure Log Analytics'">
<label class="gf-form-label query-keyword width-9">Subscription</label>
<gf-form-dropdown model="ctrl.target.subscription" allow-custom="true" lookup-text="true"
get-options="ctrl.subscriptions" on-change="ctrl.onSubscriptionChange()" 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>
......@@ -99,11 +105,9 @@
<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>
<gf-form-dropdown model="ctrl.target.azureLogAnalytics.workspace" allow-custom="true" lookup-text="true"
get-options="ctrl.workspaces" on-change="ctrl.refresh()" css-class="min-width-12">
</gf-form-dropdown>
<div class="gf-form">
<div class="width-1"></div>
</div>
......@@ -118,9 +122,6 @@
</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 gf-form--grow">
<kusto-editor
class="gf-form gf-form--grow"
......
......@@ -15,9 +15,13 @@ describe('AzureMonitorQueryCtrl', () => {
panel: { scopedVars: [], targets: [] },
};
AzureMonitorQueryCtrl.prototype.target = {} as any;
AzureMonitorQueryCtrl.prototype.datasource = {
$q: Q,
appInsightsDatasource: { isConfigured: () => false },
azureMonitorDatasource: { isConfigured: () => false },
};
queryCtrl = new AzureMonitorQueryCtrl({}, {}, new TemplateSrv());
queryCtrl.datasource = { $q: Q, appInsightsDatasource: { isConfigured: () => false } };
});
describe('init query_ctrl variables', () => {
......@@ -68,8 +72,10 @@ describe('AzureMonitorQueryCtrl', () => {
];
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.datasource.getMetricDefinitions = function(query) {
queryCtrl.datasource.getMetricDefinitions = function(subscriptionId, query) {
expect(subscriptionId).toBe('sub1');
expect(query).toBe('test');
return this.$q.when(response);
};
......@@ -99,9 +105,11 @@ describe('AzureMonitorQueryCtrl', () => {
const response = [{ text: 'test1', value: 'test1' }, { text: 'test2', value: 'test2' }];
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.datasource.getResourceNames = function(resourceGroup, metricDefinition) {
queryCtrl.datasource.getResourceNames = function(subscriptionId, resourceGroup, metricDefinition) {
expect(subscriptionId).toBe('sub1');
expect(resourceGroup).toBe('test');
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
return this.$q.when(response);
......@@ -133,10 +141,17 @@ describe('AzureMonitorQueryCtrl', () => {
const response = [{ text: 'metric1', value: 'metric1' }, { text: 'metric2', value: 'metric2' }];
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.resourceName = 'test';
queryCtrl.datasource.getMetricNames = function(resourceGroup, metricDefinition, resourceName) {
queryCtrl.datasource.getMetricNames = function(
subscriptionId,
resourceGroup,
metricDefinition,
resourceName
) {
expect(subscriptionId).toBe('sub1');
expect(resourceGroup).toBe('test');
expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
expect(resourceName).toBe('test');
......
......@@ -2,7 +2,6 @@ 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';
import './editor/editor_component';
export interface ResultFormat {
......@@ -18,6 +17,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
target: {
refId: string;
queryType: string;
subscription: string;
azureMonitor: {
resourceGroup: string;
resourceName: string;
......@@ -100,6 +100,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
showLastQuery: boolean;
lastQuery: string;
lastQueryError?: string;
subscriptions: Array<{ text: string; value: string }>;
/** @ngInject */
constructor($scope, $injector, private templateSrv) {
......@@ -112,6 +113,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
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' }];
this.getSubscriptions();
if (this.target.queryType === 'Azure Log Analytics') {
this.getWorkspaces();
}
......@@ -122,8 +124,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.lastQuery = '';
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
if (anySeriesFromQuery) {
this.lastQuery = anySeriesFromQuery.query;
if (anySeriesFromQuery && anySeriesFromQuery.meta) {
this.lastQuery = anySeriesFromQuery.meta.query;
}
}
......@@ -179,13 +181,42 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}
}
getSubscriptions() {
if (!this.datasource.azureMonitorDatasource.isConfigured()) {
return;
}
return this.datasource.azureMonitorDatasource.getSubscriptions().then(subs => {
this.subscriptions = subs;
if (!this.target.subscription && this.target.queryType === 'Azure Monitor') {
this.target.subscription = this.datasource.azureMonitorDatasource.subscriptionId;
} else if (!this.target.subscription && this.target.queryType === 'Azure Log Analytics') {
this.target.subscription = this.datasource.azureLogAnalyticsDatasource.logAnalyticsSubscriptionId;
}
if (!this.target.subscription && this.subscriptions.length > 0) {
this.target.subscription = this.subscriptions[0].value;
}
});
}
onSubscriptionChange() {
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));
return this.datasource
.getResourceGroups(
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId)
)
.catch(this.handleQueryCtrlError.bind(this));
}
getMetricDefinitions(query) {
......@@ -197,7 +228,10 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return;
}
return this.datasource
.getMetricDefinitions(this.replace(this.target.azureMonitor.resourceGroup))
.getMetricDefinitions(
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
this.replace(this.target.azureMonitor.resourceGroup)
)
.catch(this.handleQueryCtrlError.bind(this));
}
......@@ -214,6 +248,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return this.datasource
.getResourceNames(
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
this.replace(this.target.azureMonitor.resourceGroup),
this.replace(this.target.azureMonitor.metricDefinition)
)
......@@ -235,6 +270,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return this.datasource
.getMetricNames(
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
this.replace(this.target.azureMonitor.resourceGroup),
this.replace(this.target.azureMonitor.metricDefinition),
this.replace(this.target.azureMonitor.resourceName)
......@@ -306,7 +342,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
getWorkspaces = () => {
return this.datasource.azureLogAnalyticsDatasource
.getWorkspaces()
.getWorkspaces(this.target.subscription)
.then(list => {
this.workspaces = list;
if (list.length > 0 && !this.target.azureLogAnalytics.workspace) {
......
import { DataQuery } from '@grafana/ui/src/types';
export interface AzureMonitorQuery extends DataQuery {
format: string;
subscription: string;
azureMonitor: AzureMetricQuery;
azureLogAnalytics: AzureLogsQuery;
// appInsights: any;
}
export interface AzureMetricQuery {
resourceGroup: string;
resourceName: string;
metricDefinition: string;
metricName: string;
timeGrainUnit: string;
timeGrain: string;
timeGrains: string[];
aggregation: string;
dimension: string;
dimensionFilter: string;
alias: string;
}
export interface AzureLogsQuery {
query: string;
resultFormat: string;
workspace: string;
}
// Azure Log Analytics types
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 AzureLogsVariable {
text: string;
value: string;
}
export interface AzureLogsTableData {
columns: AzureLogsTableColumn[];
rows: any[];
type: string;
refId: string;
meta: {
query: string;
};
}
export interface AzureLogsTableColumn {
text: string;
type: string;
}
......@@ -2,7 +2,7 @@
echo -e "Collecting code stats (typescript errors & more)"
ERROR_COUNT_LIMIT=5617
ERROR_COUNT_LIMIT=5564
DIRECTIVES_LIMIT=172
CONTROLLERS_LIMIT=139
......
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