Commit 8211f7d0 by Daniel Lee

stackdriver: add templating support for metric, filter and group by

Still have to figure out if we should have templating for aggregation
fields
parent 8d1f2936
......@@ -254,6 +254,24 @@ func TestStackdriver(t *testing.T) {
So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-us-east-1 us-east1-b")
})
})
Convey("when data from query with no aggregation and alias by", func() {
data, err := loadTestFile("./test-data/2-series-response-no-agg.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metric.label.instance_name}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
Convey("Should use alias by formatting and only show instance name", func() {
So(len(res.Series), ShouldEqual, 3)
So(res.Series[0].Name, ShouldEqual, "collector-asia-east-1")
So(res.Series[1].Name, ShouldEqual, "collector-europe-west-1")
So(res.Series[2].Name, ShouldEqual, "collector-us-east-1")
})
})
})
})
}
......
......@@ -10,6 +10,7 @@ type StackdriverQuery struct {
Params url.Values
RefID string
GroupBys []string
AliasBy string
}
type StackdriverResponse struct {
......
......@@ -5,7 +5,7 @@ export default class StackdriverDatasource {
baseUrl: string;
projectName: string;
constructor(instanceSettings, private backendSrv) {
constructor(instanceSettings, private backendSrv, private templateSrv) {
this.baseUrl = `/stackdriver/`;
this.url = instanceSettings.url;
this.doRequest = this.doRequest;
......@@ -22,21 +22,22 @@ export default class StackdriverDatasource {
if (!t.hasOwnProperty('aggregation')) {
t.aggregation = {
crossSeriesReducer: 'REDUCE_MEAN',
secondaryCrossSeriesReducer: 'REDUCE_NONE',
groupBys: [],
};
}
return {
refId: t.refId,
datasourceId: this.id,
metricType: t.metricType,
primaryAggregation: t.aggregation.crossSeriesReducer,
secondaryAggregation: t.aggregation.secondaryCrossSeriesReducer,
perSeriesAligner: t.aggregation.perSeriesAligner,
alignmentPeriod: t.aggregation.alignmentPeriod,
groupBys: t.aggregation.groupBys,
metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
primaryAggregation: this.templateSrv.replace(t.aggregation.crossSeriesReducer, options.scopedVars || {}),
perSeriesAligner: this.templateSrv.replace(t.aggregation.perSeriesAligner, options.scopedVars || {}),
alignmentPeriod: this.templateSrv.replace(t.aggregation.alignmentPeriod, options.scopedVars || {}),
groupBys: this.interpolateGroupBys(t.aggregation.groupBys, options.scopedVars),
view: t.view || 'FULL',
filters: t.filters,
filters: (t.filters || []).map(f => {
return this.templateSrv.replace(f, options.scopedVars || {});
}),
aliasBy: this.templateSrv.replace(t.aliasBy, options.scopedVars || {}),
};
});
......@@ -52,6 +53,19 @@ export default class StackdriverDatasource {
return data;
}
interpolateGroupBys(groupBys: string[], scopedVars): string[] {
let interpolatedGroupBys = [];
(groupBys || []).forEach(gb => {
const interpolated = this.templateSrv.replace(gb, scopedVars || {}, 'csv').split(',');
if (Array.isArray(interpolated)) {
interpolatedGroupBys = interpolatedGroupBys.concat(interpolated);
} else {
interpolatedGroupBys.push(interpolated);
}
});
return interpolatedGroupBys;
}
async query(options) {
const result = [];
const data = await this.getTimeSeries(options);
......
......@@ -51,17 +51,6 @@
</div>
<div class="gf-form-group" ng-if="ctrl.target.showAggregationOptions">
<div class="gf-form offset-width-9">
<label class="gf-form-label query-keyword width-12">Secondary Aggregation</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select class="gf-form-input width-14" ng-model="ctrl.target.aggregation.secondaryCrossSeriesReducer" ng-options="f.value as f.text for f in ctrl.stackdriverConstants.aggOptions"
ng-change="ctrl.refresh()"></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 offset-width-9">
<label class="gf-form-label query-keyword width-12">Aligner</label>
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
<select class="gf-form-input width-14" ng-model="ctrl.target.aggregation.perSeriesAligner" ng-options="f.value as f.text for f in ctrl.stackdriverConstants.alignOptions"
......@@ -86,6 +75,15 @@
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label query-keyword width-9">Alias By</span>
<input type="text" class="gf-form-input width-12" ng-model="ctrl.target.aliasBy" />
</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">
<span class="gf-form-label width-9">Project</span>
<input class="gf-form-input" disabled type="text" ng-model='ctrl.target.project.name' get-options="ctrl.getProjects()" css-class="min-width-12"
/>
......@@ -120,4 +118,4 @@ Help text for aliasing
<div class="gf-form" ng-show="ctrl.lastQueryError">
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
</div>
</query-editor-row>
\ No newline at end of file
</query-editor-row>
......@@ -22,12 +22,12 @@ export class StackdriverQueryCtrl extends QueryCtrl {
refId: string;
aggregation: {
crossSeriesReducer: string;
secondaryCrossSeriesReducer: string;
alignmentPeriod: string;
perSeriesAligner: string;
groupBys: string[];
};
filters: string[];
aliasBy: string;
};
defaultDropdownValue = 'select metric';
defaultFilterValue = 'select value';
......@@ -44,13 +44,13 @@ export class StackdriverQueryCtrl extends QueryCtrl {
metricType: this.defaultDropdownValue,
aggregation: {
crossSeriesReducer: 'REDUCE_MEAN',
secondaryCrossSeriesReducer: 'REDUCE_NONE',
alignmentPeriod: 'auto',
perSeriesAligner: 'ALIGN_MEAN',
groupBys: [],
},
filters: [],
showAggregationOptions: false,
aliasBy: '',
};
groupBySegments: any[];
......@@ -64,7 +64,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
resourceLabels: { [key: string]: string[] };
/** @ngInject */
constructor($scope, $injector, private uiSegmentSrv, private timeSrv) {
constructor($scope, $injector, private uiSegmentSrv, private timeSrv, private templateSrv) {
super($scope, $injector);
_.defaultsDeep(this.target, this.defaults);
......@@ -154,7 +154,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
{
refId: this.target.refId,
datasourceId: this.datasource.id,
metricType: this.target.metricType,
metricType: this.templateSrv.replace(this.target.metricType),
aggregation: {
crossSeriesReducer: 'REDUCE_NONE',
},
......@@ -261,7 +261,11 @@ export class StackdriverQueryCtrl extends QueryCtrl {
}
if (segment.type === 'value') {
const filterKey = this.filterSegments[index - 2].value;
const filterKey = this.templateSrv.replace(this.filterSegments[index - 2].value);
if (!filterKey || !this.metricLabels || Object.keys(this.metricLabels).length === 0) {
return [];
}
const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7);
if (filterKey.startsWith('metric.label.') && this.metricLabels.hasOwnProperty(shortKey)) {
......
import StackdriverDataSource from '../datasource';
import { metricDescriptors } from './testData';
import moment from 'moment';
import { TemplateSrvStub } from 'test/specs/helpers';
describe('StackdriverDataSource', () => {
const instanceSettings = {
......@@ -8,6 +9,8 @@ describe('StackdriverDataSource', () => {
projectName: 'testproject',
},
};
const templateSrv = new TemplateSrvStub();
describe('when performing testDataSource', () => {
describe('and call to stackdriver api succeeds', () => {
let ds;
......@@ -18,7 +21,7 @@ describe('StackdriverDataSource', () => {
return Promise.resolve({ status: 200 });
},
};
ds = new StackdriverDataSource(instanceSettings, backendSrv);
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv);
result = await ds.testDatasource();
});
it('should return successfully', () => {
......@@ -33,7 +36,7 @@ describe('StackdriverDataSource', () => {
const backendSrv = {
datasourceRequest: async () => Promise.resolve({ status: 200, data: metricDescriptors }),
};
ds = new StackdriverDataSource(instanceSettings, backendSrv);
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv);
result = await ds.testDatasource();
});
it('should return status success', () => {
......@@ -52,7 +55,7 @@ describe('StackdriverDataSource', () => {
data: { error: { code: 400, message: 'Field interval.endTime had an invalid value' } },
}),
};
ds = new StackdriverDataSource(instanceSettings, backendSrv);
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv);
result = await ds.testDatasource();
});
......@@ -88,7 +91,7 @@ describe('StackdriverDataSource', () => {
return Promise.resolve({ status: 200, data: response });
},
};
ds = new StackdriverDataSource(instanceSettings, backendSrv);
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv);
result = await ds.getProjects();
});
......@@ -137,7 +140,7 @@ describe('StackdriverDataSource', () => {
const backendSrv = {
datasourceRequest: async () => Promise.resolve({ status: 200, data: response }),
};
ds = new StackdriverDataSource(instanceSettings, backendSrv);
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv);
});
it('should return a list of datapoints', () => {
......@@ -171,7 +174,7 @@ describe('StackdriverDataSource', () => {
});
},
};
ds = new StackdriverDataSource(instanceSettings, backendSrv);
ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv);
result = await ds.getMetricTypes();
});
it('should return successfully', () => {
......@@ -180,4 +183,39 @@ describe('StackdriverDataSource', () => {
expect(result[0].name).toBe('test metric name 1');
});
});
describe('when interpolating a template variable for group bys', () => {
let interpolated;
describe('and is single value variable', () => {
beforeEach(() => {
templateSrv.data = {
test: 'groupby1',
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
it('should replace the variable with the value', () => {
expect(interpolated.length).toBe(1);
expect(interpolated[0]).toBe('groupby1');
});
});
describe('and is multi value variable', () => {
beforeEach(() => {
templateSrv.data = {
test: 'groupby1,groupby2',
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
it('should replace the variable with an array of group bys', () => {
expect(interpolated.length).toBe(2);
expect(interpolated[0]).toBe('groupby1');
expect(interpolated[1]).toBe('groupby2');
});
});
});
});
import { StackdriverQueryCtrl } from '../query_ctrl';
import { TemplateSrvStub } from 'test/specs/helpers';
describe('StackdriverQueryCtrl', () => {
let ctrl;
......@@ -388,7 +389,7 @@ function createCtrlWithFakes(existingFilters?: string[]) {
return { type: 'condition', value: val };
},
};
return new StackdriverQueryCtrl(null, null, fakeSegmentServer, null);
return new StackdriverQueryCtrl(null, null, fakeSegmentServer, null, new TemplateSrvStub());
}
function createTarget(existingFilters?: string[]) {
......@@ -401,11 +402,11 @@ function createTarget(existingFilters?: string[]) {
refId: 'A',
aggregation: {
crossSeriesReducer: '',
secondaryCrossSeriesReducer: '',
alignmentPeriod: '',
perSeriesAligner: '',
groupBys: [],
},
filters: existingFilters || [],
aliasBy: '',
};
}
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