From cf1be5fdfc7a2ff27022473dd26963da73c03aa5 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 8 Jun 2017 23:52:07 +0900 Subject: [PATCH 01/49] support multiple histogram series --- .../app/plugins/panel/graph/data_processor.ts | 17 ++++--- public/app/plugins/panel/graph/graph.ts | 18 +++++--- public/app/plugins/panel/graph/histogram.ts | 45 ++++++++++++++++--- .../plugins/panel/graph/specs/graph_specs.ts | 44 ++++++++++++++++++ .../panel/graph/specs/histogram.jest.ts | 6 +-- .../app/plugins/panel/graph/tab_display.html | 2 +- 6 files changed, 110 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 2930c871bc9..f8162c57a10 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -29,12 +29,17 @@ export class DataProcessor { }); } case 'histogram': { - let histogramDataList = [ - { - target: 'count', - datapoints: _.concat([], _.flatten(_.map(options.dataList, 'datapoints'))), - }, - ]; + let histogramDataList; + if (this.panel.stack) { + histogramDataList = options.dataList; + } else { + histogramDataList = [ + { + target: 'count', + datapoints: _.concat([], _.flatten(_.map(options.dataList, 'datapoints'))), + }, + ]; + } return histogramDataList.map((item, index) => { return this.timeSeriesHandler(item, index, options); }); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3ed8cbc1836..5edd5c38aac 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -17,7 +17,7 @@ import { appEvents, coreModule, updateLegendValues } from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; import { EventManager } from 'app/features/annotations/all'; -import { convertValuesToHistogram, getSeriesValues } from './histogram'; +import { convertToHistogramData } from './histogram'; import config from 'app/core/config'; /** @ngInject **/ @@ -236,15 +236,15 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { } case 'histogram': { let bucketSize: number; - let values = getSeriesValues(data); - if (data.length && values.length) { + if (data.length) { let histMin = _.min(_.map(data, s => s.stats.min)); let histMax = _.max(_.map(data, s => s.stats.max)); let ticks = panel.xaxis.buckets || panelWidth / 50; bucketSize = tickStep(histMin, histMax, ticks); - let histogram = convertValuesToHistogram(values, bucketSize); - data[0].data = histogram; + + data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, panel.stack, histMin, histMax); + options.series.bars.barWidth = bucketSize * 0.8; } else { bucketSize = 0; @@ -413,7 +413,13 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { let defaultTicks = panelWidth / 50; if (data.length && bucketSize) { - ticks = _.map(data[0].data, point => point[0]); + let tick_values = []; + for (let d of data) { + for (let point of d.data) { + tick_values[point[0]] = true; + } + } + ticks = Object.keys(tick_values).map(v => Number(v)); min = _.min(ticks); max = _.max(ticks); diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index 8b2be9efcf7..b8867c999cc 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -29,16 +29,22 @@ export function getSeriesValues(dataList: TimeSeries[]): number[] { * @param values * @param bucketSize */ -export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { +export function convertValuesToHistogram(values: number[], bucketSize: number, min: number, max: number): any[] { let histogram = {}; + let minBound = getBucketBound(min, bucketSize); + let maxBound = getBucketBound(max, bucketSize); + let bound = minBound; + let n = 0; + while (bound <= maxBound) { + histogram[bound] = 0; + bound = minBound + bucketSize * n; + n++; + } + for (let i = 0; i < values.length; i++) { let bound = getBucketBound(values[i], bucketSize); - if (histogram[bound]) { - histogram[bound] = histogram[bound] + 1; - } else { - histogram[bound] = 1; - } + histogram[bound] = histogram[bound] + 1; } let histogam_series = _.map(histogram, (count, bound) => { @@ -49,6 +55,33 @@ export function convertValuesToHistogram(values: number[], bucketSize: number): return _.sortBy(histogam_series, point => point[0]); } +/** + * Convert series into array of histogram data. + * @param data Array of series + * @param bucketSize + * @param stack + */ +export function convertToHistogramData( + data: any, + bucketSize: number, + hiddenSeries: any, + stack = false, + min: number, + max: number +): any[] { + return data.map(series => { + let values = getSeriesValues([series]); + series.histogram = true; + if (!hiddenSeries[series.alias]) { + let histogram = convertValuesToHistogram(values, bucketSize, min, max); + series.data = histogram; + } else { + series.data = []; + } + return series; + }); +} + function getBucketBound(value: number, bucketSize: number): number { return Math.floor(value / bucketSize) * bucketSize; } diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 176bb65b806..d29320a9d72 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -407,4 +407,48 @@ describe('grafanaGraph', function() { }, 10 ); + + graphScenario('when graph is histogram, and enable stack', function(ctx) { + ctx.setup(function(ctrl, data) { + ctrl.panel.xaxis.mode = 'histogram'; + ctrl.panel.stack = true; + ctrl.hiddenSeries = {}; + data[0] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series1', + }); + data[1] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series2', + }); + }); + + it('should calculate correct histogram', function() { + expect(ctx.plotData[0].data[0][0]).to.be(100); + expect(ctx.plotData[0].data[0][1]).to.be(2); + expect(ctx.plotData[1].data[0][0]).to.be(100); + expect(ctx.plotData[1].data[0][1]).to.be(2); + }); + }); + + graphScenario('when graph is histogram, and some series are hidden', function(ctx) { + ctx.setup(function(ctrl, data) { + ctrl.panel.xaxis.mode = 'histogram'; + ctrl.panel.stack = false; + ctrl.hiddenSeries = { series2: true }; + data[0] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series1', + }); + data[1] = new TimeSeries({ + datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]], + alias: 'series2', + }); + }); + + it('should calculate correct histogram', function() { + expect(ctx.plotData[0].data[0][0]).to.be(100); + expect(ctx.plotData[0].data[0][1]).to.be(2); + }); + }); }); diff --git a/public/app/plugins/panel/graph/specs/histogram.jest.ts b/public/app/plugins/panel/graph/specs/histogram.jest.ts index 4f0ca472375..0e9eaa8b98e 100644 --- a/public/app/plugins/panel/graph/specs/histogram.jest.ts +++ b/public/app/plugins/panel/graph/specs/histogram.jest.ts @@ -13,15 +13,15 @@ describe('Graph Histogam Converter', function() { bucketSize = 10; let expected = [[0, 2], [10, 3], [20, 2]]; - let histogram = convertValuesToHistogram(values, bucketSize); + let histogram = convertValuesToHistogram(values, bucketSize, 1, 29); expect(histogram).toMatchObject(expected); }); it('Should not add empty buckets', () => { bucketSize = 5; - let expected = [[0, 2], [10, 2], [15, 1], [20, 1], [25, 1]]; + let expected = [[0, 2], [5, 0], [10, 2], [15, 1], [20, 1], [25, 1]]; - let histogram = convertValuesToHistogram(values, bucketSize); + let histogram = convertValuesToHistogram(values, bucketSize, 1, 29); expect(histogram).toMatchObject(expected); }); }); diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index d5d93250e36..4c4aa7e81eb 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -71,7 +71,7 @@
Stacking & Null value
- + From 725e23ef7dc2f738eeffe5afde17341f85c41ef6 Mon Sep 17 00:00:00 2001 From: "Willy Hu (IS-TW)" Date: Mon, 12 Feb 2018 10:13:55 +0000 Subject: [PATCH 02/49] Cloudwatch dimension_values add dimension filter. issue #10029 e.g. - dimension_values($region, $namespace, cpu_usage_system, cpu) - dimension_values($region, $namespace, disk_used_percent, device, {"InstanceId": "$instance_id"}) - dimension_values($region, $namespace, disk_used_percent, path, {"InstanceId": "$instance_id", "device": "$device"}) --- .../app/plugins/datasource/cloudwatch/datasource.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index facddd2e18e..9103cfe17f5 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -212,6 +212,7 @@ export default class CloudWatchDatasource { var region; var namespace; var metricName; + var filterJson; var regionQuery = query.match(/^regions\(\)/); if (regionQuery) { @@ -237,14 +238,20 @@ export default class CloudWatchDatasource { return this.getDimensionKeys(namespace, region); } - var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/); + var dimensionValuesQuery = query.match( + /^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?(.+))?\)/ + ); if (dimensionValuesQuery) { region = dimensionValuesQuery[1]; namespace = dimensionValuesQuery[2]; metricName = dimensionValuesQuery[3]; var dimensionKey = dimensionValuesQuery[4]; + filterJson = {}; + if (dimensionValuesQuery[6]) { + filterJson = JSON.parse(this.templateSrv.replace(dimensionValuesQuery[6])); + } - return this.getDimensionValues(region, namespace, metricName, dimensionKey, {}); + return this.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson); } var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/); @@ -258,7 +265,7 @@ export default class CloudWatchDatasource { if (ec2InstanceAttributeQuery) { region = ec2InstanceAttributeQuery[1]; var targetAttributeName = ec2InstanceAttributeQuery[2]; - var filterJson = JSON.parse(this.templateSrv.replace(ec2InstanceAttributeQuery[3])); + filterJson = JSON.parse(this.templateSrv.replace(ec2InstanceAttributeQuery[3])); return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson); } From 417df1eae531d2d6afe697143b7318f1b4d400d8 Mon Sep 17 00:00:00 2001 From: "Willy Hu (IS-TW)" Date: Wed, 21 Feb 2018 10:51:28 +0800 Subject: [PATCH 03/49] docs: updated cloudwatch docs add dimension filter as a option for dimension_values query. --- docs/sources/features/datasources/cloudwatch.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 648957ed96e..005d4b287dd 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -87,7 +87,7 @@ Name | Description *namespaces()* | Returns a list of namespaces CloudWatch support. *metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region or use "default" for custom metrics) *dimension_keys(namespace)* | Returns a list of dimension keys in the namespace. -*dimension_values(region, namespace, metric, dimension_key)* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. +*dimension_values(region, namespace, metric, dimension_key, [filters])* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric`, `dimension_key` or you can use dimension `filters` to get more specific result as well. *ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`. *ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attributes matching the specified `region`, `attribute_name`, `filters`. @@ -104,6 +104,7 @@ Query | Service *dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)* | RedShift *dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS *dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3 +*dimension_values(us-east-1,CWAgent,disk_used_percent,device,{"InstanceId":"$instance_id"})* | CloudWatch Agent ## ec2_instance_attribute examples From efa869bb89dc806d52133d5ef3d44b7220288e21 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Feb 2018 15:26:45 +0300 Subject: [PATCH 04/49] 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 05/49] 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 06/49] 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 07/49] 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 08/49] 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 09/49] 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 10/49] 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 11/49] 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 12/49] 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 13/49] 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 14/49] 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 15/49] 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 16/49] 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 17/49] 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 18/49] 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 19/49] 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 20/49] 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 21/49] 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 22/49] 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 a94de51e5eeb565f8c6b7cbd3b91f160db7403a6 Mon Sep 17 00:00:00 2001 From: Aman Date: Wed, 7 Mar 2018 19:36:42 +0530 Subject: [PATCH 23/49] Add color to prefix and postfix in singlestat --- public/app/plugins/panel/singlestat/module.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 611a9a55287..776033536dd 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -426,14 +426,16 @@ class SingleStatCtrl extends MetricsPanelCtrl { var body = '
'; if (panel.prefix) { - body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.prefix); + var prefix = applyColoringThresholds(data.value, panel.prefix); + body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix); } var value = applyColoringThresholds(data.value, data.valueFormatted); body += getSpan('singlestat-panel-value', panel.valueFontSize, value); if (panel.postfix) { - body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); + var postfix = applyColoringThresholds(data.value, panel.postfix); + body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix); } body += '
'; From 759e05d09e895af8f87d3d051fd9f368114c0777 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 7 Mar 2018 17:08:34 +0300 Subject: [PATCH 24/49] 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 25/49] 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 6bfed903c4262eb26087d1b9c7eee32a2793c915 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Fri, 9 Mar 2018 17:20:24 +0000 Subject: [PATCH 26/49] Adding Timeticks unit --- public/app/core/utils/kbn.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index ed8acb73cbb..43c0a74bd01 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -783,6 +783,10 @@ kbn.valueFormats.dtdurations = function(size, decimals) { return kbn.toDuration(size, decimals, 'second'); }; +kbn.valueFormats.timeticks = function(size, decimals, scaledDecimals) { + return kbn.valueFormats.s(size / 100, decimals, scaledDecimals); +}; + kbn.valueFormats.dateTimeAsIso = function(epoch) { var time = moment(epoch); @@ -854,6 +858,7 @@ kbn.getUnitFormats = function() { { text: 'days (d)', value: 'd' }, { text: 'duration (ms)', value: 'dtdurationms' }, { text: 'duration (s)', value: 'dtdurations' }, + { text: 'Timeticks (s/100)', value: 'timeticks' }, ], }, { From a83ede01930185e07368bfe151bcdc21e9be23fd Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Sun, 12 Nov 2017 16:38:10 +0900 Subject: [PATCH 27/49] support POST for query and query_range --- pkg/api/pluginproxy/ds_proxy.go | 10 ++- .../datasource/prometheus/datasource.ts | 58 +++++++++++---- .../prometheus/partials/config.html | 18 +++-- .../prometheus/specs/datasource_specs.ts | 70 ++++++++++++++++++- .../specs/metric_find_query_specs.ts | 2 +- 5 files changed, 135 insertions(+), 23 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index ee2eba3b3b4..b861a344c75 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -189,8 +189,14 @@ func (proxy *DataSourceProxy) validateRequest() error { } if proxy.ds.Type == m.DS_PROMETHEUS { - if proxy.ctx.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxy.proxyPath, "api/") { - return errors.New("GET is only allowed on proxied Prometheus datasource") + if proxy.ctx.Req.Request.Method == "DELETE" { + return errors.New("Deletes not allowed on proxied Prometheus datasource") + } + if proxy.ctx.Req.Request.Method == "PUT" { + return errors.New("Puts not allowed on proxied Prometheus datasource") + } + if proxy.ctx.Req.Request.Method == "POST" && !(proxy.proxyPath == "api/v1/query" || proxy.proxyPath == "api/v1/query_range") { + return errors.New("Posts not allowed on proxied Prometheus datasource except on /query and /query_range") } } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 122fe9601a1..85a0f733fd2 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; +import $ from 'jquery'; import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; @@ -20,6 +21,7 @@ export class PrometheusDatasource { withCredentials: any; metricsNameCache: any; interval: string; + httpMethod: string; /** @ngInject */ constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { @@ -32,14 +34,33 @@ export class PrometheusDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; + this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; } - _request(method, url, requestId?) { + _request(method, url, data?, requestId?) { var options: any = { url: this.url + url, method: method, requestId: requestId, }; + if (method === 'GET') { + if (!_.isEmpty(data)) { + options.url = + options.url + + '?' + + _.map(data, (v, k) => { + return encodeURIComponent(k) + '=' + encodeURIComponent(v); + }).join('&'); + } + } else { + options.headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + options.transformRequest = data => { + return $.param(data); + }; + options.data = data; + } if (this.basicAuth || this.withCredentials) { options.withCredentials = true; @@ -173,21 +194,23 @@ export class PrometheusDatasource { throw { message: 'Invalid time range' }; } - var url = - '/api/v1/query_range?query=' + - encodeURIComponent(query.expr) + - '&start=' + - start + - '&end=' + - end + - '&step=' + - query.step; - return this._request('GET', url, query.requestId); + var url = '/api/v1/query_range'; + var data = { + query: query.expr, + start: start, + end: end, + step: query.step, + }; + return this._request(this.httpMethod, url, data, query.requestId); } performInstantQuery(query, time) { - var url = '/api/v1/query?query=' + encodeURIComponent(query.expr) + '&time=' + time; - return this._request('GET', url, query.requestId); + var url = '/api/v1/query'; + var data = { + query: query.expr, + time: time, + }; + return this._request(this.httpMethod, url, data, query.requestId); } performSuggestQuery(query, cache = false) { @@ -279,8 +302,13 @@ export class PrometheusDatasource { } testDatasource() { - return this.metricFindQuery('metrics(.*)').then(function() { - return { status: 'success', message: 'Data source is working' }; + let now = new Date().getTime(); + return this.performInstantQuery({ expr: '1+1' }, now / 1000).then(response => { + if (response.data.status === 'success') { + return { status: 'success', message: 'Data source is working' }; + } else { + return { status: 'error', message: response.error }; + } }); } diff --git a/public/app/plugins/datasource/prometheus/partials/config.html b/public/app/plugins/datasource/prometheus/partials/config.html index 3bb43253d4d..2cd6adcbc4d 100644 --- a/public/app/plugins/datasource/prometheus/partials/config.html +++ b/public/app/plugins/datasource/prometheus/partials/config.html @@ -4,13 +4,23 @@
- Scrape interval - + Scrape interval + - Set this to your global scrape interval defined in your Prometheus config file. This will be used as a lower limit for + Set this to your global scrape interval defined in your Prometheus config file. This will be used as a lower limit for the Prometheus step query parameter.
-
+
+ +
+ +
+ + + Specify the HTTP Method to query Prometheus. (POST is only available in Prometheus >= v2.1.0) + +
+
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 043bfcf25e0..4df79ed4ef6 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -1,5 +1,6 @@ import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; +import $ from 'jquery'; import helpers from 'test/specs/helpers'; import { PrometheusDatasource } from '../datasource'; @@ -10,7 +11,7 @@ describe('PrometheusDatasource', function() { directUrl: 'direct', user: 'test', password: 'mupp', - jsonData: {}, + jsonData: { httpMethod: 'GET' }, }; beforeEach(angularMocks.module('grafana.core')); @@ -652,3 +653,70 @@ describe('PrometheusDatasource', function() { }); }); }); + +describe('PrometheusDatasource for POST', function() { + var ctx = new helpers.ServiceTestContext(); + var instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'POST' }, + }; + + beforeEach(angularMocks.module('grafana.core')); + beforeEach(angularMocks.module('grafana.services')); + beforeEach(ctx.providePhase(['timeSrv'])); + + beforeEach( + angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + ctx.$q = $q; + ctx.$httpBackend = $httpBackend; + ctx.$rootScope = $rootScope; + ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings }); + $httpBackend.when('GET', /\.html$/).respond(''); + }) + ); + + describe('When querying prometheus with one target using query editor target spec', function() { + var results; + var urlExpected = 'proxied/api/v1/query_range'; + var dataExpected = $.param({ + query: 'test{job="testjob"}', + start: 1443438675, + end: 1443460275, + step: 60, + }); + var query = { + range: { from: moment(1443438674760), to: moment(1443460274760) }, + targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], + interval: '60s', + }; + var response = { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [[1443454528, '3846']], + }, + ], + }, + }; + beforeEach(function() { + ctx.$httpBackend.expectPOST(urlExpected, dataExpected).respond(response); + ctx.ds.query(query).then(function(data) { + results = data; + }); + ctx.$httpBackend.flush(); + }); + it('should generate the correct query', function() { + ctx.$httpBackend.verifyNoOutstandingExpectation(); + }); + it('should return series list', function() { + expect(results.data.length).to.be(1); + expect(results.data[0].target).to.be('test{job="testjob"}'); + }); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index 3f7509fd0df..e5d7aa81210 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -12,7 +12,7 @@ describe('PrometheusMetricFindQuery', function() { directUrl: 'direct', user: 'test', password: 'mupp', - jsonData: {}, + jsonData: { httpMethod: 'GET' }, }; beforeEach(angularMocks.module('grafana.core')); From 4e1501b172a7fa85d02d301357882cacf5895049 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 12 Mar 2018 13:53:05 +0900 Subject: [PATCH 28/49] set default value of httpMethod --- public/app/plugins/datasource/prometheus/config_ctrl.ts | 9 +++++++++ public/app/plugins/datasource/prometheus/datasource.ts | 2 +- public/app/plugins/datasource/prometheus/module.ts | 5 +---- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/config_ctrl.ts diff --git a/public/app/plugins/datasource/prometheus/config_ctrl.ts b/public/app/plugins/datasource/prometheus/config_ctrl.ts new file mode 100644 index 00000000000..f7949ec9824 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/config_ctrl.ts @@ -0,0 +1,9 @@ +export class PrometheusConfigCtrl { + static templateUrl = 'public/app/plugins/datasource/prometheus/partials/config.html'; + current: any; + + /** @ngInject */ + constructor($scope) { + this.current.jsonData.httpMethod = this.current.jsonData.httpMethod || 'GET'; + } +} diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 85a0f733fd2..a64690bfb88 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -34,7 +34,7 @@ export class PrometheusDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; - this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; + this.httpMethod = instanceSettings.jsonData.httpMethod; } _request(method, url, data?, requestId?) { diff --git a/public/app/plugins/datasource/prometheus/module.ts b/public/app/plugins/datasource/prometheus/module.ts index e4292704916..d7e0b8ebe2c 100644 --- a/public/app/plugins/datasource/prometheus/module.ts +++ b/public/app/plugins/datasource/prometheus/module.ts @@ -1,9 +1,6 @@ import { PrometheusDatasource } from './datasource'; import { PrometheusQueryCtrl } from './query_ctrl'; - -class PrometheusConfigCtrl { - static templateUrl = 'partials/config.html'; -} +import { PrometheusConfigCtrl } from './config_ctrl'; class PrometheusAnnotationsQueryCtrl { static templateUrl = 'partials/annotations.editor.html'; From 4362a73dd271719b0002c530abf8b7fc9f954dcc Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 12 Mar 2018 09:23:59 +0100 Subject: [PATCH 29/49] changelog: adds note about closing #9859 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba1e5007e47..7c178565cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar) * **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942) * **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda) ### Minor * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes) From 34d9983da24506c48fb683fc6ec16aa615be6f3f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 12 Mar 2018 13:57:32 +0100 Subject: [PATCH 30/49] docs: add team api link from http api reference page --- docs/sources/http_api/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/http_api/index.md b/docs/sources/http_api/index.md index 81b3c907c7a..2a74917a9fd 100644 --- a/docs/sources/http_api/index.md +++ b/docs/sources/http_api/index.md @@ -31,6 +31,7 @@ dashboards, creating users and updating data sources. * [Annotations API]({{< relref "http_api/annotations.md" >}}) * [Alerting API]({{< relref "http_api/alerting.md" >}}) * [User API]({{< relref "http_api/user.md" >}}) +* [Team API]({{< relref "http_api/team.md" >}}) * [Admin API]({{< relref "http_api/admin.md" >}}) * [Preferences API]({{< relref "http_api/preferences.md" >}}) * [Other API]({{< relref "http_api/other.md" >}}) From 1a781fcee1180fae91f65ffc69e95062835383f5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 12 Mar 2018 14:26:28 +0100 Subject: [PATCH 31/49] changelog: adds note about closing #10029 [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c178565cf0..65a1a2d8b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Minor * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes) * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw) # 5.0.1 (2018-03-08) From 5a368f99ec2f1db2276700b8e89f2e714e0204ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dobros=C5=82aw=20=C5=BBybort?= Date: Mon, 12 Mar 2018 14:38:37 +0100 Subject: [PATCH 32/49] Fix urls in plugin update_checker logs --- pkg/plugins/update_checker.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index a2594ad915a..68ccdeaf840 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -63,7 +63,7 @@ func checkForUpdates() { resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) if err != nil { - log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error()) + log.Trace("Failed to get plugins repo from grafana.com, %v", err.Error()) return } @@ -101,7 +101,7 @@ func checkForUpdates() { resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json") if err != nil { - log.Trace("Failed to get latest.json repo from github: %v", err.Error()) + log.Trace("Failed to get latest.json repo from github.com: %v", err.Error()) return } @@ -115,7 +115,7 @@ func checkForUpdates() { var githubLatest GithubLatest err = json.Unmarshal(body, &githubLatest) if err != nil { - log.Trace("Failed to unmarshal github latest, reading response from github: %v", err.Error()) + log.Trace("Failed to unmarshal github.com latest, reading response from github.com: %v", err.Error()) return } From 479209f4832ce306d76bfb828ad5bd300c14ae8f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 12 Mar 2018 17:13:05 +0300 Subject: [PATCH 33/49] 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 34/49] 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 35/49] 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); } From 185aa6d47bec9cdd9aa7f84c705f2b001cfddf1f Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Mon, 12 Mar 2018 20:59:43 +0100 Subject: [PATCH 36/49] Added concentration units and "Normal cubic metre" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ppm, ppb, ng/m3, ng/Nm3, μg/m3, μg/Nm3, mg/m3, mg/Nm3, g/m3, g/Nm3, Nm3 ppm was moved from "Dimensionless" and "submenu" to "Concentration" --- public/app/core/utils/kbn.ts | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 43c0a74bd01..0283e43a408 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -378,7 +378,6 @@ kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, [ ' Sept', ]); kbn.valueFormats.dB = kbn.formatBuilders.fixedUnit('dB'); -kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); kbn.valueFormats.percent = function(size, decimals) { if (size === null) { @@ -557,6 +556,7 @@ kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g'); kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1); kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m3'); +kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm3'); kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm3'); kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); @@ -582,6 +582,18 @@ kbn.valueFormats.radexpckg = kbn.formatBuilders.decimalSIPrefix('C/kg'); kbn.valueFormats.radr = kbn.formatBuilders.decimalSIPrefix('R'); kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h'); +// Concentration +kbn.valueFormats.conppm = kbn.formatBuilders.fixedUnit('ppm'); +kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb'); +kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m3'); +kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm3'); +kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m3'); +kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm3'); +kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m3'); +kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm3'); +kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m3'); +kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm3'); + // Time kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); @@ -821,7 +833,6 @@ kbn.getUnitFormats = function() { { text: 'percent (0-100)', value: 'percent' }, { text: 'percent (0.0-1.0)', value: 'percentunit' }, { text: 'Humidity (%H)', value: 'humidity' }, - { text: 'ppm', value: 'ppm' }, { text: 'decibel', value: 'dB' }, { text: 'hexadecimal (0x)', value: 'hex0x' }, { text: 'hexadecimal', value: 'hex' }, @@ -969,6 +980,7 @@ kbn.getUnitFormats = function() { { text: 'millilitre', value: 'mlitre' }, { text: 'litre', value: 'litre' }, { text: 'cubic metre', value: 'm3' }, + { text: 'Normal cubic metre', value: 'Nm3' }, { text: 'cubic decimetre', value: 'dm3' }, { text: 'gallons', value: 'gallons' }, ], @@ -1066,6 +1078,21 @@ kbn.getUnitFormats = function() { { text: 'Sievert/hour (Sv/h)', value: 'radsvh' }, ], }, + { + text: 'concentration', + submenu: [ + { text: 'parts-per-million (ppm)', value: 'conppm' }, + { text: 'parts-per-billion (ppb)', value: 'conppb' }, + { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' }, + { text: 'nanogram per normal cubic metre (ng/Nm3)', value: 'conngNm3' }, + { text: 'microgram per cubic metre (μg/m3)', value: 'conμgm3' }, + { text: 'microgram per normal cubic metre (μg/Nm3)', value: 'conμgNm3' }, + { text: 'milligram per cubic metre (mg/m3)', value: 'conmgm3' }, + { text: 'milligram per normal cubic metre (mg/Nm3)', value: 'conmgNm3' }, + { text: 'gram per cubic metre (g/m3)', value: 'congm3' }, + { text: 'gram per normal cubic metre (g/Nm3)', value: 'congNm3' }, + ], + }, ]; }; From fcca00c0f9cd74d070b5cc0f05c0e711a9698958 Mon Sep 17 00:00:00 2001 From: Kazumasa Kohtaka Date: Tue, 13 Mar 2018 10:27:18 +0900 Subject: [PATCH 37/49] docs: fix an outdated link to Prometheus's doc --- docs/sources/features/datasources/prometheus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 15247ba5ebd..c9bb16441ca 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -93,7 +93,7 @@ queries via the Dashboard menu / Annotations view. Prometheus supports two ways to query annotations. - A regular metric query -- A Prometheus query for pending and firing alerts (for details see [Inspecting alerts during runtime](https://prometheus.io/docs/alerting/rules/#inspecting-alerts-during-runtime)) +- A Prometheus query for pending and firing alerts (for details see [Inspecting alerts during runtime](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/#inspecting-alerts-during-runtime)) The step option is useful to limit the number of events returned from your query. From 18638c21c9fbcb10859af347f9d1a34a4a7880af Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Mar 2018 09:48:22 +0100 Subject: [PATCH 38/49] changelog: adds note about closing #10009 [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a1a2d8b8b..8c14ee6353b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 5.1.0 (unreleased) +* **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009) * **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm) * **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar) * **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942) From a3388ef45f5aeb9a456a2cb8fa0c4871a4a59c63 Mon Sep 17 00:00:00 2001 From: Yohann BARRE <1492427+gladdiologist@users.noreply.github.com> Date: Tue, 13 Mar 2018 09:52:41 +0100 Subject: [PATCH 39/49] Second to HH:mm:ss formatter (#11105) * Seconds to HH:MM:SS format --- public/app/core/specs/kbn.jest.ts | 15 +++++++++++++++ public/app/core/utils/kbn.ts | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts index 2bd7c2a9e2d..9fad1f30694 100644 --- a/public/app/core/specs/kbn.jest.ts +++ b/public/app/core/specs/kbn.jest.ts @@ -355,3 +355,18 @@ describe('volume', function() { expect(str).toBe('1000.0 m3'); }); }); + +describe('hh:mm:ss', function() { + it('00:04:06', function() { + var str = kbn.valueFormats['dthms'](246, 1); + expect(str).toBe('00:04:06'); + }); + it('24:00:00', function() { + var str = kbn.valueFormats['dthms'](86400, 1); + expect(str).toBe('24:00:00'); + }); + it('6824413:53:20', function() { + var str = kbn.valueFormats['dthms'](24567890000, 1); + expect(str).toBe('6824413:53:20'); + }); +}); diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 0283e43a408..3b78ccfc001 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -131,6 +131,17 @@ kbn.secondsToHms = function(seconds) { return 'less than a millisecond'; //'just now' //or other string you like; }; +kbn.secondsToHhmmss = function(seconds) { + var strings = []; + var numhours = Math.floor(seconds/3600); + var numminutes = Math.floor((seconds%3600)/60); + var numseconds = Math.floor((seconds%3600)%60); + numhours > 9 ? strings.push(''+numhours) : strings.push('0'+numhours); + numminutes > 9 ? strings.push(''+numminutes) : strings.push('0'+numminutes); + numseconds > 9 ? strings.push(''+numseconds) : strings.push('0'+numseconds); + return strings.join(':'); +}; + kbn.to_percent = function(nr, outof) { return Math.floor(nr / outof * 10000) / 100 + '%'; }; @@ -795,6 +806,10 @@ kbn.valueFormats.dtdurations = function(size, decimals) { return kbn.toDuration(size, decimals, 'second'); }; +kbn.valueFormats.dthms = function(size, decimals) { + return kbn.secondsToHhmmss(size); +}; + kbn.valueFormats.timeticks = function(size, decimals, scaledDecimals) { return kbn.valueFormats.s(size / 100, decimals, scaledDecimals); }; @@ -869,6 +884,7 @@ kbn.getUnitFormats = function() { { text: 'days (d)', value: 'd' }, { text: 'duration (ms)', value: 'dtdurationms' }, { text: 'duration (s)', value: 'dtdurations' }, + { text: 'duration (hh:mm:ss)', value: 'dthms' }, { text: 'Timeticks (s/100)', value: 'timeticks' }, ], }, From b400f7ccffaab912a8c80771818c840a6c3c043d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Mar 2018 09:54:46 +0100 Subject: [PATCH 40/49] changelog: adds note about closing #11107 [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c14ee6353b..c63d7ec4574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes) * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw) +* **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) # 5.0.1 (2018-03-08) From c18c0f6db7e99fa11215c8727a2f1292963427fa Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Mar 2018 10:42:58 +0100 Subject: [PATCH 41/49] changelog: adds note about closing #11143 [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c63d7ec4574..6acacd4f290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw) * **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) +* **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) # 5.0.1 (2018-03-08) From 3f2c086e6f7ab599dcff1104dc68dff9a3e3fbb9 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 13 Mar 2018 10:55:43 +0100 Subject: [PATCH 42/49] teams: removes quota on route Got added by mistake a year ago. --- pkg/api/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 1b7e30e34ad..84f0eae79c7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -150,11 +150,11 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/teams", func(teamsRoute RouteRegister) { teamsRoute.Get("/:teamId", wrap(GetTeamById)) teamsRoute.Get("/search", wrap(SearchTeams)) - teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam)) + teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) - teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) + teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) }, reqOrgAdmin) From 061a418f0fa01ae3aeb11db813fb5ce5ccfb34fd Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Mar 2018 11:23:44 +0100 Subject: [PATCH 43/49] style: dont expose func outside package --- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/dashboard.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 6342496ed26..f449bec5849 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -132,7 +132,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { return nil } -func DeleteAlertDefinition(dashboardId int64, sess *DBSession) error { +func deleteAlertDefinition(dashboardId int64, sess *DBSession) error { alerts := make([]*m.Alert, 0) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 5ee34183628..8a89c3d942c 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -330,7 +330,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { } } - if err := DeleteAlertDefinition(dashboard.Id, sess); err != nil { + if err := deleteAlertDefinition(dashboard.Id, sess); err != nil { return nil } From da19000733d8f100c027aa9389704a56779bd25c Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 13 Mar 2018 11:26:30 +0100 Subject: [PATCH 44/49] changelog: adds note about closing #11220 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6acacd4f290..f543c481987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ * **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) +# 5.0.2 (unrelease) + +* **Teams**: Remove quota restrictions from teams [#11220](https://github.com/grafana/grafana/issues/11220) + # 5.0.1 (2018-03-08) * **Postgres**: PostgreSQL error when using ipv6 address as hostname in connection string [#11055](https://github.com/grafana/grafana/issues/11055), thanks [@svenklemm](https://github.com/svenklemm) From 30d077d1d14a13e97b203439ecd7dac61862ea0c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 13 Mar 2018 15:25:28 +0300 Subject: [PATCH 45/49] graph: minor refactor of histogram mode PR #8613 --- public/app/plugins/panel/graph/graph.ts | 4 +--- public/app/plugins/panel/graph/histogram.ts | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 5edd5c38aac..369f37022f4 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -242,10 +242,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { let histMax = _.max(_.map(data, s => s.stats.max)); let ticks = panel.xaxis.buckets || panelWidth / 50; bucketSize = tickStep(histMin, histMax, ticks); - - data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, panel.stack, histMin, histMax); - options.series.bars.barWidth = bucketSize * 0.8; + data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, histMin, histMax); } else { bucketSize = 0; } diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index b8867c999cc..ad56e477a85 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -59,13 +59,11 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, m * Convert series into array of histogram data. * @param data Array of series * @param bucketSize - * @param stack */ export function convertToHistogramData( data: any, bucketSize: number, hiddenSeries: any, - stack = false, min: number, max: number ): any[] { From 89557dd67b53d95d2ea4c0bbd7357943436e9126 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Tue, 16 Jan 2018 12:12:42 -0500 Subject: [PATCH 46/49] Modify Grafana Pagerduty notifier to use Pagerduty API V2 --- pkg/services/alerting/notifiers/pagerduty.go | 36 ++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index c4067abec3b..b7bd891a4c2 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -1,6 +1,8 @@ package notifiers import ( + "os" + "time" "strconv" "fmt" @@ -38,7 +40,7 @@ func init() { } var ( - pagerdutyEventApiUrl string = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" + pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue" ) func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) { @@ -85,28 +87,39 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Notifying Pagerduty", "event_type", eventType) + payloadJSON := simplejson.New() + payloadJSON.Set("summary", evalContext.Rule.Name+" - "+evalContext.Rule.Message) + if hostname, err := os.Hostname(); err == nil { + payloadJSON.Set("source", hostname) + } + payloadJSON.Set("severity", "critical") + payloadJSON.Set("timestamp", time.Now()) + payloadJSON.Set("component", "Grafana") + payloadJSON.Set("custom_details", customData) + bodyJSON := simplejson.New() - bodyJSON.Set("service_key", this.Key) - bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message) - bodyJSON.Set("client", "Grafana") - bodyJSON.Set("details", customData) - bodyJSON.Set("event_type", eventType) - bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("routing_key", this.Key) + bodyJSON.Set("event_action", eventType) + bodyJSON.Set("dedup_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("payload", payloadJSON) ruleUrl, err := evalContext.GetRuleUrl() if err != nil { this.log.Error("Failed get rule link", "error", err) return err } - bodyJSON.Set("client_url", ruleUrl) + links := make([]interface{}, 1) + linkJSON := simplejson.New() + linkJSON.Set("href", ruleUrl) + links[0] = linkJSON + bodyJSON.Set("links", links) if evalContext.ImagePublicUrl != "" { contexts := make([]interface{}, 1) imageJSON := simplejson.New() - imageJSON.Set("type", "image") imageJSON.Set("src", evalContext.ImagePublicUrl) contexts[0] = imageJSON - bodyJSON.Set("contexts", contexts) + bodyJSON.Set("images", contexts) } body, _ := bodyJSON.MarshalJSON() @@ -115,6 +128,9 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { Url: pagerdutyEventApiUrl, Body: string(body), HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": "application/json", + }, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { From aba6f627c59b48b096a9ee520be68e978c39aad7 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Tue, 16 Jan 2018 13:20:31 -0500 Subject: [PATCH 47/49] Fix CI --- pkg/services/alerting/notifiers/pagerduty.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index b7bd891a4c2..9b1daa72a73 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -2,8 +2,8 @@ package notifiers import ( "os" - "time" "strconv" + "time" "fmt" From 87bc60b9d778161da7009f543557cd3d1f907e13 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 13 Mar 2018 13:10:55 +0100 Subject: [PATCH 48/49] alerting: adds back the link to grafana. --- pkg/services/alerting/notifiers/pagerduty.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 9b1daa72a73..6013648d9dd 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -111,6 +111,8 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { links := make([]interface{}, 1) linkJSON := simplejson.New() linkJSON.Set("href", ruleUrl) + bodyJSON.Set("client_url", ruleUrl) + bodyJSON.Set("client", "Grafana") links[0] = linkJSON bodyJSON.Set("links", links) From 0185ad5b04f0c7b76329b8e1ece65de26f726859 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 13 Mar 2018 15:54:58 +0300 Subject: [PATCH 49/49] changelog: add note about closing #8151 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f543c481987..e4de9ccb9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009) * **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm) * **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar) +* **Graph**: Support multiple series stacking in histogram mode [#8151](https://github.com/grafana/grafana/issues/8151), thx [@mtanda](https://github.com/mtanda) * **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942) * **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda) * **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)