Commit 88051258 by Erik Sundell Committed by GitHub

Azure Monitor: Add support for cross resource queries (#19115)

* Add new query mode picker with different states for each query. Also really simple migration script

* Populate cross resource dropdowns

* Cleanup. Handle change events

* Add multi select picker for subscriptions

* Fix markup issue

* Prepare for new query mode

* More cleanup

* Handle multiple queries both in ds and backend

* Refactoring

* Improve migration

* Add support for multiselect display name

* Use multiselect also for locations and resources

* Add more typings

* Fix migrations

* Custom multiselect built for array of options instead of variables

* Add url builder test

* fix datasource tests

* UI fixes

* Improve query editor init

* Fix brokens tests

* Cleanup

* Fix tslint issue

* Change query mode display name

* Make sure alerting works for single queries

* Friendly error for multi resources

* Add temporary typings
parent b5f0a5d5
......@@ -60,7 +60,11 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori
if err != nil {
queryRes.Error = err
}
result.Results[query.RefID] = queryRes
if val, ok := result.Results[query.RefID]; ok {
val.Series = append(result.Results[query.RefID].Series, queryRes.Series...)
} else {
result.Results[query.RefID] = queryRes
}
}
return result, nil
......@@ -84,11 +88,22 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
azureMonitorTarget := query.Model.Get("azureMonitor").MustMap()
azlog.Debug("AzureMonitor", "target", azureMonitorTarget)
queryMode := fmt.Sprintf("%v", azureMonitorTarget["queryMode"])
if queryMode == "crossResource" {
return nil, fmt.Errorf("Alerting not supported for multiple resource queries")
}
var azureMonitorData map[string]interface{}
if queryMode == "singleResource" {
azureMonitorData = azureMonitorTarget["data"].(map[string]interface{})[queryMode].(map[string]interface{})
} else {
azureMonitorData = azureMonitorTarget
}
urlComponents := map[string]string{}
urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"])
urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"])
urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"])
urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorData["resourceGroup"])
urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorData["metricDefinition"])
urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorData["resourceName"])
ub := urlBuilder{
DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
......@@ -100,12 +115,12 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
azureURL := ub.Build()
alias := ""
if val, ok := azureMonitorTarget["alias"]; ok {
if val, ok := azureMonitorData["alias"]; ok {
alias = fmt.Sprintf("%v", val)
}
timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"])
timeGrains := azureMonitorTarget["allowedTimeGrainsMs"]
timeGrain := fmt.Sprintf("%v", azureMonitorData["timeGrain"])
timeGrains := azureMonitorData["allowedTimeGrainsMs"]
if timeGrain == "auto" {
timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains)
if err != nil {
......@@ -117,13 +132,16 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
params.Add("api-version", "2018-01-01")
params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339)))
params.Add("interval", timeGrain)
params.Add("aggregation", fmt.Sprintf("%v", azureMonitorTarget["aggregation"]))
params.Add("metricnames", fmt.Sprintf("%v", azureMonitorTarget["metricName"]))
params.Add("metricnamespace", fmt.Sprintf("%v", azureMonitorTarget["metricNamespace"]))
params.Add("aggregation", fmt.Sprintf("%v", azureMonitorData["aggregation"]))
params.Add("metricnames", fmt.Sprintf("%v", azureMonitorData["metricName"]))
if val, ok := azureMonitorData["metricNamespace"]; ok {
params.Add("metricnamespace", fmt.Sprintf("%v", val))
}
dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimension"]))
dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimensionFilter"]))
if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" {
dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorData["dimension"]))
dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorData["dimensionFilter"]))
if azureMonitorData["dimension"] != nil && azureMonitorData["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" {
params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
}
......
......@@ -36,15 +36,20 @@ func TestAzureMonitorDatasource(t *testing.T) {
Model: simplejson.NewFromAny(map[string]interface{}{
"subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
"azureMonitor": map[string]interface{}{
"timeGrain": "PT1M",
"aggregation": "Average",
"resourceGroup": "grafanastaging",
"resourceName": "grafana",
"metricDefinition": "Microsoft.Compute/virtualMachines",
"metricNamespace": "Microsoft.Compute-virtualMachines",
"metricName": "Percentage CPU",
"alias": "testalias",
"queryType": "Azure Monitor",
"queryMode": "singleResource",
"data": map[string]interface{}{
"singleResource": map[string]interface{}{
"timeGrain": "PT1M",
"aggregation": "Average",
"resourceGroup": "grafanastaging",
"resourceName": "grafana",
"metricDefinition": "Microsoft.Compute/virtualMachines",
"metricNamespace": "Microsoft.Compute-virtualMachines",
"metricName": "Percentage CPU",
"alias": "testalias",
"queryType": "Azure Monitor",
},
},
},
}),
RefId: "A",
......
......@@ -918,8 +918,8 @@ describe('AzureMonitorDatasource', () => {
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'UsedCapacity'
'UsedCapacity',
'default'
)
.then((results: any) => {
expect(results.primaryAggType).toEqual('Total');
......@@ -992,8 +992,8 @@ describe('AzureMonitorDatasource', () => {
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'Transactions'
'Transactions',
'default'
)
.then((results: any) => {
expect(results.dimensions.length).toEqual(4);
......@@ -1011,8 +1011,8 @@ describe('AzureMonitorDatasource', () => {
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'FreeCapacity'
'FreeCapacity',
'default'
)
.then((results: any) => {
expect(results.dimensions.length).toEqual(0);
......
......@@ -5,9 +5,12 @@ import SupportedNamespaces from './supported_namespaces';
import TimegrainConverter from '../time_grain_converter';
import {
AzureMonitorQuery,
AzureMonitorQueryData,
AzureDataSourceJsonData,
AzureMonitorMetricDefinitionsResponse,
AzureMonitorResourceGroupsResponse,
AzureMonitorResourceResponse,
Resource,
} from '../types';
import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/ui';
......@@ -47,60 +50,172 @@ export default class AzureMonitorDatasource {
return !!this.subscriptionId && this.subscriptionId.length > 0;
}
async query(options: DataQueryRequest<AzureMonitorQuery>): Promise<DataQueryResponseData[]> {
const queries = _.filter(options.targets, item => {
return (
item.hide !== true &&
item.azureMonitor.resourceGroup &&
item.azureMonitor.resourceGroup !== this.defaultDropdownValue &&
item.azureMonitor.resourceName &&
item.azureMonitor.resourceName !== this.defaultDropdownValue &&
item.azureMonitor.metricDefinition &&
item.azureMonitor.metricDefinition !== this.defaultDropdownValue &&
item.azureMonitor.metricName &&
item.azureMonitor.metricName !== this.defaultDropdownValue
);
}).map(target => {
const item = target.azureMonitor;
buildQuery(
options: DataQueryRequest<AzureMonitorQuery>,
target: any,
{
resourceGroup,
resourceName,
metricDefinition,
timeGrainUnit,
timeGrain,
metricName,
metricNamespace,
allowedTimeGrainsMs,
aggregation,
dimension,
dimensionFilter,
alias,
}: AzureMonitorQueryData,
subscriptionId?: string
) {
if (timeGrainUnit && timeGrain !== 'auto') {
timeGrain = TimegrainConverter.createISO8601Duration(timeGrain, timeGrainUnit);
}
// fix for timeGrainUnit which is a deprecated/removed field name
if (item.timeGrainUnit && item.timeGrain !== 'auto') {
item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
}
const metricNamespaceParsed = this.templateSrv.replace(metricNamespace, options.scopedVars);
return {
refId: target.refId,
intervalMs: options.intervalMs,
datasourceId: this.id,
subscription: this.templateSrv.replace(
subscriptionId || target.subscription || this.subscriptionId,
options.scopedVars
),
queryType: 'Azure Monitor',
type: 'timeSeriesQuery',
raw: false,
azureMonitor: {
resourceGroup: this.templateSrv.replace(resourceGroup, options.scopedVars),
resourceName: this.templateSrv.replace(resourceName, options.scopedVars),
metricDefinition: this.templateSrv.replace(metricDefinition, options.scopedVars),
timeGrain: this.templateSrv.replace((timeGrain || '').toString(), options.scopedVars),
allowedTimeGrainsMs: allowedTimeGrainsMs,
metricName: this.templateSrv.replace(metricName, options.scopedVars),
metricNamespace:
metricNamespaceParsed && metricNamespaceParsed !== this.defaultDropdownValue
? metricNamespaceParsed
: metricDefinition,
aggregation: this.templateSrv.replace(aggregation, options.scopedVars),
dimension: this.templateSrv.replace(dimension, options.scopedVars),
dimensionFilter: this.templateSrv.replace(dimensionFilter, options.scopedVars),
alias,
format: target.format,
},
};
}
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 metricNamespace = this.templateSrv.replace(item.metricNamespace, options.scopedVars);
const metricDefinition = this.templateSrv.replace(item.metricDefinition, options.scopedVars);
const timeGrain = this.templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars);
const aggregation = this.templateSrv.replace(item.aggregation, options.scopedVars);
buildSingleQuery(
options: DataQueryRequest<AzureMonitorQuery>,
target: any,
{
resourceGroup,
resourceName,
metricDefinition,
timeGrainUnit,
timeGrain,
metricName,
metricNamespace,
allowedTimeGrainsMs,
aggregation,
dimension,
dimensionFilter,
alias,
}: AzureMonitorQueryData,
queryMode: string
) {
if (timeGrainUnit && timeGrain !== 'auto') {
timeGrain = TimegrainConverter.createISO8601Duration(timeGrain, timeGrainUnit);
}
return {
refId: target.refId,
intervalMs: options.intervalMs,
datasourceId: this.id,
subscription: subscriptionId,
queryType: 'Azure Monitor',
type: 'timeSeriesQuery',
raw: false,
azureMonitor: {
resourceGroup: resourceGroup,
resourceName: resourceName,
metricDefinition: metricDefinition,
timeGrain: timeGrain,
allowedTimeGrainsMs: item.allowedTimeGrainsMs,
metricName: this.templateSrv.replace(item.metricName, options.scopedVars),
metricNamespace:
metricNamespace && metricNamespace !== this.defaultDropdownValue ? metricNamespace : metricDefinition,
aggregation: aggregation,
dimension: this.templateSrv.replace(item.dimension, options.scopedVars),
dimensionFilter: this.templateSrv.replace(item.dimensionFilter, options.scopedVars),
alias: item.alias,
format: target.format,
const metricNamespaceParsed = this.templateSrv.replace(metricNamespace, options.scopedVars);
return {
refId: target.refId,
intervalMs: options.intervalMs,
datasourceId: this.id,
subscription: this.templateSrv.replace(target.subscription || this.subscriptionId, options.scopedVars),
queryType: 'Azure Monitor',
type: 'timeSeriesQuery',
raw: false,
azureMonitor: {
queryMode,
data: {
[queryMode]: {
resourceGroup: this.templateSrv.replace(resourceGroup, options.scopedVars),
resourceName: this.templateSrv.replace(resourceName, options.scopedVars),
metricDefinition: this.templateSrv.replace(metricDefinition, options.scopedVars),
timeGrain: this.templateSrv.replace((timeGrain || '').toString(), options.scopedVars),
allowedTimeGrainsMs: allowedTimeGrainsMs,
metricName: this.templateSrv.replace(metricName, options.scopedVars),
metricNamespace:
metricNamespaceParsed && metricNamespaceParsed !== this.defaultDropdownValue
? metricNamespaceParsed
: metricDefinition,
aggregation: this.templateSrv.replace(aggregation, options.scopedVars),
dimension: this.templateSrv.replace(dimension, options.scopedVars),
dimensionFilter: this.templateSrv.replace(dimensionFilter, options.scopedVars),
alias,
format: target.format,
},
},
};
});
},
};
}
async query(options: DataQueryRequest<any>): Promise<DataQueryResponseData[]> {
const groupedQueries: any[] = await Promise.all(
options.targets
.filter(item => {
const { data, queryMode } = item.azureMonitor;
const { resourceGroup, resourceGroups, metricDefinition, metricName } = data[queryMode];
return (
item.hide !== true &&
((resourceGroup && resourceGroup !== this.defaultDropdownValue) || resourceGroups.length) &&
metricDefinition &&
metricDefinition !== this.defaultDropdownValue &&
metricName &&
metricName !== this.defaultDropdownValue
);
})
.map(async target => {
const { data, queryMode } = target.azureMonitor;
if (queryMode === 'crossResource') {
const { resourceGroups, metricDefinition, locations } = data[queryMode];
const resources = await this.getResources(target.subscriptions).then(resources =>
resources.filter(
({ type, group, subscriptionId, location }) =>
target.subscriptions.includes(subscriptionId) &&
resourceGroups.includes(group) &&
locations.includes(location) &&
metricDefinition === type
)
);
delete data.crossResource.metricNamespace;
return resources.map(
({ type: metricDefinition, group: resourceGroup, subscriptionId, name: resourceName }) =>
this.buildQuery(
options,
target,
{
...data[queryMode],
metricDefinition,
resourceGroup,
resourceName,
},
subscriptionId
)
);
} else {
return Promise.resolve(this.buildSingleQuery(options, target, data[queryMode], queryMode));
}
})
);
const queries = _.flatten(groupedQueries);
if (!queries || queries.length === 0) {
return Promise.resolve([]);
......@@ -118,7 +233,7 @@ export default class AzureMonitorDatasource {
const result: DataQueryResponseData[] = [];
if (data.results) {
Object['values'](data.results).forEach((queryRes: any) => {
Object.values(data.results).forEach((queryRes: any) => {
if (!queryRes.series) {
return;
}
......@@ -337,12 +452,31 @@ export default class AzureMonitorDatasource {
});
}
async getResources(subscriptionIds: string[]): Promise<Resource[]> {
const responses: Resource[][] = await Promise.all(
subscriptionIds.map(subscriptionId =>
this.doRequest(`${this.baseUrl}/${subscriptionId}/resources?api-version=2018-02-01`).then(
(res: AzureMonitorResourceResponse) =>
res.data.value
.map(r => ({
...r,
group: /.*\/resourceGroups\/(.*?)\//.exec(r.id)[1],
subscriptionId,
}))
.filter(({ type }) => this.supportedMetricNamespaces.includes(type))
)
)
);
return responses.reduce((result, resources) => [...result, ...resources], []);
}
getMetricNames(
subscriptionId: string,
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricNamespace: string
metricNamespace?: string
) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.baseUrl,
......@@ -350,8 +484,8 @@ export default class AzureMonitorDatasource {
resourceGroup,
metricDefinition,
resourceName,
metricNamespace,
this.apiVersion
this.apiVersion,
metricNamespace
);
return this.doRequest(url).then((result: any) => {
......@@ -364,8 +498,8 @@ export default class AzureMonitorDatasource {
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricNamespace: string,
metricName: string
metricName: string,
metricNamespace?: string
) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.baseUrl,
......@@ -373,8 +507,8 @@ export default class AzureMonitorDatasource {
resourceGroup,
metricDefinition,
resourceName,
metricNamespace,
this.apiVersion
this.apiVersion,
metricNamespace
);
return this.doRequest(url).then((result: any) => {
......
......@@ -108,8 +108,8 @@ export default class ResponseParser {
return dimensions;
}
static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
const list: Array<{ text: string; value: string }> = [];
static parseSubscriptions(result: any): Array<{ text: string; value: string; displayName: string }> {
const list: Array<{ text: string; value: string; displayName: string }> = [];
if (!result) {
return list;
......@@ -122,6 +122,7 @@ export default class ResponseParser {
list.push({
text: `${_.get(result.data.value[i], textFieldName)} - ${_.get(result.data.value[i], valueFieldName)}`,
value: _.get(result.data.value[i], valueFieldName),
displayName: _.get(result.data.value[i], textFieldName),
});
}
}
......
......@@ -9,8 +9,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg',
'Microsoft.Sql/servers/databases',
'rn1/rn2',
'default',
'2017-05-01-preview'
'2017-05-01-preview',
'default'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
......@@ -27,8 +27,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg',
'Microsoft.Sql/servers',
'rn',
'default',
'2017-05-01-preview'
'2017-05-01-preview',
'default'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
......@@ -45,8 +45,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg',
'Microsoft.Storage/storageAccounts/blobServices',
'rn1/default',
'default',
'2017-05-01-preview'
'2017-05-01-preview',
'default'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
......@@ -63,8 +63,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg',
'Microsoft.Storage/storageAccounts/fileServices',
'rn1/default',
'default',
'2017-05-01-preview'
'2017-05-01-preview',
'default'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
......@@ -81,8 +81,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg',
'Microsoft.Storage/storageAccounts/tableServices',
'rn1/default',
'default',
'2017-05-01-preview'
'2017-05-01-preview',
'default'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
......@@ -99,8 +99,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg',
'Microsoft.Storage/storageAccounts/queueServices',
'rn1/default',
'default',
'2017-05-01-preview'
'2017-05-01-preview',
'default'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
......@@ -108,4 +108,22 @@ describe('AzureMonitorUrlBuilder', () => {
);
});
});
describe('when metric namespace is missing', () => {
it('should be excluded from the query', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/queueServices',
'rn1/default',
'2017-05-01-preview'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
);
});
});
});
......@@ -29,26 +29,24 @@ export default class UrlBuilder {
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricNamespace: string,
apiVersion: string
apiVersion: string,
metricNamespace?: string
) {
const metricNameSpaceParam = metricNamespace ? `&metricnamespace=${encodeURIComponent(metricNamespace)}` : '';
if ((metricDefinition.match(/\//g) || []).length > 1) {
const rn = resourceName.split('/');
const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
return (
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}&metricnamespace=${encodeURIComponent(
metricNamespace
)}`
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}${metricNameSpaceParam}`
);
}
return (
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}&metricnamespace=${encodeURIComponent(
metricNamespace
)}`
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}${metricNameSpaceParam}`
);
}
}
import _ from 'lodash';
import { migrateTargetSchema } from './migrations';
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';
......@@ -42,7 +43,9 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
const appInsightsOptions = _.cloneDeep(options);
const azureLogAnalyticsOptions = _.cloneDeep(options);
azureMonitorOptions.targets = _.filter(azureMonitorOptions.targets, ['queryType', 'Azure Monitor']);
azureMonitorOptions.targets = azureMonitorOptions.targets
.filter((t: any) => t.queryType === 'Azure Monitor')
.map((t: any) => migrateTargetSchema(t));
appInsightsOptions.targets = _.filter(appInsightsOptions.targets, ['queryType', 'Application Insights']);
azureLogAnalyticsOptions.targets = _.filter(azureLogAnalyticsOptions.targets, ['queryType', 'Azure Log Analytics']);
......@@ -163,7 +166,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricNamespace: string
metricNamespace?: string
) {
return this.azureMonitorDatasource.getMetricNames(
subscriptionId,
......@@ -188,19 +191,23 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
resourceGroup: string,
metricDefinition: string,
resourceName: string,
metricNamespace: string,
metricName: string
metricName: string,
metricNamespace?: string
) {
return this.azureMonitorDatasource.getMetricMetadata(
subscriptionId,
resourceGroup,
metricDefinition,
resourceName,
metricNamespace,
metricName
metricName,
metricNamespace
);
}
getResources(subscriptions: string[]) {
return this.azureMonitorDatasource.getResources(subscriptions);
}
/* Application Insights API method */
getAppInsightsMetricNames() {
return this.appInsightsDatasource.getMetricNames();
......
import { AzureMonitorQueryCtrl } from './query_ctrl';
export function migrateTargetSchema(target: any) {
if (target.azureMonitor && !target.azureMonitor.data) {
const temp = { ...target.azureMonitor };
target.azureMonitor = {
queryMode: AzureMonitorQueryCtrl.defaultQueryMode,
data: {
[AzureMonitorQueryCtrl.defaultQueryMode]: temp,
},
};
}
return target;
}
import angular from 'angular';
import _ from 'lodash';
export class MultiSelectDropdownCtrl {
dropdownVisible: boolean;
highlightIndex: number;
linkText: string;
options: Array<{ selected: boolean; text: string; value: string }>;
selectedValues: Array<{ text: string; value: string }>;
initialValues: string[];
onUpdated: any;
show() {
this.highlightIndex = -1;
this.options = this.options;
this.selectedValues = this.options.filter(({ selected }) => selected);
this.dropdownVisible = true;
}
hide() {
this.dropdownVisible = false;
}
updateLinkText() {
this.linkText =
this.selectedValues.length === 1 ? this.selectedValues[0].text : `(${this.selectedValues.length}) selected`;
}
clearSelections() {
this.selectedValues = _.filter(this.options, { selected: true });
if (this.selectedValues.length > 1) {
_.each(this.options, option => {
option.selected = false;
});
} else {
_.each(this.options, option => {
option.selected = true;
});
}
this.selectionsChanged();
}
selectValue(option: any) {
if (!option) {
return;
}
option.selected = !option.selected;
this.selectionsChanged();
}
selectionsChanged() {
this.selectedValues = _.filter(this.options, { selected: true });
if (!this.selectedValues.length && this.options.length) {
this.selectedValues = this.options.slice(0, 1);
}
this.updateLinkText();
this.onUpdated({ values: this.selectedValues.map(({ value }) => value) });
}
onClickOutside() {
this.selectedValues = _.filter(this.options, { selected: true });
if (this.selectedValues.length === 0) {
this.options[0].selected = true;
this.selectionsChanged();
}
this.dropdownVisible = false;
}
init() {
if (!this.options) {
return;
}
this.options = this.options.map(o => ({
...o,
selected: this.initialValues.includes(o.value),
}));
this.selectedValues = _.filter(this.options, { selected: true });
if (!this.selectedValues.length) {
this.options = this.options.map(o => ({
...o,
selected: true,
}));
}
this.updateLinkText();
}
updateSelection() {
this.selectedValues = _.filter(this.options, { selected: true });
if (!this.selectedValues.length && this.options.length) {
this.options = this.options.map(o => ({
...o,
selected: true,
}));
this.selectedValues = _.filter(this.options, { selected: true });
this.selectionsChanged();
}
this.updateLinkText();
}
}
/** @ngInject */
export function multiSelectDropdown($window: any, $timeout: any) {
return {
scope: { onUpdated: '&', options: '=', initialValues: '=' },
templateUrl: 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/multi-select.directive.html',
controller: MultiSelectDropdownCtrl,
controllerAs: 'vm',
bindToController: true,
link: (scope: any, elem: any) => {
const bodyEl = angular.element($window.document.body);
const linkEl = elem.find('.variable-value-link');
const inputEl = elem.find('input');
function openDropdown() {
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
inputEl.show();
linkEl.hide();
inputEl.focus();
$timeout(
() => {
bodyEl.on('click', () => {
bodyEl.on('click', bodyOnClick);
});
},
0,
false
);
}
function switchToLink() {
inputEl.hide();
linkEl.show();
bodyEl.off('click', bodyOnClick);
}
function bodyOnClick(e: any) {
if (elem.has(e.target).length === 0) {
scope.$apply(() => {
scope.vm.onClickOutside();
});
}
}
scope.$watch('vm.options', (newValue: any) => {
if (newValue) {
scope.vm.updateSelection(newValue);
}
});
scope.$watch('vm.dropdownVisible', (newValue: any) => {
if (newValue) {
openDropdown();
} else {
switchToLink();
}
});
scope.vm.init();
},
};
}
angular.module('grafana.directives').directive('multiSelect', multiSelectDropdown);
<div class="variable-link-wrapper">
<a ng-click="vm.show()" class="variable-value-link">
{{vm.linkText}}
<i class="fa fa-caret-down" style="font-size:12px"></i>
</a>
<input type="text" class="gf-form-input width-11" ng-model="vm.linkText" ng-change="vm.queryChanged()" ></input>
<div class="variable-value-dropdown multi" ng-if="vm.dropdownVisible">
<div class="variable-options-wrapper">
<div class="variable-options-column">
<a ng-if="vm.options.length > 1" class="variable-options-column-header" ng-class="{'many-selected': vm.selectedValues.length > 1}" bs-tooltip="'Clear selections'" data-placement="top" ng-click="vm.clearSelections()">
<span class="variable-option-icon"></span>
Selected ({{vm.selectedValues.length}})
</a>
<a class="variable-option pointer" ng-repeat="option in vm.options" ng-class="{'selected': option.selected, 'highlighted': $index === vm.highlightIndex}" ng-click="vm.selectValue(option, $event)">
<span class="variable-option-icon"></span>
<span>{{option.text}}</span>
</a>
</div>
</div>
</div>
</div>
\ No newline at end of file
......@@ -7,70 +7,138 @@
ng-change="ctrl.onQueryTypeChange()"></select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.target.queryType === 'Azure Monitor' || ctrl.target.queryType === 'Azure Log Analytics'">
<div class="gf-form" ng-if="ctrl.target.queryType === 'Azure Monitor'">
<label class="gf-form-label query-keyword width-9">Query Mode</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select class="gf-form-input service-dropdown" ng-model="ctrl.target.azureMonitor.queryMode"
ng-options="f.value as f.text for f in [{value: 'singleResource', text: 'Single Resource'}, {value: 'crossResource', text: 'Multiple Resources'}]"
ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="(ctrl.target.queryType === 'Azure Monitor' && ctrl.target.azureMonitor.queryMode === 'singleResource') || ctrl.target.queryType === 'Azure Log Analytics'">
<div class="gf-form" >
<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.getSubscriptions()" on-change="ctrl.onSubscriptionChange()" css-class="min-width-12">
get-options="ctrl.getSubscriptions()" on-change="ctrl.onSubscriptionChange()" css-class="min-width-6">
</gf-form-dropdown>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div ng-if="ctrl.target.queryType === 'Azure Monitor'">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Resource Group</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.resourceGroup" allow-custom="true" lookup-text="true"
get-options="ctrl.getResourceGroups($query)" on-change="ctrl.onResourceGroupChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Namespace</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.metricDefinition" allow-custom="true" lookup-text="true"
get-options="ctrl.getMetricDefinitions($query)" on-change="ctrl.onMetricDefinitionChange()" css-class="min-width-20">
</gf-form-dropdown>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Resource Name</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.resourceName" allow-custom="true" lookup-text="true"
get-options="ctrl.getResourceNames($query)" on-change="ctrl.onResourceNameChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form-inline" ng-if="(ctrl.target.queryType === 'Azure Monitor' && ctrl.target.azureMonitor.queryMode === 'crossResource')">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Subscriptions</label>
<multi-select ng-if="ctrl.subscriptionValues.length"
initial-values="ctrl.target.subscriptions"
options="ctrl.subscriptionValues"
on-updated="ctrl.onSubscriptionsChange(values)">
</multi-select>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.target.queryType === 'Azure Monitor' && ctrl.target.azureMonitor.queryMode === 'singleResource'">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Resource Group</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].resourceGroup" allow-custom="true" lookup-text="true"
get-options="ctrl.getResourceGroups($query)" on-change="ctrl.onResourceGroupChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Namespace</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].metricDefinition" allow-custom="true" lookup-text="true"
get-options="ctrl.getMetricDefinitions($query)" on-change="ctrl.onMetricDefinitionChange()" css-class="min-width-20">
</gf-form-dropdown>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Resource Name</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].resourceName" allow-custom="true" lookup-text="true"
get-options="ctrl.getResourceNames($query)" on-change="ctrl.onResourceNameChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Locations</label>
<multi-select ng-if="ctrl.locations.length"
initial-values="ctrl.target.azureMonitor.data.crossResource.locations"
options="ctrl.locations"
on-updated="ctrl.onLocationsChange(values)">
</multi-select>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Resource Groups</label>
<multi-select class="az-multi-picker"
initial-values="ctrl.target.azureMonitor.data.crossResource.resourceGroups"
options="ctrl.resourceGroups"
on-updated="ctrl.onCrossResourceGroupChange(values)">
</multi-select>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Resource Type</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.data.crossResource.metricDefinition" allow-custom="true" lookup-text="true"
get-options="ctrl.getCrossResourceMetricDefinitions($query)" on-change="ctrl.onCrossResourceMetricDefinitionChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<div class="gf-form" ng-if="ctrl.target.azureMonitor.queryMode === 'singleResource'">
<label class="gf-form-label query-keyword width-9">Metric Namespace</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.metricNamespace" allow-custom="true" lookup-text="true"
<gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].metricNamespace" allow-custom="true" lookup-text="true"
get-options="ctrl.getMetricNamespaces($query)" on-change="ctrl.onMetricNamespacesChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Metric</label>
<gf-form-dropdown model="ctrl.target.azureMonitor.metricName" allow-custom="true" lookup-text="true"
<gf-form-dropdown ng-if="ctrl.target.azureMonitor.queryMode === 'singleResource'" model="ctrl.target.azureMonitor.data.singleResource.metricName" allow-custom="true" lookup-text="true"
get-options="ctrl.getMetricNames($query)" on-change="ctrl.onMetricNameChange()" css-class="min-width-12">
</gf-form-dropdown>
<gf-form-dropdown ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'" model="ctrl.target.azureMonitor.data.crossResource.metricName" allow-custom="true" lookup-text="true"
get-options="ctrl.getCrossResourceMetricNames($query)" on-change="ctrl.onCrossResourceMetricNameChange()" css-class="min-width-12">
</gf-form-dropdown>
</div>
<div class="gf-form gf-form--grow aggregation-dropdown-wrapper">
<div class="gf-form aggregation-dropdown-wrapper">
<label class="gf-form-label query-keyword width-9">Aggregation</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select class="gf-form-input width-11" ng-model="ctrl.target.azureMonitor.aggregation" ng-options="f as f for f in ctrl.target.azureMonitor.aggOptions"
<select class="gf-form-input width-11" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].aggregation" ng-options="f as f for f in ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].aggOptions"
ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Time Grain</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper">
<select class="gf-form-input" ng-model="ctrl.target.azureMonitor.timeGrain" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.timeGrains"
<select class="gf-form-input" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].timeGrain" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].timeGrains"
ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.target.azureMonitor.timeGrain.trim() === 'auto'">
<div class="gf-form" ng-show="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].timeGrain.trim() === 'auto'">
<label class="gf-form-label">Auto Interval</label>
<label class="gf-form-label">{{ctrl.getAutoInterval()}}</label>
</div>
......@@ -78,17 +146,17 @@
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-show="ctrl.target.azureMonitor.dimensions.length > 0">
<div class="gf-form-inline" ng-show="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimensions.length > 0">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Dimension</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select class="gf-form-input min-width-12" ng-model="ctrl.target.azureMonitor.dimension" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.dimensions"
<select class="gf-form-input min-width-12" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimension" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimensions"
ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-3">eq</label>
<input type="text" class="gf-form-input width-17" ng-model="ctrl.target.azureMonitor.dimensionFilter"
<input type="text" class="gf-form-input width-17" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimensionFilter"
spellcheck="false" placeholder="auto" ng-blur="ctrl.refresh()">
</div>
<div class="gf-form gf-form--grow">
......@@ -98,7 +166,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Legend Format</label>
<input type="text" class="gf-form-input width-30" ng-model="ctrl.target.azureMonitor.alias" spellcheck="false"
<input type="text" class="gf-form-input width-30" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].alias" spellcheck="false"
placeholder="alias patterns (see help for more info)" ng-blur="ctrl.refresh()">
</div>
......
......@@ -36,11 +36,11 @@ describe('AzureMonitorQueryCtrl', () => {
});
it('should set query parts to select', () => {
expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select');
expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select');
expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
expect(queryCtrl.target.azureMonitor.data.singleResource.resourceGroup).toBe('select');
expect(queryCtrl.target.azureMonitor.data.singleResource.metricDefinition).toBe('select');
expect(queryCtrl.target.azureMonitor.data.singleResource.resourceName).toBe('select');
expect(queryCtrl.target.azureMonitor.data.singleResource.metricNamespace).toBe('select');
expect(queryCtrl.target.azureMonitor.data.singleResource.metricName).toBe('select');
expect(queryCtrl.target.appInsights.groupBy).toBe('none');
});
});
......@@ -76,7 +76,7 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.datasource.getMetricDefinitions = function(subscriptionId: any, query: any) {
expect(subscriptionId).toBe('sub1');
expect(query).toBe('test');
......@@ -94,7 +94,7 @@ describe('AzureMonitorQueryCtrl', () => {
describe('and resource group has no value', () => {
beforeEach(() => {
queryCtrl.target.azureMonitor.resourceGroup = 'select';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
});
it('should return without making a call to datasource', () => {
......@@ -109,8 +109,8 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.datasource.getResourceNames = function(
subscriptionId: any,
resourceGroup: any,
......@@ -133,8 +133,8 @@ describe('AzureMonitorQueryCtrl', () => {
describe('and resourceGroup and metricDefinition do not have values', () => {
beforeEach(() => {
queryCtrl.target.azureMonitor.resourceGroup = 'select';
queryCtrl.target.azureMonitor.metricDefinition = 'select';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'select';
});
it('should return without making a call to datasource', () => {
......@@ -149,10 +149,10 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.resourceName = 'test';
queryCtrl.target.azureMonitor.metricNamespace = 'test';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'test';
queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'test';
queryCtrl.datasource.getMetricNames = function(
subscriptionId: any,
resourceGroup: any,
......@@ -179,10 +179,10 @@ describe('AzureMonitorQueryCtrl', () => {
describe('and resourceGroup, metricDefinition, resourceName and metricNamespace do not have values', () => {
beforeEach(() => {
queryCtrl.target.azureMonitor.resourceGroup = 'select';
queryCtrl.target.azureMonitor.metricDefinition = 'select';
queryCtrl.target.azureMonitor.resourceName = 'select';
queryCtrl.target.azureMonitor.metricNamespace = 'select';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'select';
queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'select';
queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'select';
});
it('should return without making a call to datasource', () => {
......@@ -201,18 +201,18 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => {
queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.resourceName = 'test';
queryCtrl.target.azureMonitor.metricNamespace = 'test';
queryCtrl.target.azureMonitor.metricName = 'Percentage CPU';
queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'test';
queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'test';
queryCtrl.target.azureMonitor.data.singleResource.metricName = 'Percentage CPU';
queryCtrl.datasource.getMetricMetadata = function(
subscription: any,
resourceGroup: any,
metricDefinition: any,
resourceName: any,
metricNamespace: any,
metricName: any
metricName: any,
metricNamespace: any
) {
expect(subscription).toBe('sub1');
expect(resourceGroup).toBe('test');
......@@ -226,9 +226,9 @@ describe('AzureMonitorQueryCtrl', () => {
it('should set the options and default selected value for the Aggregations dropdown', () => {
queryCtrl.onMetricNameChange().then(() => {
expect(queryCtrl.target.azureMonitor.aggregation).toBe('Average');
expect(queryCtrl.target.azureMonitor.aggOptions).toBe(['Average', 'Total']);
expect(queryCtrl.target.azureMonitor.timeGrains).toBe(['PT1M', 'P1D']);
expect(queryCtrl.target.azureMonitor.data.singleResource.aggregation).toBe('Average');
expect(queryCtrl.target.azureMonitor.data.singleResource.aggOptions).toBe(['Average', 'Total']);
expect(queryCtrl.target.azureMonitor.data.singleResource.timeGrains).toBe(['PT1M', 'P1D']);
});
});
});
......
import _ from 'lodash';
import { QueryCtrl } from 'app/plugins/sdk';
// import './css/query_editor.css';
import TimegrainConverter from './time_grain_converter';
import './editor/editor_component';
import kbn from 'app/core/utils/kbn';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { auto } from 'angular';
import { DataFrame } from '@grafana/data';
import { Resource } from './types';
import { migrateTargetSchema } from './migrations';
import TimegrainConverter from './time_grain_converter';
import './editor/editor_component';
import './multi-select.directive';
export interface ResultFormat {
text: string;
value: string;
}
interface AzureMonitor {
resourceGroup: string;
resourceGroups: string[];
resourceName: string;
metricDefinition: string;
metricNamespace: string;
metricName: string;
dimensionFilter: string;
timeGrain: string;
timeGrainUnit: string;
timeGrains: Option[];
allowedTimeGrainsMs: number[];
dimensions: any[];
dimension: any;
aggregation: string;
aggOptions: string[];
locations: string[];
queryMode: string;
}
interface Option {
value: string;
text: string;
displayName?: string;
}
export class AzureMonitorQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
static defaultQueryMode = 'singleResource';
defaultDropdownValue = 'select';
......@@ -23,21 +53,10 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
refId: string;
queryType: string;
subscription: string;
subscriptions: string[];
azureMonitor: {
resourceGroup: string;
resourceName: string;
metricDefinition: string;
metricNamespace: string;
metricName: string;
dimensionFilter: string;
timeGrain: string;
timeGrainUnit: string;
timeGrains: Array<{ text: string; value: string }>;
allowedTimeGrainsMs: number[];
dimensions: any[];
dimension: any;
aggregation: string;
aggOptions: string[];
queryMode: string;
data: { [queryMode: string]: AzureMonitor };
};
azureLogAnalytics: {
query: string;
......@@ -63,14 +82,30 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
defaults = {
queryType: 'Azure Monitor',
subscriptions: new Array<string>(),
azureMonitor: {
resourceGroup: this.defaultDropdownValue,
metricDefinition: this.defaultDropdownValue,
resourceName: this.defaultDropdownValue,
metricNamespace: this.defaultDropdownValue,
metricName: this.defaultDropdownValue,
dimensionFilter: '*',
timeGrain: 'auto',
queryMode: 'singleResource',
data: {
singleResource: {
resourceGroups: new Array<string>(),
resourceGroup: this.defaultDropdownValue,
metricDefinition: this.defaultDropdownValue,
metricNamespace: this.defaultDropdownValue,
metricName: this.defaultDropdownValue,
resourceName: this.defaultDropdownValue,
dimensionFilter: '*',
timeGrain: 'auto',
},
crossResource: {
resourceGroups: new Array<string>(),
locations: new Array<string>(),
metricDefinition: this.defaultDropdownValue,
resourceName: this.defaultDropdownValue,
metricName: this.defaultDropdownValue,
dimensionFilter: '*',
timeGrain: 'auto',
},
},
},
azureLogAnalytics: {
query: [
......@@ -108,12 +143,17 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
showLastQuery: boolean;
lastQuery: string;
lastQueryError?: string;
subscriptions: Array<{ text: string; value: string }>;
subscriptions: Option[];
subscriptionValues: string[];
resources: Resource[];
locations: Option[];
resourceGroups: Option[];
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, private templateSrv: TemplateSrv) {
super($scope, $injector);
this.target = migrateTargetSchema(this.target);
_.defaultsDeep(this.target, this.defaults);
this.migrateTimeGrains();
......@@ -125,12 +165,35 @@ 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();
this.resources = new Array<Resource>();
this.subscriptionValues = [];
this.init();
if (this.target.queryType === 'Azure Log Analytics') {
this.getWorkspaces();
}
}
async init() {
const subscriptions = await this.getSubscriptions();
this.datasource.getResources(subscriptions.map((s: Option) => s.value)).then(async (resources: Resource[]) => {
if (!this.target.subscriptions.length) {
this.target.subscriptions = this.subscriptions.map(s => s.value);
}
this.resources = resources;
this.updateLocations();
this.updateCrossResourceGroups();
});
}
updateLocations() {
this.locations = this.getLocations().map(l => ({ text: l, value: l }));
}
updateCrossResourceGroups() {
this.resourceGroups = this.getCrossResourceGroups().map(rg => ({ text: rg, value: rg }));
}
onDataReceived(dataList: DataFrame[]) {
this.lastQueryError = undefined;
this.lastQuery = '';
......@@ -170,24 +233,28 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}
migrateTimeGrains() {
if (this.target.azureMonitor.timeGrainUnit) {
if (this.target.azureMonitor.timeGrain !== 'auto') {
this.target.azureMonitor.timeGrain = TimegrainConverter.createISO8601Duration(
this.target.azureMonitor.timeGrain,
this.target.azureMonitor.timeGrainUnit
const { queryMode } = this.target.azureMonitor;
if (this.target.azureMonitor.data[queryMode].timeGrainUnit) {
if (this.target.azureMonitor.data[queryMode].timeGrain !== 'auto') {
this.target.azureMonitor.data[queryMode].timeGrain = TimegrainConverter.createISO8601Duration(
this.target.azureMonitor.data[queryMode].timeGrain,
this.target.azureMonitor.data[queryMode].timeGrainUnit
);
}
delete this.target.azureMonitor.timeGrainUnit;
delete this.target.azureMonitor.data[queryMode].timeGrainUnit;
this.onMetricNameChange();
}
if (
this.target.azureMonitor.timeGrains &&
this.target.azureMonitor.timeGrains.length > 0 &&
(!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0)
this.target.azureMonitor.data[queryMode].timeGrains &&
this.target.azureMonitor.data[queryMode].timeGrains.length > 0 &&
(!this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs ||
this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs.length === 0)
) {
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.azureMonitor.timeGrains);
this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs = this.convertTimeGrainsToMs(
this.target.azureMonitor.data[queryMode].timeGrains
);
}
}
......@@ -197,15 +264,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}
async migrateToDefaultNamespace() {
const { queryMode } = this.target.azureMonitor;
if (
this.target.azureMonitor.metricNamespace &&
this.target.azureMonitor.metricNamespace !== this.defaultDropdownValue &&
this.target.azureMonitor.metricDefinition
this.target.azureMonitor.data[queryMode].metricNamespace &&
this.target.azureMonitor.data[queryMode].metricNamespace !== this.defaultDropdownValue &&
this.target.azureMonitor.data[queryMode].metricDefinition
) {
return;
}
this.target.azureMonitor.metricNamespace = this.target.azureMonitor.metricDefinition;
this.target.azureMonitor.data[queryMode].metricNamespace = this.target.azureMonitor.data[
queryMode
].metricDefinition;
}
replace(variable: string) {
......@@ -218,13 +288,19 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}
}
getSubscriptions() {
async getSubscriptions() {
if (!this.datasource.azureMonitorDatasource.isConfigured()) {
return;
}
return this.datasource.azureMonitorDatasource.getSubscriptions().then((subs: any) => {
this.subscriptions = subs;
this.subscriptionValues = subs.map((s: Option) => ({ value: s.value, text: s.displayName }));
if (!this.target.subscriptions.length) {
this.target.subscriptions = subs.map((s: Option) => s.value);
}
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') {
......@@ -244,16 +320,36 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return this.getWorkspaces();
}
const { queryMode } = this.target.azureMonitor;
if (this.target.queryType === 'Azure Monitor') {
this.target.azureMonitor.resourceGroup = this.defaultDropdownValue;
this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = [];
this.target.azureMonitor.dimension = '';
this.target.azureMonitor.data[queryMode].resourceGroup = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricDefinition = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
}
}
async onSubscriptionsChange(values: any) {
if (!_.isEqual(this.target.subscriptions.sort(), values.sort())) {
this.target.subscriptions = values;
this.resources = await this.datasource.getResources(this.target.subscriptions);
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].resourceGroup = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricDefinition = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
this.updateLocations();
this.updateCrossResourceGroups();
}
}
......@@ -270,29 +366,70 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
.catch(this.handleQueryCtrlError.bind(this));
}
getCrossResourceGroups() {
if (this.target.queryType !== 'Azure Monitor' || !this.datasource.azureMonitorDatasource.isConfigured()) {
return [];
}
return this.resources
.filter(({ location, subscriptionId }) => {
if (this.target.azureMonitor.data.crossResource.locations.length) {
return (
this.target.azureMonitor.data.crossResource.locations.includes(location) &&
this.target.subscriptions.includes(subscriptionId)
);
}
return this.target.subscriptions.includes(subscriptionId);
})
.reduce((options, { group }: Resource) => (options.some(o => o === group) ? options : [...options, group]), []);
}
async getCrossResourceMetricDefinitions(query: any) {
const { locations, resourceGroups } = this.target.azureMonitor.data.crossResource;
return this.resources
.filter(({ location, group }) => locations.includes(location) && resourceGroups.includes(group))
.reduce(
(options: Option[], { type }: Resource) =>
options.some(o => o.value === type) ? options : [...options, { text: type, value: type }],
[]
);
}
getLocations() {
return this.resources
.filter(({ subscriptionId }) => this.target.subscriptions.includes(subscriptionId))
.reduce(
(options: string[], { location }: Resource) =>
options.some(o => o === location) ? options : [...options, location],
[]
);
}
getMetricDefinitions(query: any) {
const { queryMode } = this.target.azureMonitor;
if (
this.target.queryType !== 'Azure Monitor' ||
!this.target.azureMonitor.resourceGroup ||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue
!this.target.azureMonitor.data[queryMode].resourceGroup ||
this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue
) {
return;
}
return this.datasource
.getMetricDefinitions(
this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
this.replace(this.target.azureMonitor.resourceGroup)
this.replace(this.target.azureMonitor.data[queryMode].resourceGroup)
)
.catch(this.handleQueryCtrlError.bind(this));
}
getResourceNames(query: any) {
const { queryMode } = this.target.azureMonitor;
if (
this.target.queryType !== 'Azure Monitor' ||
!this.target.azureMonitor.resourceGroup ||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
!this.target.azureMonitor.metricDefinition ||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue
!this.target.azureMonitor.data[queryMode].resourceGroup ||
this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue ||
!this.target.azureMonitor.data[queryMode].metricDefinition ||
this.target.azureMonitor.data[queryMode].metricDefinition === this.defaultDropdownValue
) {
return;
}
......@@ -300,21 +437,22 @@ 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)
this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
this.replace(this.target.azureMonitor.data[queryMode].metricDefinition)
)
.catch(this.handleQueryCtrlError.bind(this));
}
getMetricNamespaces() {
const { queryMode } = this.target.azureMonitor;
if (
this.target.queryType !== 'Azure Monitor' ||
!this.target.azureMonitor.resourceGroup ||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
!this.target.azureMonitor.metricDefinition ||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
!this.target.azureMonitor.resourceName ||
this.target.azureMonitor.resourceName === this.defaultDropdownValue
!this.target.azureMonitor.data[queryMode].resourceGroup ||
this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue ||
!this.target.azureMonitor.data[queryMode].metricDefinition ||
this.target.azureMonitor.data[queryMode].metricDefinition === this.defaultDropdownValue ||
!this.target.azureMonitor.data[queryMode].resourceName ||
this.target.azureMonitor.data[queryMode].resourceName === this.defaultDropdownValue
) {
return;
}
......@@ -322,24 +460,50 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return this.datasource
.getMetricNamespaces(
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)
this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
this.replace(this.target.azureMonitor.data[queryMode].metricDefinition),
this.replace(this.target.azureMonitor.data[queryMode].resourceName)
)
.catch(this.handleQueryCtrlError.bind(this));
}
async getCrossResourceMetricNames() {
const { locations, resourceGroups, metricDefinition } = this.target.azureMonitor.data.crossResource;
const resources = this.resources.filter(
({ type, location, name, group }) =>
resourceGroups.includes(group) && type === metricDefinition && locations.includes(location)
);
const uniqueResources = _.uniqBy(resources, ({ subscriptionId, name, type, group }: Resource) =>
[subscriptionId, name, locations, group].join()
);
const responses = await Promise.all(
uniqueResources.map(({ subscriptionId, group, type, name }) =>
this.datasource
.getMetricNames(subscriptionId, group, type, name)
.then((metrics: any) => metrics.map((m: any) => ({ ...m, subscriptionIds: [subscriptionId] })), [
{ text: this.defaultDropdownValue, value: this.defaultDropdownValue },
])
)
);
return _.uniqBy(responses.reduce((result, resources) => [...result, ...resources], []), ({ value }) => value);
}
getMetricNames() {
const { queryMode } = this.target.azureMonitor;
if (
this.target.queryType !== 'Azure Monitor' ||
!this.target.azureMonitor.resourceGroup ||
this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
!this.target.azureMonitor.metricDefinition ||
this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
!this.target.azureMonitor.resourceName ||
this.target.azureMonitor.resourceName === this.defaultDropdownValue ||
!this.target.azureMonitor.metricNamespace ||
this.target.azureMonitor.metricNamespace === this.defaultDropdownValue
!this.target.azureMonitor.data[queryMode].resourceGroup ||
this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue ||
!this.target.azureMonitor.data[queryMode].metricDefinition ||
this.target.azureMonitor.data[queryMode].metricDefinition === this.defaultDropdownValue ||
!this.target.azureMonitor.data[queryMode].resourceName ||
this.target.azureMonitor.data[queryMode].resourceName === this.defaultDropdownValue ||
!this.target.azureMonitor.data[queryMode].metricNamespace ||
this.target.azureMonitor.data[queryMode].metricNamespace === this.defaultDropdownValue
) {
return;
}
......@@ -347,87 +511,168 @@ 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),
this.replace(this.target.azureMonitor.metricNamespace)
this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
this.replace(this.target.azureMonitor.data[queryMode].metricDefinition),
this.replace(this.target.azureMonitor.data[queryMode].resourceName),
this.replace(this.target.azureMonitor.data[queryMode].metricNamespace)
)
.catch(this.handleQueryCtrlError.bind(this));
}
onResourceGroupChange() {
this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = [];
this.target.azureMonitor.dimension = '';
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].metricDefinition = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
this.refresh();
}
onCrossResourceGroupChange(values: string[]) {
if (!_.isEqual(this.target.azureMonitor.data.crossResource.resourceGroups.sort(), values.sort())) {
this.target.azureMonitor.data.crossResource.resourceGroups = values;
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].metricDefinition = '';
this.target.azureMonitor.data[queryMode].metricName = '';
this.refresh();
}
}
onCrossResourceMetricDefinitionChange() {
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
this.refresh();
}
async onLocationsChange(values: string[]) {
if (!_.isEqual(this.target.azureMonitor.data.crossResource.locations.sort(), values.sort())) {
this.target.azureMonitor.data.crossResource.locations = values;
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].metricDefinition = '';
this.target.azureMonitor.data[queryMode].resourceGroup = '';
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
this.updateCrossResourceGroups();
this.refresh();
}
}
onMetricDefinitionChange() {
this.target.azureMonitor.resourceName = this.defaultDropdownValue;
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = [];
this.target.azureMonitor.dimension = '';
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
}
onResourceNameChange() {
this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.aggregation = '';
this.target.azureMonitor.timeGrains = [];
this.target.azureMonitor.timeGrain = '';
this.target.azureMonitor.dimensions = [];
this.target.azureMonitor.dimension = '';
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].metricNamespace = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].aggregation = '';
this.target.azureMonitor.data[queryMode].timeGrains = [];
this.target.azureMonitor.data[queryMode].timeGrain = '';
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
this.refresh();
}
onMetricNamespacesChange() {
this.target.azureMonitor.metricName = this.defaultDropdownValue;
this.target.azureMonitor.dimensions = [];
this.target.azureMonitor.dimension = '';
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
this.target.azureMonitor.data[queryMode].dimensions = [];
this.target.azureMonitor.data[queryMode].dimension = '';
}
setMetricMetadata(metadata: any) {
const { queryMode } = this.target.azureMonitor;
this.target.azureMonitor.data[queryMode].aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
this.target.azureMonitor.data[queryMode].aggregation = metadata.primaryAggType;
this.target.azureMonitor.data[queryMode].timeGrains = [{ text: 'auto', value: 'auto' }].concat(
metadata.supportedTimeGrains
);
this.target.azureMonitor.data[queryMode].timeGrain = 'auto';
this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs = this.convertTimeGrainsToMs(
metadata.supportedTimeGrains || []
);
this.target.azureMonitor.data[queryMode].dimensions = metadata.dimensions;
if (metadata.dimensions.length > 0) {
this.target.azureMonitor.data[queryMode].dimension = metadata.dimensions[0].value;
}
return this.refresh();
}
onMetricNameChange() {
if (!this.target.azureMonitor.metricName || this.target.azureMonitor.metricName === this.defaultDropdownValue) {
onCrossResourceMetricNameChange() {
const { queryMode } = this.target.azureMonitor;
if (
!this.target.azureMonitor.data[queryMode].metricName ||
this.target.azureMonitor.data[queryMode].metricName === this.defaultDropdownValue
) {
return;
}
const { resourceGroups, metricDefinition, metricName } = this.target.azureMonitor.data[queryMode];
const resource = this.resources.find(
({ type, group }) => type === metricDefinition && resourceGroups.includes(group)
);
return this.datasource
.getMetricMetadata(
this.replace(this.target.subscription),
this.replace(this.target.azureMonitor.resourceGroup),
this.replace(this.target.azureMonitor.metricDefinition),
this.replace(this.target.azureMonitor.resourceName),
this.replace(this.target.azureMonitor.metricNamespace),
this.replace(this.target.azureMonitor.metricName)
this.replace(this.target.subscriptions[0]),
resource.group,
metricDefinition,
resource.name,
metricName
)
.then((metadata: any) => {
this.target.azureMonitor.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
this.target.azureMonitor.aggregation = metadata.primaryAggType;
this.target.azureMonitor.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
this.target.azureMonitor.timeGrain = 'auto';
.then(this.setMetricMetadata.bind(this))
.then(() => this.refresh())
.catch(this.handleQueryCtrlError.bind(this));
}
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []);
onMetricNameChange() {
const { queryMode } = this.target.azureMonitor;
if (
!this.target.azureMonitor.data[queryMode].metricName ||
this.target.azureMonitor.data[queryMode].metricName === this.defaultDropdownValue
) {
return;
}
this.target.azureMonitor.dimensions = metadata.dimensions;
if (metadata.dimensions.length > 0) {
this.target.azureMonitor.dimension = metadata.dimensions[0].value;
}
return this.refresh();
})
return this.datasource
.getMetricMetadata(
this.replace(this.target.subscription),
this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
this.replace(this.target.azureMonitor.data[queryMode].metricDefinition),
this.replace(this.target.azureMonitor.data[queryMode].resourceName),
this.replace(this.target.azureMonitor.data[queryMode].metricName),
this.replace(this.target.azureMonitor.data[queryMode].metricNamespace)
)
.then(this.setMetricMetadata.bind(this))
.catch(this.handleQueryCtrlError.bind(this));
}
convertTimeGrainsToMs(timeGrains: Array<{ text: string; value: string }>) {
convertTimeGrainsToMs(timeGrains: Option[]) {
const allowedTimeGrainsMs: number[] = [];
timeGrains.forEach((tg: any) => {
if (tg.value !== 'auto') {
......@@ -438,10 +683,11 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}
getAutoInterval() {
if (this.target.azureMonitor.timeGrain === 'auto') {
const { queryMode } = this.target.azureMonitor;
if (this.target.azureMonitor.data[queryMode].timeGrain === 'auto') {
return TimegrainConverter.findClosestTimeGrain(
this.templateSrv.getBuiltInIntervalValue(),
_.map(this.target.azureMonitor.timeGrains, o =>
_.map(this.target.azureMonitor.data[queryMode].timeGrains, o =>
TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)
) || ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d']
);
......
......@@ -3,6 +3,7 @@ import { DataQuery, DataSourceJsonData } from '@grafana/ui';
export interface AzureMonitorQuery extends DataQuery {
format: string;
subscription: string;
subscriptions: string[];
azureMonitor: AzureMetricQuery;
azureLogAnalytics: AzureLogsQuery;
// appInsights: any;
......@@ -26,9 +27,9 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData {
// App Insights
appInsightsAppId?: string;
}
export interface AzureMetricQuery {
export interface AzureMonitorQueryData {
resourceGroup: string;
resourceGroups: string[];
resourceName: string;
metricDefinition: string;
metricNamespace: string;
......@@ -41,6 +42,12 @@ export interface AzureMetricQuery {
dimension: string;
dimensionFilter: string;
alias: string;
locations: string[];
}
export interface AzureMetricQuery extends AzureMonitorQueryData {
queryMode: string;
data: { [queryMode: string]: AzureMonitorQueryData };
}
export interface AzureLogsQuery {
......@@ -67,6 +74,24 @@ export interface AzureMonitorResourceGroupsResponse {
statusText: string;
}
export interface Resource {
id: string;
name: string;
type: string;
location: string;
kind: string;
subscriptionId: string;
group: string;
}
export interface AzureMonitorResourceResponse {
data: {
value: Resource[];
status: number;
statusText: string;
};
}
// Azure Log Analytics types
export interface KustoSchema {
Databases: { [key: string]: KustoDatabase };
......
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