From efa869bb89dc806d52133d5ef3d44b7220288e21 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Feb 2018 15:26:45 +0300 Subject: [PATCH 01/24] prometheus: initial heatmap support --- .../datasource/prometheus/datasource.ts | 66 ++++++++++++++++++- .../datasource/prometheus/query_ctrl.ts | 9 ++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 122fe9601a1..ba3dcda390e 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -106,17 +106,29 @@ export class PrometheusDatasource { }); return this.$q.all(allQueryPromise).then(responseList => { - var result = []; + let result = []; _.each(responseList, (response, index) => { if (response.status === 'error') { throw response.error; } + let prometheusResult = response.data.data.result; + if (activeTargets[index].format === 'table') { - result.push(self.transformMetricDataToTable(response.data.data.result, responseList.length, index)); + result.push(self.transformMetricDataToTable(prometheusResult, responseList.length, index)); + } else if (activeTargets[index].format === 'heatmap') { + let seriesList = []; + prometheusResult.sort(sortSeriesByLabel); + for (let metricData of prometheusResult) { + seriesList.push( + self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step) + ); + } + seriesList = self.transformToHistogramOverTime(seriesList); + result.push(...seriesList); } else { - for (let metricData of response.data.data.result) { + for (let metricData of prometheusResult) { if (response.data.data.resultType === 'matrix') { result.push(self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step)); } else if (response.data.data.resultType === 'vector') { @@ -378,6 +390,24 @@ export class PrometheusDatasource { return { target: metricLabel, datapoints: dps }; } + transformToHistogramOverTime(seriesList, options?) { + /* t1 = timestamp1, t2 = timestamp2 etc. + t1 t2 t3 t1 t2 t3 + le10 10 10 0 => 10 10 0 + le20 20 10 30 => 10 0 30 + le30 30 10 35 => 10 0 5 + */ + for (let i = seriesList.length - 1; i > 0; i--) { + let topSeries = seriesList[i].datapoints; + let bottomSeries = seriesList[i - 1].datapoints; + for (let j = 0; j < topSeries.length; j++) { + topSeries[j][0] -= bottomSeries[j][0]; + } + } + + return seriesList; + } + createMetricLabel(labelData, options) { if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { return this.getOriginalMetricName(labelData); @@ -412,3 +442,33 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } } + +function sortSeriesByLabel(s1, s2) { + let le1, le2; + + try { + // fail if not integer. might happen with bad queries + le1 = parseHistogramLabel(s1.metric.le); + le2 = parseHistogramLabel(s2.metric.le); + } catch (err) { + console.log(err); + return 0; + } + + if (le1 > le2) { + return 1; + } + + if (le1 < le2) { + return -1; + } + + return 0; +} + +function parseHistogramLabel(le: string): number { + if (le === '+Inf') { + return +Infinity; + } + return parseInt(le); +} diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 2674fc55af4..ea5588b253a 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -31,7 +31,11 @@ class PrometheusQueryCtrl extends QueryCtrl { return { factor: f, label: '1/' + f }; }); - this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; + this.formats = [ + { text: 'Time series', value: 'time_series' }, + { text: 'Table', value: 'table' }, + { text: 'Heatmap', value: 'heatmap' }, + ]; this.instant = false; @@ -45,7 +49,10 @@ class PrometheusQueryCtrl extends QueryCtrl { getDefaultFormat() { if (this.panelCtrl.panel.type === 'table') { return 'table'; + } else if (this.panelCtrl.panel.type === 'heatmap') { + return 'heatmap'; } + return 'time_series'; } From e361a31a2ac23f0a534269aa40cb4b9fa39e2fac Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Feb 2018 20:22:46 +0300 Subject: [PATCH 02/24] prometheus: tests for heatmap format --- .../prometheus/specs/datasource.jest.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 public/app/plugins/datasource/prometheus/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts new file mode 100644 index 00000000000..cca74e023e7 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -0,0 +1,104 @@ +import _ from 'lodash'; +import moment from 'moment'; +import q from 'q'; +import { PrometheusDatasource } from '../datasource'; + +describe('PrometheusDatasource', () => { + let ctx: any = {}; + let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: {}, + }; + + ctx.backendSrvMock = {}; + ctx.templateSrvMock = { + replace: a => a, + }; + ctx.timeSrvMock = {}; + + beforeEach(() => { + ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + }); + + describe('When converting prometheus histogram to heatmap format', () => { + beforeEach(() => { + ctx.query = { + range: { from: moment(1443454528000), to: moment(1443454528000) }, + targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }], + interval: '60s', + }; + }); + + it('should convert cumullative histogram to ordinary', () => { + const resultMock = [ + { + metric: { __name__: 'metric', job: 'testjob', le: '10' }, + values: [[1443454528.0, '10'], [1443454528.0, '10']], + }, + { + metric: { __name__: 'metric', job: 'testjob', le: '20' }, + values: [[1443454528.0, '20'], [1443454528.0, '10']], + }, + { + metric: { __name__: 'metric', job: 'testjob', le: '30' }, + values: [[1443454528.0, '25'], [1443454528.0, '10']], + }, + ]; + const responseMock = { data: { data: { result: resultMock } } }; + + const expected = [ + { + target: '10', + datapoints: [[10, 1443454528000], [10, 1443454528000]], + }, + { + target: '20', + datapoints: [[10, 1443454528000], [0, 1443454528000]], + }, + { + target: '30', + datapoints: [[5, 1443454528000], [0, 1443454528000]], + }, + ]; + + ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock); + return ctx.ds.query(ctx.query).then(result => { + let results = result.data; + return expect(results).toEqual(expected); + }); + }); + + it('should sort series by label value', () => { + const resultMock = [ + { + metric: { __name__: 'metric', job: 'testjob', le: '2' }, + values: [[1443454528.0, '10'], [1443454528.0, '10']], + }, + { + metric: { __name__: 'metric', job: 'testjob', le: '4' }, + values: [[1443454528.0, '20'], [1443454528.0, '10']], + }, + { + metric: { __name__: 'metric', job: 'testjob', le: '+Inf' }, + values: [[1443454528.0, '25'], [1443454528.0, '10']], + }, + { + metric: { __name__: 'metric', job: 'testjob', le: '1' }, + values: [[1443454528.0, '25'], [1443454528.0, '10']], + }, + ]; + const responseMock = { data: { data: { result: resultMock } } }; + + const expected = ['1', '2', '4', '+Inf']; + + ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock); + return ctx.ds.query(ctx.query).then(result => { + let seriesLabels = _.map(result.data, 'target'); + return expect(seriesLabels).toEqual(expected); + }); + }); + }); +}); From 97c54e695691b072de2208dcfb3881210102739e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Feb 2018 15:43:08 +0300 Subject: [PATCH 03/24] heatmap: use buckets from histogram with 'tsbuckets' mode --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 16 +++---- .../panel/heatmap/heatmap_data_converter.ts | 40 ++++++++++++++++ public/app/plugins/panel/heatmap/rendering.ts | 48 +++++++++++++++++-- 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index c47af640af2..df07c60ff28 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -5,12 +5,7 @@ import TimeSeries from 'app/core/time_series2'; import { axesEditor } from './axes_editor'; import { heatmapDisplayEditor } from './display_editor'; import rendering from './rendering'; -import { - convertToHeatMap, - convertToCards, - elasticHistogramToHeatmap, - calculateBucketSize, -} from './heatmap_data_converter'; +import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from './heatmap_data_converter'; let X_BUCKET_NUMBER_DEFAULT = 30; let Y_BUCKET_NUMBER_DEFAULT = 10; @@ -139,12 +134,13 @@ export class HeatmapCtrl extends MetricsPanelCtrl { return; } - let xBucketSize, yBucketSize, heatmapStats, bucketsData; + let xBucketSize, yBucketSize, heatmapStats, bucketsData, tsBuckets; let logBase = this.panel.yAxis.logBase; if (this.panel.dataFormat === 'tsbuckets') { heatmapStats = this.parseHistogramSeries(this.series); - bucketsData = elasticHistogramToHeatmap(this.series); + bucketsData = histogramToHeatmap(this.series); + tsBuckets = _.map(this.series, 'label'); // Calculate bucket size based on ES heatmap data let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); @@ -210,6 +206,10 @@ export class HeatmapCtrl extends MetricsPanelCtrl { cards: cards, cardStats: cardStats, }; + + if (tsBuckets) { + this.data.tsBuckets = tsBuckets; + } } onDataReceived(dataList) { diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index bccd62235f5..f580cc585b7 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -51,6 +51,45 @@ function elasticHistogramToHeatmap(seriesList) { return heatmap; } +function histogramToHeatmap(seriesList) { + let heatmap = {}; + + for (let i = 0; i < seriesList.length; i++) { + let series = seriesList[i]; + let bound = i; + if (isNaN(bound)) { + return heatmap; + } + + for (let point of series.datapoints) { + let count = point[VALUE_INDEX]; + let time = point[TIME_INDEX]; + + if (!_.isNumber(count)) { + continue; + } + + let bucket = heatmap[time]; + if (!bucket) { + bucket = heatmap[time] = { x: time, buckets: {} }; + } + + bucket.buckets[bound] = { + y: bound, + count: count, + bounds: { + top: null, + bottom: bound, + }, + values: [], + points: [], + }; + } + } + + return heatmap; +} + /** * Convert buckets into linear array of "cards" - objects, represented heatmap elements. * @param {Object} buckets @@ -433,6 +472,7 @@ function emptyXOR(foo: any, bar: any): boolean { export { convertToHeatMap, + histogramToHeatmap, elasticHistogramToHeatmap, convertToCards, mergeZeroBuckets, diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 90dac42dc8d..fe2aa46a584 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -296,6 +296,42 @@ export default function link(scope, elem, attrs, ctrl) { .remove(); } + function addYAxisFromBuckets() { + const tsBuckets = data.tsBuckets; + + scope.yScale = yScale = d3 + .scaleLinear() + .domain([0, tsBuckets.length - 1]) + .range([chartHeight, 0]); + + const tick_values = _.map(tsBuckets, (b, i) => i); + const tickFormatter = val => tsBuckets[val]; + + let yAxis = d3 + .axisLeft(yScale) + .tickValues(tick_values) + .tickFormat(tickFormatter) + .tickSizeInner(0 - width) + .tickSizeOuter(0) + .tickPadding(Y_AXIS_TICK_PADDING); + + heatmap + .append('g') + .attr('class', 'axis axis-y') + .call(yAxis); + + // Calculate Y axis width first, then move axis into visible area + const posY = margin.top; + const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; + heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); + + // Remove vertical line in the right of axis labels (called domain in d3) + heatmap + .select('.axis-y') + .select('.domain') + .remove(); + } + // Adjust data range to log base function adjustLogRange(min, max, logBase) { let y_min, y_max; @@ -362,10 +398,14 @@ export default function link(scope, elem, attrs, ctrl) { chartTop = margin.top; chartBottom = chartTop + chartHeight; - if (panel.yAxis.logBase === 1) { - addYAxis(); + if (panel.dataFormat === 'tsbuckets') { + addYAxisFromBuckets(); } else { - addLogYAxis(); + if (panel.yAxis.logBase === 1) { + addYAxis(); + } else { + addLogYAxis(); + } } yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; @@ -414,7 +454,7 @@ export default function link(scope, elem, attrs, ctrl) { addHeatmapCanvas(); addAxes(); - if (panel.yAxis.logBase !== 1) { + if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') { let log_base = panel.yAxis.logBase; let domain = yScale.domain(); let tick_values = logScaleTickValues(domain, log_base); From bc47380032cce6414ae88731efbb37863dc8fa78 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Feb 2018 16:06:21 +0300 Subject: [PATCH 04/24] heatmap: fix tooltip histogram for 'tsbuckets' mode --- public/app/plugins/panel/heatmap/heatmap_tooltip.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 823c56425a5..948e9bdfad7 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -177,7 +177,16 @@ export class HeatmapTooltip { addHistogram(data) { let xBucket = this.scope.ctrl.data.buckets[data.x]; let yBucketSize = this.scope.ctrl.data.yBucketSize; - let { min, max, ticks } = this.scope.ctrl.data.yAxis; + let min, max, ticks; + if (this.scope.ctrl.data.tsBuckets) { + min = 0; + max = this.scope.ctrl.data.tsBuckets.length - 1; + ticks = this.scope.ctrl.data.tsBuckets.length; + } else { + min = this.scope.ctrl.data.yAxis.min; + max = this.scope.ctrl.data.yAxis.max; + ticks = this.scope.ctrl.data.yAxis.ticks; + } let histogramData = _.map(xBucket.buckets, bucket => { let count = bucket.count !== undefined ? bucket.count : bucket.values.length; return [bucket.bounds.bottom, count]; From cc34c9a6514ff2035257bd755ec88f5f4da84f03 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Feb 2018 16:12:42 +0300 Subject: [PATCH 05/24] heatmap tooltip: fix count decimals --- public/app/plugins/panel/heatmap/heatmap_tooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 948e9bdfad7..82041718478 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -117,7 +117,7 @@ export class HeatmapTooltip { let bottom = yData.y ? yData.bounds.bottom : 0; boundBottom = valueFormatter(bottom); boundTop = valueFormatter(yData.bounds.top); - valuesNumber = yData.count; + valuesNumber = valueFormatter(yData.count); tooltipHtml += `
bucket: ${boundBottom} - ${boundTop}
count: ${valuesNumber}
From e0a874f6779f28cbc6bcbed9fcd30cd3109fad95 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Feb 2018 16:33:45 +0300 Subject: [PATCH 06/24] heatmap tooltip: fix bucket bounds for 'tsbuckets' mode --- .../app/plugins/panel/heatmap/heatmap_tooltip.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 82041718478..d760539f27d 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -113,10 +113,15 @@ export class HeatmapTooltip { if (yData) { if (yData.bounds) { - // Display 0 if bucket is a special 'zero' bucket - let bottom = yData.y ? yData.bounds.bottom : 0; - boundBottom = valueFormatter(bottom); - boundTop = valueFormatter(yData.bounds.top); + if (data.tsBuckets) { + boundBottom = data.tsBuckets[yBucketIndex]; + boundTop = yBucketIndex < data.tsBuckets.length - 1 ? data.tsBuckets[yBucketIndex + 1] : ''; + } else { + // Display 0 if bucket is a special 'zero' bucket + let bottom = yData.y ? yData.bounds.bottom : 0; + boundBottom = valueFormatter(bottom); + boundTop = valueFormatter(yData.bounds.top); + } valuesNumber = valueFormatter(yData.count); tooltipHtml += `
bucket: ${boundBottom} - ${boundTop}
@@ -163,6 +168,9 @@ export class HeatmapTooltip { getYBucketIndex(offsetY, data) { let y = this.scope.yScale.invert(offsetY - this.scope.chartTop); + if (data.tsBuckets) { + return Math.floor(y); + } let yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); return yBucketIndex; } From 2a2675c1afaafda40c2ac3de8637c3f857723b8f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Feb 2018 16:54:28 +0300 Subject: [PATCH 07/24] heatmap: fix bucket labels shift --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index df07c60ff28..54854ac6a4e 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -141,6 +141,8 @@ export class HeatmapCtrl extends MetricsPanelCtrl { heatmapStats = this.parseHistogramSeries(this.series); bucketsData = histogramToHeatmap(this.series); tsBuckets = _.map(this.series, 'label'); + // Add empty bottom bucket label + tsBuckets = [''].concat(tsBuckets); // Calculate bucket size based on ES heatmap data let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); From 26ddb9977f56b7462c519dc2d94a25dd49d40b2a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 26 Feb 2018 16:26:54 +0300 Subject: [PATCH 08/24] heatmap: fix Y bucket size calculation for 'tsbuckets' mode --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 54854ac6a4e..e2974c0d57f 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -146,13 +146,9 @@ export class HeatmapCtrl extends MetricsPanelCtrl { // Calculate bucket size based on ES heatmap data let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); - let yBucketBoundSet = _.map(this.series, series => Number(series.alias)); xBucketSize = calculateBucketSize(xBucketBoundSet); - yBucketSize = calculateBucketSize(yBucketBoundSet, logBase); - if (logBase !== 1) { - // Use yBucketSize in meaning of "Split factor" for log scales - yBucketSize = 1 / yBucketSize; - } + // Always let yBucketSize=1 in 'tsbuckets' mode + yBucketSize = 1; } else { let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); From 7c78e8e3830c4d3b5321c98d609b8b41d3fe98d2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Feb 2018 14:17:29 +0300 Subject: [PATCH 09/24] heatmap: add few tests for histogram converter --- .../specs/heatmap_data_converter.jest.ts | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts index 76bf7c39538..b6a8713a3e9 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts @@ -4,7 +4,7 @@ import TimeSeries from 'app/core/time_series2'; import { convertToHeatMap, convertToCards, - elasticHistogramToHeatmap, + histogramToHeatmap, calculateBucketSize, isHeatmapDataEqual, } from '../heatmap_data_converter'; @@ -216,7 +216,7 @@ describe('HeatmapDataConverter', () => { }); }); -describe('ES Histogram converter', () => { +describe('Histogram converter', () => { let ctx: any = {}; beforeEach(() => { @@ -244,7 +244,7 @@ describe('ES Histogram converter', () => { ); }); - describe('when converting ES histogram', () => { + describe('when converting histogram', () => { beforeEach(() => {}); it('should build proper heatmap data', () => { @@ -252,60 +252,72 @@ describe('ES Histogram converter', () => { '1422774000000': { x: 1422774000000, buckets: { - '1': { - y: 1, + '0': { + y: 0, count: 1, + bounds: { bottom: 0, top: null }, values: [], points: [], + }, + '1': { + y: 1, + count: 5, bounds: { bottom: 1, top: null }, + values: [], + points: [], }, '2': { y: 2, - count: 5, - values: [], - points: [], - bounds: { bottom: 2, top: null }, - }, - '3': { - y: 3, count: 0, + bounds: { bottom: 2, top: null }, values: [], points: [], - bounds: { bottom: 3, top: null }, }, }, }, '1422774060000': { x: 1422774060000, buckets: { - '1': { - y: 1, + '0': { + y: 0, count: 0, + bounds: { bottom: 0, top: null }, values: [], points: [], + }, + '1': { + y: 1, + count: 3, bounds: { bottom: 1, top: null }, + values: [], + points: [], }, '2': { y: 2, - count: 3, - values: [], - points: [], - bounds: { bottom: 2, top: null }, - }, - '3': { - y: 3, count: 1, + bounds: { bottom: 2, top: null }, values: [], points: [], - bounds: { bottom: 3, top: null }, }, }, }, }; - let heatmap = elasticHistogramToHeatmap(ctx.series); + const heatmap = histogramToHeatmap(ctx.series); expect(heatmap).toEqual(expectedHeatmap); }); + + it('should use bucket index as a bound', () => { + const heatmap = histogramToHeatmap(ctx.series); + const bucketLabels = _.map(heatmap['1422774000000'].buckets, (b, label) => label); + const bucketYs = _.map(heatmap['1422774000000'].buckets, 'y'); + const bucketBottoms = _.map(heatmap['1422774000000'].buckets, b => b.bounds.bottom); + const expectedBounds = [0, 1, 2]; + + expect(bucketLabels).toEqual(_.map(expectedBounds, b => b.toString())); + expect(bucketYs).toEqual(expectedBounds); + expect(bucketBottoms).toEqual(expectedBounds); + }); }); }); From f21cebeefd55a30fcceb2b1d8b7dc1234fff746f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Feb 2018 14:17:57 +0300 Subject: [PATCH 10/24] heatmap: refactor --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 124 ++++++++++-------- .../panel/heatmap/heatmap_data_converter.ts | 43 +----- 2 files changed, 75 insertions(+), 92 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index e2974c0d57f..6656d43f9c7 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -134,59 +134,53 @@ export class HeatmapCtrl extends MetricsPanelCtrl { return; } - let xBucketSize, yBucketSize, heatmapStats, bucketsData, tsBuckets; - let logBase = this.panel.yAxis.logBase; - if (this.panel.dataFormat === 'tsbuckets') { - heatmapStats = this.parseHistogramSeries(this.series); - bucketsData = histogramToHeatmap(this.series); - tsBuckets = _.map(this.series, 'label'); - // Add empty bottom bucket label - tsBuckets = [''].concat(tsBuckets); - - // Calculate bucket size based on ES heatmap data - let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); - xBucketSize = calculateBucketSize(xBucketBoundSet); - // Always let yBucketSize=1 in 'tsbuckets' mode - yBucketSize = 1; + this.convertHistogramToHeatmapData(); } else { - let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; - let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); - - // Parse X bucket size (number or interval) - let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); - if (isIntervalString) { - xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); - } else if ( - isNaN(Number(this.panel.xBucketSize)) || - this.panel.xBucketSize === '' || - this.panel.xBucketSize === null - ) { - xBucketSize = xBucketSizeByNumber; - } else { - xBucketSize = Number(this.panel.xBucketSize); - } - - // Calculate Y bucket size - heatmapStats = this.parseSeries(this.series); - let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; - if (logBase !== 1) { - yBucketSize = this.panel.yAxis.splitFactor; - } else { - if (heatmapStats.max === heatmapStats.min) { - if (heatmapStats.max) { - yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; - } else { - yBucketSize = 1; - } - } else { - yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; - } - yBucketSize = this.panel.yBucketSize || yBucketSize; - } - - bucketsData = convertToHeatMap(this.series, yBucketSize, xBucketSize, logBase); + this.convertTimeSeriesToHeatmapData(); } + } + + convertTimeSeriesToHeatmapData() { + let xBucketSize, yBucketSize, bucketsData, heatmapStats; + const logBase = this.panel.yAxis.logBase; + + let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; + let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); + + // Parse X bucket size (number or interval) + let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); + if (isIntervalString) { + xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); + } else if ( + isNaN(Number(this.panel.xBucketSize)) || + this.panel.xBucketSize === '' || + this.panel.xBucketSize === null + ) { + xBucketSize = xBucketSizeByNumber; + } else { + xBucketSize = Number(this.panel.xBucketSize); + } + + // Calculate Y bucket size + heatmapStats = this.parseSeries(this.series); + let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; + if (logBase !== 1) { + yBucketSize = this.panel.yAxis.splitFactor; + } else { + if (heatmapStats.max === heatmapStats.min) { + if (heatmapStats.max) { + yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; + } else { + yBucketSize = 1; + } + } else { + yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; + } + yBucketSize = this.panel.yBucketSize || yBucketSize; + } + + bucketsData = convertToHeatMap(this.series, yBucketSize, xBucketSize, logBase); // Set default Y range if no data if (!heatmapStats.min && !heatmapStats.max) { @@ -204,10 +198,34 @@ export class HeatmapCtrl extends MetricsPanelCtrl { cards: cards, cardStats: cardStats, }; + } - if (tsBuckets) { - this.data.tsBuckets = tsBuckets; - } + convertHistogramToHeatmapData() { + let xBucketSize, yBucketSize, bucketsData, tsBuckets; + + // Convert histogram to heatmap. Each histogram bucket represented by the series which name is + // a top bucket bound. Further, these values will be used as X axis labels. + bucketsData = histogramToHeatmap(this.series); + tsBuckets = _.map(this.series, 'label'); + // Add empty bottom bucket label + tsBuckets = [''].concat(tsBuckets); + + // Calculate bucket size based on heatmap data + let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); + xBucketSize = calculateBucketSize(xBucketBoundSet); + // Always let yBucketSize=1 in 'tsbuckets' mode + yBucketSize = 1; + + let { cards, cardStats } = convertToCards(bucketsData); + + this.data = { + buckets: bucketsData, + xBucketSize: xBucketSize, + yBucketSize: yBucketSize, + tsBuckets: tsBuckets, + cards: cards, + cardStats: cardStats, + }; } onDataReceived(dataList) { diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index f580cc585b7..178eea9ec7f 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -13,44 +13,10 @@ interface YBucket { values: number[]; } -function elasticHistogramToHeatmap(seriesList) { - let heatmap = {}; - - for (let series of seriesList) { - let bound = Number(series.alias); - if (isNaN(bound)) { - return heatmap; - } - - for (let point of series.datapoints) { - let count = point[VALUE_INDEX]; - let time = point[TIME_INDEX]; - - if (!_.isNumber(count)) { - continue; - } - - let bucket = heatmap[time]; - if (!bucket) { - bucket = heatmap[time] = { x: time, buckets: {} }; - } - - bucket.buckets[bound] = { - y: bound, - count: count, - bounds: { - top: null, - bottom: bound, - }, - values: [], - points: [], - }; - } - } - - return heatmap; -} - +/** + * Convert histogram represented by the list of series to heatmap object. + * @param seriesList List of time series + */ function histogramToHeatmap(seriesList) { let heatmap = {}; @@ -473,7 +439,6 @@ function emptyXOR(foo: any, bar: any): boolean { export { convertToHeatMap, histogramToHeatmap, - elasticHistogramToHeatmap, convertToCards, mergeZeroBuckets, getValueBucketBound, From 5c73ed6ecdd38e95b0e2d2ff1e844c0439422f8c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Feb 2018 15:12:12 +0300 Subject: [PATCH 11/24] heatmap: use series names as top or bottom bounds, depends of datasource --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 6656d43f9c7..bb64229dc77 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -204,11 +204,17 @@ export class HeatmapCtrl extends MetricsPanelCtrl { let xBucketSize, yBucketSize, bucketsData, tsBuckets; // Convert histogram to heatmap. Each histogram bucket represented by the series which name is - // a top bucket bound. Further, these values will be used as X axis labels. + // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels. bucketsData = histogramToHeatmap(this.series); tsBuckets = _.map(this.series, 'label'); - // Add empty bottom bucket label - tsBuckets = [''].concat(tsBuckets); + + if (this.datasource && this.datasource.type === 'prometheus') { + // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label. + tsBuckets = [''].concat(tsBuckets); + } else { + // Elasticsearch uses labels as bottom bucket bounds, so add empty top bucket label. + tsBuckets.push(''); + } // Calculate bucket size based on heatmap data let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); From dfe8b0a0d219d477e2f54de47600e935404bdb70 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 28 Feb 2018 15:58:28 +0300 Subject: [PATCH 12/24] heatmap: add rendering tests for tsbuckets mode --- .../panel/heatmap/specs/renderer_specs.ts | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 88e4953936b..f52b6d1d985 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -8,7 +8,7 @@ import TimeSeries from 'app/core/time_series2'; import moment from 'moment'; import { Emitter } from 'app/core/core'; import rendering from '../rendering'; -import { convertToHeatMap, convertToCards } from '../heatmap_data_converter'; +import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter'; describe('grafanaHeatmap', function() { beforeEach(angularMocks.module('grafana.core')); @@ -119,7 +119,12 @@ describe('grafanaHeatmap', function() { setupFunc(ctrl, ctx); let logBase = ctrl.panel.yAxis.logBase; - let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); + let bucketsData; + if (ctrl.panel.dataFormat === 'tsbuckets') { + bucketsData = histogramToHeatmap(ctx.series); + } else { + bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); + } ctx.data.buckets = bucketsData; let { cards, cardStats } = convertToCards(bucketsData); @@ -265,6 +270,38 @@ describe('grafanaHeatmap', function() { expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']); }); }); + + heatmapScenario('when data format is Time series buckets', function(ctx) { + ctx.setup(function(ctrl, ctx) { + ctrl.panel.dataFormat = 'tsbuckets'; + + const series = [ + { + alias: '1', + datapoints: [[1000, 1422774000000], [200000, 1422774060000]], + }, + { + alias: '2', + datapoints: [[3000, 1422774000000], [400000, 1422774060000]], + }, + { + alias: '3', + datapoints: [[2000, 1422774000000], [300000, 1422774060000]], + }, + ]; + ctx.series = series.map(s => new TimeSeries(s)); + + ctx.data.tsBuckets = series.map(s => s.alias).concat(''); + ctx.data.yBucketSize = 1; + let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]); + ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet); + }); + + it('should draw correct Y axis', function() { + var yTicks = getTicks(ctx.element, '.axis-y'); + expect(yTicks).to.eql(['1', '2', '3', '']); + }); + }); }); function getTicks(element, axisSelector) { From 76c684cc0114e48cb56253d4becc4bfd2a235a82 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 28 Feb 2018 16:19:58 +0300 Subject: [PATCH 13/24] heatmap: format numeric tick labels in tsbuckets mode --- public/app/plugins/panel/heatmap/rendering.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index fe2aa46a584..a7c55094e75 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -305,7 +305,15 @@ export default function link(scope, elem, attrs, ctrl) { .range([chartHeight, 0]); const tick_values = _.map(tsBuckets, (b, i) => i); - const tickFormatter = val => tsBuckets[val]; + + function tickFormatter(valIndex) { + let valueFormatted = tsBuckets[valIndex]; + if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { + // Try to format numeric tick labels + valueFormatted = tickValueFormatter(0)(valueFormatted); + } + return valueFormatted; + } let yAxis = d3 .axisLeft(yScale) From d6087eb5f4699d546639bee4dbb0a230e8734384 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 28 Feb 2018 16:30:00 +0300 Subject: [PATCH 14/24] heatmap: hide unused Y axis controls for tsbuckets mode --- .../panel/heatmap/partials/axes_editor.html | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html index 5d25bedd667..61ab48026d3 100644 --- a/public/app/plugins/panel/heatmap/partials/axes_editor.html +++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html @@ -9,25 +9,27 @@ dropdown-typeahead-on-select="editor.setUnitFormat($subItem)">
-
- -
- +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
-
-
- - -
-
- - -
-
- -
From 5037f93a783bcf0acab9180eb70e0e214d8f7680 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 5 Mar 2018 14:01:55 +0300 Subject: [PATCH 15/24] heatmap: sort series before converting to heatmap. This allows to use histogram series from arbitrary datasource and display it properly. --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 11 ++++-- .../panel/heatmap/heatmap_data_converter.ts | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index bb64229dc77..5a3b04905ef 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -5,7 +5,13 @@ import TimeSeries from 'app/core/time_series2'; import { axesEditor } from './axes_editor'; import { heatmapDisplayEditor } from './display_editor'; import rendering from './rendering'; -import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from './heatmap_data_converter'; +import { + convertToHeatMap, + convertToCards, + histogramToHeatmap, + calculateBucketSize, + sortSeriesByLabel, +} from './heatmap_data_converter'; let X_BUCKET_NUMBER_DEFAULT = 30; let Y_BUCKET_NUMBER_DEFAULT = 10; @@ -205,9 +211,10 @@ export class HeatmapCtrl extends MetricsPanelCtrl { // Convert histogram to heatmap. Each histogram bucket represented by the series which name is // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels. + this.series.sort(sortSeriesByLabel); bucketsData = histogramToHeatmap(this.series); - tsBuckets = _.map(this.series, 'label'); + tsBuckets = _.map(this.series, 'label'); if (this.datasource && this.datasource.type === 'prometheus') { // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label. tsBuckets = [''].concat(tsBuckets); diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 178eea9ec7f..89b1f1c714e 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -56,6 +56,39 @@ function histogramToHeatmap(seriesList) { return heatmap; } +/** + * Sort series representing histogram by label value. + */ +function sortSeriesByLabel(s1, s2) { + let label1, label2; + + try { + // fail if not integer. might happen with bad queries + label1 = parseHistogramLabel(s1.label); + label2 = parseHistogramLabel(s2.label); + } catch (err) { + console.log(err); + return 0; + } + + if (label1 > label2) { + return 1; + } + + if (label1 < label2) { + return -1; + } + + return 0; +} + +function parseHistogramLabel(label: string): number { + if (label === '+Inf') { + return +Infinity; + } + return parseInt(label); +} + /** * Convert buckets into linear array of "cards" - objects, represented heatmap elements. * @param {Object} buckets @@ -444,4 +477,5 @@ export { getValueBucketBound, isHeatmapDataEqual, calculateBucketSize, + sortSeriesByLabel, }; From 0912f61ea3717d2645d80006a0623a324024108d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 6 Mar 2018 15:21:37 +0300 Subject: [PATCH 16/24] heatmap: fix tooltip count and bucket bound format --- .../plugins/panel/heatmap/heatmap_tooltip.ts | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index d760539f27d..32e7bd7ca11 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -97,15 +97,17 @@ export class HeatmapTooltip { let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); // Decimals override. Code from panel/graph/graph.ts - let valueFormatter; + let countValueFormatter, bucketBoundFormatter; if (_.isNumber(this.panel.tooltipDecimals)) { - valueFormatter = this.valueFormatter(this.panel.tooltipDecimals, null); + countValueFormatter = this.countValueFormatter(this.panel.tooltipDecimals, null); + bucketBoundFormatter = this.bucketBoundFormatter(this.panel.tooltipDecimals, null); } else { // auto decimals // legend and tooltip gets one more decimal precision // than graph legend ticks let decimals = (this.panelCtrl.decimals || -1) + 1; - valueFormatter = this.valueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); + countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); + bucketBoundFormatter = this.bucketBoundFormatter(decimals, this.panelCtrl.scaledDecimals + 2); } let tooltipHtml = `
${time}
@@ -114,15 +116,25 @@ export class HeatmapTooltip { if (yData) { if (yData.bounds) { if (data.tsBuckets) { - boundBottom = data.tsBuckets[yBucketIndex]; - boundTop = yBucketIndex < data.tsBuckets.length - 1 ? data.tsBuckets[yBucketIndex + 1] : ''; + const tickFormatter = valIndex => { + let valueFormatted = data.tsBuckets[valIndex]; + if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { + // Try to format numeric tick labels + valueFormatted = this.bucketBoundFormatter(0)(valueFormatted); + } + return valueFormatted; + }; + const tsBucketsTickFormatter = tickFormatter.bind(this); + + boundBottom = tsBucketsTickFormatter(yBucketIndex); + boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tsBucketsTickFormatter(yBucketIndex + 1) : ''; } else { // Display 0 if bucket is a special 'zero' bucket let bottom = yData.y ? yData.bounds.bottom : 0; - boundBottom = valueFormatter(bottom); - boundTop = valueFormatter(yData.bounds.top); + boundBottom = bucketBoundFormatter(bottom); + boundTop = bucketBoundFormatter(yData.bounds.top); } - valuesNumber = valueFormatter(yData.count); + valuesNumber = countValueFormatter(yData.count); tooltipHtml += `
bucket: ${boundBottom} - ${boundTop}
count: ${valuesNumber}
@@ -268,7 +280,14 @@ export class HeatmapTooltip { return this.tooltip.style('left', left + 'px').style('top', top + 'px'); } - valueFormatter(decimals, scaledDecimals = null) { + countValueFormatter(decimals, scaledDecimals = null) { + let format = 'none'; + return function(value) { + return kbn.valueFormats[format](value, decimals, scaledDecimals); + }; + } + + bucketBoundFormatter(decimals, scaledDecimals = null) { let format = this.panel.yAxis.format; return function(value) { return kbn.valueFormats[format](value, decimals, scaledDecimals); From a791a92d79df63423f3f1d102a6fa5b58718edbd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 7 Mar 2018 13:28:44 +0300 Subject: [PATCH 17/24] heatmap: fix Y axis and tooltip decimals and units issues --- .../datasource/prometheus/datasource.ts | 2 +- .../panel/heatmap/heatmap_data_converter.ts | 2 +- .../plugins/panel/heatmap/heatmap_tooltip.ts | 10 +++++++-- .../panel/heatmap/partials/axes_editor.html | 12 +++++----- public/app/plugins/panel/heatmap/rendering.ts | 22 ++++++++++++++----- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ba3dcda390e..067c85cc3a4 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -470,5 +470,5 @@ function parseHistogramLabel(le: string): number { if (le === '+Inf') { return +Infinity; } - return parseInt(le); + return Number(le); } diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 89b1f1c714e..133193e1369 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -86,7 +86,7 @@ function parseHistogramLabel(label: string): number { if (label === '+Inf') { return +Infinity; } - return parseInt(label); + return Number(label); } /** diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 32e7bd7ca11..0ef1832e7e6 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -116,11 +116,12 @@ export class HeatmapTooltip { if (yData) { if (yData.bounds) { if (data.tsBuckets) { + const decimals = this.panelCtrl.decimals || 0; const tickFormatter = valIndex => { let valueFormatted = data.tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { // Try to format numeric tick labels - valueFormatted = this.bucketBoundFormatter(0)(valueFormatted); + valueFormatted = this.bucketBoundFormatter(decimals)(_.toNumber(valueFormatted)); } return valueFormatted; }; @@ -290,7 +291,12 @@ export class HeatmapTooltip { bucketBoundFormatter(decimals, scaledDecimals = null) { let format = this.panel.yAxis.format; return function(value) { - return kbn.valueFormats[format](value, decimals, scaledDecimals); + try { + return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; + } catch (err) { + console.error(err.message || err); + return value; + } }; } } diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html index 61ab48026d3..d97bb0361d0 100644 --- a/public/app/plugins/panel/heatmap/partials/axes_editor.html +++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html @@ -24,12 +24,12 @@
-
- - -
+
+
+ +
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index a7c55094e75..0ccba2952cd 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -305,12 +305,15 @@ export default function link(scope, elem, attrs, ctrl) { .range([chartHeight, 0]); const tick_values = _.map(tsBuckets, (b, i) => i); + const decimalsAuto = _.max(_.map(tsBuckets, getStringPrecision)); + const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; + ctrl.decimals = decimals; function tickFormatter(valIndex) { let valueFormatted = tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { // Try to format numeric tick labels - valueFormatted = tickValueFormatter(0)(valueFormatted); + valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted)); } return valueFormatted; } @@ -390,7 +393,12 @@ export default function link(scope, elem, attrs, ctrl) { function tickValueFormatter(decimals, scaledDecimals = null) { let format = panel.yAxis.format; return function(value) { - return kbn.valueFormats[format](value, decimals, scaledDecimals); + try { + return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; + } catch (err) { + console.error(err.message || err); + return value; + } }; } @@ -849,12 +857,16 @@ function logp(value, base) { return Math.log(value) / Math.log(base); } -function getPrecision(num) { +function getPrecision(num: number): number { let str = num.toString(); - let dot_index = str.indexOf('.'); + return getStringPrecision(str); +} + +function getStringPrecision(num: string): number { + let dot_index = num.indexOf('.'); if (dot_index === -1) { return 0; } else { - return str.length - dot_index - 1; + return num.length - dot_index - 1; } } From 18a90667babfb1b7a7aed775fd3f5c02e457b6b8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 7 Mar 2018 16:33:33 +0300 Subject: [PATCH 18/24] heatmap: refactor --- public/app/core/utils/ticks.ts | 58 ++++++++++++++ .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 19 ++++- .../panel/heatmap/heatmap_data_converter.ts | 10 ++- .../plugins/panel/heatmap/heatmap_tooltip.ts | 32 ++------ public/app/plugins/panel/heatmap/rendering.ts | 78 +++++-------------- 5 files changed, 109 insertions(+), 88 deletions(-) diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 834b7bd0cc4..db65104cfc0 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -156,3 +156,61 @@ export function getFlotTickDecimals(data, axis) { const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); return { tickDecimals, scaledDecimals }; } + +/** + * Format timestamp similar to Grafana graph panel. + * @param ticks Number of ticks + * @param min Time from (in milliseconds) + * @param max Time to (in milliseconds) + */ +export function grafanaTimeFormat(ticks, min, max) { + if (min && max && ticks) { + let range = max - min; + let secPerTick = range / ticks / 1000; + let oneDay = 86400000; + let oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + +/** + * Logarithm of value for arbitrary base. + */ +export function logp(value, base) { + return Math.log(value) / Math.log(base); +} + +/** + * Get decimal precision of number (3.14 => 2) + */ +export function getPrecision(num: number): number { + let str = num.toString(); + return getStringPrecision(str); +} + +/** + * Get decimal precision of number stored as a string ("3.14" => 2) + */ +export function getStringPrecision(num: string): number { + let dot_index = num.indexOf('.'); + if (dot_index === -1) { + return 0; + } else { + return num.length - dot_index - 1; + } +} diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 5a3b04905ef..47d3e6616b9 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -89,6 +89,8 @@ let colorSchemes = [ { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }, ]; +const ds_support_histogram_sort = ['prometheus', 'elasticsearch']; + export class HeatmapCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -207,15 +209,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } convertHistogramToHeatmapData() { + const panelDatasource = this.getPanelDataSourceType(); let xBucketSize, yBucketSize, bucketsData, tsBuckets; + // Try to sort series by bucket bound, if datasource doesn't do it. + if (!_.includes(ds_support_histogram_sort, panelDatasource)) { + this.series.sort(sortSeriesByLabel); + } + // Convert histogram to heatmap. Each histogram bucket represented by the series which name is // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels. - this.series.sort(sortSeriesByLabel); bucketsData = histogramToHeatmap(this.series); tsBuckets = _.map(this.series, 'label'); - if (this.datasource && this.datasource.type === 'prometheus') { + if (panelDatasource === 'prometheus') { // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label. tsBuckets = [''].concat(tsBuckets); } else { @@ -241,6 +248,14 @@ export class HeatmapCtrl extends MetricsPanelCtrl { }; } + getPanelDataSourceType() { + if (this.datasource.meta && this.datasource.meta.id) { + return this.datasource.meta.id; + } else { + return 'unknown'; + } + } + onDataReceived(dataList) { this.series = dataList.map(this.seriesHandler.bind(this)); diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 133193e1369..048b19de911 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -67,7 +67,7 @@ function sortSeriesByLabel(s1, s2) { label1 = parseHistogramLabel(s1.label); label2 = parseHistogramLabel(s2.label); } catch (err) { - console.log(err); + console.log(err.message || err); return 0; } @@ -83,10 +83,14 @@ function sortSeriesByLabel(s1, s2) { } function parseHistogramLabel(label: string): number { - if (label === '+Inf') { + if (label === '+Inf' || label === 'inf') { return +Infinity; } - return Number(label); + const value = Number(label); + if (isNaN(value)) { + throw new Error(`Error parsing histogram label: ${label} is not a number`); + } + return value; } /** diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 0ef1832e7e6..1caa9ae9c69 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -100,14 +100,14 @@ export class HeatmapTooltip { let countValueFormatter, bucketBoundFormatter; if (_.isNumber(this.panel.tooltipDecimals)) { countValueFormatter = this.countValueFormatter(this.panel.tooltipDecimals, null); - bucketBoundFormatter = this.bucketBoundFormatter(this.panel.tooltipDecimals, null); + bucketBoundFormatter = this.panelCtrl.tickValueFormatter(this.panelCtrl.decimals, null); } else { // auto decimals // legend and tooltip gets one more decimal precision // than graph legend ticks let decimals = (this.panelCtrl.decimals || -1) + 1; countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); - bucketBoundFormatter = this.bucketBoundFormatter(decimals, this.panelCtrl.scaledDecimals + 2); + bucketBoundFormatter = this.panelCtrl.tickValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); } let tooltipHtml = `
${time}
@@ -116,19 +116,13 @@ export class HeatmapTooltip { if (yData) { if (yData.bounds) { if (data.tsBuckets) { - const decimals = this.panelCtrl.decimals || 0; + // Use Y-axis labels const tickFormatter = valIndex => { - let valueFormatted = data.tsBuckets[valIndex]; - if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { - // Try to format numeric tick labels - valueFormatted = this.bucketBoundFormatter(decimals)(_.toNumber(valueFormatted)); - } - return valueFormatted; + return data.tsBucketsFormatted ? data.tsBucketsFormatted[valIndex] : data.tsBuckets[valIndex]; }; - const tsBucketsTickFormatter = tickFormatter.bind(this); - boundBottom = tsBucketsTickFormatter(yBucketIndex); - boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tsBucketsTickFormatter(yBucketIndex + 1) : ''; + boundBottom = tickFormatter(yBucketIndex); + boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : ''; } else { // Display 0 if bucket is a special 'zero' bucket let bottom = yData.y ? yData.bounds.bottom : 0; @@ -282,21 +276,9 @@ export class HeatmapTooltip { } countValueFormatter(decimals, scaledDecimals = null) { - let format = 'none'; + let format = 'short'; return function(value) { return kbn.valueFormats[format](value, decimals, scaledDecimals); }; } - - bucketBoundFormatter(decimals, scaledDecimals = null) { - let format = this.panel.yAxis.format; - return function(value) { - try { - return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; - } catch (err) { - console.error(err.message || err); - return value; - } - }; - } } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 0ccba2952cd..55dd91f4b63 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -4,7 +4,7 @@ import moment from 'moment'; import * as d3 from 'd3'; import kbn from 'app/core/utils/kbn'; import { appEvents, contextSrv } from 'app/core/core'; -import { tickStep, getScaledDecimals, getFlotTickSize } from 'app/core/utils/ticks'; +import * as ticksUtils from 'app/core/utils/ticks'; import { HeatmapTooltip } from './heatmap_tooltip'; import { mergeZeroBuckets } from './heatmap_data_converter'; import { getColorScale, getOpacityScale } from './color_scale'; @@ -108,7 +108,7 @@ export default function link(scope, elem, attrs, ctrl) { .range([0, chartWidth]); let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; - let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to); let timeFormat; let dashboardTimeZone = ctrl.dashboard.getTimezone(); if (dashboardTimeZone === 'utc') { @@ -141,7 +141,7 @@ export default function link(scope, elem, attrs, ctrl) { function addYAxis() { let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX); - let tick_interval = tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); + let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval); // Rewrite min and max if it have been set explicitly @@ -149,14 +149,14 @@ export default function link(scope, elem, attrs, ctrl) { y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max; // Adjust ticks after Y range widening - tick_interval = tickStep(y_min, y_max, ticks); + tick_interval = ticksUtils.tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); - let decimalsAuto = getPrecision(tick_interval); + let decimalsAuto = ticksUtils.getPrecision(tick_interval); let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto); - let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); + let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); + let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); ctrl.decimals = decimals; ctrl.scaledDecimals = scaledDecimals; @@ -248,12 +248,12 @@ export default function link(scope, elem, attrs, ctrl) { let domain = yScale.domain(); let tick_values = logScaleTickValues(domain, log_base); - let decimalsAuto = getPrecision(y_min); + let decimalsAuto = ticksUtils.getPrecision(y_min); let decimals = panel.yAxis.decimals || decimalsAuto; // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js) - let flot_tick_size = getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); - let scaledDecimals = getScaledDecimals(decimals, flot_tick_size); + let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); + let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); ctrl.decimals = decimals; ctrl.scaledDecimals = scaledDecimals; @@ -305,7 +305,7 @@ export default function link(scope, elem, attrs, ctrl) { .range([chartHeight, 0]); const tick_values = _.map(tsBuckets, (b, i) => i); - const decimalsAuto = _.max(_.map(tsBuckets, getStringPrecision)); + const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision)); const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals; ctrl.decimals = decimals; @@ -318,6 +318,9 @@ export default function link(scope, elem, attrs, ctrl) { return valueFormatted; } + const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i)); + data.tsBucketsFormatted = tsBucketsFormatted; + let yAxis = d3 .axisLeft(yScale) .tickValues(tick_values) @@ -361,11 +364,11 @@ export default function link(scope, elem, attrs, ctrl) { } function adjustLogMax(max, base) { - return Math.pow(base, Math.ceil(logp(max, base))); + return Math.pow(base, Math.ceil(ticksUtils.logp(max, base))); } function adjustLogMin(min, base) { - return Math.pow(base, Math.floor(logp(min, base))); + return Math.pow(base, Math.floor(ticksUtils.logp(min, base))); } function logScaleTickValues(domain, base) { @@ -374,14 +377,14 @@ export default function link(scope, elem, attrs, ctrl) { let tickValues = []; if (domainMin < 1) { - let under_one_ticks = Math.floor(logp(domainMin, base)); + let under_one_ticks = Math.floor(ticksUtils.logp(domainMin, base)); for (let i = under_one_ticks; i < 0; i++) { let tick_value = Math.pow(base, i); tickValues.push(tick_value); } } - let ticks = Math.ceil(logp(domainMax, base)); + let ticks = Math.ceil(ticksUtils.logp(domainMax, base)); for (let i = 0; i <= ticks; i++) { let tick_value = Math.pow(base, i); tickValues.push(tick_value); @@ -402,6 +405,8 @@ export default function link(scope, elem, attrs, ctrl) { }; } + ctrl.tickValueFormatter = tickValueFormatter; + function fixYAxisTickSize() { heatmap .select('.axis-y') @@ -827,46 +832,3 @@ export default function link(scope, elem, attrs, ctrl) { $heatmap.on('mousemove', onMouseMove); $heatmap.on('mouseleave', onMouseLeave); } - -function grafanaTimeFormat(ticks, min, max) { - if (min && max && ticks) { - let range = max - min; - let secPerTick = range / ticks / 1000; - let oneDay = 86400000; - let oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; -} - -function logp(value, base) { - return Math.log(value) / Math.log(base); -} - -function getPrecision(num: number): number { - let str = num.toString(); - return getStringPrecision(str); -} - -function getStringPrecision(num: string): number { - let dot_index = num.indexOf('.'); - if (dot_index === -1) { - return 0; - } else { - return num.length - dot_index - 1; - } -} From 5e452e445c92f7142e74f08276035ef800a80f99 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 7 Mar 2018 16:57:46 +0300 Subject: [PATCH 19/24] heatmap: able to set upper/lower bucket bound manually --- public/app/plugins/panel/heatmap/axes_editor.ts | 7 +++++++ public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 7 +++++-- .../app/plugins/panel/heatmap/partials/axes_editor.html | 9 +++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/axes_editor.ts b/public/app/plugins/panel/heatmap/axes_editor.ts index 05ed63e0289..81df957e2ea 100644 --- a/public/app/plugins/panel/heatmap/axes_editor.ts +++ b/public/app/plugins/panel/heatmap/axes_editor.ts @@ -6,6 +6,7 @@ export class AxesEditorCtrl { unitFormats: any; logScales: any; dataFormats: any; + yBucketBoundModes: any; /** @ngInject */ constructor($scope, uiSegmentSrv) { @@ -26,6 +27,12 @@ export class AxesEditorCtrl { 'Time series': 'timeseries', 'Time series buckets': 'tsbuckets', }; + + this.yBucketBoundModes = { + Auto: 'auto', + Upper: 'upper', + Lower: 'lower', + }; } setUnitFormat(subItem) { diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 47d3e6616b9..11fbad47b99 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -33,6 +33,7 @@ let panelDefaults = { show: false, }, dataFormat: 'timeseries', + yBucketBound: 'auto', xAxis: { show: true, }, @@ -222,11 +223,13 @@ export class HeatmapCtrl extends MetricsPanelCtrl { bucketsData = histogramToHeatmap(this.series); tsBuckets = _.map(this.series, 'label'); - if (panelDatasource === 'prometheus') { + const yBucketBound = this.panel.yBucketBound; + if ((panelDatasource === 'prometheus' && yBucketBound !== 'lower') || yBucketBound === 'upper') { // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label. tsBuckets = [''].concat(tsBuckets); } else { - // Elasticsearch uses labels as bottom bucket bounds, so add empty top bucket label. + // Elasticsearch uses labels as lower bucket bounds, so add empty top bucket label. + // Use this as a default mode as well. tsBuckets.push(''); } diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html index d97bb0361d0..65a5ab18080 100644 --- a/public/app/plugins/panel/heatmap/partials/axes_editor.html +++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html @@ -31,6 +31,15 @@ bs-tooltip="'Override automatic decimal precision for axis.'" ng-model="ctrl.panel.yAxis.decimals" ng-change="ctrl.render()" ng-model-onblur> +
+ +
+ +
+
From 759e05d09e895af8f87d3d051fd9f368114c0777 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 7 Mar 2018 17:08:34 +0300 Subject: [PATCH 20/24] heatmap: add explanation of Time series buckets mode --- public/app/plugins/panel/heatmap/partials/axes_editor.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html index 65a5ab18080..0b12ac52e24 100644 --- a/public/app/plugins/panel/heatmap/partials/axes_editor.html +++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html @@ -93,7 +93,9 @@
- +
From 425d2cfd3a9a6f114a53498734ce468d069aaaa0 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 7 Mar 2018 21:18:35 +0300 Subject: [PATCH 21/24] docker: add prometheus/example-golang-random to docker-compose blocks --- docker/blocks/prometheus/docker-compose.yaml | 6 ++++++ docker/blocks/prometheus/prometheus.yml | 8 ++++++-- docker/blocks/prometheus2/docker-compose.yaml | 6 ++++++ docker/blocks/prometheus2/prometheus.yml | 8 ++++++-- .../blocks/prometheus_random_data/Dockerfile | 18 ++++++++++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 docker/blocks/prometheus_random_data/Dockerfile diff --git a/docker/blocks/prometheus/docker-compose.yaml b/docker/blocks/prometheus/docker-compose.yaml index ccb1238a179..a65bb9a9e4f 100644 --- a/docker/blocks/prometheus/docker-compose.yaml +++ b/docker/blocks/prometheus/docker-compose.yaml @@ -23,3 +23,9 @@ network_mode: host ports: - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + network_mode: host + ports: + - "8080:8080" diff --git a/docker/blocks/prometheus/prometheus.yml b/docker/blocks/prometheus/prometheus.yml index ae40dfdf067..2a6579e691e 100644 --- a/docker/blocks/prometheus/prometheus.yml +++ b/docker/blocks/prometheus/prometheus.yml @@ -25,11 +25,15 @@ scrape_configs: - job_name: 'node_exporter' static_configs: - targets: ['127.0.0.1:9100'] - + - job_name: 'fake-data-gen' static_configs: - targets: ['127.0.0.1:9091'] - + - job_name: 'grafana' static_configs: - targets: ['127.0.0.1:3000'] + + - job_name: 'prometheus-random-data' + static_configs: + - targets: ['127.0.0.1:8080'] diff --git a/docker/blocks/prometheus2/docker-compose.yaml b/docker/blocks/prometheus2/docker-compose.yaml index 7b133ed9000..68c0358b7d0 100644 --- a/docker/blocks/prometheus2/docker-compose.yaml +++ b/docker/blocks/prometheus2/docker-compose.yaml @@ -23,3 +23,9 @@ network_mode: host ports: - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + network_mode: host + ports: + - "8080:8080" diff --git a/docker/blocks/prometheus2/prometheus.yml b/docker/blocks/prometheus2/prometheus.yml index 83dda78bb3c..57232aaa439 100644 --- a/docker/blocks/prometheus2/prometheus.yml +++ b/docker/blocks/prometheus2/prometheus.yml @@ -25,11 +25,15 @@ scrape_configs: - job_name: 'node_exporter' static_configs: - targets: ['127.0.0.1:9100'] - + - job_name: 'fake-data-gen' static_configs: - targets: ['127.0.0.1:9091'] - + - job_name: 'grafana' static_configs: - targets: ['127.0.0.1:3000'] + + - job_name: 'prometheus-random-data' + static_configs: + - targets: ['127.0.0.1:8080'] diff --git a/docker/blocks/prometheus_random_data/Dockerfile b/docker/blocks/prometheus_random_data/Dockerfile new file mode 100644 index 00000000000..3aad497c94d --- /dev/null +++ b/docker/blocks/prometheus_random_data/Dockerfile @@ -0,0 +1,18 @@ +# This Dockerfile builds an image for a client_golang example. + +# Builder image, where we build the example. +FROM golang:1.9.0 AS builder +# Download prometheus/client_golang/examples/random first +RUN go get github.com/prometheus/client_golang/examples/random +WORKDIR /go/src/github.com/prometheus/client_golang +WORKDIR /go/src/github.com/prometheus/client_golang/prometheus +RUN go get -d +WORKDIR /go/src/github.com/prometheus/client_golang/examples/random +RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' + +# Final image. +FROM scratch +LABEL maintainer "The Prometheus Authors " +COPY --from=builder /go/src/github.com/prometheus/client_golang/examples/random . +EXPOSE 8080 +ENTRYPOINT ["/random"] From 479209f4832ce306d76bfb828ad5bd300c14ae8f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 12 Mar 2018 17:13:05 +0300 Subject: [PATCH 22/24] prometheus: datasource refactor --- .../datasource/prometheus/datasource.ts | 210 ++---------------- .../prometheus/result_transformer.ts | 199 +++++++++++++++++ .../prometheus/specs/datasource_specs.ts | 50 +---- .../specs/result_transformer.jest.ts | 78 +++++++ 4 files changed, 293 insertions(+), 244 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/result_transformer.ts create mode 100644 public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 067c85cc3a4..9ad720f8917 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; -import TableModel from 'app/core/table_model'; +import { ResultTransformer } from './result_transformer'; function prometheusSpecialRegexEscape(value) { return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&'); @@ -20,6 +20,7 @@ export class PrometheusDatasource { withCredentials: any; metricsNameCache: any; interval: string; + resultTransformer: ResultTransformer; /** @ngInject */ constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { @@ -32,6 +33,7 @@ export class PrometheusDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; + this.resultTransformer = new ResultTransformer(templateSrv); } _request(method, url, requestId?) { @@ -73,7 +75,6 @@ export class PrometheusDatasource { } query(options) { - var self = this; var start = this.getPrometheusTime(options.range.from, false); var end = this.getPrometheusTime(options.range.to, true); var range = Math.ceil(end - start); @@ -113,29 +114,17 @@ export class PrometheusDatasource { throw response.error; } - let prometheusResult = response.data.data.result; + let transformerOptions = { + format: activeTargets[index].format, + step: queries[index].step, + legendFormat: activeTargets[index].legendFormat, + start: start, + end: end, + responseListLength: responseList.length, + responseIndex: index, + }; - if (activeTargets[index].format === 'table') { - result.push(self.transformMetricDataToTable(prometheusResult, responseList.length, index)); - } else if (activeTargets[index].format === 'heatmap') { - let seriesList = []; - prometheusResult.sort(sortSeriesByLabel); - for (let metricData of prometheusResult) { - seriesList.push( - self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step) - ); - } - seriesList = self.transformToHistogramOverTime(seriesList); - result.push(...seriesList); - } else { - for (let metricData of prometheusResult) { - if (response.data.data.resultType === 'matrix') { - result.push(self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step)); - } else if (response.data.data.resultType === 'vector') { - result.push(self.transformInstantMetricData(metricData, activeTargets[index])); - } - } - } + this.resultTransformer.transform(result, response, transformerOptions); }); return { data: result }; @@ -276,9 +265,9 @@ export class PrometheusDatasource { var event = { annotation: annotation, time: Math.floor(parseFloat(value[0])) * 1000, - title: self.renderTemplate(titleFormat, series.metric), + title: self.resultTransformer.renderTemplate(titleFormat, series.metric), tags: tags, - text: self.renderTemplate(textFormat, series.metric), + text: self.resultTransformer.renderTemplate(textFormat, series.metric), }; eventList.push(event); @@ -296,145 +285,6 @@ export class PrometheusDatasource { }); } - transformMetricData(md, options, start, end, step) { - var dps = [], - metricLabel = null; - - metricLabel = this.createMetricLabel(md.metric, options); - - var stepMs = step * 1000; - var baseTimestamp = start * 1000; - for (let value of md.values) { - var dp_value = parseFloat(value[1]); - if (_.isNaN(dp_value)) { - dp_value = null; - } - - var timestamp = parseFloat(value[0]) * 1000; - for (let t = baseTimestamp; t < timestamp; t += stepMs) { - dps.push([null, t]); - } - baseTimestamp = timestamp + stepMs; - dps.push([dp_value, timestamp]); - } - - var endTimestamp = end * 1000; - for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) { - dps.push([null, t]); - } - - return { target: metricLabel, datapoints: dps }; - } - - transformMetricDataToTable(md, resultCount: number, resultIndex: number) { - var table = new TableModel(); - var i, j; - var metricLabels = {}; - - if (md.length === 0) { - return table; - } - - // Collect all labels across all metrics - _.each(md, function(series) { - for (var label in series.metric) { - if (!metricLabels.hasOwnProperty(label)) { - metricLabels[label] = 1; - } - } - }); - - // Sort metric labels, create columns for them and record their index - var sortedLabels = _.keys(metricLabels).sort(); - table.columns.push({ text: 'Time', type: 'time' }); - _.each(sortedLabels, function(label, labelIndex) { - metricLabels[label] = labelIndex + 1; - table.columns.push({ text: label }); - }); - let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value'; - table.columns.push({ text: valueText }); - - // Populate rows, set value to empty string when label not present. - _.each(md, function(series) { - if (series.value) { - series.values = [series.value]; - } - if (series.values) { - for (i = 0; i < series.values.length; i++) { - var values = series.values[i]; - var reordered: any = [values[0] * 1000]; - if (series.metric) { - for (j = 0; j < sortedLabels.length; j++) { - var label = sortedLabels[j]; - if (series.metric.hasOwnProperty(label)) { - reordered.push(series.metric[label]); - } else { - reordered.push(''); - } - } - } - reordered.push(parseFloat(values[1])); - table.rows.push(reordered); - } - } - }); - - return table; - } - - transformInstantMetricData(md, options) { - var dps = [], - metricLabel = null; - metricLabel = this.createMetricLabel(md.metric, options); - dps.push([parseFloat(md.value[1]), md.value[0] * 1000]); - return { target: metricLabel, datapoints: dps }; - } - - transformToHistogramOverTime(seriesList, options?) { - /* t1 = timestamp1, t2 = timestamp2 etc. - t1 t2 t3 t1 t2 t3 - le10 10 10 0 => 10 10 0 - le20 20 10 30 => 10 0 30 - le30 30 10 35 => 10 0 5 - */ - for (let i = seriesList.length - 1; i > 0; i--) { - let topSeries = seriesList[i].datapoints; - let bottomSeries = seriesList[i - 1].datapoints; - for (let j = 0; j < topSeries.length; j++) { - topSeries[j][0] -= bottomSeries[j][0]; - } - } - - return seriesList; - } - - createMetricLabel(labelData, options) { - if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { - return this.getOriginalMetricName(labelData); - } - - return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}'; - } - - renderTemplate(aliasPattern, aliasData) { - var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g; - return aliasPattern.replace(aliasRegex, function(match, g1) { - if (aliasData[g1]) { - return aliasData[g1]; - } - return g1; - }); - } - - getOriginalMetricName(labelData) { - var metricName = labelData.__name__ || ''; - delete labelData.__name__; - var labelPart = _.map(_.toPairs(labelData), function(label) { - return label[0] + '="' + label[1] + '"'; - }).join(','); - return metricName + '{' + labelPart + '}'; - } - getPrometheusTime(date, roundUp) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); @@ -442,33 +292,3 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } } - -function sortSeriesByLabel(s1, s2) { - let le1, le2; - - try { - // fail if not integer. might happen with bad queries - le1 = parseHistogramLabel(s1.metric.le); - le2 = parseHistogramLabel(s2.metric.le); - } catch (err) { - console.log(err); - return 0; - } - - if (le1 > le2) { - return 1; - } - - if (le1 < le2) { - return -1; - } - - return 0; -} - -function parseHistogramLabel(le: string): number { - if (le === '+Inf') { - return +Infinity; - } - return Number(le); -} diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts new file mode 100644 index 00000000000..6d97b783983 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -0,0 +1,199 @@ +import _ from 'lodash'; +import TableModel from 'app/core/table_model'; + +export class ResultTransformer { + constructor(private templateSrv) {} + + transform(result: any, response: any, options: any) { + let prometheusResult = response.data.data.result; + + if (options.format === 'table') { + result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.responseIndex)); + } else if (options.format === 'heatmap') { + let seriesList = []; + prometheusResult.sort(sortSeriesByLabel); + for (let metricData of prometheusResult) { + seriesList.push(this.transformMetricData(metricData, options, options.start, options.end)); + } + seriesList = this.transformToHistogramOverTime(seriesList); + result.push(...seriesList); + } else { + for (let metricData of prometheusResult) { + if (response.data.data.resultType === 'matrix') { + result.push(this.transformMetricData(metricData, options, options.start, options.end)); + } else if (response.data.data.resultType === 'vector') { + result.push(this.transformInstantMetricData(metricData, options)); + } + } + } + } + + transformMetricData(md, options, start, end) { + let dps = [], + metricLabel = null; + + metricLabel = this.createMetricLabel(md.metric, options); + + const stepMs = parseInt(options.step) * 1000; + let baseTimestamp = start * 1000; + for (let value of md.values) { + let dp_value = parseFloat(value[1]); + if (_.isNaN(dp_value)) { + dp_value = null; + } + + const timestamp = parseFloat(value[0]) * 1000; + for (let t = baseTimestamp; t < timestamp; t += stepMs) { + dps.push([null, t]); + } + baseTimestamp = timestamp + stepMs; + dps.push([dp_value, timestamp]); + } + + const endTimestamp = end * 1000; + for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) { + dps.push([null, t]); + } + + return { target: metricLabel, datapoints: dps }; + } + + transformMetricDataToTable(md, resultCount: number, resultIndex: number) { + var table = new TableModel(); + var i, j; + var metricLabels = {}; + + if (md.length === 0) { + return table; + } + + // Collect all labels across all metrics + _.each(md, function(series) { + for (var label in series.metric) { + if (!metricLabels.hasOwnProperty(label)) { + metricLabels[label] = 1; + } + } + }); + + // Sort metric labels, create columns for them and record their index + var sortedLabels = _.keys(metricLabels).sort(); + table.columns.push({ text: 'Time', type: 'time' }); + _.each(sortedLabels, function(label, labelIndex) { + metricLabels[label] = labelIndex + 1; + table.columns.push({ text: label }); + }); + let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value'; + table.columns.push({ text: valueText }); + + // Populate rows, set value to empty string when label not present. + _.each(md, function(series) { + if (series.value) { + series.values = [series.value]; + } + if (series.values) { + for (i = 0; i < series.values.length; i++) { + var values = series.values[i]; + var reordered: any = [values[0] * 1000]; + if (series.metric) { + for (j = 0; j < sortedLabels.length; j++) { + var label = sortedLabels[j]; + if (series.metric.hasOwnProperty(label)) { + reordered.push(series.metric[label]); + } else { + reordered.push(''); + } + } + } + reordered.push(parseFloat(values[1])); + table.rows.push(reordered); + } + } + }); + + return table; + } + + transformInstantMetricData(md, options) { + var dps = [], + metricLabel = null; + metricLabel = this.createMetricLabel(md.metric, options); + dps.push([parseFloat(md.value[1]), md.value[0] * 1000]); + return { target: metricLabel, datapoints: dps }; + } + + createMetricLabel(labelData, options) { + if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { + return this.getOriginalMetricName(labelData); + } + + return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}'; + } + + renderTemplate(aliasPattern, aliasData) { + var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g; + return aliasPattern.replace(aliasRegex, function(match, g1) { + if (aliasData[g1]) { + return aliasData[g1]; + } + return g1; + }); + } + + getOriginalMetricName(labelData) { + var metricName = labelData.__name__ || ''; + delete labelData.__name__; + var labelPart = _.map(_.toPairs(labelData), function(label) { + return label[0] + '="' + label[1] + '"'; + }).join(','); + return metricName + '{' + labelPart + '}'; + } + + transformToHistogramOverTime(seriesList) { + /* t1 = timestamp1, t2 = timestamp2 etc. + t1 t2 t3 t1 t2 t3 + le10 10 10 0 => 10 10 0 + le20 20 10 30 => 10 0 30 + le30 30 10 35 => 10 0 5 + */ + for (let i = seriesList.length - 1; i > 0; i--) { + let topSeries = seriesList[i].datapoints; + let bottomSeries = seriesList[i - 1].datapoints; + for (let j = 0; j < topSeries.length; j++) { + topSeries[j][0] -= bottomSeries[j][0]; + } + } + + return seriesList; + } +} + +function sortSeriesByLabel(s1, s2): number { + let le1, le2; + + try { + // fail if not integer. might happen with bad queries + le1 = parseHistogramLabel(s1.metric.le); + le2 = parseHistogramLabel(s2.metric.le); + } catch (err) { + console.log(err); + return 0; + } + + if (le1 > le2) { + return 1; + } + + if (le1 < le2) { + return -1; + } + + return 0; +} + +function parseHistogramLabel(le: string): number { + if (le === '+Inf') { + return +Infinity; + } + return Number(le); +} diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 043bfcf25e0..55dd7ef7d42 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -223,43 +223,6 @@ describe('PrometheusDatasource', function() { expect(results[0].time).to.be(1443454528 * 1000); }); }); - describe('When resultFormat is table', function() { - var response = { - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { __name__: 'test', job: 'testjob' }, - values: [[1443454528, '3846']], - }, - { - metric: { - __name__: 'test', - instance: 'localhost:8080', - job: 'otherjob', - }, - values: [[1443454529, '3847']], - }, - ], - }, - }; - it('should return table model', function() { - var table = ctx.ds.transformMetricDataToTable(response.data.result); - expect(table.type).to.be('table'); - expect(table.rows).to.eql([ - [1443454528000, 'test', '', 'testjob', 3846], - [1443454529000, 'test', 'localhost:8080', 'otherjob', 3847], - ]); - expect(table.columns).to.eql([ - { text: 'Time', type: 'time' }, - { text: '__name__' }, - { text: 'instance' }, - { text: 'job' }, - { text: 'Value' }, - ]); - }); - }); describe('When resultFormat is table and instant = true', function() { var results; @@ -293,19 +256,8 @@ describe('PrometheusDatasource', function() { it('should return result', () => { expect(results).not.to.be(null); }); - - it('should return table model', function() { - var table = ctx.ds.transformMetricDataToTable(response.data.result); - expect(table.type).to.be('table'); - expect(table.rows).to.eql([[1443454528000, 'test', 'testjob', 3846]]); - expect(table.columns).to.eql([ - { text: 'Time', type: 'time' }, - { text: '__name__' }, - { text: 'job' }, - { text: 'Value' }, - ]); - }); }); + describe('The "step" query parameter', function() { var response = { status: 'success', diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts new file mode 100644 index 00000000000..35e9780badd --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts @@ -0,0 +1,78 @@ +import { ResultTransformer } from '../result_transformer'; + +describe('Prometheus Result Transformer', () => { + let ctx: any = {}; + + beforeEach(() => { + ctx.templateSrv = { + replace: str => str, + }; + ctx.resultTransformer = new ResultTransformer(ctx.templateSrv); + }); + + describe('When resultFormat is table', () => { + var response = { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[1443454528, '3846']], + }, + { + metric: { + __name__: 'test', + instance: 'localhost:8080', + job: 'otherjob', + }, + values: [[1443454529, '3847']], + }, + ], + }, + }; + + it('should return table model', () => { + var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); + expect(table.type).toBe('table'); + expect(table.rows).toEqual([ + [1443454528000, 'test', '', 'testjob', 3846], + [1443454529000, 'test', 'localhost:8080', 'otherjob', 3847], + ]); + expect(table.columns).toEqual([ + { text: 'Time', type: 'time' }, + { text: '__name__' }, + { text: 'instance' }, + { text: 'job' }, + { text: 'Value' }, + ]); + }); + }); + + describe('When resultFormat is table and instant = true', () => { + var response = { + status: 'success', + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + value: [1443454528, '3846'], + }, + ], + }, + }; + + it('should return table model', () => { + var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); + expect(table.type).toBe('table'); + expect(table.rows).toEqual([[1443454528000, 'test', 'testjob', 3846]]); + expect(table.columns).toEqual([ + { text: 'Time', type: 'time' }, + { text: '__name__' }, + { text: 'job' }, + { text: 'Value' }, + ]); + }); + }); +}); From 46d2067af273301dff72b28df93f47fd0250a758 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 12 Mar 2018 17:37:21 +0300 Subject: [PATCH 23/24] prometheus: add tests for heatmap mode --- .../specs/result_transformer.jest.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts index 35e9780badd..abcc46d7ea8 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts @@ -75,4 +75,44 @@ describe('Prometheus Result Transformer', () => { ]); }); }); + + describe('When resultFormat is heatmap', () => { + var response = { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob', le: '1' }, + values: [[1445000010, '10'], [1445000020, '10'], [1445000030, '0']], + }, + { + metric: { __name__: 'test', job: 'testjob', le: '2' }, + values: [[1445000010, '20'], [1445000020, '10'], [1445000030, '30']], + }, + { + metric: { __name__: 'test', job: 'testjob', le: '3' }, + values: [[1445000010, '30'], [1445000020, '10'], [1445000030, '40']], + }, + ], + }, + }; + + it('should convert cumulative histogram to regular', () => { + let result = []; + let options = { + format: 'heatmap', + start: 1445000010, + end: 1445000030, + legendFormat: '{{le}}', + }; + + ctx.resultTransformer.transform(result, { data: response }, options); + expect(result).toEqual([ + { target: '1', datapoints: [[10, 1445000010000], [10, 1445000020000], [0, 1445000030000]] }, + { target: '2', datapoints: [[10, 1445000010000], [0, 1445000020000], [30, 1445000030000]] }, + { target: '3', datapoints: [[10, 1445000010000], [0, 1445000020000], [10, 1445000030000]] }, + ]); + }); + }); }); From 989ba9763f2908a7a663c3558808d9dd1ff2efd8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 12 Mar 2018 19:41:07 +0300 Subject: [PATCH 24/24] prometheus: fix bug introduced by #9859 (httpMethod is undefined) --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ca14e83e8fb..4c736f2c664 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -35,7 +35,7 @@ export class PrometheusDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; - this.httpMethod = instanceSettings.jsonData.httpMethod; + this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; this.resultTransformer = new ResultTransformer(templateSrv); }