diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts index 7b46ee7b9fd..555ec8cd9be 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts @@ -3,24 +3,48 @@ export class AzureMonitorAnnotationsQueryCtrl { datasource: any; annotation: any; workspaces: any[]; + subscriptions: Array<{ text: string; value: string }>; defaultQuery = '\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); + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts index 35e15a5c885..c46ba9e2f92 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts @@ -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 }); }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts index a4a98edb031..23f24e6f1fb 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts @@ -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, + }; } } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 199a63583d2..c6ff6dc5f75 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -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 { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 9a5a1e259ec..3df2a244e97 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -1,6 +1,8 @@ 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) { 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; }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts index 610dfc61b84..679ff5950a0 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts @@ -1,66 +1,15 @@ 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: '' }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index c585724990a..f36d72709d4 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -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); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 6f82ca72558..cc7b34551ab 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -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) { 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) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts index 5ae25609996..878ac961939 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts @@ -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; + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts index a0f5a654813..51883ee252d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts @@ -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' ); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts index 47937076428..cd84a68bf35 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts @@ -1,6 +1,7 @@ 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}` ); } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts index 4ee5c94fad6..cc94cc89914 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts @@ -1,27 +1,78 @@ 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; + } } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css b/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css index 305c9eadb09..cfd1aaf78d9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css @@ -15,7 +15,7 @@ } .gf-form-select-wrapper--caret-indent.gf-form-select-wrapper::after { - right: 0.775rem + right: 0.775rem; } .service-dropdown { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index c4155cf1cd7..dd1ea139b45 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -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 { 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) { 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(); } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html index e4a8c863984..a5b2b2adc5b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html @@ -5,6 +5,12 @@ +
+ +
+ +
+
@@ -23,8 +29,13 @@
- +
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html index b3d7ad0f092..0f5cb0c2be0 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html @@ -1,32 +1,21 @@ -

Azure Monitor API Details

+

Azure Monitor Details

- Azure Cloud -
+ Azure Cloud +
- +

Choose an Azure Cloud.

- Subscription Id - - -

In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.

- **Click - here for detailed instructions on setting up an Azure Active Directory (AD) application.** -
-
-
-
-
- Tenant Id + Tenant Id

In the Azure Portal, navigate to Azure Active Directory -> Properties -> Directory ID.

@@ -37,7 +26,7 @@
- Client Id + Client Id

In the Azure Portal, navigate to Azure Active Directory -> App Registrations -> Choose your app -> @@ -49,7 +38,7 @@

- Client Secret + Client Secret

To create a new key, log in to Azure Portal, navigate to Azure Active Directory -> App Registrations -> @@ -61,10 +50,26 @@

- Client Secret + Client Secret reset
+
+
+
+ Default Subscription + + + +

Choose the default/preferred Subscription for Azure Metrics.

+

In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.

+ **Click + here for detailed instructions on setting up an Azure Active Directory (AD) application.** +
+
+
+

Azure Log Analytics API Details

@@ -82,19 +87,7 @@
- Subscription Id - - -

In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.

- **Click - here for detailed instructions on setting up an Azure Active Directory (AD) application.** -
-
-
-
-
- Tenant Id + Tenant Id @@ -106,7 +99,7 @@
- Client Id + Client Id @@ -120,7 +113,7 @@
- Client Secret + Client Secret @@ -133,20 +126,34 @@
- Client Secret + Client Secret reset
+
+
+ Default Subscription + + + +

Choose the default/preferred Subscription for Azure Metrics.

+

In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.

+ **Click + here for detailed instructions on setting up an Azure Active Directory (AD) application.** +
+
+
- Default Workspace -
+ Default Workspace +
- +

Choose the default/preferred Workspace for Azure Log Analytics queries.

diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index 6299947b30a..c2461eb67ea 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -7,6 +7,12 @@ ng-change="ctrl.onQueryTypeChange()">
+
+ + + +
@@ -99,11 +105,9 @@
-
- -
-
+ +
@@ -118,9 +122,6 @@
- -
{ 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'); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index 6374af8c599..3d896f27a0a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -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) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts new file mode 100644 index 00000000000..9a77ae61259 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts @@ -0,0 +1,80 @@ +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; +} diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 9c1c062e91c..85464aff3e0 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/scripts/ci-frontend-metrics.sh @@ -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