Azure: Application Insights metrics to Frame and support multiple query dimensions (#25849)

- The Application Insights Service now returns a dataframe. This is a "wide" formatted dataframe with a single time index.
- Multiple "group by" dimensions may now be selected instead of just one with Application Insights.
- Some types are copied / slightly altered from the Azure Go SDK but that SDK is not imported at this time.

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Kyle Brandt
2020-06-29 15:06:58 -04:00
committed by GitHub
parent 1a711e7df0
commit 9a8289b6d9
13 changed files with 682 additions and 379 deletions

View File

@@ -375,7 +375,7 @@ describe('AppInsightsDatasource', () => {
expect(options.url).toContain('/api/ds/query');
expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined();
expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server');
expect(options.data.queries[0].appInsights.dimension).toBe('client/city');
expect([...options.data.queries[0].appInsights.dimension]).toMatchObject(['client/city']);
return Promise.resolve({ data: response, status: 200 });
});
});

View File

@@ -1,7 +1,7 @@
import { ScopedVars } from '@grafana/data';
import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
import _ from 'lodash';
import _, { isString } from 'lodash';
import TimegrainConverter from '../time_grain_converter';
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from '../types';
@@ -84,12 +84,24 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
// migration for non-standard names
if (old.groupBy && !item.dimension) {
item.dimension = old.groupBy;
item.dimension = [old.groupBy];
}
if (old.filter && !item.dimensionFilter) {
item.dimensionFilter = old.filter;
}
// Migrate single dimension string to array
if (isString(item.dimension)) {
if (item.dimension === 'None') {
item.dimension = [];
} else {
item.dimension = [item.dimension as string];
}
}
if (!item.dimension) {
item.dimension = [];
}
const templateSrv = getTemplateSrv();
return {
@@ -102,7 +114,7 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
allowedTimeGrainsMs: item.allowedTimeGrainsMs,
metricName: templateSrv.replace(item.metricName, scopedVars),
aggregation: templateSrv.replace(item.aggregation, scopedVars),
dimension: templateSrv.replace(item.dimension, scopedVars),
dimension: item.dimension.map(d => templateSrv.replace(d, scopedVars)),
dimensionFilter: templateSrv.replace(item.dimensionFilter, scopedVars),
alias: item.alias,
format: target.format,

View File

@@ -363,27 +363,30 @@
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Group By</label>
</div>
<div ng-repeat="d in ctrl.target.appInsights.dimension track by $index"
class="gf-form"
ng-click="ctrl.removeGroupBy($index);"
onmouseover="this.style['text-decoration'] = 'line-through';"
onmouseout="this.style['text-decoration'] = '';">
<label class="gf-form-label"
style="cursor: pointer;">{{d}} <icon name="'times'"></icon></label>
</div>
<div>
<gf-form-dropdown
allow-custom="true"
ng-hide="ctrl.target.appInsights.dimension !== 'none'"
model="ctrl.target.appInsights.dimension"
lookup-text="true"
placeholder="Add"
model="ctrl.dummyDiminsionString"
get-options="ctrl.getAppInsightsGroupBySegments($query)"
on-change="ctrl.refresh()"
css-class="min-width-20"
on-change="ctrl.getAppInsightsGroupBySegments"
css-class="min-width-5"
>
</gf-form-dropdown>
<label
class="gf-form-label min-width-20 pointer"
ng-hide="ctrl.target.appInsights.dimension === 'none'"
ng-click="ctrl.resetAppInsightsGroupBy()"
>{{ctrl.target.appInsights.dimension}}
<icon name="'times'"></icon>
</label>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-9">Filter</label>
<label class="gf-form-label query-keyword">Filter</label>
<input
type="text"
class="gf-form-input width-17"

View File

@@ -40,7 +40,7 @@ describe('AzureMonitorQueryCtrl', () => {
expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
expect(queryCtrl.target.appInsights.dimension).toBe('none');
expect(queryCtrl.target.appInsights.dimension).toMatchObject([]);
});
});

View File

@@ -20,6 +20,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
defaultDropdownValue = 'select';
dummyDiminsionString = '+';
target: {
// should be: AzureMonitorQuery
refId: string;
@@ -104,7 +106,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
},
appInsights: {
metricName: this.defaultDropdownValue,
dimension: 'none',
// dimension: [],
timeGrain: 'auto',
},
insightsAnalytics: {
@@ -135,6 +137,8 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
this.migrateApplicationInsightsKeys();
this.migrateApplicationInsightsDimensions();
this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope);
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
this.resultFormats = [
@@ -270,6 +274,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
}
}
migrateApplicationInsightsDimensions() {
const { appInsights } = this.target;
if (!appInsights.dimension) {
appInsights.dimension = [];
}
if (_.isString(appInsights.dimension)) {
appInsights.dimension = [appInsights.dimension as string];
}
}
replace(variable: string) {
return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars);
}
@@ -625,8 +641,27 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
return this.datasource.appInsightsDatasource.getQuerySchema().catch(this.handleQueryCtrlError.bind(this));
};
removeGroupBy = (index: number) => {
const { appInsights } = this.target;
appInsights.dimension.splice(index, 1);
this.refresh();
};
getAppInsightsGroupBySegments(query: any) {
return _.map(this.target.appInsights.dimensions, (option: string) => {
const { appInsights } = this.target;
// HACK alert... there must be a better way!
if (this.dummyDiminsionString && this.dummyDiminsionString.length && '+' !== this.dummyDiminsionString) {
if (!appInsights.dimension) {
appInsights.dimension = [];
}
appInsights.dimension.push(this.dummyDiminsionString);
this.dummyDiminsionString = '+';
this.refresh();
}
// Return the list of dimensions stored on the query object from the last request :(
return _.map(appInsights.dimensions, (option: string) => {
return { text: option, value: option };
});
}

View File

@@ -73,7 +73,8 @@ export interface ApplicationInsightsQuery {
timeGrain: string;
allowedTimeGrainsMs: number[];
aggregation: string;
dimension: string;
dimension: string[]; // Was string before 7.1
// dimensions: string[]; why is this metadata stored on the object!
dimensionFilter: string;
alias: string;
}