diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev
new file mode 100644
index 0000000..147c670
--- /dev/null
+++ b/docker/Dockerfile.dev
@@ -0,0 +1,25 @@
+FROM centos:7
+
+ENV GRAFANA_VERSION 4.1.2-1486989747
+ENV GF_PATHS_DATA /var/lib/grafana/data
+
+# Install grafana
+RUN yum install -y https://grafanarel.s3.amazonaws.com/builds/grafana-${GRAFANA_VERSION}.x86_64.rpm && \
+ yum install -y unzip && \
+ yum clean all
+
+COPY dist /var/lib/grafana/plugins/hawkular-datasource
+
+# Fix permissions so will run on OCP under "restricted" SCC
+COPY ./run.sh /run.sh
+COPY fix-permissions /usr/bin/fix-permissions
+
+RUN chmod +x /usr/bin/fix-permissions && \
+ /usr/bin/fix-permissions /run.sh && \
+ /usr/bin/fix-permissions /var/lib/grafana && \
+ /usr/bin/fix-permissions /var/log/grafana && \
+ /usr/bin/fix-permissions /etc/grafana
+
+EXPOSE 3000
+
+ENTRYPOINT ["/run.sh"]
diff --git a/docker/instructions-dev-build.md b/docker/instructions-dev-build.md
new file mode 100644
index 0000000..0c927e5
--- /dev/null
+++ b/docker/instructions-dev-build.md
@@ -0,0 +1,27 @@
+# Building a development Docker image
+
+1. Build the plugin from repository root:
+
+```bash
+grunt
+```
+
+2. Move (or copy) the `dist` directory to `docker/` and `cd` to it.
+
+```bash
+mv dist docker && cd docker
+```
+
+3. Build the docker image from Dockerfile.dev
+
+```bash
+docker build -f Dockerfile.dev -t hawkular/hawkular-grafana-datasource:dev-build .
+```
+
+4. Test the image
+
+```bash
+docker run -i -p 3000:3000 --name hawkular-grafana-datasource --rm hawkular/hawkular-grafana-datasource:dev-build
+```
+
+And login on http://localhost:3000/
diff --git a/spec/datasource-tenant-per-query_spec.js b/spec/datasource-tenant-per-query_spec.js
index 4ddaef0..6cafb39 100644
--- a/spec/datasource-tenant-per-query_spec.js
+++ b/spec/datasource-tenant-per-query_spec.js
@@ -67,6 +67,63 @@ describe('HawkularDatasource tenant per query', () => {
}).then(v => done(), err => done(err));
});
+ it('should query raw data with templated tenant', done => {
+
+ let options = {
+ range: {
+ from: 15,
+ to: 30
+ },
+ targets: [{
+ id: 'memory',
+ type: 'gauge',
+ rate: false,
+ tenant: '$tenant'
+ }]
+ };
+ ctx.templateSrv.variables = [{
+ name: 'tenant'
+ }];
+ ctx.templateSrv.replace = (target, vars) => {
+ expect(target).to.equal('$tenant');
+ return '{t1,t2}';
+ };
+
+ let ireq = 1;
+ ctx.backendSrv.datasourceRequest = request => {
+ expectRequestWithTenant(request, 'POST', 'gauges/raw/query', 't' + ireq);
+ expect(request.data).to.deep.equal({
+ start: options.range.from,
+ end: options.range.to,
+ ids: ['memory'],
+ order: 'ASC'
+ });
+ ireq++;
+
+ return ctx.$q.when({
+ status: 200,
+ data: [{
+ id: 'memory',
+ data: [{
+ timestamp: 13,
+ value: 3 * ireq
+ }, {
+ timestamp: 19,
+ value: 4 * ireq
+ }]
+ }]
+ });
+ };
+
+ ctx.ds.query(options).then(result => {
+ expect(result.data).to.have.length(2);
+ expect(result.data[0].target).to.equal('[t1] memory');
+ expect(result.data[0].datapoints).to.deep.equal([[6, 13], [8, 19]]);
+ expect(result.data[1].target).to.equal('[t2] memory');
+ expect(result.data[1].datapoints).to.deep.equal([[9, 13], [12, 19]]);
+ }).then(v => done(), err => done(err));
+ });
+
it('should query annotations with ad-hoc tenant', done => {
let options = {
@@ -166,6 +223,70 @@ describe('HawkularDatasource tenant per query', () => {
}).then(v => done(), err => done(err));
});
+ it('should query stats with templated tenant', done => {
+ let options = {
+ range: {
+ from: 20,
+ to: 30
+ },
+ targets: [{
+ seriesAggFn: 'none',
+ stats: ['min','max'],
+ tags: [{name: 'type', value: 'memory'}],
+ type: 'gauge',
+ rate: false,
+ raw: false,
+ tenant: '$tenant'
+ }]
+ };
+ ctx.templateSrv.variables = [{
+ name: 'tenant'
+ }];
+ ctx.templateSrv.replace = (target, vars) => {
+ expect(target).to.equal('$tenant');
+ return '{t1,t2}';
+ };
+
+ let ireq = 1;
+ ctx.backendSrv.datasourceRequest = request => {
+ expectRequestWithTenant(request, 'POST', 'metrics/stats/query', 't' + ireq);
+ expect(request.data).to.deep.equal({
+ start: options.range.from,
+ end: options.range.to,
+ tags: 'type:memory',
+ buckets: 60,
+ types: ['gauge']
+ });
+ ireq++;
+
+ return ctx.$q.when({
+ status: 200,
+ data: {'gauge':
+ { 'gauge_1':
+ [{
+ start: 20,
+ end: 25,
+ min: 3 * ireq,
+ max: 4 * ireq
+ }]
+ }
+ }
+ });
+ };
+
+ ctx.ds.query(options).then(result => {
+ expect(result.data).to.have.length(4);
+ expect(result.data[0].target).to.equal('[t1] gauge_1 [max]');
+ expect(result.data[0].datapoints).to.deep.equal([[8, 20]]);
+ expect(result.data[1].target).to.equal('[t1] gauge_1 [min]');
+ expect(result.data[1].datapoints).to.deep.equal([[6, 20]]);
+ expect(result.data[2].target).to.equal('[t2] gauge_1 [max]');
+ expect(result.data[2].datapoints).to.deep.equal([[12, 20]]);
+ expect(result.data[3].target).to.equal('[t2] gauge_1 [min]');
+ expect(result.data[3].datapoints).to.deep.equal([[9, 20]]);
+ }).then(v => done(), err => done(err));
+ });
+
it('should suggest metrics with ad-hoc tenant', done => {
ctx.backendSrv.datasourceRequest = request => {
expectRequestWithTenant(request, 'GET', 'metrics?type=gauge&tags=host=cartago', 'ad-hoc');
diff --git a/src/datasource.js b/src/datasource.js
index c120af7..ea16771 100644
--- a/src/datasource.js
+++ b/src/datasource.js
@@ -28,8 +28,8 @@ export class HawkularDatasource {
};
this.variablesHelper = new VariablesHelper(templateSrv);
this.capabilitiesPromise = this.queryVersion().then(version => new Capabilities(version));
- this.queryProcessor = new QueryProcessor($q, backendSrv, this.variablesHelper, this.capabilitiesPromise, this.metricsUrl,
- this.getHeaders.bind(this), this.typeResources);
+ this.queryProcessor = new QueryProcessor($q, this.multiTenantsQuery.bind(this), this.variablesHelper, this.capabilitiesPromise, this.metricsUrl,
+ this.typeResources);
}
getHeaders(tenant) {
@@ -47,6 +47,23 @@ export class HawkularDatasource {
return headers;
}
+ multiTenantsQuery(tenants, url, params, data, method) {
+ return this.q.all(tenants.map(tenant => {
+ return this.backendSrv.datasourceRequest({
+ url: url,
+ params: params,
+ data: data,
+ method: method,
+ headers: this.getHeaders(tenant)
+ }).then(response => {
+ return {
+ tenant: tenant,
+ result: (response.status == 200) ? response.data : null
+ }
+ });
+ }));
+ }
+
query(options) {
const validTargets = options.targets
.filter(target => !target.hide)
@@ -178,6 +195,13 @@ export class HawkularDatasource {
});
}
+ getTargetTenants(target) {
+ if (target.tenant) {
+ return this.variablesHelper.resolve(target.tenant, {});
+ }
+ return [null];
+ }
+
suggestMetrics(target) {
let url = this.metricsUrl + '/metrics?type=' + target.type;
if (target.tagsQL && target.tagsQL.length > 0) {
@@ -185,41 +209,71 @@ export class HawkularDatasource {
} else if (target.tags && target.tags.length > 0) {
url += '&tags=' + tagsModelToString(target.tags, this.variablesHelper, {});
}
- return this.backendSrv.datasourceRequest({
- url: url,
- method: 'GET',
- headers: this.getHeaders(target.tenant)
- }).then(response => response.status == 200 ? response.data : [])
- .then(result => {
- return result.map(m => m.id)
- .sort()
- .map(id => {
- return {text: id, value: id};
+ const tenants = this.getTargetTenants(target);
+ return this.multiTenantsQuery(tenants, url, null, null, 'GET')
+ .then(multiTenantsData => {
+ // Eliminate possible duplicates from multi-tenancy
+ let ids = {};
+ multiTenantsData.forEach(tenantData => {
+ if (tenantData.result) {
+ tenantData.result.forEach(metric => {
+ ids[metric.id] = true;
+ });
+ }
});
- });
+ return Object.keys(ids)
+ .sort()
+ .map(id => {
+ return {text: id, value: id};
+ });
+ });
}
suggestTags(target, key) {
if (!key) {
return this.q.when([]);
}
- return this.backendSrv.datasourceRequest({
- url: `${this.metricsUrl}/${this.typeResources[target.type]}/tags/${key}:*`,
- method: 'GET',
- headers: this.getHeaders(target.tenant)
- }).then(result => result.data.hasOwnProperty(key) ? result.data[key] : [])
- .then(tags => tags.map(tag => {
- return {text: tag, value: tag};
- }));
+ const tenants = this.getTargetTenants(target);
+ const url = `${this.metricsUrl}/${this.typeResources[target.type]}/tags/${key}:*`;
+ return this.multiTenantsQuery(tenants, url, null, null, 'GET')
+ .then(multiTenantsData => {
+ // Eliminate possible duplicates from multi-tenancy
+ let mergedTags = {};
+ multiTenantsData.forEach(tenantData => {
+ if (tenantData.result) {
+ if (tenantData.result.hasOwnProperty(key)) {
+ tenantData.result[key].forEach(tag => {
+ mergedTags[tag] = true;
+ });
+ }
+ }
+ });
+ return Object.keys(mergedTags)
+ .sort()
+ .map(tag => {
+ return {text: tag, value: tag};
+ });
+ });
}
suggestTagKeys(target) {
- return this.backendSrv.datasourceRequest({
- url: this.metricsUrl + '/metrics/tags',
- method: 'GET',
- headers: this.getHeaders(target.tenant)
- }).then(response => response.status == 200 ? response.data : [])
- .then(result => result.map(key => ({text: key, value: key})));
+ const tenants = this.getTargetTenants(target);
+ return this.multiTenantsQuery(tenants, this.metricsUrl + '/metrics/tags', null, null, 'GET')
+ .then(multiTenantsData => {
+ // Eliminate possible duplicates from multi-tenancy
+ let mergedTags = {};
+ multiTenantsData.forEach(tenantData => {
+ if (tenantData.result) {
+ tenantData.result.forEach(tag => {
+ mergedTags[tag] = true;
+ });
+ }
+ });
+ return Object.keys(mergedTags)
+ .map(tag => {
+ return {text: tag, value: tag};
+ });
+ });
}
metricFindQuery(query) {
diff --git a/src/partials/query.editor.html b/src/partials/query.editor.html
index a2179d7..7992f4a 100644
--- a/src/partials/query.editor.html
+++ b/src/partials/query.editor.html
@@ -2,7 +2,7 @@
-
+
diff --git a/src/queryProcessor.js b/src/queryProcessor.js
index 6242c0e..b9c9487 100644
--- a/src/queryProcessor.js
+++ b/src/queryProcessor.js
@@ -1,16 +1,16 @@
+import _ from 'lodash';
import {modelToString as tagsModelToString} from './tagsKVPairsController';
const STATS_BUCKETS = 60;
export class QueryProcessor {
- constructor(q, backendSrv, variablesHelper, capabilities, url, getHeaders, typeResources) {
+ constructor(q, multiTenantsQuery, variablesHelper, capabilities, url, typeResources) {
this.q = q;
- this.backendSrv = backendSrv;
+ this.multiTenantsQuery = multiTenantsQuery;
this.variablesHelper = variablesHelper;
this.capabilities = capabilities;
this.url = url;
- this.getHeaders = getHeaders;
this.typeResources = typeResources;
this.numericMapping = point => [point.value, point.timestamp];
this.availMapping = point => [point.value == 'up' ? 1 : 0, point.timestamp];
@@ -23,26 +23,30 @@ export class QueryProcessor {
end: options.range.to.valueOf(),
order: 'ASC'
};
+ let tenants = [null];
+ if (target.tenant) {
+ tenants = this.variablesHelper.resolve(target.tenant, options);
+ }
if (target.id) {
const metricIds = this.variablesHelper.resolve(target.id, options);
if (caps.QUERY_POST_ENDPOINTS) {
if (target.raw) {
postData.ids = metricIds;
- return this.rawQuery(target, postData);
+ return this.rawQuery(target, postData, tenants);
} else if (target.timeAggFn == 'live') {
// Need to change postData
- return this.singleStatLiveQuery(target, {ids: metricIds, limit: 1});
+ return this.singleStatLiveQuery(target, {ids: metricIds, limit: 1}, tenants);
} else if (target.timeAggFn) {
// Query single stat
postData.metrics = metricIds;
- return this.singleStatQuery(target, postData);
+ return this.singleStatQuery(target, postData, tenants);
} else {
// Query stats for chart
postData.metrics = metricIds;
- return this.statsQuery(target, postData);
+ return this.statsQuery(target, postData, tenants);
}
} else {
- return this.rawQueryLegacy(target, options.range, metricIds);
+ return this.rawQueryLegacy(target, options.range, metricIds, tenants);
}
} else {
if (caps.TAGS_QUERY_LANGUAGE) {
@@ -59,52 +63,46 @@ export class QueryProcessor {
}
}
if (target.raw) {
- return this.rawQuery(target, postData);
+ return this.rawQuery(target, postData, tenants);
} else if (target.timeAggFn == 'live') {
// Need to change postData
- return this.singleStatLiveQuery(target, {tags: postData.tags, limit: 1});
+ return this.singleStatLiveQuery(target, {tags: postData.tags, limit: 1}, tenants);
} else if (target.timeAggFn) {
// Query single stat
- return this.singleStatQuery(target, postData);
+ return this.singleStatQuery(target, postData, tenants);
} else {
// Query stats for chart
- return this.statsQuery(target, postData);
+ return this.statsQuery(target, postData, tenants);
}
}
});
}
- rawQuery(target, postData) {
+ rawQuery(target, postData, tenants) {
const url = `${this.url}/${this.typeResources[target.type]}/${target.rate ? 'rate' : 'raw'}/query`;
-
- return this.backendSrv.datasourceRequest({
- url: url,
- data: postData,
- method: 'POST',
- headers: this.getHeaders(target.tenant)
- }).then(response => this.processRawResponse(target, response.status == 200 ? response.data : []));
+ return this.multiTenantsQuery(tenants, url, null, postData, 'POST')
+ .then(res => this.tenantsPrefixer(res))
+ .then(allSeries => this.processRawResponse(target, allSeries));
}
- rawQueryLegacy(target, range, metricIds) {
+ rawQueryLegacy(target, range, metricIds, tenants) {
return this.q.all(metricIds.map(metric => {
const url = `${this.url}/${this.typeResources[target.type]}/${encodeURIComponent(metric).replace('+', '%20')}/data`;
- return this.backendSrv.datasourceRequest({
- url: url,
- params: {
- start: range.from.valueOf(),
- end: range.to.valueOf()
- },
- method: 'GET',
- headers: this.getHeaders(target.tenant)
- }).then(response => this.processRawResponseLegacy(target, metric, response.status == 200 ? response.data : []));
+ const params = {
+ start: range.from.valueOf(),
+ end: range.to.valueOf()
+ };
+ return this.multiTenantsQuery(tenants, url, params, null, 'GET')
+ .then(res => this.tenantsPrefixer(res))
+ .then(allSeries => this.processRawResponseLegacy(target, metric, allSeries));
}));
}
- processRawResponse(target, data) {
- return data.map(timeSerie => {
+ processRawResponse(target, allSeries) {
+ return allSeries.map(timeSerie => {
return {
refId: target.refId,
- target: timeSerie.id,
+ target: timeSerie.prefix + timeSerie.id,
datapoints: timeSerie.data.map(target.type == 'availability' ? this.availMapping : this.numericMapping)
};
});
@@ -141,9 +139,9 @@ export class QueryProcessor {
};
}
- statsQuery(target, postData) {
+ statsQuery(target, postData, tenants) {
if (target.seriesAggFn === 'none') {
- return this.statsQueryUnmerged(target, postData);
+ return this.statsQueryUnmerged(target, postData, tenants);
}
const url = `${this.url}/${this.typeResources[target.type]}/stats/query`;
delete postData.order;
@@ -153,36 +151,40 @@ export class QueryProcessor {
if (percentiles.length > 0) {
postData.percentiles = percentiles.join(',');
}
- return this.backendSrv.datasourceRequest({
- url: url,
- data: postData,
- method: 'POST',
- headers: this.getHeaders(target.tenant)
- }).then(response => this.processStatsResponse(target, response.status == 200 ? response.data : []));
+ return this.multiTenantsQuery(tenants, url, null, postData, 'POST')
+ .then(multiTenantsData => this.processStatsResponse(target, multiTenantsData));
}
- processStatsResponse(target, data) {
- // Response example: [{start:1234, end:5678, avg:100.0, min:90.0, max:110.0, (...), percentiles:[{quantile: 90, value: 105.0}]}]
- return target.stats.map(stat => {
- const percentile = this.getPercentileValue(stat);
- if (percentile) {
- return {
- refId: target.refId,
- target: stat,
- datapoints: data.filter(bucket => !bucket.empty)
- .map(bucket => [this.findQuantileInBucket(percentile, bucket), bucket.start])
- };
- } else {
- return {
- refId: target.refId,
- target: stat,
- datapoints: data.filter(bucket => !bucket.empty).map(bucket => [bucket[stat], bucket.start])
- };
+ processStatsResponse(target, multiTenantsData) {
+ // Response example: [ { tenant: 't1', result: [...] }, { tenant: 't2', result: [...] } ]
+ // Detailed `data[i].result`: [{start:1234, end:5678, avg:100.0, min:90.0, max:110.0, (...), percentiles:[{quantile: 90, value: 105.0}]}]
+ const flatten = [];
+ const prefixer = multiTenantsData.length > 1 ? (tenant) => `[${tenant}] ` : (tenant) => '';
+ multiTenantsData.forEach(tenantData => {
+ if (tenantData.result) {
+ target.stats.forEach(stat => {
+ const percentile = this.getPercentileValue(stat);
+ if (percentile) {
+ flatten.push({
+ refId: target.refId,
+ target: prefixer(tenantData.tenant) + stat,
+ datapoints: tenantData.result.filter(bucket => !bucket.empty)
+ .map(bucket => [this.findQuantileInBucket(percentile, bucket), bucket.start])
+ });
+ } else {
+ flatten.push({
+ refId: target.refId,
+ target: prefixer(tenantData.tenant) + stat,
+ datapoints: tenantData.result.filter(bucket => !bucket.empty).map(bucket => [bucket[stat], bucket.start])
+ });
+ }
+ });
}
});
+ return flatten;
}
- statsQueryUnmerged(target, postData) {
+ statsQueryUnmerged(target, postData, tenants) {
const url = `${this.url}/metrics/stats/query`;
delete postData.order;
postData.buckets = STATS_BUCKETS;
@@ -196,43 +198,46 @@ export class QueryProcessor {
if (percentiles.length > 0) {
postData.percentiles = percentiles.join(',');
}
- return this.backendSrv.datasourceRequest({
- url: url,
- data: postData,
- method: 'POST',
- headers: this.getHeaders(target.tenant)
- }).then(response => this.processUnmergedStatsResponse(target, response.status == 200 ? response.data : []));
+ return this.multiTenantsQuery(tenants, url, null, postData, 'POST')
+ .then(multiTenantsData => this.processUnmergedStatsResponse(target, multiTenantsData));
}
- processUnmergedStatsResponse(target, data) {
- // Response example:
+ processUnmergedStatsResponse(target, multiTenantsData) {
+ // Response example: [ { tenant: 't1', result: {...} }, { tenant: 't2', result: {...} } ]
+ // Detailed `data[i].result`:
// {"gauge": {"my_metric": [
// {start:1234, end:5678, avg:100.0, min:90.0, max:110.0, (...), percentiles:[{quantile: 90, value: 105.0}]}
// ]}}
const series = [];
- const allMetrics = data[target.type];
- for (let metricId in allMetrics) {
- if (allMetrics.hasOwnProperty(metricId)) {
- const buckets = allMetrics[metricId];
- target.stats.forEach(stat => {
- const percentile = this.getPercentileValue(stat);
- if (percentile) {
- series.push({
- refId: target.refId,
- target: `${metricId} [${stat}]`,
- datapoints: buckets.filter(bucket => !bucket.empty)
- .map(bucket => [this.findQuantileInBucket(percentile, bucket), bucket.start])
- });
- } else {
- series.push({
- refId: target.refId,
- target: `${metricId} [${stat}]`,
- datapoints: buckets.filter(bucket => !bucket.empty).map(bucket => [bucket[stat], bucket.start])
+ const prefixer = multiTenantsData.length > 1 ? (tenant) => `[${tenant}] ` : (tenant) => '';
+ multiTenantsData.forEach(tenantData => {
+ if (tenantData.result) {
+ const allMetrics = tenantData.result[target.type];
+ const prefix = prefixer(tenantData.tenant);
+ for (let metricId in allMetrics) {
+ if (allMetrics.hasOwnProperty(metricId)) {
+ const buckets = allMetrics[metricId];
+ target.stats.forEach(stat => {
+ const percentile = this.getPercentileValue(stat);
+ if (percentile) {
+ series.push({
+ refId: target.refId,
+ target: `${prefix}${metricId} [${stat}]`,
+ datapoints: buckets.filter(bucket => !bucket.empty)
+ .map(bucket => [this.findQuantileInBucket(percentile, bucket), bucket.start])
+ });
+ } else {
+ series.push({
+ refId: target.refId,
+ target: `${prefix}${metricId} [${stat}]`,
+ datapoints: buckets.filter(bucket => !bucket.empty).map(bucket => [bucket[stat], bucket.start])
+ });
+ }
});
}
- });
+ }
}
- }
+ });
return series;
}
@@ -255,7 +260,7 @@ export class QueryProcessor {
return null;
}
- singleStatQuery(target, postData) {
+ singleStatQuery(target, postData, tenants) {
// Query for singlestat => we just ask for a single bucket
// But because of that we need to override Grafana behaviour, and manage ourselves the min/max/avg/etc. selection
let fnBucket;
@@ -270,55 +275,71 @@ export class QueryProcessor {
delete postData.order;
postData.buckets = 1;
postData.stacked = target.seriesAggFn === 'sum';
- return this.backendSrv.datasourceRequest({
- url: url,
- data: postData,
- method: 'POST',
- headers: this.getHeaders(target.tenant)
- }).then(response => this.processSingleStatResponse(target, fnBucket, response.status == 200 ? response.data : []));
+ return this.multiTenantsQuery(tenants, url, null, postData, 'POST')
+ .then(multiTenantsData => this.processSingleStatResponse(target, fnBucket, multiTenantsData));
}
- processSingleStatResponse(target, fnBucket, data) {
- return data.map(bucket => {
- return {
- refId: target.refId,
- target: 'Aggregate',
- datapoints: [[fnBucket(bucket), bucket.start]]
- };
- });
+ processSingleStatResponse(target, fnBucket, multiTenantsData) {
+ return _.flatten(multiTenantsData.map(tenantData => {
+ if (tenantData.result) {
+ return tenantData.result.map(bucket => {
+ return {
+ refId: target.refId,
+ target: 'Aggregate',
+ datapoints: [[fnBucket(bucket), bucket.start]]
+ };
+ });
+ }
+ }));
}
- singleStatLiveQuery(target, postData) {
+ singleStatLiveQuery(target, postData, tenants) {
const url = `${this.url}/${this.typeResources[target.type]}/${target.rate ? 'rate' : 'raw'}/query`;
// Set start to now - 5m
postData.start = Date.now() - 300000;
- return this.backendSrv.datasourceRequest({
- url: url,
- data: postData,
- method: 'POST',
- headers: this.getHeaders(target.tenant)
- }).then(response => this.processSingleStatLiveResponse(target, response.status == 200 ? response.data : []));
+ return this.multiTenantsQuery(tenants, url, null, postData, 'POST')
+ .then(multiTenantsData => this.processSingleStatLiveResponse(target, multiTenantsData));
}
- processSingleStatLiveResponse(target, data) {
+ processSingleStatLiveResponse(target, multiTenantsData) {
let reduceFunc;
if (target.seriesAggFn === 'sum') {
reduceFunc = (presentValues => presentValues.reduce((a,b) => a+b));
} else {
reduceFunc = (presentValues => presentValues.reduce((a,b) => a+b) / presentValues.length);
}
- let datapoints;
- const latestPoints = data.filter(timeSeries => timeSeries.data.length > 0)
- .map(timeSeries => timeSeries.data[0]);
- if (latestPoints.length === 0) {
- datapoints = [];
- } else {
- datapoints = [[reduceFunc(latestPoints.map(dp => dp.value)), latestPoints[0].timestamp]];
- }
- return [{
- refId: target.refId,
- target: 'Aggregate',
- datapoints: datapoints
- }];
+ return _.flatten(multiTenantsData.map(tenantData => {
+ if (tenantData.result) {
+ let datapoints;
+ const latestPoints = tenantData.result.filter(timeSeries => timeSeries.data.length > 0)
+ .map(timeSeries => timeSeries.data[0]);
+ if (latestPoints.length === 0) {
+ datapoints = [];
+ } else {
+ datapoints = [[reduceFunc(latestPoints.map(dp => dp.value)), latestPoints[0].timestamp]];
+ }
+ return [{
+ refId: target.refId,
+ target: 'Aggregate',
+ datapoints: datapoints
+ }];
+ }
+ }));
+ }
+
+ tenantsPrefixer(allTenantTimeSeries) {
+ // Exemple of input:
+ // [ { tenant: 't1', result: [ {id: metricA, data: []} ] }, { tenant: 't2', result: [ {id: metricB, data: []} ] } ]
+ const flatten = [];
+ const prefixer = allTenantTimeSeries.length > 1 ? (tenant) => `[${tenant}] ` : (tenant) => '';
+ allTenantTimeSeries.forEach(oneTenantTimeSeries => {
+ if (oneTenantTimeSeries.result) {
+ oneTenantTimeSeries.result.forEach(timeSeries => {
+ timeSeries.prefix = prefixer(oneTenantTimeSeries.tenant);
+ flatten.push(timeSeries);
+ })
+ }
+ })
+ return flatten;
}
}