From 7588ee974d8113b5229a881a7518533deab84b8e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 14 Aug 2016 17:33:18 +0300 Subject: [PATCH 01/74] Working on non time series X-axis feature. --- public/app/plugins/panel/graph/graph.js | 69 ++++++++++++++++---- public/app/plugins/panel/graph/module.ts | 9 ++- public/app/plugins/panel/graph/tab_axes.html | 11 ++++ 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 0a6a2bc0e45..66d201588df 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -28,7 +28,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var ctrl = scope.ctrl; var dashboard = ctrl.dashboard; var panel = ctrl.panel; - var data, annotations; + var data, annotations, histogramData; var sortedSeries; var legendSideLastValue = null; var rootScope = scope.$root; @@ -226,22 +226,37 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } }; - for (var i = 0; i < data.length; i++) { - var series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); + if (panel.xaxis.mode === 'histogram') { + histogramData = formatToHistogram(data, _.last); - // if hidden remove points and disable stack - if (ctrl.hiddenSeries[series.alias]) { - series.data = []; - series.stack = false; + if (histogramData.length && histogramData[0].ticks.length) { + // options.series.bars.barWidth = histogramData[0].ticks.length / 1.5; + options.series.bars.barWidth = 0.7; + // options.series.bars.align = 'center'; + } + } else { + for (var i = 0; i < data.length; i++) { + var series = data[i]; + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); + + // if hidden remove points and disable stack + if (ctrl.hiddenSeries[series.alias]) { + series.data = []; + series.stack = false; + } + } + + if (data.length && data[0].stats.timeStep) { + options.series.bars.barWidth = data[0].stats.timeStep / 1.5; } } - if (data.length && data[0].stats.timeStep) { - options.series.bars.barWidth = data[0].stats.timeStep / 1.5; + if (panel.xaxis.mode === 'histogram') { + addXAxis(options); + } else { + addTimeAxis(options); } - addTimeAxis(options); addGridThresholds(options, panel); addAnnotations(options); configureAxisOptions(data, options); @@ -275,6 +290,24 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } } + function formatToHistogram(data, getValueCallback) { + var histogram = [data[0]]; + + histogram[0].data = _.map(data, function(series, index) { + var values = _.remove(_.map(series.datapoints, function(point) { + return point[0]; + }), null); + var calculatedPoint = getValueCallback(values); + return [index, calculatedPoint]; + }); + + histogram[0].ticks = _.map(data, function(series, index) { + return [index, series.alias]; + }); + + return histogram; + } + function translateFillOption(fill) { return fill === 0 ? 0.001 : fill/10; } @@ -305,6 +338,20 @@ function (angular, $, moment, _, kbn, GraphTooltip) { }; } + function addXAxis(options) { + var ticks = histogramData[0].ticks; + + options.xaxis = { + timezone: dashboard.getTimezone(), + show: panel.xaxis.show, + mode: null, + min: 0, + max: ticks.length, + label: "Datetime", + ticks: ticks + }; + } + function addGridThresholds(options, panel) { if (_.isNumber(panel.grid.threshold1)) { var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 8fbde9e84a1..1f71ad63829 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -20,6 +20,7 @@ class GraphCtrl extends MetricsPanelCtrl { seriesList: any = []; logScales: any; unitFormats: any; + xAxisModes: any; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -50,7 +51,8 @@ class GraphCtrl extends MetricsPanelCtrl { } ], xaxis: { - show: true + show: true, + mode: 'timeseries' }, grid : { threshold1: null, @@ -138,6 +140,11 @@ class GraphCtrl extends MetricsPanelCtrl { 'log (base 1024)': 1024 }; this.unitFormats = kbn.getUnitFormats(); + + this.xAxisModes = { + 'Time Series': 'timeseries', + 'Histogram': 'histogram' + }; } onInitPanelActions(actions) { diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index eeaf27aff78..0469aff547f 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -40,6 +40,17 @@
X-Axis
+ +
+ +
+ +
+
From e39e5f9a9be1f645ecba79957ee5de7b111c5c46 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 16 Aug 2016 18:45:57 +0300 Subject: [PATCH 02/74] Graph-panel: Add initial histogram option, issue #5812. --- public/app/plugins/panel/graph/graph.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 66d201588df..eae8c774a67 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -232,8 +232,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (histogramData.length && histogramData[0].ticks.length) { // options.series.bars.barWidth = histogramData[0].ticks.length / 1.5; options.series.bars.barWidth = 0.7; - // options.series.bars.align = 'center'; + options.series.bars.align = 'center'; } + + addXAxis(options); } else { for (var i = 0; i < data.length; i++) { var series = data[i]; @@ -249,11 +251,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (data.length && data[0].stats.timeStep) { options.series.bars.barWidth = data[0].stats.timeStep / 1.5; } - } - if (panel.xaxis.mode === 'histogram') { - addXAxis(options); - } else { addTimeAxis(options); } @@ -298,11 +296,11 @@ function (angular, $, moment, _, kbn, GraphTooltip) { return point[0]; }), null); var calculatedPoint = getValueCallback(values); - return [index, calculatedPoint]; + return [index + 1, calculatedPoint]; }); histogram[0].ticks = _.map(data, function(series, index) { - return [index, series.alias]; + return [index + 1, series.alias]; }); return histogram; @@ -346,7 +344,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { show: panel.xaxis.show, mode: null, min: 0, - max: ticks.length, + max: ticks.length + 1, label: "Datetime", ticks: ticks }; From 63886598e9be33687e9b5f35ed7ea281583785a8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 16 Aug 2016 19:41:18 +0300 Subject: [PATCH 03/74] Graph panel: add value option (min, max, avg, etc), issue #5812. --- public/app/plugins/panel/graph/graph.js | 26 +++++++++++++++++++- public/app/plugins/panel/graph/module.ts | 7 +++++- public/app/plugins/panel/graph/tab_axes.html | 10 ++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index eae8c774a67..e5b2218d86a 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -227,7 +227,17 @@ function (angular, $, moment, _, kbn, GraphTooltip) { }; if (panel.xaxis.mode === 'histogram') { - histogramData = formatToHistogram(data, _.last); + // Format to histogram + + var getValueFuncs = { + 'min': _.min, + 'max': _.max, + 'avg': seriesAvg, + 'current': _.last, + 'total': seriesSum + }; + + histogramData = formatToHistogram(data, getValueFuncs[panel.xaxis.histogramValue]); if (histogramData.length && histogramData[0].ticks.length) { // options.series.bars.barWidth = histogramData[0].ticks.length / 1.5; @@ -306,6 +316,20 @@ function (angular, $, moment, _, kbn, GraphTooltip) { return histogram; } + function seriesSum(values) { + return _.reduce(values, function(sum, num) { + return sum + num; + }); + } + + function seriesAvg(values) { + if (values.length) { + return seriesSum(values) / values.length; + } else { + return null; + } + } + function translateFillOption(fill) { return fill === 0 ? 0.001 : fill/10; } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 1f71ad63829..9a10c45a9f8 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -21,6 +21,7 @@ class GraphCtrl extends MetricsPanelCtrl { logScales: any; unitFormats: any; xAxisModes: any; + xAxisHistogramValues: any; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -52,7 +53,8 @@ class GraphCtrl extends MetricsPanelCtrl { ], xaxis: { show: true, - mode: 'timeseries' + mode: 'timeseries', + histogramValue: 'avg' }, grid : { threshold1: null, @@ -116,6 +118,7 @@ class GraphCtrl extends MetricsPanelCtrl { _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); _.defaults(this.panel.grid, this.panelDefaults.grid); _.defaults(this.panel.legend, this.panelDefaults.legend); + _.defaults(this.panel.xaxis, this.panelDefaults.xaxis); this.colors = $scope.$root.colors; @@ -145,6 +148,8 @@ class GraphCtrl extends MetricsPanelCtrl { 'Time Series': 'timeseries', 'Histogram': 'histogram' }; + + this.xAxisHistogramValues = ['min', 'max', 'avg', 'current', 'total']; } onInitPanelActions(actions) { diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index 0469aff547f..b4d7aedf0a7 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -51,6 +51,16 @@
+
+ +
+ +
+
From 113173be3d59a6e79806d2806b907d7d2b3fca5c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 16 Aug 2016 21:08:15 +0300 Subject: [PATCH 04/74] Graph panel: preserve series options (colors and other), issue #5812. --- public/app/plugins/panel/graph/graph.js | 82 +++++++------------------ 1 file changed, 23 insertions(+), 59 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index e5b2218d86a..2ba0d101af8 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -28,7 +28,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var ctrl = scope.ctrl; var dashboard = ctrl.dashboard; var panel = ctrl.panel; - var data, annotations, histogramData; + var data, annotations; var sortedSeries; var legendSideLastValue = null; var rootScope = scope.$root; @@ -226,38 +226,32 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } }; + for (var i = 0; i < data.length; i++) { + var series = data[i]; + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); + + if (panel.xaxis.mode === 'histogram') { + series.data = [ + [i + 1, series.stats[panel.xaxis.histogramValue]] + ]; + } + + // if hidden remove points and disable stack + if (ctrl.hiddenSeries[series.alias]) { + series.data = []; + series.stack = false; + } + } + if (panel.xaxis.mode === 'histogram') { - // Format to histogram - - var getValueFuncs = { - 'min': _.min, - 'max': _.max, - 'avg': seriesAvg, - 'current': _.last, - 'total': seriesSum - }; - - histogramData = formatToHistogram(data, getValueFuncs[panel.xaxis.histogramValue]); - - if (histogramData.length && histogramData[0].ticks.length) { - // options.series.bars.barWidth = histogramData[0].ticks.length / 1.5; + if (data.length) { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; } addXAxis(options); + } else { - for (var i = 0; i < data.length; i++) { - var series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - - // if hidden remove points and disable stack - if (ctrl.hiddenSeries[series.alias]) { - series.data = []; - series.stack = false; - } - } - if (data.length && data[0].stats.timeStep) { options.series.bars.barWidth = data[0].stats.timeStep / 1.5; } @@ -298,38 +292,6 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } } - function formatToHistogram(data, getValueCallback) { - var histogram = [data[0]]; - - histogram[0].data = _.map(data, function(series, index) { - var values = _.remove(_.map(series.datapoints, function(point) { - return point[0]; - }), null); - var calculatedPoint = getValueCallback(values); - return [index + 1, calculatedPoint]; - }); - - histogram[0].ticks = _.map(data, function(series, index) { - return [index + 1, series.alias]; - }); - - return histogram; - } - - function seriesSum(values) { - return _.reduce(values, function(sum, num) { - return sum + num; - }); - } - - function seriesAvg(values) { - if (values.length) { - return seriesSum(values) / values.length; - } else { - return null; - } - } - function translateFillOption(fill) { return fill === 0 ? 0.001 : fill/10; } @@ -361,7 +323,9 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } function addXAxis(options) { - var ticks = histogramData[0].ticks; + var ticks = _.map(data, function(series, index) { + return [index + 1, series.alias]; + }); options.xaxis = { timezone: dashboard.getTimezone(), From 93515d0ffc6e3bbbbdbc43fb332389e855ce1896 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 16 Aug 2016 21:53:53 +0300 Subject: [PATCH 05/74] Graph panel: display only bars in histogram mode, issue #5812. --- public/app/plugins/panel/graph/graph.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 2ba0d101af8..75b3bc2b5e1 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -247,6 +247,9 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (data.length) { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; + options.series.bars.show = true; + options.series.points.show = false; + options.series.lines.show = false; } addXAxis(options); From 284a6ee62965711fc5c00fdd3ce5ef4c233c79e2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 18 Aug 2016 15:34:32 +0300 Subject: [PATCH 06/74] Graph panel: rename X axis modes, issue #5812. --- public/app/plugins/panel/graph/graph.js | 6 +++--- public/app/plugins/panel/graph/module.ts | 12 ++++++------ public/app/plugins/panel/graph/tab_axes.html | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 75b3bc2b5e1..25774f799f2 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -230,9 +230,9 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var series = data[i]; series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - if (panel.xaxis.mode === 'histogram') { + if (panel.xaxis.mode === 'series') { series.data = [ - [i + 1, series.stats[panel.xaxis.histogramValue]] + [i + 1, series.stats[panel.xaxis.seriesValue]] ]; } @@ -243,7 +243,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } } - if (panel.xaxis.mode === 'histogram') { + if (panel.xaxis.mode === 'series') { if (data.length) { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 9a10c45a9f8..22ec7235cde 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -21,7 +21,7 @@ class GraphCtrl extends MetricsPanelCtrl { logScales: any; unitFormats: any; xAxisModes: any; - xAxisHistogramValues: any; + xAxisSeriesValues: any; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -53,8 +53,8 @@ class GraphCtrl extends MetricsPanelCtrl { ], xaxis: { show: true, - mode: 'timeseries', - histogramValue: 'avg' + mode: 'time', + seriesValue: 'avg' }, grid : { threshold1: null, @@ -145,11 +145,11 @@ class GraphCtrl extends MetricsPanelCtrl { this.unitFormats = kbn.getUnitFormats(); this.xAxisModes = { - 'Time Series': 'timeseries', - 'Histogram': 'histogram' + 'Time': 'time', + 'Series': 'series' }; - this.xAxisHistogramValues = ['min', 'max', 'avg', 'current', 'total']; + this.xAxisSeriesValues = ['min', 'max', 'avg', 'current', 'total']; } onInitPanelActions(actions) { diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index b4d7aedf0a7..5c868115596 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -51,12 +51,12 @@
-
+
From c683c7a4487353ff307a4b05b8d821117acbd7c4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 18 Aug 2016 20:12:08 +0300 Subject: [PATCH 07/74] Graph panel: initial support for table format, issue #5812. --- public/app/plugins/panel/graph/graph.js | 37 +++++++++++- public/app/plugins/panel/graph/module.ts | 61 +++++++++++++++++++- public/app/plugins/panel/graph/tab_axes.html | 16 ++++- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 25774f799f2..bde6339cb24 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -234,6 +234,16 @@ function (angular, $, moment, _, kbn, GraphTooltip) { series.data = [ [i + 1, series.stats[panel.xaxis.seriesValue]] ]; + } else if (panel.xaxis.mode === 'table') { + series.data = []; + for (var j = 0; j < series.datapoints.length; j++) { + var dataIndex = i * series.datapoints.length + j; + series.datapoints[j]; + series.data.push([ + dataIndex + 1, + series.datapoints[j][0] + ]); + } } // if hidden remove points and disable stack @@ -252,7 +262,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) { options.series.lines.show = false; } - addXAxis(options); + addXSeriesAxis(options); + + } else if (panel.xaxis.mode === 'table') { + addXTableAxis(options); } else { if (data.length && data[0].stats.timeStep) { @@ -325,7 +338,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { }; } - function addXAxis(options) { + function addXSeriesAxis(options) { var ticks = _.map(data, function(series, index) { return [index + 1, series.alias]; }); @@ -341,6 +354,26 @@ function (angular, $, moment, _, kbn, GraphTooltip) { }; } + function addXTableAxis(options) { + var ticks = _.map(data, function(series, seriesIndex) { + return _.map(series.datapoints, function(point, pointIndex) { + var tickIndex = seriesIndex * series.datapoints.length + pointIndex; + return [tickIndex + 1, point[1]]; + }); + }); + ticks = _.flatten(ticks, true); + + options.xaxis = { + timezone: dashboard.getTimezone(), + show: panel.xaxis.show, + mode: null, + min: 0, + max: ticks.length + 1, + label: "Datetime", + ticks: ticks + }; + } + function addGridThresholds(options, panel) { if (_.isNumber(panel.grid.threshold1)) { var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 22ec7235cde..b098ca4b11d 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -22,6 +22,7 @@ class GraphCtrl extends MetricsPanelCtrl { unitFormats: any; xAxisModes: any; xAxisSeriesValues: any; + xAxisColumns: any = []; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -146,7 +147,8 @@ class GraphCtrl extends MetricsPanelCtrl { this.xAxisModes = { 'Time': 'time', - 'Series': 'series' + 'Series': 'series', + 'Table': 'table' }; this.xAxisSeriesValues = ['min', 'max', 'avg', 'current', 'total']; @@ -186,7 +188,26 @@ class GraphCtrl extends MetricsPanelCtrl { this.datapointsWarning = false; this.datapointsCount = 0; this.datapointsOutside = false; - this.seriesList = dataList.map(this.seriesHandler.bind(this)); + + let dataHandler: (seriesData, index)=>any; + if (this.panel.xaxis.mode === 'table') { + if (dataList.length) { + // Table panel uses only first enabled tagret, so we can use dataList[0] + // for table data representation + this.xAxisColumns = _.map(dataList[0].columns, function(column, index) { + return { + text: column.text, + index: index + }; + }); + } + + dataHandler = this.tableHandler; + } else { + dataHandler = this.seriesHandler; + } + + this.seriesList = dataList.map(dataHandler.bind(this)); this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside; this.annotationsPromise.then(annotations => { @@ -227,6 +248,42 @@ class GraphCtrl extends MetricsPanelCtrl { return series; } + tableHandler(seriesData, index) { + var xColumnIndex = Number(this.panel.xaxis.columnIndex); + var datapoints = _.map(seriesData.rows, (row) => { + return [ + _.last(row), // Y value (always last column) + row[xColumnIndex] // X value + ]; + }); + + var alias = seriesData.columns[xColumnIndex].text; + + var colorIndex = index % this.colors.length; + var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; + + var series = new TimeSeries({ + datapoints: datapoints, + alias: alias, + color: color, + unit: seriesData.unit, + }); + + if (datapoints && datapoints.length > 0) { + var last = moment.utc(datapoints[datapoints.length - 1][1]); + var from = moment.utc(this.range.from); + if (last - from < -10000) { + this.datapointsOutside = true; + } + + this.datapointsCount += datapoints.length; + this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); + } + + + return series; + } + onRender() { if (!this.seriesList) { return; } diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index 5c868115596..233d22013a5 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -47,11 +47,12 @@
-
+ +
+ +
+
From d23e9fa3c35cfb3e00d03266dd8124c4b0029777 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 23 Aug 2016 20:44:58 +0300 Subject: [PATCH 08/74] Graph panel: table format support improvements, issue #5812. --- public/app/plugins/panel/graph/graph.js | 5 +++++ public/app/plugins/panel/graph/module.ts | 12 ++++++++++-- public/app/plugins/panel/graph/tab_axes.html | 11 +++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index bde6339cb24..c5b21beab2f 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -265,6 +265,11 @@ function (angular, $, moment, _, kbn, GraphTooltip) { addXSeriesAxis(options); } else if (panel.xaxis.mode === 'table') { + if (data.length) { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + } + addXTableAxis(options); } else { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index b098ca4b11d..52e70ce4c1f 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -194,12 +194,18 @@ class GraphCtrl extends MetricsPanelCtrl { if (dataList.length) { // Table panel uses only first enabled tagret, so we can use dataList[0] // for table data representation + dataList.splice(1, dataList.length - 1); this.xAxisColumns = _.map(dataList[0].columns, function(column, index) { return { text: column.text, index: index }; }); + + // Set last column as default value + if (!this.panel.xaxis.valueColumnIndex) { + this.panel.xaxis.valueColumnIndex = this.xAxisColumns.length - 1; + } } dataHandler = this.tableHandler; @@ -250,14 +256,16 @@ class GraphCtrl extends MetricsPanelCtrl { tableHandler(seriesData, index) { var xColumnIndex = Number(this.panel.xaxis.columnIndex); + var valueColumnIndex = this.panel.xaxis.valueColumnIndex; var datapoints = _.map(seriesData.rows, (row) => { + var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); return [ - _.last(row), // Y value (always last column) + value, // Y value row[xColumnIndex] // X value ]; }); - var alias = seriesData.columns[xColumnIndex].text; + var alias = seriesData.columns[valueColumnIndex].text; var colorIndex = index % this.colors.length; var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index 233d22013a5..86faa57e467 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -73,6 +73,17 @@
+ +
+ +
+ +
+
From 06af566f3face5db21374b08ed9c958414c83896 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 25 Aug 2016 21:53:49 +0300 Subject: [PATCH 09/74] Graph panel: initial elastic raw document format support, issue #5812. --- public/app/plugins/panel/graph/graph.js | 6 +- public/app/plugins/panel/graph/module.ts | 90 +++++++++++++++++++- public/app/plugins/panel/graph/tab_axes.html | 25 ++++++ 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index c5b21beab2f..70efb24678f 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -234,7 +234,8 @@ function (angular, $, moment, _, kbn, GraphTooltip) { series.data = [ [i + 1, series.stats[panel.xaxis.seriesValue]] ]; - } else if (panel.xaxis.mode === 'table') { + } else if (panel.xaxis.mode === 'table' || + panel.xaxis.mode === 'elastic') { series.data = []; for (var j = 0; j < series.datapoints.length; j++) { var dataIndex = i * series.datapoints.length + j; @@ -264,7 +265,8 @@ function (angular, $, moment, _, kbn, GraphTooltip) { addXSeriesAxis(options); - } else if (panel.xaxis.mode === 'table') { + } else if (panel.xaxis.mode === 'table' || + panel.xaxis.mode === 'elastic') { if (data.length) { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 52e70ce4c1f..5f1bab37127 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -148,7 +148,8 @@ class GraphCtrl extends MetricsPanelCtrl { this.xAxisModes = { 'Time': 'time', 'Series': 'series', - 'Table': 'table' + 'Table': 'table', + 'Elastic Raw Doc': 'elastic' }; this.xAxisSeriesValues = ['min', 'max', 'avg', 'current', 'total']; @@ -195,7 +196,7 @@ class GraphCtrl extends MetricsPanelCtrl { // Table panel uses only first enabled tagret, so we can use dataList[0] // for table data representation dataList.splice(1, dataList.length - 1); - this.xAxisColumns = _.map(dataList[0].columns, function(column, index) { + this.xAxisColumns = _.map(dataList[0].columns, (column, index) => { return { text: column.text, index: index @@ -209,6 +210,14 @@ class GraphCtrl extends MetricsPanelCtrl { } dataHandler = this.tableHandler; + } else if (this.panel.xaxis.mode === 'elastic') { + if (dataList.length) { + dataList.splice(1, dataList.length - 1); + var point = _.first(dataList[0].datapoints); + this.xAxisColumns = getFieldsFromESDoc(point); + } + + dataHandler = this.esRawDocHandler; } else { dataHandler = this.seriesHandler; } @@ -250,13 +259,12 @@ class GraphCtrl extends MetricsPanelCtrl { this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); } - return series; } tableHandler(seriesData, index) { var xColumnIndex = Number(this.panel.xaxis.columnIndex); - var valueColumnIndex = this.panel.xaxis.valueColumnIndex; + var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); var datapoints = _.map(seriesData.rows, (row) => { var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); return [ @@ -288,6 +296,46 @@ class GraphCtrl extends MetricsPanelCtrl { this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); } + return series; + } + + esRawDocHandler(seriesData, index) { + let xField = this.panel.xaxis.esField; + let valueField = this.panel.xaxis.esValueField; + let datapoints = _.map(seriesData.datapoints, (doc) => { + return [ + pluckDeep(doc, valueField), // Y value + pluckDeep(doc, xField) // X value + ]; + }); + + // Remove empty points + datapoints = _.filter(datapoints, (point) => { + return point[0] !== undefined; + }); + + var alias = valueField; + + var colorIndex = index % this.colors.length; + var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; + + var series = new TimeSeries({ + datapoints: datapoints, + alias: alias, + color: color, + unit: seriesData.unit, + }); + + if (datapoints && datapoints.length > 0) { + var last = moment.utc(datapoints[datapoints.length - 1][1]); + var from = moment.utc(this.range.from); + if (last - from < -10000) { + this.datapointsOutside = true; + } + + this.datapointsCount += datapoints.length; + this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); + } return series; } @@ -396,4 +444,38 @@ class GraphCtrl extends MetricsPanelCtrl { } } +function getFieldsFromESDoc(doc) { + let fields = []; + let fieldNameParts = []; + + function getFieldsRecursive(obj) { + _.forEach(obj, (value, key) => { + if (_.isObject(value)) { + fieldNameParts.push(key); + getFieldsRecursive(value); + } else { + let field = fieldNameParts.concat(key).join('.'); + fields.push(field); + } + }); + fieldNameParts.pop(); + } + + getFieldsRecursive(doc); + return fields; +} + +function pluckDeep(obj: any, property: string) { + let propertyParts = property.split('.'); + let value = obj; + for (let i = 0; i < propertyParts.length; ++i) { + if (value[propertyParts[i]]) { + value = value[propertyParts[i]]; + } else { + return undefined; + } + } + return value; +} + export {GraphCtrl, GraphCtrl as PanelCtrl} diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index 86faa57e467..1f0c434c113 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -52,6 +52,7 @@
+
@@ -63,6 +64,7 @@
+
@@ -84,6 +86,29 @@
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
From 7a6d32138b8aa9f662f468c76c10aacd74f61e83 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 26 Aug 2016 20:47:12 +0300 Subject: [PATCH 10/74] Graph panel: refactor, issue #5812. --- public/app/plugins/panel/graph/module.ts | 59 +++++------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 5f1bab37127..550f8acd23b 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -219,7 +219,7 @@ class GraphCtrl extends MetricsPanelCtrl { dataHandler = this.esRawDocHandler; } else { - dataHandler = this.seriesHandler; + dataHandler = this.timeSeriesHandler; } this.seriesList = dataList.map(dataHandler.bind(this)); @@ -235,9 +235,7 @@ class GraphCtrl extends MetricsPanelCtrl { }); } - seriesHandler(seriesData, index) { - var datapoints = seriesData.datapoints; - var alias = seriesData.target; + seriesHandler(seriesData, index, datapoints, alias) { var colorIndex = index % this.colors.length; var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; @@ -262,6 +260,13 @@ class GraphCtrl extends MetricsPanelCtrl { return series; } + timeSeriesHandler(seriesData, index) { + var datapoints = seriesData.datapoints; + var alias = seriesData.target; + + return this.seriesHandler(seriesData, index, datapoints, alias); + } + tableHandler(seriesData, index) { var xColumnIndex = Number(this.panel.xaxis.columnIndex); var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); @@ -275,28 +280,7 @@ class GraphCtrl extends MetricsPanelCtrl { var alias = seriesData.columns[valueColumnIndex].text; - var colorIndex = index % this.colors.length; - var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; - - var series = new TimeSeries({ - datapoints: datapoints, - alias: alias, - color: color, - unit: seriesData.unit, - }); - - if (datapoints && datapoints.length > 0) { - var last = moment.utc(datapoints[datapoints.length - 1][1]); - var from = moment.utc(this.range.from); - if (last - from < -10000) { - this.datapointsOutside = true; - } - - this.datapointsCount += datapoints.length; - this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); - } - - return series; + return this.seriesHandler(seriesData, index, datapoints, alias); } esRawDocHandler(seriesData, index) { @@ -316,28 +300,7 @@ class GraphCtrl extends MetricsPanelCtrl { var alias = valueField; - var colorIndex = index % this.colors.length; - var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; - - var series = new TimeSeries({ - datapoints: datapoints, - alias: alias, - color: color, - unit: seriesData.unit, - }); - - if (datapoints && datapoints.length > 0) { - var last = moment.utc(datapoints[datapoints.length - 1][1]); - var from = moment.utc(this.range.from); - if (last - from < -10000) { - this.datapointsOutside = true; - } - - this.datapointsCount += datapoints.length; - this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); - } - - return series; + return this.seriesHandler(seriesData, index, datapoints, alias); } onRender() { From 31642b472c7812c93b5f6b8fbc0708304c3f4a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Sep 2016 11:07:41 +0200 Subject: [PATCH 11/74] refactoring(graph panel): #5917 --- public/app/plugins/panel/graph/graph.js | 4 +- public/app/plugins/panel/graph/module.ts | 55 +++++++------------- public/app/plugins/panel/graph/tab_axes.html | 14 ++--- 3 files changed, 26 insertions(+), 47 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 5657d1eb9d2..83129a3efc8 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -238,9 +238,7 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) { series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); if (panel.xaxis.mode === 'series') { - series.data = [ - [i + 1, series.stats[panel.xaxis.seriesValue]] - ]; + series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; } else if (panel.xaxis.mode === 'table' || panel.xaxis.mode === 'elastic') { series.data = []; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index f616bb28f70..f1b54c03e11 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -23,8 +23,7 @@ class GraphCtrl extends MetricsPanelCtrl { logScales: any; unitFormats: any; xAxisModes: any; - xAxisSeriesValues: any; - xAxisColumns: any = []; + xAxisSeriesStats: any; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -58,11 +57,8 @@ class GraphCtrl extends MetricsPanelCtrl { xaxis: { show: true, mode: 'time', - seriesValue: 'avg' - }, - alert: { - warn: {op: '>', value: undefined}, - crit: {op: '>', value: undefined}, + name: null, + values: [], }, // show/hide lines lines : true, @@ -120,7 +116,6 @@ class GraphCtrl extends MetricsPanelCtrl { _.defaults(this.panel, this.panelDefaults); _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); - _.defaults(this.panel.alert, this.panelDefaults.alert); _.defaults(this.panel.legend, this.panelDefaults.legend); _.defaults(this.panel.xaxis, this.panelDefaults.xaxis); @@ -156,10 +151,10 @@ class GraphCtrl extends MetricsPanelCtrl { 'Time': 'time', 'Series': 'series', 'Table': 'table', - 'Elastic Raw Doc': 'elastic' + 'Json': 'json' }; - this.xAxisSeriesValues = ['min', 'max', 'avg', 'current', 'total']; + this.xAxisSeriesStats = ['min', 'max', 'avg', 'current', 'count', 'total']; this.subTabIndex = 0; } @@ -199,35 +194,21 @@ class GraphCtrl extends MetricsPanelCtrl { this.datapointsOutside = false; let dataHandler: (seriesData, index)=>any; - if (this.panel.xaxis.mode === 'table') { - if (dataList.length) { - // Table panel uses only first enabled tagret, so we can use dataList[0] - // for table data representation - dataList.splice(1, dataList.length - 1); - this.xAxisColumns = _.map(dataList[0].columns, (column, index) => { - return { - text: column.text, - index: index - }; - }); - - // Set last column as default value - if (!this.panel.xaxis.valueColumnIndex) { - this.panel.xaxis.valueColumnIndex = this.xAxisColumns.length - 1; - } + switch (this.panel.xaxis.mode) { + case 'series': + case 'time': { + dataHandler = this.timeSeriesHandler; + break; } - - dataHandler = this.tableHandler; - } else if (this.panel.xaxis.mode === 'elastic') { - if (dataList.length) { - dataList.splice(1, dataList.length - 1); - var point = _.first(dataList[0].datapoints); - this.xAxisColumns = getFieldsFromESDoc(point); + case 'table': { + // Table panel uses only first enabled target, so we can use dataList[0] + dataList.splice(1, dataList.length - 1); + dataHandler = this.tableHandler; + break; + } + case 'json': { + break; } - - dataHandler = this.esRawDocHandler; - } else { - dataHandler = this.timeSeriesHandler; } this.seriesList = dataList.map(dataHandler.bind(this)); diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index 3a2135cd360..c5ebfcc92d0 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -57,8 +57,8 @@
@@ -69,7 +69,7 @@
@@ -80,7 +80,7 @@
@@ -88,11 +88,11 @@
-
+
@@ -103,7 +103,7 @@
From 395213abd75a899da58a42dc57bf76e82c5db2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Sep 2016 11:46:16 +0200 Subject: [PATCH 12/74] feat(graph panel): more refactoring of #5917 --- public/app/core/directives/metric_segment.js | 5 +- public/app/core/services/segment_srv.js | 1 + .../app/features/panel/metrics_ds_selector.ts | 4 +- public/app/plugins/panel/graph/module.ts | 20 +++++- public/app/plugins/panel/graph/tab_axes.html | 63 +++---------------- public/sass/components/_gf-form.scss | 2 +- 6 files changed, 33 insertions(+), 62 deletions(-) diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 7669f2fb709..36eac942c3e 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -23,10 +23,10 @@ function (_, $, coreModule) { getOptions: "&", onChange: "&", }, - link: function($scope, elem, attrs) { + link: function($scope, elem) { var $input = $(inputTemplate); - var $button = $(attrs.styleMode === 'select' ? selectTemplate : linkTemplate); var segment = $scope.segment; + var $button = $(segment.selectMode ? selectTemplate : linkTemplate); var options = null; var cancelBlur = null; var linkMode = true; @@ -179,6 +179,7 @@ function (_, $, coreModule) { cssClass: attrs.cssClass, custom: attrs.custom, value: option ? option.text : value, + selectMode: attrs.selectMode, }; return uiSegmentSrv.newSegment(segment); }; diff --git a/public/app/core/services/segment_srv.js b/public/app/core/services/segment_srv.js index d05a2bb011f..9d13e8e27e3 100644 --- a/public/app/core/services/segment_srv.js +++ b/public/app/core/services/segment_srv.js @@ -28,6 +28,7 @@ function (angular, _, coreModule) { this.type = options.type; this.fake = options.fake; this.value = options.value; + this.selectMode = options.selectMode; this.type = options.type; this.expandable = options.expandable; this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value)); diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts index c0e1b776062..f925b2afb42 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_ds_selector.ts @@ -16,7 +16,7 @@ var template = ` Panel data source -
@@ -67,7 +67,7 @@ export class MetricsDsSelectorCtrl { this.current = {name: dsValue + ' not found', value: null}; } - this.dsSegment = uiSegmentSrv.newSegment(this.current.name); + this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); } getOptions() { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index f1b54c03e11..cd2ac329ef5 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -23,7 +23,7 @@ class GraphCtrl extends MetricsPanelCtrl { logScales: any; unitFormats: any; xAxisModes: any; - xAxisSeriesStats: any; + xNameSegment: any; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -154,7 +154,6 @@ class GraphCtrl extends MetricsPanelCtrl { 'Json': 'json' }; - this.xAxisSeriesStats = ['min', 'max', 'avg', 'current', 'count', 'total']; this.subTabIndex = 0; } @@ -288,7 +287,6 @@ class GraphCtrl extends MetricsPanelCtrl { }); var alias = valueField; - return this.seriesHandler(seriesData, index, datapoints, alias); } @@ -396,6 +394,22 @@ class GraphCtrl extends MetricsPanelCtrl { fileExport.exportSeriesListToCsvColumns(this.seriesList); } + xAxisModeChanged() { + // set defaults + this.refresh(); + } + + getXAxisNameOptions() { + return this.$q.when([ + {text: 'Avg', value: 'avg'} + ]); + } + + getXAxisValueOptions() { + return this.$q.when([ + {text: 'Avg', value: 'avg'} + ]); + } } function getFieldsFromESDoc(doc) { diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index c5ebfcc92d0..d939c61c7ff 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -47,68 +47,23 @@
+ +
+ + +
+ -
+
-
- -
+
- -
- -
- -
-
- -
- -
- -
-
- - -
- -
- -
-
- -
- -
- -
-
diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 82ca9073a4a..f8f92e9ebf8 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -135,7 +135,7 @@ $gf-form-margin: 0.25rem; &::after { position: absolute; top: 35%; - right: $input-padding-x/2; + right: $input-padding-x; background-color: transparent; color: $input-color; font: normal normal normal $font-size-sm/1 FontAwesome; From 43d8bd5a254733582baf3c1bc209a9b2db3e0866 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 07:01:53 +0200 Subject: [PATCH 13/74] feat(prometheus): initial support for prometheus --- pkg/services/alerting/conditions/query.go | 47 +- pkg/services/alerting/init/init.go | 1 + pkg/tsdb/batch.go | 2 +- pkg/tsdb/models.go | 12 +- pkg/tsdb/prometheus/prometheus.go | 96 +++++ pkg/tsdb/prometheus/types.go | 1 + pkg/tsdb/query.go | 12 - pkg/tsdb/time_range.go | 77 ++++ pkg/tsdb/time_range_test.go | 78 ++++ .../app/features/alerting/alert_tab_ctrl.ts | 4 +- .../prometheus/client_golang/LICENSE | 201 +++++++++ .../prometheus/client_golang/NOTICE | 23 + .../client_golang/api/prometheus/api.go | 348 +++++++++++++++ vendor/github.com/prometheus/common/LICENSE | 201 +++++++++ vendor/github.com/prometheus/common/NOTICE | 5 + .../prometheus/common/model/alert.go | 136 ++++++ .../prometheus/common/model/fingerprinting.go | 105 +++++ .../github.com/prometheus/common/model/fnv.go | 42 ++ .../prometheus/common/model/labels.go | 206 +++++++++ .../prometheus/common/model/labelset.go | 169 ++++++++ .../prometheus/common/model/metric.go | 98 +++++ .../prometheus/common/model/model.go | 16 + .../prometheus/common/model/signature.go | 144 +++++++ .../prometheus/common/model/silence.go | 106 +++++ .../prometheus/common/model/time.go | 249 +++++++++++ .../prometheus/common/model/value.go | 403 ++++++++++++++++++ vendor/golang.org/x/net/LICENSE | 27 ++ vendor/golang.org/x/net/PATENTS | 22 + .../x/net/context/ctxhttp/ctxhttp.go | 74 ++++ .../x/net/context/ctxhttp/ctxhttp_pre17.go | 147 +++++++ vendor/vendor.json | 21 +- 31 files changed, 3046 insertions(+), 27 deletions(-) create mode 100644 pkg/tsdb/prometheus/prometheus.go create mode 100644 pkg/tsdb/prometheus/types.go delete mode 100644 pkg/tsdb/query.go create mode 100644 pkg/tsdb/time_range.go create mode 100644 pkg/tsdb/time_range_test.go create mode 100644 vendor/github.com/prometheus/client_golang/LICENSE create mode 100644 vendor/github.com/prometheus/client_golang/NOTICE create mode 100644 vendor/github.com/prometheus/client_golang/api/prometheus/api.go create mode 100644 vendor/github.com/prometheus/common/LICENSE create mode 100644 vendor/github.com/prometheus/common/NOTICE create mode 100644 vendor/github.com/prometheus/common/model/alert.go create mode 100644 vendor/github.com/prometheus/common/model/fingerprinting.go create mode 100644 vendor/github.com/prometheus/common/model/fnv.go create mode 100644 vendor/github.com/prometheus/common/model/labels.go create mode 100644 vendor/github.com/prometheus/common/model/labelset.go create mode 100644 vendor/github.com/prometheus/common/model/metric.go create mode 100644 vendor/github.com/prometheus/common/model/model.go create mode 100644 vendor/github.com/prometheus/common/model/signature.go create mode 100644 vendor/github.com/prometheus/common/model/silence.go create mode 100644 vendor/github.com/prometheus/common/model/time.go create mode 100644 vendor/github.com/prometheus/common/model/value.go create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/context/ctxhttp/ctxhttp.go create mode 100644 vendor/golang.org/x/net/context/ctxhttp/ctxhttp_pre17.go diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index f966985f132..208527287f3 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -2,6 +2,8 @@ package conditions import ( "fmt" + "strings" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -32,7 +34,8 @@ type AlertQuery struct { } func (c *QueryCondition) Eval(context *alerting.EvalContext) { - seriesList, err := c.executeQuery(context) + timerange := tsdb.NewTimerange(c.Query.From, c.Query.To) + seriesList, err := c.executeQuery(context, timerange) if err != nil { context.Error = err return @@ -66,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { context.Firing = len(context.EvalMatches) > 0 } -func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeSeriesSlice, error) { +func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ Id: c.Query.DatasourceId, OrgId: context.Rule.OrgId, @@ -76,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS return nil, fmt.Errorf("Could not find datasource") } - req := c.getRequestForAlertRule(getDsInfo.Result) + req := c.getRequestForAlertRule(getDsInfo.Result, timerange) result := make(tsdb.TimeSeriesSlice, 0) resp, err := c.HandleRequest(req) @@ -102,12 +105,9 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS return result, nil } -func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.Request { +func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timerange tsdb.TimeRange) *tsdb.Request { req := &tsdb.Request{ - TimeRange: tsdb.TimeRange{ - From: c.Query.From, - To: c.Query.To, - }, + TimeRange: timerange, Queries: []*tsdb.Query{ { RefId: "A", @@ -141,6 +141,15 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro condition.Query.Model = queryJson.Get("model") condition.Query.From = queryJson.Get("params").MustArray()[1].(string) condition.Query.To = queryJson.Get("params").MustArray()[2].(string) + + if err := validateFromValue(condition.Query.From); err != nil { + return nil, err + } + + if err := validateToValue(condition.Query.To); err != nil { + return nil, err + } + condition.Query.DatasourceId = queryJson.Get("datasourceId").MustInt64() reducerJson := model.Get("reducer") @@ -155,3 +164,25 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro condition.Evaluator = evaluator return &condition, nil } + +func validateFromValue(from string) error { + fromRaw := strings.Replace(from, "now-", "", 1) + + _, err := time.ParseDuration("-" + fromRaw) + return err +} + +func validateToValue(to string) error { + if to == "now" { + return nil + } else if strings.HasPrefix(to, "now-") { + withoutNow := strings.Replace(to, "now-", "", 1) + + _, err := time.ParseDuration("-" + withoutNow) + if err == nil { + return nil + } + } + + return fmt.Errorf("cannot parse to value %s", to) +} diff --git a/pkg/services/alerting/init/init.go b/pkg/services/alerting/init/init.go index b6627a359e6..b9cba2fd353 100644 --- a/pkg/services/alerting/init/init.go +++ b/pkg/services/alerting/init/init.go @@ -6,6 +6,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" _ "github.com/grafana/grafana/pkg/tsdb/graphite" + _ "github.com/grafana/grafana/pkg/tsdb/prometheus" ) var engine *alerting.Engine diff --git a/pkg/tsdb/batch.go b/pkg/tsdb/batch.go index bc16ed1e75a..4dee7b31c86 100644 --- a/pkg/tsdb/batch.go +++ b/pkg/tsdb/batch.go @@ -26,7 +26,7 @@ func (bg *Batch) process(context *QueryContext) { if executor == nil { bg.Done = true result := &BatchResult{ - Error: errors.New("Could not find executor for data source type " + bg.Queries[0].DataSource.PluginId), + Error: errors.New("Could not find executor for data source type: " + bg.Queries[0].DataSource.PluginId), QueryResults: make(map[string]*QueryResult), } for _, query := range bg.Queries { diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 05a8b13ef84..117b00efe3a 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -1,10 +1,16 @@ package tsdb -type TimeRange struct { - From string - To string +type Query struct { + RefId string + Query string + Depends []string + DataSource *DataSourceInfo + Results []*TimeSeries + Exclude bool } +type QuerySlice []*Query + type Request struct { TimeRange TimeRange MaxDataPoints int diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go new file mode 100644 index 00000000000..2b2ccd382af --- /dev/null +++ b/pkg/tsdb/prometheus/prometheus.go @@ -0,0 +1,96 @@ +package prometheus + +import ( + "context" + "net/http" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/tsdb" + "github.com/prometheus/client_golang/api/prometheus" + pmodel "github.com/prometheus/common/model" +) + +type PrometheusExecutor struct { + *tsdb.DataSourceInfo +} + +func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { + return &PrometheusExecutor{dsInfo} +} + +var ( + plog log.Logger + HttpClient http.Client +) + +func init() { + plog = log.New("tsdb.prometheus") + tsdb.RegisterExecutor("prometheus", NewPrometheusExecutor) +} + +func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) { + cfg := prometheus.Config{ + Address: e.DataSourceInfo.Url, + } + + client, err := prometheus.New(cfg) + if err != nil { + return nil, err + } + + return prometheus.NewQueryAPI(client), nil +} + +func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { + result := &tsdb.BatchResult{} + + client, err := e.getClient() + if err != nil { + result.Error = err + return result + } + + from, _ := queryContext.TimeRange.FromTime() + to, _ := queryContext.TimeRange.ToTime() + timeRange := prometheus.Range{ + Start: from, + End: to, + Step: time.Second, + } + + ctx := context.Background() + value, err := client.QueryRange(ctx, "counters_logins", timeRange) + + if err != nil { + result.Error = err + return result + } + + result.QueryResults = parseResponse(value) + return result +} + +func parseResponse(value pmodel.Value) map[string]*tsdb.QueryResult { + queryResults := make(map[string]*tsdb.QueryResult) + queryRes := &tsdb.QueryResult{} + + data := value.(pmodel.Matrix) + + for _, v := range data { + var points [][2]*float64 + for _, k := range v.Values { + dummie := float64(k.Timestamp) + d2 := float64(k.Value) + points = append(points, [2]*float64{&d2, &dummie}) + } + + queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ + Name: v.Metric.String(), + Points: points, + }) + } + + queryResults["A"] = queryRes + return queryResults +} diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/types.go new file mode 100644 index 00000000000..7b1b4c03ead --- /dev/null +++ b/pkg/tsdb/prometheus/types.go @@ -0,0 +1 @@ +package prometheus diff --git a/pkg/tsdb/query.go b/pkg/tsdb/query.go deleted file mode 100644 index bcead660450..00000000000 --- a/pkg/tsdb/query.go +++ /dev/null @@ -1,12 +0,0 @@ -package tsdb - -type Query struct { - RefId string - Query string - Depends []string - DataSource *DataSourceInfo - Results []*TimeSeries - Exclude bool -} - -type QuerySlice []*Query diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go new file mode 100644 index 00000000000..e7e54cef5f9 --- /dev/null +++ b/pkg/tsdb/time_range.go @@ -0,0 +1,77 @@ +package tsdb + +import ( + "fmt" + "strings" + "time" +) + +func NewTimerange(from, to string) TimeRange { + return TimeRange{ + From: from, + To: to, + Now: time.Now(), + } +} + +type TimeRange struct { + From string + To string + Now time.Time +} + +func (tr TimeRange) FromUnix() (int64, error) { + fromRaw := strings.Replace(tr.From, "now-", "", 1) + + diff, err := time.ParseDuration("-" + fromRaw) + if err != nil { + return 0, err + } + + return tr.Now.Add(diff).Unix(), nil +} + +func (tr TimeRange) FromTime() (time.Time, error) { + fromRaw := strings.Replace(tr.From, "now-", "", 1) + + diff, err := time.ParseDuration("-" + fromRaw) + if err != nil { + return time.Time{}, err + } + + return tr.Now.Add(diff), nil +} + +func (tr TimeRange) ToUnix() (int64, error) { + if tr.To == "now" { + return tr.Now.Unix(), nil + } else if strings.HasPrefix(tr.To, "now-") { + withoutNow := strings.Replace(tr.To, "now-", "", 1) + + diff, err := time.ParseDuration("-" + withoutNow) + if err != nil { + return 0, nil + } + + return tr.Now.Add(diff).Unix(), nil + } + + return 0, fmt.Errorf("cannot parse to value %s", tr.To) +} + +func (tr TimeRange) ToTime() (time.Time, error) { + if tr.To == "now" { + return tr.Now, nil + } else if strings.HasPrefix(tr.To, "now-") { + withoutNow := strings.Replace(tr.To, "now-", "", 1) + + diff, err := time.ParseDuration("-" + withoutNow) + if err != nil { + return time.Time{}, nil + } + + return tr.Now.Add(diff), nil + } + + return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To) +} diff --git a/pkg/tsdb/time_range_test.go b/pkg/tsdb/time_range_test.go new file mode 100644 index 00000000000..d64eb8cc86e --- /dev/null +++ b/pkg/tsdb/time_range_test.go @@ -0,0 +1,78 @@ +package tsdb + +import ( + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestTimeRange(t *testing.T) { + Convey("Time range", t, func() { + + now := time.Now() + + Convey("Can parse 5m, now", func() { + tr := TimeRange{ + From: "5m", + To: "now", + Now: now, + } + + Convey("5m ago ", func() { + fiveMinAgo, _ := time.ParseDuration("-5m") + expected := now.Add(fiveMinAgo) + + res, err := tr.FromUnix() + So(err, ShouldBeNil) + So(res, ShouldAlmostEqual, expected.Unix()) + }) + + Convey("now ", func() { + res, err := tr.ToUnix() + So(err, ShouldBeNil) + So(res, ShouldAlmostEqual, now.Unix()) + }) + }) + + Convey("Can parse 5h, now-10m", func() { + tr := TimeRange{ + From: "5h", + To: "now-10m", + Now: now, + } + + Convey("5h ago ", func() { + fiveMinAgo, _ := time.ParseDuration("-5h") + expected := now.Add(fiveMinAgo) + + res, err := tr.FromUnix() + So(err, ShouldBeNil) + So(res, ShouldAlmostEqual, expected.Unix()) + }) + + Convey("now-10m ", func() { + fiveMinAgo, _ := time.ParseDuration("-10m") + expected := now.Add(fiveMinAgo) + res, err := tr.ToUnix() + So(err, ShouldBeNil) + So(res, ShouldAlmostEqual, expected.Unix()) + }) + }) + + Convey("Cannot parse asdf", func() { + var err error + tr := TimeRange{ + From: "asdf", + To: "asdf", + Now: now, + } + + _, err = tr.FromUnix() + So(err, ShouldNotBeNil) + + _, err = tr.ToUnix() + So(err, ShouldNotBeNil) + }) + }) +} diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 01cacaf9740..ac4f6af5e38 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -227,8 +227,8 @@ export class AlertTabCtrl { var datasourceName = foundTarget.datasource || this.panel.datasource; this.datasourceSrv.get(datasourceName).then(ds => { - if (ds.meta.id !== 'graphite') { - this.error = 'Currently the alerting backend only supports Graphite queries'; + if (ds.meta.id !== 'graphite' && ds.meta.id !== 'prometheus') { + this.error = 'You datsource does not support alerting queries'; } else if (this.templateSrv.variableExists(foundTarget.target)) { this.error = 'Template variables are not supported in alert queries'; } else { diff --git a/vendor/github.com/prometheus/client_golang/LICENSE b/vendor/github.com/prometheus/client_golang/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/prometheus/client_golang/NOTICE b/vendor/github.com/prometheus/client_golang/NOTICE new file mode 100644 index 00000000000..dd878a30ee9 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/NOTICE @@ -0,0 +1,23 @@ +Prometheus instrumentation library for Go applications +Copyright 2012-2015 The Prometheus Authors + +This product includes software developed at +SoundCloud Ltd. (http://soundcloud.com/). + + +The following components are included in this product: + +perks - a fork of https://github.com/bmizerany/perks +https://github.com/beorn7/perks +Copyright 2013-2015 Blake Mizerany, Björn Rabenstein +See https://github.com/beorn7/perks/blob/master/README.md for license details. + +Go support for Protocol Buffers - Google's data interchange format +http://github.com/golang/protobuf/ +Copyright 2010 The Go Authors +See source code for license details. + +Support for streaming Protocol Buffer messages for the Go language (golang). +https://github.com/matttproud/golang_protobuf_extensions +Copyright 2013 Matt T. Proud +Licensed under the Apache License, Version 2.0 diff --git a/vendor/github.com/prometheus/client_golang/api/prometheus/api.go b/vendor/github.com/prometheus/client_golang/api/prometheus/api.go new file mode 100644 index 00000000000..cc5cbc364d3 --- /dev/null +++ b/vendor/github.com/prometheus/client_golang/api/prometheus/api.go @@ -0,0 +1,348 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package prometheus provides bindings to the Prometheus HTTP API: +// http://prometheus.io/docs/querying/api/ +package prometheus + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "time" + + "github.com/prometheus/common/model" + "golang.org/x/net/context" + "golang.org/x/net/context/ctxhttp" +) + +const ( + statusAPIError = 422 + apiPrefix = "/api/v1" + + epQuery = "/query" + epQueryRange = "/query_range" + epLabelValues = "/label/:name/values" + epSeries = "/series" +) + +// ErrorType models the different API error types. +type ErrorType string + +// Possible values for ErrorType. +const ( + ErrBadData ErrorType = "bad_data" + ErrTimeout = "timeout" + ErrCanceled = "canceled" + ErrExec = "execution" + ErrBadResponse = "bad_response" +) + +// Error is an error returned by the API. +type Error struct { + Type ErrorType + Msg string +} + +func (e *Error) Error() string { + return fmt.Sprintf("%s: %s", e.Type, e.Msg) +} + +// CancelableTransport is like net.Transport but provides +// per-request cancelation functionality. +type CancelableTransport interface { + http.RoundTripper + CancelRequest(req *http.Request) +} + +// DefaultTransport is used if no Transport is set in Config. +var DefaultTransport CancelableTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} + +// Config defines configuration parameters for a new client. +type Config struct { + // The address of the Prometheus to connect to. + Address string + + // Transport is used by the Client to drive HTTP requests. If not + // provided, DefaultTransport will be used. + Transport CancelableTransport +} + +func (cfg *Config) transport() CancelableTransport { + if cfg.Transport == nil { + return DefaultTransport + } + return cfg.Transport +} + +// Client is the interface for an API client. +type Client interface { + url(ep string, args map[string]string) *url.URL + do(context.Context, *http.Request) (*http.Response, []byte, error) +} + +// New returns a new Client. +// +// It is safe to use the returned Client from multiple goroutines. +func New(cfg Config) (Client, error) { + u, err := url.Parse(cfg.Address) + if err != nil { + return nil, err + } + u.Path = strings.TrimRight(u.Path, "/") + apiPrefix + + return &httpClient{ + endpoint: u, + transport: cfg.transport(), + }, nil +} + +type httpClient struct { + endpoint *url.URL + transport CancelableTransport +} + +func (c *httpClient) url(ep string, args map[string]string) *url.URL { + p := path.Join(c.endpoint.Path, ep) + + for arg, val := range args { + arg = ":" + arg + p = strings.Replace(p, arg, val, -1) + } + + u := *c.endpoint + u.Path = p + + return &u +} + +func (c *httpClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + resp, err := ctxhttp.Do(ctx, &http.Client{Transport: c.transport}, req) + + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + if err != nil { + return nil, nil, err + } + + var body []byte + done := make(chan struct{}) + go func() { + body, err = ioutil.ReadAll(resp.Body) + close(done) + }() + + select { + case <-ctx.Done(): + err = resp.Body.Close() + <-done + if err == nil { + err = ctx.Err() + } + case <-done: + } + + return resp, body, err +} + +// apiClient wraps a regular client and processes successful API responses. +// Successful also includes responses that errored at the API level. +type apiClient struct { + Client +} + +type apiResponse struct { + Status string `json:"status"` + Data json.RawMessage `json:"data"` + ErrorType ErrorType `json:"errorType"` + Error string `json:"error"` +} + +func (c apiClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { + resp, body, err := c.Client.do(ctx, req) + if err != nil { + return resp, body, err + } + + code := resp.StatusCode + + if code/100 != 2 && code != statusAPIError { + return resp, body, &Error{ + Type: ErrBadResponse, + Msg: fmt.Sprintf("bad response code %d", resp.StatusCode), + } + } + + var result apiResponse + + if err = json.Unmarshal(body, &result); err != nil { + return resp, body, &Error{ + Type: ErrBadResponse, + Msg: err.Error(), + } + } + + if (code == statusAPIError) != (result.Status == "error") { + err = &Error{ + Type: ErrBadResponse, + Msg: "inconsistent body for response code", + } + } + + if code == statusAPIError && result.Status == "error" { + err = &Error{ + Type: result.ErrorType, + Msg: result.Error, + } + } + + return resp, []byte(result.Data), err +} + +// Range represents a sliced time range. +type Range struct { + // The boundaries of the time range. + Start, End time.Time + // The maximum time between two slices within the boundaries. + Step time.Duration +} + +// queryResult contains result data for a query. +type queryResult struct { + Type model.ValueType `json:"resultType"` + Result interface{} `json:"result"` + + // The decoded value. + v model.Value +} + +func (qr *queryResult) UnmarshalJSON(b []byte) error { + v := struct { + Type model.ValueType `json:"resultType"` + Result json.RawMessage `json:"result"` + }{} + + err := json.Unmarshal(b, &v) + if err != nil { + return err + } + + switch v.Type { + case model.ValScalar: + var sv model.Scalar + err = json.Unmarshal(v.Result, &sv) + qr.v = &sv + + case model.ValVector: + var vv model.Vector + err = json.Unmarshal(v.Result, &vv) + qr.v = vv + + case model.ValMatrix: + var mv model.Matrix + err = json.Unmarshal(v.Result, &mv) + qr.v = mv + + default: + err = fmt.Errorf("unexpected value type %q", v.Type) + } + return err +} + +// QueryAPI provides bindings the Prometheus's query API. +type QueryAPI interface { + // Query performs a query for the given time. + Query(ctx context.Context, query string, ts time.Time) (model.Value, error) + // Query performs a query for the given range. + QueryRange(ctx context.Context, query string, r Range) (model.Value, error) +} + +// NewQueryAPI returns a new QueryAPI for the client. +// +// It is safe to use the returned QueryAPI from multiple goroutines. +func NewQueryAPI(c Client) QueryAPI { + return &httpQueryAPI{client: apiClient{c}} +} + +type httpQueryAPI struct { + client Client +} + +func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) { + u := h.client.url(epQuery, nil) + q := u.Query() + + q.Set("query", query) + q.Set("time", ts.Format(time.RFC3339Nano)) + + u.RawQuery = q.Encode() + + req, _ := http.NewRequest("GET", u.String(), nil) + + _, body, err := h.client.do(ctx, req) + if err != nil { + return nil, err + } + + var qres queryResult + err = json.Unmarshal(body, &qres) + + return model.Value(qres.v), err +} + +func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) { + u := h.client.url(epQueryRange, nil) + q := u.Query() + + var ( + start = r.Start.Format(time.RFC3339Nano) + end = r.End.Format(time.RFC3339Nano) + step = strconv.FormatFloat(r.Step.Seconds(), 'f', 3, 64) + ) + + q.Set("query", query) + q.Set("start", start) + q.Set("end", end) + q.Set("step", step) + + u.RawQuery = q.Encode() + + req, _ := http.NewRequest("GET", u.String(), nil) + + _, body, err := h.client.do(ctx, req) + if err != nil { + return nil, err + } + + var qres queryResult + err = json.Unmarshal(body, &qres) + + return model.Value(qres.v), err +} diff --git a/vendor/github.com/prometheus/common/LICENSE b/vendor/github.com/prometheus/common/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/vendor/github.com/prometheus/common/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/prometheus/common/NOTICE b/vendor/github.com/prometheus/common/NOTICE new file mode 100644 index 00000000000..636a2c1a5e8 --- /dev/null +++ b/vendor/github.com/prometheus/common/NOTICE @@ -0,0 +1,5 @@ +Common libraries shared by Prometheus Go components. +Copyright 2015 The Prometheus Authors + +This product includes software developed at +SoundCloud Ltd. (http://soundcloud.com/). diff --git a/vendor/github.com/prometheus/common/model/alert.go b/vendor/github.com/prometheus/common/model/alert.go new file mode 100644 index 00000000000..35e739c7ad2 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/alert.go @@ -0,0 +1,136 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "fmt" + "time" +) + +type AlertStatus string + +const ( + AlertFiring AlertStatus = "firing" + AlertResolved AlertStatus = "resolved" +) + +// Alert is a generic representation of an alert in the Prometheus eco-system. +type Alert struct { + // Label value pairs for purpose of aggregation, matching, and disposition + // dispatching. This must minimally include an "alertname" label. + Labels LabelSet `json:"labels"` + + // Extra key/value information which does not define alert identity. + Annotations LabelSet `json:"annotations"` + + // The known time range for this alert. Both ends are optional. + StartsAt time.Time `json:"startsAt,omitempty"` + EndsAt time.Time `json:"endsAt,omitempty"` + GeneratorURL string `json:"generatorURL"` +} + +// Name returns the name of the alert. It is equivalent to the "alertname" label. +func (a *Alert) Name() string { + return string(a.Labels[AlertNameLabel]) +} + +// Fingerprint returns a unique hash for the alert. It is equivalent to +// the fingerprint of the alert's label set. +func (a *Alert) Fingerprint() Fingerprint { + return a.Labels.Fingerprint() +} + +func (a *Alert) String() string { + s := fmt.Sprintf("%s[%s]", a.Name(), a.Fingerprint().String()[:7]) + if a.Resolved() { + return s + "[resolved]" + } + return s + "[active]" +} + +// Resolved returns true iff the activity interval ended in the past. +func (a *Alert) Resolved() bool { + return a.ResolvedAt(time.Now()) +} + +// ResolvedAt returns true off the activity interval ended before +// the given timestamp. +func (a *Alert) ResolvedAt(ts time.Time) bool { + if a.EndsAt.IsZero() { + return false + } + return !a.EndsAt.After(ts) +} + +// Status returns the status of the alert. +func (a *Alert) Status() AlertStatus { + if a.Resolved() { + return AlertResolved + } + return AlertFiring +} + +// Validate checks whether the alert data is inconsistent. +func (a *Alert) Validate() error { + if a.StartsAt.IsZero() { + return fmt.Errorf("start time missing") + } + if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) { + return fmt.Errorf("start time must be before end time") + } + if err := a.Labels.Validate(); err != nil { + return fmt.Errorf("invalid label set: %s", err) + } + if len(a.Labels) == 0 { + return fmt.Errorf("at least one label pair required") + } + if err := a.Annotations.Validate(); err != nil { + return fmt.Errorf("invalid annotations: %s", err) + } + return nil +} + +// Alert is a list of alerts that can be sorted in chronological order. +type Alerts []*Alert + +func (as Alerts) Len() int { return len(as) } +func (as Alerts) Swap(i, j int) { as[i], as[j] = as[j], as[i] } + +func (as Alerts) Less(i, j int) bool { + if as[i].StartsAt.Before(as[j].StartsAt) { + return true + } + if as[i].EndsAt.Before(as[j].EndsAt) { + return true + } + return as[i].Fingerprint() < as[j].Fingerprint() +} + +// HasFiring returns true iff one of the alerts is not resolved. +func (as Alerts) HasFiring() bool { + for _, a := range as { + if !a.Resolved() { + return true + } + } + return false +} + +// Status returns StatusFiring iff at least one of the alerts is firing. +func (as Alerts) Status() AlertStatus { + if as.HasFiring() { + return AlertFiring + } + return AlertResolved +} diff --git a/vendor/github.com/prometheus/common/model/fingerprinting.go b/vendor/github.com/prometheus/common/model/fingerprinting.go new file mode 100644 index 00000000000..fc4de4106e8 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/fingerprinting.go @@ -0,0 +1,105 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "fmt" + "strconv" +) + +// Fingerprint provides a hash-capable representation of a Metric. +// For our purposes, FNV-1A 64-bit is used. +type Fingerprint uint64 + +// FingerprintFromString transforms a string representation into a Fingerprint. +func FingerprintFromString(s string) (Fingerprint, error) { + num, err := strconv.ParseUint(s, 16, 64) + return Fingerprint(num), err +} + +// ParseFingerprint parses the input string into a fingerprint. +func ParseFingerprint(s string) (Fingerprint, error) { + num, err := strconv.ParseUint(s, 16, 64) + if err != nil { + return 0, err + } + return Fingerprint(num), nil +} + +func (f Fingerprint) String() string { + return fmt.Sprintf("%016x", uint64(f)) +} + +// Fingerprints represents a collection of Fingerprint subject to a given +// natural sorting scheme. It implements sort.Interface. +type Fingerprints []Fingerprint + +// Len implements sort.Interface. +func (f Fingerprints) Len() int { + return len(f) +} + +// Less implements sort.Interface. +func (f Fingerprints) Less(i, j int) bool { + return f[i] < f[j] +} + +// Swap implements sort.Interface. +func (f Fingerprints) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} + +// FingerprintSet is a set of Fingerprints. +type FingerprintSet map[Fingerprint]struct{} + +// Equal returns true if both sets contain the same elements (and not more). +func (s FingerprintSet) Equal(o FingerprintSet) bool { + if len(s) != len(o) { + return false + } + + for k := range s { + if _, ok := o[k]; !ok { + return false + } + } + + return true +} + +// Intersection returns the elements contained in both sets. +func (s FingerprintSet) Intersection(o FingerprintSet) FingerprintSet { + myLength, otherLength := len(s), len(o) + if myLength == 0 || otherLength == 0 { + return FingerprintSet{} + } + + subSet := s + superSet := o + + if otherLength < myLength { + subSet = o + superSet = s + } + + out := FingerprintSet{} + + for k := range subSet { + if _, ok := superSet[k]; ok { + out[k] = struct{}{} + } + } + + return out +} diff --git a/vendor/github.com/prometheus/common/model/fnv.go b/vendor/github.com/prometheus/common/model/fnv.go new file mode 100644 index 00000000000..038fc1c9003 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/fnv.go @@ -0,0 +1,42 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +// Inline and byte-free variant of hash/fnv's fnv64a. + +const ( + offset64 = 14695981039346656037 + prime64 = 1099511628211 +) + +// hashNew initializies a new fnv64a hash value. +func hashNew() uint64 { + return offset64 +} + +// hashAdd adds a string to a fnv64a hash value, returning the updated hash. +func hashAdd(h uint64, s string) uint64 { + for i := 0; i < len(s); i++ { + h ^= uint64(s[i]) + h *= prime64 + } + return h +} + +// hashAddByte adds a byte to a fnv64a hash value, returning the updated hash. +func hashAddByte(h uint64, b byte) uint64 { + h ^= uint64(b) + h *= prime64 + return h +} diff --git a/vendor/github.com/prometheus/common/model/labels.go b/vendor/github.com/prometheus/common/model/labels.go new file mode 100644 index 00000000000..3b72e7ff8f6 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/labels.go @@ -0,0 +1,206 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + "unicode/utf8" +) + +const ( + // AlertNameLabel is the name of the label containing the an alert's name. + AlertNameLabel = "alertname" + + // ExportedLabelPrefix is the prefix to prepend to the label names present in + // exported metrics if a label of the same name is added by the server. + ExportedLabelPrefix = "exported_" + + // MetricNameLabel is the label name indicating the metric name of a + // timeseries. + MetricNameLabel = "__name__" + + // SchemeLabel is the name of the label that holds the scheme on which to + // scrape a target. + SchemeLabel = "__scheme__" + + // AddressLabel is the name of the label that holds the address of + // a scrape target. + AddressLabel = "__address__" + + // MetricsPathLabel is the name of the label that holds the path on which to + // scrape a target. + MetricsPathLabel = "__metrics_path__" + + // ReservedLabelPrefix is a prefix which is not legal in user-supplied + // label names. + ReservedLabelPrefix = "__" + + // MetaLabelPrefix is a prefix for labels that provide meta information. + // Labels with this prefix are used for intermediate label processing and + // will not be attached to time series. + MetaLabelPrefix = "__meta_" + + // TmpLabelPrefix is a prefix for temporary labels as part of relabelling. + // Labels with this prefix are used for intermediate label processing and + // will not be attached to time series. This is reserved for use in + // Prometheus configuration files by users. + TmpLabelPrefix = "__tmp_" + + // ParamLabelPrefix is a prefix for labels that provide URL parameters + // used to scrape a target. + ParamLabelPrefix = "__param_" + + // JobLabel is the label name indicating the job from which a timeseries + // was scraped. + JobLabel = "job" + + // InstanceLabel is the label name used for the instance label. + InstanceLabel = "instance" + + // BucketLabel is used for the label that defines the upper bound of a + // bucket of a histogram ("le" -> "less or equal"). + BucketLabel = "le" + + // QuantileLabel is used for the label that defines the quantile in a + // summary. + QuantileLabel = "quantile" +) + +// LabelNameRE is a regular expression matching valid label names. +var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") + +// A LabelName is a key for a LabelSet or Metric. It has a value associated +// therewith. +type LabelName string + +// IsValid is true iff the label name matches the pattern of LabelNameRE. +func (ln LabelName) IsValid() bool { + if len(ln) == 0 { + return false + } + for i, b := range ln { + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { + return false + } + } + return true +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + if !LabelNameRE.MatchString(s) { + return fmt.Errorf("%q is not a valid label name", s) + } + *ln = LabelName(s) + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (ln *LabelName) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + if !LabelNameRE.MatchString(s) { + return fmt.Errorf("%q is not a valid label name", s) + } + *ln = LabelName(s) + return nil +} + +// LabelNames is a sortable LabelName slice. In implements sort.Interface. +type LabelNames []LabelName + +func (l LabelNames) Len() int { + return len(l) +} + +func (l LabelNames) Less(i, j int) bool { + return l[i] < l[j] +} + +func (l LabelNames) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (l LabelNames) String() string { + labelStrings := make([]string, 0, len(l)) + for _, label := range l { + labelStrings = append(labelStrings, string(label)) + } + return strings.Join(labelStrings, ", ") +} + +// A LabelValue is an associated value for a LabelName. +type LabelValue string + +// IsValid returns true iff the string is a valid UTF8. +func (lv LabelValue) IsValid() bool { + return utf8.ValidString(string(lv)) +} + +// LabelValues is a sortable LabelValue slice. It implements sort.Interface. +type LabelValues []LabelValue + +func (l LabelValues) Len() int { + return len(l) +} + +func (l LabelValues) Less(i, j int) bool { + return string(l[i]) < string(l[j]) +} + +func (l LabelValues) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +// LabelPair pairs a name with a value. +type LabelPair struct { + Name LabelName + Value LabelValue +} + +// LabelPairs is a sortable slice of LabelPair pointers. It implements +// sort.Interface. +type LabelPairs []*LabelPair + +func (l LabelPairs) Len() int { + return len(l) +} + +func (l LabelPairs) Less(i, j int) bool { + switch { + case l[i].Name > l[j].Name: + return false + case l[i].Name < l[j].Name: + return true + case l[i].Value > l[j].Value: + return false + case l[i].Value < l[j].Value: + return true + default: + return false + } +} + +func (l LabelPairs) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} diff --git a/vendor/github.com/prometheus/common/model/labelset.go b/vendor/github.com/prometheus/common/model/labelset.go new file mode 100644 index 00000000000..5f931cdb9b3 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/labelset.go @@ -0,0 +1,169 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "encoding/json" + "fmt" + "sort" + "strings" +) + +// A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet +// may be fully-qualified down to the point where it may resolve to a single +// Metric in the data store or not. All operations that occur within the realm +// of a LabelSet can emit a vector of Metric entities to which the LabelSet may +// match. +type LabelSet map[LabelName]LabelValue + +// Validate checks whether all names and values in the label set +// are valid. +func (ls LabelSet) Validate() error { + for ln, lv := range ls { + if !ln.IsValid() { + return fmt.Errorf("invalid name %q", ln) + } + if !lv.IsValid() { + return fmt.Errorf("invalid value %q", lv) + } + } + return nil +} + +// Equal returns true iff both label sets have exactly the same key/value pairs. +func (ls LabelSet) Equal(o LabelSet) bool { + if len(ls) != len(o) { + return false + } + for ln, lv := range ls { + olv, ok := o[ln] + if !ok { + return false + } + if olv != lv { + return false + } + } + return true +} + +// Before compares the metrics, using the following criteria: +// +// If m has fewer labels than o, it is before o. If it has more, it is not. +// +// If the number of labels is the same, the superset of all label names is +// sorted alphanumerically. The first differing label pair found in that order +// determines the outcome: If the label does not exist at all in m, then m is +// before o, and vice versa. Otherwise the label value is compared +// alphanumerically. +// +// If m and o are equal, the method returns false. +func (ls LabelSet) Before(o LabelSet) bool { + if len(ls) < len(o) { + return true + } + if len(ls) > len(o) { + return false + } + + lns := make(LabelNames, 0, len(ls)+len(o)) + for ln := range ls { + lns = append(lns, ln) + } + for ln := range o { + lns = append(lns, ln) + } + // It's probably not worth it to de-dup lns. + sort.Sort(lns) + for _, ln := range lns { + mlv, ok := ls[ln] + if !ok { + return true + } + olv, ok := o[ln] + if !ok { + return false + } + if mlv < olv { + return true + } + if mlv > olv { + return false + } + } + return false +} + +// Clone returns a copy of the label set. +func (ls LabelSet) Clone() LabelSet { + lsn := make(LabelSet, len(ls)) + for ln, lv := range ls { + lsn[ln] = lv + } + return lsn +} + +// Merge is a helper function to non-destructively merge two label sets. +func (l LabelSet) Merge(other LabelSet) LabelSet { + result := make(LabelSet, len(l)) + + for k, v := range l { + result[k] = v + } + + for k, v := range other { + result[k] = v + } + + return result +} + +func (l LabelSet) String() string { + lstrs := make([]string, 0, len(l)) + for l, v := range l { + lstrs = append(lstrs, fmt.Sprintf("%s=%q", l, v)) + } + + sort.Strings(lstrs) + return fmt.Sprintf("{%s}", strings.Join(lstrs, ", ")) +} + +// Fingerprint returns the LabelSet's fingerprint. +func (ls LabelSet) Fingerprint() Fingerprint { + return labelSetToFingerprint(ls) +} + +// FastFingerprint returns the LabelSet's Fingerprint calculated by a faster hashing +// algorithm, which is, however, more susceptible to hash collisions. +func (ls LabelSet) FastFingerprint() Fingerprint { + return labelSetToFastFingerprint(ls) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *LabelSet) UnmarshalJSON(b []byte) error { + var m map[LabelName]LabelValue + if err := json.Unmarshal(b, &m); err != nil { + return err + } + // encoding/json only unmarshals maps of the form map[string]T. It treats + // LabelName as a string and does not call its UnmarshalJSON method. + // Thus, we have to replicate the behavior here. + for ln := range m { + if !LabelNameRE.MatchString(string(ln)) { + return fmt.Errorf("%q is not a valid label name", ln) + } + } + *l = LabelSet(m) + return nil +} diff --git a/vendor/github.com/prometheus/common/model/metric.go b/vendor/github.com/prometheus/common/model/metric.go new file mode 100644 index 00000000000..a5da59a5055 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/metric.go @@ -0,0 +1,98 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "fmt" + "regexp" + "sort" + "strings" +) + +var ( + separator = []byte{0} + MetricNameRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_:]*$`) +) + +// A Metric is similar to a LabelSet, but the key difference is that a Metric is +// a singleton and refers to one and only one stream of samples. +type Metric LabelSet + +// Equal compares the metrics. +func (m Metric) Equal(o Metric) bool { + return LabelSet(m).Equal(LabelSet(o)) +} + +// Before compares the metrics' underlying label sets. +func (m Metric) Before(o Metric) bool { + return LabelSet(m).Before(LabelSet(o)) +} + +// Clone returns a copy of the Metric. +func (m Metric) Clone() Metric { + clone := Metric{} + for k, v := range m { + clone[k] = v + } + return clone +} + +func (m Metric) String() string { + metricName, hasName := m[MetricNameLabel] + numLabels := len(m) - 1 + if !hasName { + numLabels = len(m) + } + labelStrings := make([]string, 0, numLabels) + for label, value := range m { + if label != MetricNameLabel { + labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value)) + } + } + + switch numLabels { + case 0: + if hasName { + return string(metricName) + } + return "{}" + default: + sort.Strings(labelStrings) + return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", ")) + } +} + +// Fingerprint returns a Metric's Fingerprint. +func (m Metric) Fingerprint() Fingerprint { + return LabelSet(m).Fingerprint() +} + +// FastFingerprint returns a Metric's Fingerprint calculated by a faster hashing +// algorithm, which is, however, more susceptible to hash collisions. +func (m Metric) FastFingerprint() Fingerprint { + return LabelSet(m).FastFingerprint() +} + +// IsValidMetricName returns true iff name matches the pattern of MetricNameRE. +func IsValidMetricName(n LabelValue) bool { + if len(n) == 0 { + return false + } + for i, b := range n { + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)) { + return false + } + } + return true +} diff --git a/vendor/github.com/prometheus/common/model/model.go b/vendor/github.com/prometheus/common/model/model.go new file mode 100644 index 00000000000..a7b9691707e --- /dev/null +++ b/vendor/github.com/prometheus/common/model/model.go @@ -0,0 +1,16 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package model contains common data structures that are shared across +// Prometheus components and libraries. +package model diff --git a/vendor/github.com/prometheus/common/model/signature.go b/vendor/github.com/prometheus/common/model/signature.go new file mode 100644 index 00000000000..8762b13c63d --- /dev/null +++ b/vendor/github.com/prometheus/common/model/signature.go @@ -0,0 +1,144 @@ +// Copyright 2014 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "sort" +) + +// SeparatorByte is a byte that cannot occur in valid UTF-8 sequences and is +// used to separate label names, label values, and other strings from each other +// when calculating their combined hash value (aka signature aka fingerprint). +const SeparatorByte byte = 255 + +var ( + // cache the signature of an empty label set. + emptyLabelSignature = hashNew() +) + +// LabelsToSignature returns a quasi-unique signature (i.e., fingerprint) for a +// given label set. (Collisions are possible but unlikely if the number of label +// sets the function is applied to is small.) +func LabelsToSignature(labels map[string]string) uint64 { + if len(labels) == 0 { + return emptyLabelSignature + } + + labelNames := make([]string, 0, len(labels)) + for labelName := range labels { + labelNames = append(labelNames, labelName) + } + sort.Strings(labelNames) + + sum := hashNew() + for _, labelName := range labelNames { + sum = hashAdd(sum, labelName) + sum = hashAddByte(sum, SeparatorByte) + sum = hashAdd(sum, labels[labelName]) + sum = hashAddByte(sum, SeparatorByte) + } + return sum +} + +// labelSetToFingerprint works exactly as LabelsToSignature but takes a LabelSet as +// parameter (rather than a label map) and returns a Fingerprint. +func labelSetToFingerprint(ls LabelSet) Fingerprint { + if len(ls) == 0 { + return Fingerprint(emptyLabelSignature) + } + + labelNames := make(LabelNames, 0, len(ls)) + for labelName := range ls { + labelNames = append(labelNames, labelName) + } + sort.Sort(labelNames) + + sum := hashNew() + for _, labelName := range labelNames { + sum = hashAdd(sum, string(labelName)) + sum = hashAddByte(sum, SeparatorByte) + sum = hashAdd(sum, string(ls[labelName])) + sum = hashAddByte(sum, SeparatorByte) + } + return Fingerprint(sum) +} + +// labelSetToFastFingerprint works similar to labelSetToFingerprint but uses a +// faster and less allocation-heavy hash function, which is more susceptible to +// create hash collisions. Therefore, collision detection should be applied. +func labelSetToFastFingerprint(ls LabelSet) Fingerprint { + if len(ls) == 0 { + return Fingerprint(emptyLabelSignature) + } + + var result uint64 + for labelName, labelValue := range ls { + sum := hashNew() + sum = hashAdd(sum, string(labelName)) + sum = hashAddByte(sum, SeparatorByte) + sum = hashAdd(sum, string(labelValue)) + result ^= sum + } + return Fingerprint(result) +} + +// SignatureForLabels works like LabelsToSignature but takes a Metric as +// parameter (rather than a label map) and only includes the labels with the +// specified LabelNames into the signature calculation. The labels passed in +// will be sorted by this function. +func SignatureForLabels(m Metric, labels ...LabelName) uint64 { + if len(labels) == 0 { + return emptyLabelSignature + } + + sort.Sort(LabelNames(labels)) + + sum := hashNew() + for _, label := range labels { + sum = hashAdd(sum, string(label)) + sum = hashAddByte(sum, SeparatorByte) + sum = hashAdd(sum, string(m[label])) + sum = hashAddByte(sum, SeparatorByte) + } + return sum +} + +// SignatureWithoutLabels works like LabelsToSignature but takes a Metric as +// parameter (rather than a label map) and excludes the labels with any of the +// specified LabelNames from the signature calculation. +func SignatureWithoutLabels(m Metric, labels map[LabelName]struct{}) uint64 { + if len(m) == 0 { + return emptyLabelSignature + } + + labelNames := make(LabelNames, 0, len(m)) + for labelName := range m { + if _, exclude := labels[labelName]; !exclude { + labelNames = append(labelNames, labelName) + } + } + if len(labelNames) == 0 { + return emptyLabelSignature + } + sort.Sort(labelNames) + + sum := hashNew() + for _, labelName := range labelNames { + sum = hashAdd(sum, string(labelName)) + sum = hashAddByte(sum, SeparatorByte) + sum = hashAdd(sum, string(m[labelName])) + sum = hashAddByte(sum, SeparatorByte) + } + return sum +} diff --git a/vendor/github.com/prometheus/common/model/silence.go b/vendor/github.com/prometheus/common/model/silence.go new file mode 100644 index 00000000000..7538e299774 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/silence.go @@ -0,0 +1,106 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "encoding/json" + "fmt" + "regexp" + "time" +) + +// Matcher describes a matches the value of a given label. +type Matcher struct { + Name LabelName `json:"name"` + Value string `json:"value"` + IsRegex bool `json:"isRegex"` +} + +func (m *Matcher) UnmarshalJSON(b []byte) error { + type plain Matcher + if err := json.Unmarshal(b, (*plain)(m)); err != nil { + return err + } + + if len(m.Name) == 0 { + return fmt.Errorf("label name in matcher must not be empty") + } + if m.IsRegex { + if _, err := regexp.Compile(m.Value); err != nil { + return err + } + } + return nil +} + +// Validate returns true iff all fields of the matcher have valid values. +func (m *Matcher) Validate() error { + if !m.Name.IsValid() { + return fmt.Errorf("invalid name %q", m.Name) + } + if m.IsRegex { + if _, err := regexp.Compile(m.Value); err != nil { + return fmt.Errorf("invalid regular expression %q", m.Value) + } + } else if !LabelValue(m.Value).IsValid() || len(m.Value) == 0 { + return fmt.Errorf("invalid value %q", m.Value) + } + return nil +} + +// Silence defines the representation of a silence definiton +// in the Prometheus eco-system. +type Silence struct { + ID uint64 `json:"id,omitempty"` + + Matchers []*Matcher `json:"matchers"` + + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + + CreatedAt time.Time `json:"createdAt,omitempty"` + CreatedBy string `json:"createdBy"` + Comment string `json:"comment,omitempty"` +} + +// Validate returns true iff all fields of the silence have valid values. +func (s *Silence) Validate() error { + if len(s.Matchers) == 0 { + return fmt.Errorf("at least one matcher required") + } + for _, m := range s.Matchers { + if err := m.Validate(); err != nil { + return fmt.Errorf("invalid matcher: %s", err) + } + } + if s.StartsAt.IsZero() { + return fmt.Errorf("start time missing") + } + if s.EndsAt.IsZero() { + return fmt.Errorf("end time missing") + } + if s.EndsAt.Before(s.StartsAt) { + return fmt.Errorf("start time must be before end time") + } + if s.CreatedBy == "" { + return fmt.Errorf("creator information missing") + } + if s.Comment == "" { + return fmt.Errorf("comment missing") + } + if s.CreatedAt.IsZero() { + return fmt.Errorf("creation timestamp missing") + } + return nil +} diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go new file mode 100644 index 00000000000..548968aebe6 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/time.go @@ -0,0 +1,249 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" +) + +const ( + // MinimumTick is the minimum supported time resolution. This has to be + // at least time.Second in order for the code below to work. + minimumTick = time.Millisecond + // second is the Time duration equivalent to one second. + second = int64(time.Second / minimumTick) + // The number of nanoseconds per minimum tick. + nanosPerTick = int64(minimumTick / time.Nanosecond) + + // Earliest is the earliest Time representable. Handy for + // initializing a high watermark. + Earliest = Time(math.MinInt64) + // Latest is the latest Time representable. Handy for initializing + // a low watermark. + Latest = Time(math.MaxInt64) +) + +// Time is the number of milliseconds since the epoch +// (1970-01-01 00:00 UTC) excluding leap seconds. +type Time int64 + +// Interval describes and interval between two timestamps. +type Interval struct { + Start, End Time +} + +// Now returns the current time as a Time. +func Now() Time { + return TimeFromUnixNano(time.Now().UnixNano()) +} + +// TimeFromUnix returns the Time equivalent to the Unix Time t +// provided in seconds. +func TimeFromUnix(t int64) Time { + return Time(t * second) +} + +// TimeFromUnixNano returns the Time equivalent to the Unix Time +// t provided in nanoseconds. +func TimeFromUnixNano(t int64) Time { + return Time(t / nanosPerTick) +} + +// Equal reports whether two Times represent the same instant. +func (t Time) Equal(o Time) bool { + return t == o +} + +// Before reports whether the Time t is before o. +func (t Time) Before(o Time) bool { + return t < o +} + +// After reports whether the Time t is after o. +func (t Time) After(o Time) bool { + return t > o +} + +// Add returns the Time t + d. +func (t Time) Add(d time.Duration) Time { + return t + Time(d/minimumTick) +} + +// Sub returns the Duration t - o. +func (t Time) Sub(o Time) time.Duration { + return time.Duration(t-o) * minimumTick +} + +// Time returns the time.Time representation of t. +func (t Time) Time() time.Time { + return time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick) +} + +// Unix returns t as a Unix time, the number of seconds elapsed +// since January 1, 1970 UTC. +func (t Time) Unix() int64 { + return int64(t) / second +} + +// UnixNano returns t as a Unix time, the number of nanoseconds elapsed +// since January 1, 1970 UTC. +func (t Time) UnixNano() int64 { + return int64(t) * nanosPerTick +} + +// The number of digits after the dot. +var dotPrecision = int(math.Log10(float64(second))) + +// String returns a string representation of the Time. +func (t Time) String() string { + return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64) +} + +// MarshalJSON implements the json.Marshaler interface. +func (t Time) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *Time) UnmarshalJSON(b []byte) error { + p := strings.Split(string(b), ".") + switch len(p) { + case 1: + v, err := strconv.ParseInt(string(p[0]), 10, 64) + if err != nil { + return err + } + *t = Time(v * second) + + case 2: + v, err := strconv.ParseInt(string(p[0]), 10, 64) + if err != nil { + return err + } + v *= second + + prec := dotPrecision - len(p[1]) + if prec < 0 { + p[1] = p[1][:dotPrecision] + } else if prec > 0 { + p[1] = p[1] + strings.Repeat("0", prec) + } + + va, err := strconv.ParseInt(p[1], 10, 32) + if err != nil { + return err + } + + *t = Time(v + va) + + default: + return fmt.Errorf("invalid time %q", string(b)) + } + return nil +} + +// Duration wraps time.Duration. It is used to parse the custom duration format +// from YAML. +// This type should not propagate beyond the scope of input/output processing. +type Duration time.Duration + +var durationRE = regexp.MustCompile("^([0-9]+)(y|w|d|h|m|s|ms)$") + +// StringToDuration parses a string into a time.Duration, assuming that a year +// always has 365d, a week always has 7d, and a day always has 24h. +func ParseDuration(durationStr string) (Duration, error) { + matches := durationRE.FindStringSubmatch(durationStr) + if len(matches) != 3 { + return 0, fmt.Errorf("not a valid duration string: %q", durationStr) + } + var ( + n, _ = strconv.Atoi(matches[1]) + dur = time.Duration(n) * time.Millisecond + ) + switch unit := matches[2]; unit { + case "y": + dur *= 1000 * 60 * 60 * 24 * 365 + case "w": + dur *= 1000 * 60 * 60 * 24 * 7 + case "d": + dur *= 1000 * 60 * 60 * 24 + case "h": + dur *= 1000 * 60 * 60 + case "m": + dur *= 1000 * 60 + case "s": + dur *= 1000 + case "ms": + // Value already correct + default: + return 0, fmt.Errorf("invalid time unit in duration string: %q", unit) + } + return Duration(dur), nil +} + +func (d Duration) String() string { + var ( + ms = int64(time.Duration(d) / time.Millisecond) + unit = "ms" + ) + factors := map[string]int64{ + "y": 1000 * 60 * 60 * 24 * 365, + "w": 1000 * 60 * 60 * 24 * 7, + "d": 1000 * 60 * 60 * 24, + "h": 1000 * 60 * 60, + "m": 1000 * 60, + "s": 1000, + "ms": 1, + } + + switch int64(0) { + case ms % factors["y"]: + unit = "y" + case ms % factors["w"]: + unit = "w" + case ms % factors["d"]: + unit = "d" + case ms % factors["h"]: + unit = "h" + case ms % factors["m"]: + unit = "m" + case ms % factors["s"]: + unit = "s" + } + return fmt.Sprintf("%v%v", ms/factors[unit], unit) +} + +// MarshalYAML implements the yaml.Marshaler interface. +func (d Duration) MarshalYAML() (interface{}, error) { + return d.String(), nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + dur, err := ParseDuration(s) + if err != nil { + return err + } + *d = dur + return nil +} diff --git a/vendor/github.com/prometheus/common/model/value.go b/vendor/github.com/prometheus/common/model/value.go new file mode 100644 index 00000000000..dbf5d10e431 --- /dev/null +++ b/vendor/github.com/prometheus/common/model/value.go @@ -0,0 +1,403 @@ +// Copyright 2013 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "encoding/json" + "fmt" + "math" + "sort" + "strconv" + "strings" +) + +// A SampleValue is a representation of a value for a given sample at a given +// time. +type SampleValue float64 + +// MarshalJSON implements json.Marshaler. +func (v SampleValue) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *SampleValue) UnmarshalJSON(b []byte) error { + if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { + return fmt.Errorf("sample value must be a quoted string") + } + f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64) + if err != nil { + return err + } + *v = SampleValue(f) + return nil +} + +// Equal returns true if the value of v and o is equal or if both are NaN. Note +// that v==o is false if both are NaN. If you want the conventional float +// behavior, use == to compare two SampleValues. +func (v SampleValue) Equal(o SampleValue) bool { + if v == o { + return true + } + return math.IsNaN(float64(v)) && math.IsNaN(float64(o)) +} + +func (v SampleValue) String() string { + return strconv.FormatFloat(float64(v), 'f', -1, 64) +} + +// SamplePair pairs a SampleValue with a Timestamp. +type SamplePair struct { + Timestamp Time + Value SampleValue +} + +// MarshalJSON implements json.Marshaler. +func (s SamplePair) MarshalJSON() ([]byte, error) { + t, err := json.Marshal(s.Timestamp) + if err != nil { + return nil, err + } + v, err := json.Marshal(s.Value) + if err != nil { + return nil, err + } + return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *SamplePair) UnmarshalJSON(b []byte) error { + v := [...]json.Unmarshaler{&s.Timestamp, &s.Value} + return json.Unmarshal(b, &v) +} + +// Equal returns true if this SamplePair and o have equal Values and equal +// Timestamps. The sematics of Value equality is defined by SampleValue.Equal. +func (s *SamplePair) Equal(o *SamplePair) bool { + return s == o || (s.Value.Equal(o.Value) && s.Timestamp.Equal(o.Timestamp)) +} + +func (s SamplePair) String() string { + return fmt.Sprintf("%s @[%s]", s.Value, s.Timestamp) +} + +// Sample is a sample pair associated with a metric. +type Sample struct { + Metric Metric `json:"metric"` + Value SampleValue `json:"value"` + Timestamp Time `json:"timestamp"` +} + +// Equal compares first the metrics, then the timestamp, then the value. The +// sematics of value equality is defined by SampleValue.Equal. +func (s *Sample) Equal(o *Sample) bool { + if s == o { + return true + } + + if !s.Metric.Equal(o.Metric) { + return false + } + if !s.Timestamp.Equal(o.Timestamp) { + return false + } + if s.Value.Equal(o.Value) { + return false + } + + return true +} + +func (s Sample) String() string { + return fmt.Sprintf("%s => %s", s.Metric, SamplePair{ + Timestamp: s.Timestamp, + Value: s.Value, + }) +} + +// MarshalJSON implements json.Marshaler. +func (s Sample) MarshalJSON() ([]byte, error) { + v := struct { + Metric Metric `json:"metric"` + Value SamplePair `json:"value"` + }{ + Metric: s.Metric, + Value: SamplePair{ + Timestamp: s.Timestamp, + Value: s.Value, + }, + } + + return json.Marshal(&v) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *Sample) UnmarshalJSON(b []byte) error { + v := struct { + Metric Metric `json:"metric"` + Value SamplePair `json:"value"` + }{ + Metric: s.Metric, + Value: SamplePair{ + Timestamp: s.Timestamp, + Value: s.Value, + }, + } + + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + s.Metric = v.Metric + s.Timestamp = v.Value.Timestamp + s.Value = v.Value.Value + + return nil +} + +// Samples is a sortable Sample slice. It implements sort.Interface. +type Samples []*Sample + +func (s Samples) Len() int { + return len(s) +} + +// Less compares first the metrics, then the timestamp. +func (s Samples) Less(i, j int) bool { + switch { + case s[i].Metric.Before(s[j].Metric): + return true + case s[j].Metric.Before(s[i].Metric): + return false + case s[i].Timestamp.Before(s[j].Timestamp): + return true + default: + return false + } +} + +func (s Samples) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Equal compares two sets of samples and returns true if they are equal. +func (s Samples) Equal(o Samples) bool { + if len(s) != len(o) { + return false + } + + for i, sample := range s { + if !sample.Equal(o[i]) { + return false + } + } + return true +} + +// SampleStream is a stream of Values belonging to an attached COWMetric. +type SampleStream struct { + Metric Metric `json:"metric"` + Values []SamplePair `json:"values"` +} + +func (ss SampleStream) String() string { + vals := make([]string, len(ss.Values)) + for i, v := range ss.Values { + vals[i] = v.String() + } + return fmt.Sprintf("%s =>\n%s", ss.Metric, strings.Join(vals, "\n")) +} + +// Value is a generic interface for values resulting from a query evaluation. +type Value interface { + Type() ValueType + String() string +} + +func (Matrix) Type() ValueType { return ValMatrix } +func (Vector) Type() ValueType { return ValVector } +func (*Scalar) Type() ValueType { return ValScalar } +func (*String) Type() ValueType { return ValString } + +type ValueType int + +const ( + ValNone ValueType = iota + ValScalar + ValVector + ValMatrix + ValString +) + +// MarshalJSON implements json.Marshaler. +func (et ValueType) MarshalJSON() ([]byte, error) { + return json.Marshal(et.String()) +} + +func (et *ValueType) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + switch s { + case "": + *et = ValNone + case "scalar": + *et = ValScalar + case "vector": + *et = ValVector + case "matrix": + *et = ValMatrix + case "string": + *et = ValString + default: + return fmt.Errorf("unknown value type %q", s) + } + return nil +} + +func (e ValueType) String() string { + switch e { + case ValNone: + return "" + case ValScalar: + return "scalar" + case ValVector: + return "vector" + case ValMatrix: + return "matrix" + case ValString: + return "string" + } + panic("ValueType.String: unhandled value type") +} + +// Scalar is a scalar value evaluated at the set timestamp. +type Scalar struct { + Value SampleValue `json:"value"` + Timestamp Time `json:"timestamp"` +} + +func (s Scalar) String() string { + return fmt.Sprintf("scalar: %v @[%v]", s.Value, s.Timestamp) +} + +// MarshalJSON implements json.Marshaler. +func (s Scalar) MarshalJSON() ([]byte, error) { + v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64) + return json.Marshal([...]interface{}{s.Timestamp, string(v)}) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *Scalar) UnmarshalJSON(b []byte) error { + var f string + v := [...]interface{}{&s.Timestamp, &f} + + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + value, err := strconv.ParseFloat(f, 64) + if err != nil { + return fmt.Errorf("error parsing sample value: %s", err) + } + s.Value = SampleValue(value) + return nil +} + +// String is a string value evaluated at the set timestamp. +type String struct { + Value string `json:"value"` + Timestamp Time `json:"timestamp"` +} + +func (s *String) String() string { + return s.Value +} + +// MarshalJSON implements json.Marshaler. +func (s String) MarshalJSON() ([]byte, error) { + return json.Marshal([]interface{}{s.Timestamp, s.Value}) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *String) UnmarshalJSON(b []byte) error { + v := [...]interface{}{&s.Timestamp, &s.Value} + return json.Unmarshal(b, &v) +} + +// Vector is basically only an alias for Samples, but the +// contract is that in a Vector, all Samples have the same timestamp. +type Vector []*Sample + +func (vec Vector) String() string { + entries := make([]string, len(vec)) + for i, s := range vec { + entries[i] = s.String() + } + return strings.Join(entries, "\n") +} + +func (vec Vector) Len() int { return len(vec) } +func (vec Vector) Swap(i, j int) { vec[i], vec[j] = vec[j], vec[i] } + +// Less compares first the metrics, then the timestamp. +func (vec Vector) Less(i, j int) bool { + switch { + case vec[i].Metric.Before(vec[j].Metric): + return true + case vec[j].Metric.Before(vec[i].Metric): + return false + case vec[i].Timestamp.Before(vec[j].Timestamp): + return true + default: + return false + } +} + +// Equal compares two sets of samples and returns true if they are equal. +func (vec Vector) Equal(o Vector) bool { + if len(vec) != len(o) { + return false + } + + for i, sample := range vec { + if !sample.Equal(o[i]) { + return false + } + } + return true +} + +// Matrix is a list of time series. +type Matrix []*SampleStream + +func (m Matrix) Len() int { return len(m) } +func (m Matrix) Less(i, j int) bool { return m[i].Metric.Before(m[j].Metric) } +func (m Matrix) Swap(i, j int) { m[i], m[j] = m[j], m[i] } + +func (mat Matrix) String() string { + matCp := make(Matrix, len(mat)) + copy(matCp, mat) + sort.Sort(matCp) + + strs := make([]string, len(matCp)) + + for i, ss := range matCp { + strs[i] = ss.String() + } + + return strings.Join(strs, "\n") +} diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE new file mode 100644 index 00000000000..6a66aea5eaf --- /dev/null +++ b/vendor/golang.org/x/net/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS new file mode 100644 index 00000000000..733099041f8 --- /dev/null +++ b/vendor/golang.org/x/net/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/net/context/ctxhttp/ctxhttp.go b/vendor/golang.org/x/net/context/ctxhttp/ctxhttp.go new file mode 100644 index 00000000000..606cf1f9726 --- /dev/null +++ b/vendor/golang.org/x/net/context/ctxhttp/ctxhttp.go @@ -0,0 +1,74 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.7 + +// Package ctxhttp provides helper functions for performing context-aware HTTP requests. +package ctxhttp // import "golang.org/x/net/context/ctxhttp" + +import ( + "io" + "net/http" + "net/url" + "strings" + + "golang.org/x/net/context" +) + +// Do sends an HTTP request with the provided http.Client and returns +// an HTTP response. +// +// If the client is nil, http.DefaultClient is used. +// +// The provided ctx must be non-nil. If it is canceled or times out, +// ctx.Err() will be returned. +func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(req.WithContext(ctx)) + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + if err != nil { + select { + case <-ctx.Done(): + err = ctx.Err() + default: + } + } + return resp, err +} + +// Get issues a GET request via the Do function. +func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + return Do(ctx, client, req) +} + +// Head issues a HEAD request via the Do function. +func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return nil, err + } + return Do(ctx, client, req) +} + +// Post issues a POST request via the Do function. +func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", bodyType) + return Do(ctx, client, req) +} + +// PostForm issues a POST request via the Do function. +func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) { + return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} diff --git a/vendor/golang.org/x/net/context/ctxhttp/ctxhttp_pre17.go b/vendor/golang.org/x/net/context/ctxhttp/ctxhttp_pre17.go new file mode 100644 index 00000000000..926870cc23f --- /dev/null +++ b/vendor/golang.org/x/net/context/ctxhttp/ctxhttp_pre17.go @@ -0,0 +1,147 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.7 + +package ctxhttp // import "golang.org/x/net/context/ctxhttp" + +import ( + "io" + "net/http" + "net/url" + "strings" + + "golang.org/x/net/context" +) + +func nop() {} + +var ( + testHookContextDoneBeforeHeaders = nop + testHookDoReturned = nop + testHookDidBodyClose = nop +) + +// Do sends an HTTP request with the provided http.Client and returns an HTTP response. +// If the client is nil, http.DefaultClient is used. +// If the context is canceled or times out, ctx.Err() will be returned. +func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { + if client == nil { + client = http.DefaultClient + } + + // TODO(djd): Respect any existing value of req.Cancel. + cancel := make(chan struct{}) + req.Cancel = cancel + + type responseAndError struct { + resp *http.Response + err error + } + result := make(chan responseAndError, 1) + + // Make local copies of test hooks closed over by goroutines below. + // Prevents data races in tests. + testHookDoReturned := testHookDoReturned + testHookDidBodyClose := testHookDidBodyClose + + go func() { + resp, err := client.Do(req) + testHookDoReturned() + result <- responseAndError{resp, err} + }() + + var resp *http.Response + + select { + case <-ctx.Done(): + testHookContextDoneBeforeHeaders() + close(cancel) + // Clean up after the goroutine calling client.Do: + go func() { + if r := <-result; r.resp != nil { + testHookDidBodyClose() + r.resp.Body.Close() + } + }() + return nil, ctx.Err() + case r := <-result: + var err error + resp, err = r.resp, r.err + if err != nil { + return resp, err + } + } + + c := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + close(cancel) + case <-c: + // The response's Body is closed. + } + }() + resp.Body = ¬ifyingReader{resp.Body, c} + + return resp, nil +} + +// Get issues a GET request via the Do function. +func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + return Do(ctx, client, req) +} + +// Head issues a HEAD request via the Do function. +func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return nil, err + } + return Do(ctx, client, req) +} + +// Post issues a POST request via the Do function. +func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", bodyType) + return Do(ctx, client, req) +} + +// PostForm issues a POST request via the Do function. +func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) { + return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} + +// notifyingReader is an io.ReadCloser that closes the notify channel after +// Close is called or a Read fails on the underlying ReadCloser. +type notifyingReader struct { + io.ReadCloser + notify chan<- struct{} +} + +func (r *notifyingReader) Read(p []byte) (int, error) { + n, err := r.ReadCloser.Read(p) + if err != nil && r.notify != nil { + close(r.notify) + r.notify = nil + } + return n, err +} + +func (r *notifyingReader) Close() error { + err := r.ReadCloser.Close() + if r.notify != nil { + close(r.notify) + r.notify = nil + } + return err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 4bc76ca4d73..1d4dafbb9ae 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1,6 +1,25 @@ { "comment": "", "ignore": "test", - "package": [], + "package": [ + { + "checksumSHA1": "SMUvX2B8eoFd9wnPofwBKlN6btE=", + "path": "github.com/prometheus/client_golang/api/prometheus", + "revision": "5636dc67ae776adf5590da7349e70fbb9559972d", + "revisionTime": "2016-09-16T18:03:40Z" + }, + { + "checksumSHA1": "Jx0GXl5hGnO25s3ryyvtdWHdCpw=", + "path": "github.com/prometheus/common/model", + "revision": "9a94032291f2192936512bab367bc45e77990d6a", + "revisionTime": "2016-09-17T18:44:01Z" + }, + { + "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=", + "path": "golang.org/x/net/context/ctxhttp", + "revision": "71a035914f99bb58fe82eac0f1289f10963d876c", + "revisionTime": "2016-09-12T21:59:12Z" + } + ], "rootPath": "github.com/grafana/grafana" } From 4c88db3e43e1772ef6a193574a5ad38d9a310962 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 11:17:29 +0200 Subject: [PATCH 14/74] feat(prometheus): add support for legend formatting --- pkg/services/alerting/conditions/query.go | 2 +- pkg/tsdb/graphite/graphite.go | 2 +- pkg/tsdb/models.go | 3 ++ pkg/tsdb/prometheus/prometheus.go | 41 ++++++++++++++++--- pkg/tsdb/prometheus/prometheus_test.go | 26 ++++++++++++ pkg/tsdb/prometheus/types.go | 8 ++++ .../datasource/prometheus/datasource.ts | 1 - 7 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 pkg/tsdb/prometheus/prometheus_test.go diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 208527287f3..15db31838b0 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -111,7 +111,7 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timera Queries: []*tsdb.Query{ { RefId: "A", - Query: c.Query.Model.Get("target").MustString(), + Model: c.Query.Model, DataSource: &tsdb.DataSourceInfo{ Id: datasource.Id, Name: datasource.Name, diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 4042702378c..32e1ab4fa76 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -54,7 +54,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC } for _, query := range queries { - formData["target"] = []string{query.Query} + formData["target"] = []string{query.Model.Get("target").MustString()} } if setting.Env == setting.DEV { diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 117b00efe3a..262be5cbd24 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -1,8 +1,11 @@ package tsdb +import "github.com/grafana/grafana/pkg/components/simplejson" + type Query struct { RefId string Query string + Model *simplejson.Json Depends []string DataSource *DataSourceInfo Results []*TimeSeries diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 2b2ccd382af..d835d5a822c 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -3,6 +3,8 @@ package prometheus import ( "context" "net/http" + "regexp" + "strings" "time" "github.com/grafana/grafana/pkg/log" @@ -53,25 +55,52 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb from, _ := queryContext.TimeRange.FromTime() to, _ := queryContext.TimeRange.ToTime() + + query := parseQuery(queries) + timeRange := prometheus.Range{ Start: from, End: to, - Step: time.Second, + Step: query.Step, } - ctx := context.Background() - value, err := client.QueryRange(ctx, "counters_logins", timeRange) + value, err := client.QueryRange(context.Background(), query.Expr, timeRange) if err != nil { result.Error = err return result } - result.QueryResults = parseResponse(value) + result.QueryResults = parseResponse(value, query) return result } -func parseResponse(value pmodel.Value) map[string]*tsdb.QueryResult { +func formatLegend(metric pmodel.Metric, query PrometheusQuery) string { + r, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) + + result := r.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { + ind := strings.Replace(strings.Replace(string(in), "{{", "", 1), "}}", "", 1) + if val, exists := metric[pmodel.LabelName(ind)]; exists { + return []byte(val) + } + + return in + }) + + return string(result) +} + +func parseQuery(queries tsdb.QuerySlice) PrometheusQuery { + queryModel := queries[0] + + return PrometheusQuery{ + Expr: queryModel.Model.Get("expr").MustString(), + Step: time.Second * time.Duration(queryModel.Model.Get("step").MustInt64(1)), + LegendFormat: queryModel.Model.Get("legendFormat").MustString(), + } +} + +func parseResponse(value pmodel.Value, query PrometheusQuery) map[string]*tsdb.QueryResult { queryResults := make(map[string]*tsdb.QueryResult) queryRes := &tsdb.QueryResult{} @@ -86,7 +115,7 @@ func parseResponse(value pmodel.Value) map[string]*tsdb.QueryResult { } queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ - Name: v.Metric.String(), + Name: formatLegend(v.Metric, query), Points: points, }) } diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go new file mode 100644 index 00000000000..6edb6260a09 --- /dev/null +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -0,0 +1,26 @@ +package prometheus + +import ( + "testing" + + p "github.com/prometheus/common/model" + . "github.com/smartystreets/goconvey/convey" +) + +func TestPrometheus(t *testing.T) { + Convey("Prometheus", t, func() { + + Convey("converting metric name", func() { + metric := map[p.LabelName]p.LabelValue{ + p.LabelName("app"): p.LabelValue("backend"), + p.LabelName("device"): p.LabelValue("mobile"), + } + + query := PrometheusQuery{ + LegendFormat: "legend {{app}} {{device}} {{broken}}", + } + + So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}") + }) + }) +} diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/types.go index 7b1b4c03ead..67d9ae40670 100644 --- a/pkg/tsdb/prometheus/types.go +++ b/pkg/tsdb/prometheus/types.go @@ -1 +1,9 @@ package prometheus + +import "time" + +type PrometheusQuery struct { + Expr string + Step time.Duration + LegendFormat string +} diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 53ce91144a4..1d4112789da 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -113,7 +113,6 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS throw response.error; } delete self.lastErrors.query; - _.each(response.data.data.result, function(metricData) { result.push(self.transformMetricData(metricData, activeTargets[index], start, end)); }); From 3e73be8d2e34ec09f1b75516b23e576628f2a376 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 11:31:56 +0200 Subject: [PATCH 15/74] feat(prometheus): improve error handling --- pkg/tsdb/prometheus/prometheus.go | 46 +++++++++++++++++++------- pkg/tsdb/prometheus/prometheus_test.go | 2 +- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index d835d5a822c..b32a99220b2 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -49,14 +49,17 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb client, err := e.getClient() if err != nil { - result.Error = err - return result + return resultWithError(result, err) } from, _ := queryContext.TimeRange.FromTime() to, _ := queryContext.TimeRange.ToTime() - query := parseQuery(queries) + query, err := parseQuery(queries) + + if err != nil { + return resultWithError(result, err) + } timeRange := prometheus.Range{ Start: from, @@ -67,15 +70,14 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb value, err := client.QueryRange(context.Background(), query.Expr, timeRange) if err != nil { - result.Error = err - return result + return resultWithError(result, err) } result.QueryResults = parseResponse(value, query) return result } -func formatLegend(metric pmodel.Metric, query PrometheusQuery) string { +func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { r, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) result := r.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { @@ -90,17 +92,32 @@ func formatLegend(metric pmodel.Metric, query PrometheusQuery) string { return string(result) } -func parseQuery(queries tsdb.QuerySlice) PrometheusQuery { +func parseQuery(queries tsdb.QuerySlice) (*PrometheusQuery, error) { queryModel := queries[0] - return PrometheusQuery{ - Expr: queryModel.Model.Get("expr").MustString(), - Step: time.Second * time.Duration(queryModel.Model.Get("step").MustInt64(1)), - LegendFormat: queryModel.Model.Get("legendFormat").MustString(), + expr, err := queryModel.Model.Get("expr").String() + if err != nil { + return nil, err } + + step, err := queryModel.Model.Get("step").Int64() + if err != nil { + return nil, err + } + + format, err := queryModel.Model.Get("legendFormat").String() + if err != nil { + return nil, err + } + + return &PrometheusQuery{ + Expr: expr, + Step: time.Second * time.Duration(step), + LegendFormat: format, + }, nil } -func parseResponse(value pmodel.Value, query PrometheusQuery) map[string]*tsdb.QueryResult { +func parseResponse(value pmodel.Value, query *PrometheusQuery) map[string]*tsdb.QueryResult { queryResults := make(map[string]*tsdb.QueryResult) queryRes := &tsdb.QueryResult{} @@ -123,3 +140,8 @@ func parseResponse(value pmodel.Value, query PrometheusQuery) map[string]*tsdb.Q queryResults["A"] = queryRes return queryResults } + +func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult { + result.Error = err + return result +} diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index 6edb6260a09..f7489ae9afc 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -16,7 +16,7 @@ func TestPrometheus(t *testing.T) { p.LabelName("device"): p.LabelValue("mobile"), } - query := PrometheusQuery{ + query := &PrometheusQuery{ LegendFormat: "legend {{app}} {{device}} {{broken}}", } From ee0f1a0f36826a3f1458feac9d630c3cdbcd357e Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 13:17:36 +0200 Subject: [PATCH 16/74] feat(prometheus): handle more errors --- pkg/tsdb/prometheus/prometheus.go | 28 ++++++++++++++++++---------- pkg/tsdb/prometheus/types.go | 2 ++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index b32a99220b2..2ac21794ef9 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -52,18 +52,14 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb return resultWithError(result, err) } - from, _ := queryContext.TimeRange.FromTime() - to, _ := queryContext.TimeRange.ToTime() - - query, err := parseQuery(queries) - + query, err := parseQuery(queries, queryContext) if err != nil { return resultWithError(result, err) } timeRange := prometheus.Range{ - Start: from, - End: to, + Start: query.Start, + End: query.End, Step: query.Step, } @@ -78,9 +74,9 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb } func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { - r, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) + reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) - result := r.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { + result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { ind := strings.Replace(strings.Replace(string(in), "{{", "", 1), "}}", "", 1) if val, exists := metric[pmodel.LabelName(ind)]; exists { return []byte(val) @@ -92,7 +88,7 @@ func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { return string(result) } -func parseQuery(queries tsdb.QuerySlice) (*PrometheusQuery, error) { +func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*PrometheusQuery, error) { queryModel := queries[0] expr, err := queryModel.Model.Get("expr").String() @@ -110,10 +106,22 @@ func parseQuery(queries tsdb.QuerySlice) (*PrometheusQuery, error) { return nil, err } + start, err := queryContext.TimeRange.FromTime() + if err != nil { + return nil, err + } + + end, err := queryContext.TimeRange.ToTime() + if err != nil { + return nil, err + } + return &PrometheusQuery{ Expr: expr, Step: time.Second * time.Duration(step), LegendFormat: format, + Start: start, + End: end, }, nil } diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/types.go index 67d9ae40670..8ed665d0123 100644 --- a/pkg/tsdb/prometheus/types.go +++ b/pkg/tsdb/prometheus/types.go @@ -6,4 +6,6 @@ type PrometheusQuery struct { Expr string Step time.Duration LegendFormat string + Start time.Time + End time.Time } From 9534a04d3de7a8577ea92e16cde2002f70c8b221 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 13:30:41 +0200 Subject: [PATCH 17/74] fix(prometheus): only accept matrix result --- pkg/tsdb/prometheus/prometheus.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 2ac21794ef9..781eb91dd8f 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -2,6 +2,7 @@ package prometheus import ( "context" + "fmt" "net/http" "regexp" "strings" @@ -69,7 +70,11 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb return resultWithError(result, err) } - result.QueryResults = parseResponse(value, query) + queryResult, err := parseResponse(value, query) + if err != nil { + return resultWithError(result, err) + } + result.QueryResults = queryResult return result } @@ -125,18 +130,21 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom }, nil } -func parseResponse(value pmodel.Value, query *PrometheusQuery) map[string]*tsdb.QueryResult { +func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) { queryResults := make(map[string]*tsdb.QueryResult) queryRes := &tsdb.QueryResult{} - data := value.(pmodel.Matrix) + data, ok := value.(pmodel.Matrix) + if !ok { + return queryResults, fmt.Errorf("Unsupported result format: %s", value.Type().String()) + } for _, v := range data { var points [][2]*float64 for _, k := range v.Values { - dummie := float64(k.Timestamp) - d2 := float64(k.Value) - points = append(points, [2]*float64{&d2, &dummie}) + timestamp := float64(k.Timestamp) + val := float64(k.Value) + points = append(points, [2]*float64{&val, ×tamp}) } queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ @@ -146,7 +154,7 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) map[string]*tsdb. } queryResults["A"] = queryRes - return queryResults + return queryResults, nil } func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult { From af551b8825dfa811d2fe991d46d698effd36416e Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 13:41:25 +0200 Subject: [PATCH 18/74] refactor(tsdb): remove toUnix from timerange --- pkg/tsdb/time_range.go | 28 ---------------------------- pkg/tsdb/time_range_test.go | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index e7e54cef5f9..8e1a1c66e3d 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -20,17 +20,6 @@ type TimeRange struct { Now time.Time } -func (tr TimeRange) FromUnix() (int64, error) { - fromRaw := strings.Replace(tr.From, "now-", "", 1) - - diff, err := time.ParseDuration("-" + fromRaw) - if err != nil { - return 0, err - } - - return tr.Now.Add(diff).Unix(), nil -} - func (tr TimeRange) FromTime() (time.Time, error) { fromRaw := strings.Replace(tr.From, "now-", "", 1) @@ -42,23 +31,6 @@ func (tr TimeRange) FromTime() (time.Time, error) { return tr.Now.Add(diff), nil } -func (tr TimeRange) ToUnix() (int64, error) { - if tr.To == "now" { - return tr.Now.Unix(), nil - } else if strings.HasPrefix(tr.To, "now-") { - withoutNow := strings.Replace(tr.To, "now-", "", 1) - - diff, err := time.ParseDuration("-" + withoutNow) - if err != nil { - return 0, nil - } - - return tr.Now.Add(diff).Unix(), nil - } - - return 0, fmt.Errorf("cannot parse to value %s", tr.To) -} - func (tr TimeRange) ToTime() (time.Time, error) { if tr.To == "now" { return tr.Now, nil diff --git a/pkg/tsdb/time_range_test.go b/pkg/tsdb/time_range_test.go index d64eb8cc86e..56ea9d24490 100644 --- a/pkg/tsdb/time_range_test.go +++ b/pkg/tsdb/time_range_test.go @@ -23,15 +23,15 @@ func TestTimeRange(t *testing.T) { fiveMinAgo, _ := time.ParseDuration("-5m") expected := now.Add(fiveMinAgo) - res, err := tr.FromUnix() + res, err := tr.FromTime() So(err, ShouldBeNil) - So(res, ShouldAlmostEqual, expected.Unix()) + So(res.Unix(), ShouldEqual, expected.Unix()) }) Convey("now ", func() { - res, err := tr.ToUnix() + res, err := tr.ToTime() So(err, ShouldBeNil) - So(res, ShouldAlmostEqual, now.Unix()) + So(res.Unix(), ShouldEqual, now.Unix()) }) }) @@ -43,20 +43,20 @@ func TestTimeRange(t *testing.T) { } Convey("5h ago ", func() { - fiveMinAgo, _ := time.ParseDuration("-5h") - expected := now.Add(fiveMinAgo) + fiveHourAgo, _ := time.ParseDuration("-5h") + expected := now.Add(fiveHourAgo) - res, err := tr.FromUnix() + res, err := tr.FromTime() So(err, ShouldBeNil) - So(res, ShouldAlmostEqual, expected.Unix()) + So(res.Unix(), ShouldEqual, expected.Unix()) }) Convey("now-10m ", func() { fiveMinAgo, _ := time.ParseDuration("-10m") expected := now.Add(fiveMinAgo) - res, err := tr.ToUnix() + res, err := tr.ToTime() So(err, ShouldBeNil) - So(res, ShouldAlmostEqual, expected.Unix()) + So(res.Unix(), ShouldEqual, expected.Unix()) }) }) @@ -68,10 +68,10 @@ func TestTimeRange(t *testing.T) { Now: now, } - _, err = tr.FromUnix() + _, err = tr.FromTime() So(err, ShouldNotBeNil) - _, err = tr.ToUnix() + _, err = tr.ToTime() So(err, ShouldNotBeNil) }) }) From c084145cba96499de04e67f91abbd4b553c9f9af Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 13:46:10 +0200 Subject: [PATCH 19/74] refactor(prometheus): add timerange to alert context --- pkg/services/alerting/conditions/query.go | 8 ++++---- pkg/services/alerting/eval_context.go | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 15db31838b0..aea4fdebd7b 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -34,8 +34,8 @@ type AlertQuery struct { } func (c *QueryCondition) Eval(context *alerting.EvalContext) { - timerange := tsdb.NewTimerange(c.Query.From, c.Query.To) - seriesList, err := c.executeQuery(context, timerange) + context.TimeRange = tsdb.NewTimerange(c.Query.From, c.Query.To) + seriesList, err := c.executeQuery(context) if err != nil { context.Error = err return @@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { context.Firing = len(context.EvalMatches) > 0 } -func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { +func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ Id: c.Query.DatasourceId, OrgId: context.Rule.OrgId, @@ -79,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange t return nil, fmt.Errorf("Could not find datasource") } - req := c.getRequestForAlertRule(getDsInfo.Result, timerange) + req := c.getRequestForAlertRule(getDsInfo.Result, context.TimeRange) result := make(tsdb.TimeSeriesSlice, 0) resp, err := c.HandleRequest(req) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 13067c25f08..aa1442a0ca3 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb" ) type EvalContext struct { @@ -28,6 +29,7 @@ type EvalContext struct { ImageOnDiskPath string NoDataFound bool RetryCount int + TimeRange tsdb.TimeRange } type StateDescription struct { From ae7345b04db505c243d0b1f5310de1f7cd712fb4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 13:46:26 +0200 Subject: [PATCH 20/74] style(prometheus): remove commented test --- pkg/tsdb/graphite/graphite_test.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index 59007b43ed5..7a3bba9035b 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -1,23 +1 @@ package graphite - -// func TestGraphite(t *testing.T) { -// -// Convey("When executing graphite query", t, func() { -// executor := NewGraphiteExecutor(&tsdb.DataSourceInfo{ -// Url: "http://localhost:8080", -// }) -// -// queries := tsdb.QuerySlice{ -// &tsdb.Query{Query: "{\"target\": \"apps.backend.*.counters.requests.count\"}"}, -// } -// -// context := tsdb.NewQueryContext(queries, tsdb.TimeRange{}) -// result := executor.Execute(queries, context) -// So(result.Error, ShouldBeNil) -// -// Convey("Should return series", func() { -// So(result.QueryResults, ShouldNotBeEmpty) -// }) -// }) -// -// } From b856d7e193bd627f7f7c62dbb4e2d08d155ec196 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 13:55:42 +0200 Subject: [PATCH 21/74] fix(prometheus): remove timerange from context --- pkg/services/alerting/conditions/query.go | 8 ++++---- pkg/services/alerting/eval_context.go | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index aea4fdebd7b..15db31838b0 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -34,8 +34,8 @@ type AlertQuery struct { } func (c *QueryCondition) Eval(context *alerting.EvalContext) { - context.TimeRange = tsdb.NewTimerange(c.Query.From, c.Query.To) - seriesList, err := c.executeQuery(context) + timerange := tsdb.NewTimerange(c.Query.From, c.Query.To) + seriesList, err := c.executeQuery(context, timerange) if err != nil { context.Error = err return @@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { context.Firing = len(context.EvalMatches) > 0 } -func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeSeriesSlice, error) { +func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ Id: c.Query.DatasourceId, OrgId: context.Rule.OrgId, @@ -79,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS return nil, fmt.Errorf("Could not find datasource") } - req := c.getRequestForAlertRule(getDsInfo.Result, context.TimeRange) + req := c.getRequestForAlertRule(getDsInfo.Result, timerange) result := make(tsdb.TimeSeriesSlice, 0) resp, err := c.HandleRequest(req) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index aa1442a0ca3..13067c25f08 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb" ) type EvalContext struct { @@ -29,7 +28,6 @@ type EvalContext struct { ImageOnDiskPath string NoDataFound bool RetryCount int - TimeRange tsdb.TimeRange } type StateDescription struct { From d65fbcbb4291c8ad126781fea6c59935e6fb9a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Sep 2016 20:04:09 +0200 Subject: [PATCH 22/74] fix(graphite): fixed minor graphite lexer issue when nodes begin with octal syntax and end with dash identifier, fixes #6049# --- .../features/dashboard/submenu/submenu.html | 2 +- .../app/plugins/datasource/graphite/lexer.ts | 460 +++++++++--------- .../app/plugins/datasource/graphite/parser.ts | 5 +- .../datasource/graphite/specs/lexer_specs.ts | 8 + 4 files changed, 239 insertions(+), 236 deletions(-) diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index 2b2f7af6fbe..04bbba2f59d 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -2,7 +2,7 @@
-
+
Available
@@ -72,7 +72,7 @@
-
+
Selected
diff --git a/public/app/features/playlist/playlist_search.ts b/public/app/features/playlist/playlist_search.ts index e00c2cb3a36..b0ccd58eaeb 100644 --- a/public/app/features/playlist/playlist_search.ts +++ b/public/app/features/playlist/playlist_search.ts @@ -14,7 +14,7 @@ export class PlaylistSearchCtrl { /** @ngInject */ constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv) { - this.query = { query: '', tag: [], starred: false }; + this.query = {query: '', tag: [], starred: false, limit: 30}; $timeout(() => { this.query.query = ''; From d8df421b4317c60408627fba965d0c0c000af4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 10:22:16 +0200 Subject: [PATCH 27/74] fix(build): fixed issues with optimized build, fixes #6096 --- .../features/templating/constant_variable.ts | 2 +- .../templating/datasource_variable.ts | 2 +- public/app/features/templating/editor_ctrl.ts | 2 +- .../features/templating/interval_variable.ts | 2 +- .../app/features/templating/query_variable.ts | 1 + public/test/specs/templateValuesSrv-specs.js | 58 ------------------- 6 files changed, 5 insertions(+), 62 deletions(-) delete mode 100644 public/test/specs/templateValuesSrv-specs.js diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index 9fe2acfdfb2..bf31dba96f9 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -18,7 +18,7 @@ export class ConstantVariable implements Variable { current: {}, }; - /** @ngInject */ + /** @ngInject **/ constructor(private model, private variableSrv) { assignModelProperties(this, model, this.defaults); } diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index 96776c31163..3a297002900 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -22,7 +22,7 @@ export class DatasourceVariable implements Variable { query: '', }; - /** @ngInject */ + /** @ngInject **/ constructor(private model, private datasourceSrv, private variableSrv) { assignModelProperties(this, model, this.defaults); } diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 3c0410e1316..489625c16f8 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -6,7 +6,7 @@ import {variableTypes} from './variable'; export class VariableEditorCtrl { - /** @ngInject */ + /** @ngInject **/ constructor(private $scope, private datasourceSrv, private variableSrv, templateSrv) { $scope.variableTypes = variableTypes; $scope.ctrl = {}; diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index d53e44ae533..30b056e1c60 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -28,7 +28,7 @@ export class IntervalVariable implements Variable { auto_count: 30, }; - /** @ngInject */ + /** @ngInject **/ constructor(private model, private timeSrv, private templateSrv, private variableSrv) { assignModelProperties(this, model, this.defaults); this.refresh = 2; diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 96766d1bbfb..5ee9f0609bc 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -40,6 +40,7 @@ export class QueryVariable implements Variable { tagValuesQuery: null, }; + /** @ngInject **/ constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) { // copy model properties to this instance assignModelProperties(this, model, this.defaults); diff --git a/public/test/specs/templateValuesSrv-specs.js b/public/test/specs/templateValuesSrv-specs.js deleted file mode 100644 index f1d5375361e..00000000000 --- a/public/test/specs/templateValuesSrv-specs.js +++ /dev/null @@ -1,58 +0,0 @@ -define([ - '../mocks/dashboard-mock', - './helpers', - 'app/features/templating/templateValuesSrv' -], function(dashboardMock, helpers) { - 'use strict'; - - describe('templateValuesSrv', function() { - var ctx = new helpers.ServiceTestContext(); - - beforeEach(module('grafana.services')); - beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location'])); - beforeEach(ctx.createService('templateValuesSrv')); - - describe('when template variable is present in url', function() { - describe('and setting simple variable', function() { - var variable = { - name: 'apps', - current: {text: "test", value: "test"}, - options: [{text: "test", value: "test"}] - }; - - beforeEach(function(done) { - var dashboard = { templating: { list: [variable] } }; - var urlParams = {}; - urlParams["var-apps"] = "new"; - ctx.$location.search = sinon.stub().returns(urlParams); - ctx.service.init(dashboard).then(function() { done(); }); - ctx.$rootScope.$digest(); - }); - - it('should update current value', function() { - expect(variable.current.value).to.be("new"); - expect(variable.current.text).to.be("new"); - }); - }); - - // describe('and setting adhoc variable', function() { - // var variable = {name: 'filters', type: 'adhoc'}; - // - // beforeEach(function(done) { - // var dashboard = { templating: { list: [variable] } }; - // var urlParams = {}; - // urlParams["var-filters"] = "hostname|gt|server2"; - // ctx.$location.search = sinon.stub().returns(urlParams); - // ctx.service.init(dashboard).then(function() { done(); }); - // ctx.$rootScope.$digest(); - // }); - // - // it('should update current value', function() { - // expect(variable.tags[0]).to.eq({tag: 'hostname', value: 'server2'}); - // }); - // }); - }); - - - }); -}); From 8c05a125dc0cfb28e29fc6004ee26607ab99eba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 10:33:43 +0200 Subject: [PATCH 28/74] fix(build): updated build.go setup --- build.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build.go b/build.go index f9ef09ff5b9..202caa1837b 100644 --- a/build.go +++ b/build.go @@ -334,9 +334,7 @@ func gruntBuildArg(task string) []string { func setup() { runPrint("go", "get", "-v", "github.com/kardianos/govendor") - runPrint("go", "get", "-v", "github.com/blang/semver") - runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3") - runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3") + runPrint("go", "install", "-v", "./pkg/cmd/grafana-server") } func test(pkg string) { From 6574dfacfb2fb93faf6d884da5709f83b7f49f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 11:16:19 +0200 Subject: [PATCH 29/74] feat(internal metrics): added some total stats to metrics reporting, closes #5865 --- pkg/metrics/gauge.go | 8 ++++---- pkg/metrics/graphite.go | 2 ++ pkg/metrics/metrics.go | 12 ++++++++++++ pkg/metrics/publish.go | 24 ++++++++++++++++++++++++ pkg/models/stats.go | 8 ++++---- 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pkg/metrics/gauge.go b/pkg/metrics/gauge.go index 01cd584cb39..59758aa4ecb 100644 --- a/pkg/metrics/gauge.go +++ b/pkg/metrics/gauge.go @@ -24,10 +24,10 @@ func NewGauge(meta *MetricMeta) Gauge { } } -func RegGauge(meta *MetricMeta) Gauge { - g := NewGauge(meta) - MetricStats.Register(g) - return g +func RegGauge(name string, tagStrings ...string) Gauge { + tr := NewGauge(NewMetricMeta(name, tagStrings)) + MetricStats.Register(tr) + return tr } // GaugeSnapshot is a read-only copy of another Gauge. diff --git a/pkg/metrics/graphite.go b/pkg/metrics/graphite.go index e88df2ebb1b..59c992776de 100644 --- a/pkg/metrics/graphite.go +++ b/pkg/metrics/graphite.go @@ -63,6 +63,8 @@ func (this *GraphitePublisher) Publish(metrics []Metric) { switch metric := m.(type) { case Counter: this.addCount(buf, metricName+".count", metric.Count(), now) + case Gauge: + this.addCount(buf, metricName, metric.Value(), now) case Timer: percentiles := metric.Percentiles([]float64{0.25, 0.75, 0.90, 0.99}) this.addCount(buf, metricName+".count", metric.Count(), now) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index bbe580de218..002f2369c9b 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -49,6 +49,12 @@ var ( // Timers M_DataSource_ProxyReq_Timer Timer M_Alerting_Exeuction_Time Timer + + // StatTotals + M_StatTotal_Dashboards Gauge + M_StatTotal_Users Gauge + M_StatTotal_Orgs Gauge + M_StatTotal_Playlists Gauge ) func initMetricVars(settings *MetricSettings) { @@ -105,4 +111,10 @@ func initMetricVars(settings *MetricSettings) { // Timers M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all") M_Alerting_Exeuction_Time = RegTimer("alerting.execution_time") + + // StatTotals + M_StatTotal_Dashboards = RegGauge("stat_totals", "stat", "dashboards") + M_StatTotal_Users = RegGauge("stat_totals", "stat", "users") + M_StatTotal_Orgs = RegGauge("stat_totals", "stat", "orgs") + M_StatTotal_Playlists = RegGauge("stat_totals", "stat", "playlists") } diff --git a/pkg/metrics/publish.go b/pkg/metrics/publish.go index 9c1de6e05d2..c5bb7f61f0a 100644 --- a/pkg/metrics/publish.go +++ b/pkg/metrics/publish.go @@ -15,6 +15,7 @@ import ( ) var metricsLogger log.Logger = log.New("metrics") +var metricPublishCounter int64 = 0 func Init() { settings := readSettings() @@ -45,12 +46,35 @@ func sendMetrics(settings *MetricSettings) { return } + updateTotalStats() + metrics := MetricStats.GetSnapshots() for _, publisher := range settings.Publishers { publisher.Publish(metrics) } } +func updateTotalStats() { + + // every interval also publish totals + metricPublishCounter++ + if metricPublishCounter%2 == 0 { + metricsLogger.Info("Stats!") + + // get stats + statsQuery := m.GetSystemStatsQuery{} + if err := bus.Dispatch(&statsQuery); err != nil { + metricsLogger.Error("Failed to get system stats", "error", err) + return + } + + M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount) + M_StatTotal_Users.Update(statsQuery.Result.UserCount) + M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount) + M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount) + } +} + func sendUsageStats() { if !setting.ReportingEnabled { return diff --git a/pkg/models/stats.go b/pkg/models/stats.go index fa9cfdab6e8..067dec763e5 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -1,10 +1,10 @@ package models type SystemStats struct { - DashboardCount int - UserCount int - OrgCount int - PlaylistCount int + DashboardCount int64 + UserCount int64 + OrgCount int64 + PlaylistCount int64 } type DataSourceStats struct { From 3ed7ab93c5d29c50176fdf07e60dcd237dc6c77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 11:32:27 +0200 Subject: [PATCH 30/74] added small info text to plugins list page, #5176 --- public/app/features/dashboard/shareModalCtrl.js | 2 +- public/app/features/plugins/partials/plugin_list.html | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 36949f51f5b..d0de3dbb4a9 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -8,7 +8,7 @@ function (angular, _, require, config) { var module = angular.module('grafana.controllers'); - module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) { + module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' }; $scope.editor = { index: $scope.tabIndex || 0}; diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html index 0870b8727ec..7cfb14d238d 100644 --- a/public/app/features/plugins/partials/plugin_list.html +++ b/public/app/features/plugins/partials/plugin_list.html @@ -3,7 +3,9 @@
From 936146768f290de6618970de5ec08497e5604e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 11:46:58 +0200 Subject: [PATCH 31/74] fix(snapshots): fixed issue with viewing embedded/solo/png panel from snapshot without login, fixes #3769 --- CHANGELOG.md | 1 + pkg/api/api.go | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2609d8f7be..ff0e9979945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ * **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999) * **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467) * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) +* **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769) # 3.1.2 (unreleased) * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790) diff --git a/pkg/api/api.go b/pkg/api/api.go index 71331acda9f..35b667c55be 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -58,6 +58,7 @@ func Register(r *macaron.Macaron) { r.Get("/plugins/:id/page/:page", reqSignedIn, Index) r.Get("/dashboard/*", reqSignedIn, Index) + r.Get("/dashboard-solo/snapshot/*", Index) r.Get("/dashboard-solo/*", reqSignedIn, Index) r.Get("/import/dashboard", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) From e336fb3d60be1f0fed1dcef355ae452ae2cf0839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 11:51:39 +0200 Subject: [PATCH 32/74] fix(smtp mailer): added timeout of 10 seconds to smpt mailer, fixes #2989 --- pkg/services/notifications/mailer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 309436cb7d9..91c75a1889e 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -12,6 +12,7 @@ import ( "net/smtp" "os" "strings" + "time" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" @@ -66,7 +67,7 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error { tlsconfig.Certificates = []tls.Certificate{cert} } - conn, err := net.Dial("tcp", net.JoinHostPort(host, port)) + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second*10) if err != nil { return err } From f79600b352e4b1887dd2fe8af5f5eed984d2c69c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 11:54:47 +0200 Subject: [PATCH 33/74] fix(internal metrics): removed local dev code --- pkg/metrics/publish.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/metrics/publish.go b/pkg/metrics/publish.go index c5bb7f61f0a..4255481b8d1 100644 --- a/pkg/metrics/publish.go +++ b/pkg/metrics/publish.go @@ -58,9 +58,7 @@ func updateTotalStats() { // every interval also publish totals metricPublishCounter++ - if metricPublishCounter%2 == 0 { - metricsLogger.Info("Stats!") - + if metricPublishCounter%10 == 0 { // get stats statsQuery := m.GetSystemStatsQuery{} if err := bus.Dispatch(&statsQuery); err != nil { From 0fc7405b95da789535fa4435bc9864fd7335f577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 12:04:57 +0200 Subject: [PATCH 34/74] fix(elasticsearch): for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes #3887 --- CHANGELOG.md | 1 + .../datasource/elasticsearch/datasource.js | 6 ------ .../datasource/elasticsearch/query_builder.js | 16 ++++++++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff0e9979945..c842b3b031a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467) * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) * **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769) +* **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887) # 3.1.2 (unreleased) * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790) diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 0889c078082..9f98a794da4 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -216,11 +216,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes }); }; - function escapeForJson(value) { - var luceneQuery = JSON.stringify(value); - return luceneQuery.substr(1, luceneQuery.length - 2); - } - this.getFields = function(query) { return this._get('/_mapping').then(function(result) { var typeMap = { @@ -285,7 +280,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes var header = this.getQueryHeader('count', range.from, range.to); var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef)); - esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query)); esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf()); esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf()); esQuery = header + '\n' + esQuery + '\n'; diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index d256c6d1438..4be5404a950 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -221,12 +221,6 @@ function (queryDef) { "size": 0, "query": { "filtered": { - "query": { - "query_string": { - "analyze_wildcard": true, - "query": '$lucene_query', - } - }, "filter": { "bool": { "must": [{"range": this.getRangeFilter()}] @@ -235,6 +229,16 @@ function (queryDef) { } } }; + + if (queryDef.query) { + query.query.filtered.query = { + "query_string": { + "analyze_wildcard": true, + "query": queryDef.query, + } + }; + } + query.aggs = { "1": { "terms": { From 23246605b0b3a934821d0e0f99ea9d89c50c1cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 16:05:20 +0200 Subject: [PATCH 35/74] feat(graph panel): working on adding non time series support to graph panel --- public/app/core/core.ts | 2 + public/app/core/directives/metric_segment.js | 14 +- public/app/core/utils/colors.ts | 12 ++ .../app/plugins/panel/graph/axes_edit_tab.ts | 25 +++ .../app/plugins/panel/graph/data_processor.ts | 164 ++++++++++++++++++ public/app/plugins/panel/graph/module.ts | 161 ++++------------- public/app/plugins/panel/graph/tab_axes.html | 12 +- 7 files changed, 250 insertions(+), 140 deletions(-) create mode 100644 public/app/core/utils/colors.ts create mode 100644 public/app/plugins/panel/graph/axes_edit_tab.ts create mode 100644 public/app/plugins/panel/graph/data_processor.ts diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 1174e267f4e..d44cbf4dbfb 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -41,6 +41,7 @@ import 'app/core/routes/routes'; import './filters/filters'; import coreModule from './core_module'; import appEvents from './app_events'; +import colors from './utils/colors'; export { @@ -60,4 +61,5 @@ export { dashboardSelector, queryPartEditorDirective, WizardFlow, + colors, }; diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index d51260395de..98921753997 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -170,6 +170,7 @@ function (_, $, coreModule) { }, link: { pre: function postLink($scope, elem, attrs) { + var cachedOptions; $scope.valueToSegment = function(value) { var option = _.find($scope.options, {value: value}); @@ -189,13 +190,20 @@ function (_, $, coreModule) { }); return $q.when(optionSegments); } else { - return $scope.getOptions(); + return $scope.getOptions().then(function(options) { + cachedOptions = options; + return _.map(options, function(option) { + return uiSegmentSrv.newSegment({value: option.text}); + }); + }); } }; $scope.onSegmentChange = function() { - if ($scope.options) { - var option = _.find($scope.options, {text: $scope.segment.value}); + var options = $scope.options || cachedOptions; + + if (options) { + var option = _.find(options, {text: $scope.segment.value}); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts new file mode 100644 index 00000000000..bd774ea02ea --- /dev/null +++ b/public/app/core/utils/colors.ts @@ -0,0 +1,12 @@ + + +export default [ + "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", + "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", + "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", + "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", + "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", + "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", + "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" +]; + diff --git a/public/app/plugins/panel/graph/axes_edit_tab.ts b/public/app/plugins/panel/graph/axes_edit_tab.ts new file mode 100644 index 00000000000..119e0a0aff6 --- /dev/null +++ b/public/app/plugins/panel/graph/axes_edit_tab.ts @@ -0,0 +1,25 @@ +/// + +export class AxesEditTabCtrl { + panel: any; + panelCtrl: any; + + /** @ngInject **/ + constructor($scope) { + this.panelCtrl = $scope.ctrl; + this.panel = this.panelCtrl.panel; + $scope.ctrl = this; + } + +} + +/** @ngInject **/ +export function axesTabCtrl() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'public/app/plugins/panel/graph/tab_axes.html', + controller: AxesEditTabCtrl, + }; +} diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts new file mode 100644 index 00000000000..71a6875c858 --- /dev/null +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -0,0 +1,164 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import _ from 'lodash'; +import TimeSeries from 'app/core/time_series2'; +import {colors} from 'app/core/core'; + +export class DataProcessor { + + constructor(private panel) { + } + + getSeriesList(options) { + + switch (this.panel.xaxis.mode) { + case 'series': + case 'time': { + return options.dataList.map(this.timeSeriesHandler.bind(this)); + } + case 'table': { + // Table panel uses only first enabled target, so we can use dataList[0] + // dataList.splice(1, dataList.length - 1); + // dataHandler = this.tableHandler; + break; + } + case 'json': { + break; + } + } + } + + seriesHandler(seriesData, index, datapoints, alias) { + var colorIndex = index % colors.length; + var color = this.panel.aliasColors[alias] || colors[colorIndex]; + + var series = new TimeSeries({datapoints: datapoints, alias: alias, color: color, unit: seriesData.unit}); + + // if (datapoints && datapoints.length > 0) { + // var last = moment.utc(datapoints[datapoints.length - 1][1]); + // var from = moment.utc(this.range.from); + // if (last - from < -10000) { + // this.datapointsOutside = true; + // } + // + // this.datapointsCount += datapoints.length; + // this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); + // } + + return series; + } + + timeSeriesHandler(seriesData, index) { + var datapoints = seriesData.datapoints; + var alias = seriesData.target; + + return this.seriesHandler(seriesData, index, datapoints, alias); + } + + tableHandler(seriesData, index) { + var xColumnIndex = Number(this.panel.xaxis.columnIndex); + var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); + var datapoints = _.map(seriesData.rows, (row) => { + var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); + return [ + value, // Y value + row[xColumnIndex] // X value + ]; + }); + + var alias = seriesData.columns[valueColumnIndex].text; + + return this.seriesHandler(seriesData, index, datapoints, alias); + } + + // esRawDocHandler(seriesData, index) { + // let xField = this.panel.xaxis.esField; + // let valueField = this.panel.xaxis.esValueField; + // let datapoints = _.map(seriesData.datapoints, (doc) => { + // return [ + // pluckDeep(doc, valueField), // Y value + // pluckDeep(doc, xField) // X value + // ]; + // }); + // + // // Remove empty points + // datapoints = _.filter(datapoints, (point) => { + // return point[0] !== undefined; + // }); + // + // var alias = valueField; + // return this.seriesHandler(seriesData, index, datapoints, alias); + // } + // + validateXAxisSeriesValue() { + switch (this.panel.xaxis.mode) { + case 'series': { + if (this.panel.xaxis.values.length === 0) { + this.panel.xaxis.values = ['total']; + return; + } + + var validOptions = this.getXAxisValueOptions({}); + var found = _.find(validOptions, {value: this.panel.xaxis.values[0]}); + if (!found) { + this.panel.xaxis.values = ['total']; + } + return; + } + } + } + + getXAxisValueOptions(options) { + switch (this.panel.xaxis.mode) { + case 'time': { + return []; + } + case 'series': { + return [ + {text: 'Avg', value: 'avg'}, + {text: 'Min', value: 'min'}, + {text: 'Max', value: 'min'}, + {text: 'Total', value: 'total'}, + {text: 'Count', value: 'count'}, + ]; + } + } + } +} + +// function getFieldsFromESDoc(doc) { +// let fields = []; +// let fieldNameParts = []; +// +// function getFieldsRecursive(obj) { +// _.forEach(obj, (value, key) => { +// if (_.isObject(value)) { +// fieldNameParts.push(key); +// getFieldsRecursive(value); +// } else { +// let field = fieldNameParts.concat(key).join('.'); +// fields.push(field); +// } +// }); +// fieldNameParts.pop(); +// } +// +// getFieldsRecursive(doc); +// return fields; +// } +// +// function pluckDeep(obj: any, property: string) { +// let propertyParts = property.split('.'); +// let value = obj; +// for (let i = 0; i < propertyParts.length; ++i) { +// if (value[propertyParts[i]]) { +// value = value[propertyParts[i]]; +// } else { +// return undefined; +// } +// } +// return value; +// } + + diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 712ca04c423..90c323d4c87 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -14,15 +14,18 @@ import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; import * as fileExport from 'app/core/utils/file_export'; import {MetricsPanelCtrl, alertTab} from 'app/plugins/sdk'; +import {DataProcessor} from './data_processor'; class GraphCtrl extends MetricsPanelCtrl { static template = template; hiddenSeries: any = {}; seriesList: any = []; + dataList: any = []; logScales: any; unitFormats: any; xAxisModes: any; + xAxisStatOptions: any; xNameSegment: any; annotationsPromise: any; datapointsCount: number; @@ -30,6 +33,7 @@ class GraphCtrl extends MetricsPanelCtrl { datapointsWarning: boolean; colors: any = []; subTabIndex: number; + processor: DataProcessor; panelDefaults = { // datasource name, null = default datasource @@ -118,7 +122,7 @@ class GraphCtrl extends MetricsPanelCtrl { _.defaults(this.panel.legend, this.panelDefaults.legend); _.defaults(this.panel.xaxis, this.panelDefaults.xaxis); - this.colors = $scope.$root.colors; + this.processor = new DataProcessor(this.panel); this.events.on('render', this.onRender.bind(this)); this.events.on('data-received', this.onDataReceived.bind(this)); @@ -144,6 +148,7 @@ class GraphCtrl extends MetricsPanelCtrl { 'log (base 32)': 32, 'log (base 1024)': 1024 }; + this.unitFormats = kbn.getUnitFormats(); this.xAxisModes = { @@ -153,6 +158,14 @@ class GraphCtrl extends MetricsPanelCtrl { 'Json': 'json' }; + this.xAxisStatOptions = [ + {text: 'Avg', value: 'avg'}, + {text: 'Min', value: 'min'}, + {text: 'Max', value: 'min'}, + {text: 'Total', value: 'total'}, + {text: 'Count', value: 'count'}, + ]; + this.subTabIndex = 0; } @@ -199,25 +212,8 @@ class GraphCtrl extends MetricsPanelCtrl { this.datapointsCount = 0; this.datapointsOutside = false; - let dataHandler: (seriesData, index)=>any; - switch (this.panel.xaxis.mode) { - case 'series': - case 'time': { - dataHandler = this.timeSeriesHandler; - break; - } - case 'table': { - // Table panel uses only first enabled target, so we can use dataList[0] - dataList.splice(1, dataList.length - 1); - dataHandler = this.tableHandler; - break; - } - case 'json': { - break; - } - } - - this.seriesList = dataList.map(dataHandler.bind(this)); + this.dataList = dataList; + this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range}); this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside; this.annotationsPromise.then(annotations => { @@ -230,73 +226,6 @@ class GraphCtrl extends MetricsPanelCtrl { }); } - seriesHandler(seriesData, index, datapoints, alias) { - var colorIndex = index % this.colors.length; - var color = this.panel.aliasColors[alias] || this.colors[colorIndex]; - - var series = new TimeSeries({ - datapoints: datapoints, - alias: alias, - color: color, - unit: seriesData.unit, - }); - - if (datapoints && datapoints.length > 0) { - var last = moment.utc(datapoints[datapoints.length - 1][1]); - var from = moment.utc(this.range.from); - if (last - from < -10000) { - this.datapointsOutside = true; - } - - this.datapointsCount += datapoints.length; - this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); - } - - return series; - } - - timeSeriesHandler(seriesData, index) { - var datapoints = seriesData.datapoints; - var alias = seriesData.target; - - return this.seriesHandler(seriesData, index, datapoints, alias); - } - - tableHandler(seriesData, index) { - var xColumnIndex = Number(this.panel.xaxis.columnIndex); - var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); - var datapoints = _.map(seriesData.rows, (row) => { - var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); - return [ - value, // Y value - row[xColumnIndex] // X value - ]; - }); - - var alias = seriesData.columns[valueColumnIndex].text; - - return this.seriesHandler(seriesData, index, datapoints, alias); - } - - esRawDocHandler(seriesData, index) { - let xField = this.panel.xaxis.esField; - let valueField = this.panel.xaxis.esValueField; - let datapoints = _.map(seriesData.datapoints, (doc) => { - return [ - pluckDeep(doc, valueField), // Y value - pluckDeep(doc, xField) // X value - ]; - }); - - // Remove empty points - datapoints = _.filter(datapoints, (point) => { - return point[0] !== undefined; - }); - - var alias = valueField; - return this.seriesHandler(seriesData, index, datapoints, alias); - } - onRender() { if (!this.seriesList) { return; } @@ -380,13 +309,11 @@ class GraphCtrl extends MetricsPanelCtrl { this.render(); } - // Called from panel menu toggleLegend() { this.panel.legend.show = !this.panel.legend.show; this.refresh(); } - legendValuesOptionChanged() { var legend = this.panel.legend; legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total; @@ -401,9 +328,21 @@ class GraphCtrl extends MetricsPanelCtrl { fileExport.exportSeriesListToCsvColumns(this.seriesList); } - xAxisModeChanged() { - // set defaults - this.refresh(); + xAxisOptionChanged() { + switch (this.panel.xaxis.mode) { + case 'time': { + this.panel.tooltip.shared = true; + this.panel.xaxis.values = []; + this.onDataReceived(this.dataList); + break; + } + case 'series': { + this.panel.tooltip.shared = false; + this.processor.validateXAxisSeriesValue(); + this.onDataReceived(this.dataList); + break; + } + } } getXAxisNameOptions() { @@ -413,44 +352,8 @@ class GraphCtrl extends MetricsPanelCtrl { } getXAxisValueOptions() { - return this.$q.when([ - {text: 'Avg', value: 'avg'} - ]); + return this.$q.when(this.processor.getXAxisValueOptions({dataList: this.dataList})); } } -function getFieldsFromESDoc(doc) { - let fields = []; - let fieldNameParts = []; - - function getFieldsRecursive(obj) { - _.forEach(obj, (value, key) => { - if (_.isObject(value)) { - fieldNameParts.push(key); - getFieldsRecursive(value); - } else { - let field = fieldNameParts.concat(key).join('.'); - fields.push(field); - } - }); - fieldNameParts.pop(); - } - - getFieldsRecursive(doc); - return fields; -} - -function pluckDeep(obj: any, property: string) { - let propertyParts = property.split('.'); - let value = obj; - for (let i = 0; i < propertyParts.length; ++i) { - if (value[propertyParts[i]]) { - value = value[propertyParts[i]]; - } else { - return undefined; - } - } - return value; -} - export {GraphCtrl, GraphCtrl as PanelCtrl} diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index d0915f7e17c..f139e88de5a 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -44,24 +44,20 @@
- +
- +
-
+
- +
From 76b8cff44533dc49a201cfacd60b913cb0bc7d9e Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 22 Sep 2016 16:16:58 +0200 Subject: [PATCH 36/74] fix(gnet): remove trailing , --- pkg/plugins/update_checker.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index ed43398357e..980c813888f 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -39,17 +39,16 @@ func StartPluginUpdateChecker() { } func getAllExternalPluginSlugs() string { - str := "" - + var result []string for _, plug := range Plugins { if plug.IsCorePlugin { continue } - str += plug.Id + "," + result = append(result, plug.Id) } - return str + return strings.Join(result, ",") } func checkForUpdates() { From 6cd4db12c73081cb6ec38c446f77688c7c6a2b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 16:19:44 +0200 Subject: [PATCH 37/74] feat(graph panel): refactoring axes tab options into it's own component --- .../app/plugins/panel/graph/axes_edit_tab.ts | 25 ----- .../graph/{tab_axes.html => axes_editor.html} | 0 public/app/plugins/panel/graph/axes_editor.ts | 95 +++++++++++++++++++ public/app/plugins/panel/graph/module.ts | 66 +------------ public/app/plugins/panel/table/options.html | 2 - 5 files changed, 97 insertions(+), 91 deletions(-) delete mode 100644 public/app/plugins/panel/graph/axes_edit_tab.ts rename public/app/plugins/panel/graph/{tab_axes.html => axes_editor.html} (100%) create mode 100644 public/app/plugins/panel/graph/axes_editor.ts delete mode 100644 public/app/plugins/panel/table/options.html diff --git a/public/app/plugins/panel/graph/axes_edit_tab.ts b/public/app/plugins/panel/graph/axes_edit_tab.ts deleted file mode 100644 index 119e0a0aff6..00000000000 --- a/public/app/plugins/panel/graph/axes_edit_tab.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// - -export class AxesEditTabCtrl { - panel: any; - panelCtrl: any; - - /** @ngInject **/ - constructor($scope) { - this.panelCtrl = $scope.ctrl; - this.panel = this.panelCtrl.panel; - $scope.ctrl = this; - } - -} - -/** @ngInject **/ -export function axesTabCtrl() { - 'use strict'; - return { - restrict: 'E', - scope: true, - templateUrl: 'public/app/plugins/panel/graph/tab_axes.html', - controller: AxesEditTabCtrl, - }; -} diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/axes_editor.html similarity index 100% rename from public/app/plugins/panel/graph/tab_axes.html rename to public/app/plugins/panel/graph/axes_editor.html diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts new file mode 100644 index 00000000000..e25fe1b3a8e --- /dev/null +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -0,0 +1,95 @@ +/// + +import kbn from 'app/core/utils/kbn'; + +export class AxesEditorCtrl { + panel: any; + panelCtrl: any; + unitFormats: any; + logScales: any; + xAxisModes: any; + xAxisStatOptions: any; + xNameSegment: any; + + /** @ngInject **/ + constructor(private $scope, private $q) { + this.panelCtrl = $scope.ctrl; + this.panel = this.panelCtrl.panel; + $scope.ctrl = this; + + this.unitFormats = kbn.getUnitFormats(); + + this.logScales = { + 'linear': 1, + 'log (base 2)': 2, + 'log (base 10)': 10, + 'log (base 32)': 32, + 'log (base 1024)': 1024 + }; + + this.xAxisModes = { + 'Time': 'time', + 'Series': 'series', + 'Table': 'table', + 'Json': 'json' + }; + + this.xAxisStatOptions = [ + {text: 'Avg', value: 'avg'}, + {text: 'Min', value: 'min'}, + {text: 'Max', value: 'min'}, + {text: 'Total', value: 'total'}, + {text: 'Count', value: 'count'}, + ]; + } + + setUnitFormat(axis, subItem) { + axis.format = subItem.value; + this.panelCtrl.render(); + } + + render() { + this.panelCtrl.render(); + } + + xAxisOptionChanged() { + switch (this.panel.xaxis.mode) { + case 'time': { + this.panel.tooltip.shared = true; + this.panel.xaxis.values = []; + this.panelCtrl.onDataReceived(this.panelCtrl.dataList); + break; + } + case 'series': { + this.panel.tooltip.shared = false; + this.panelCtrl.processor.validateXAxisSeriesValue(); + this.panelCtrl.onDataReceived(this.panelCtrl.dataList); + break; + } + } + } + + getXAxisNameOptions() { + return this.$q.when([ + {text: 'Avg', value: 'avg'} + ]); + } + + getXAxisValueOptions() { + return this.$q.when(this.panelCtrl.processor.getXAxisValueOptions({ + dataList: this.panelCtrl.dataList + })); + } + +} + +/** @ngInject **/ +export function axesEditorComponent() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'public/app/plugins/panel/graph/tab_axes.html', + controller: AxesEditorCtrl, + }; +} diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 90c323d4c87..9edcb7fe1ff 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -8,13 +8,13 @@ import './thresholds_form'; import template from './template'; import angular from 'angular'; import moment from 'moment'; -import kbn from 'app/core/utils/kbn'; import _ from 'lodash'; import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; import * as fileExport from 'app/core/utils/file_export'; import {MetricsPanelCtrl, alertTab} from 'app/plugins/sdk'; import {DataProcessor} from './data_processor'; +import {axesEditorComponent} from './axes_editor'; class GraphCtrl extends MetricsPanelCtrl { static template = template; @@ -22,11 +22,6 @@ class GraphCtrl extends MetricsPanelCtrl { hiddenSeries: any = {}; seriesList: any = []; dataList: any = []; - logScales: any; - unitFormats: any; - xAxisModes: any; - xAxisStatOptions: any; - xNameSegment: any; annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; @@ -133,39 +128,13 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab('Axes', 'public/app/plugins/panel/graph/tab_axes.html', 2); + this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { this.addEditorTab('Alert', alertTab, 5); } - - this.logScales = { - 'linear': 1, - 'log (base 2)': 2, - 'log (base 10)': 10, - 'log (base 32)': 32, - 'log (base 1024)': 1024 - }; - - this.unitFormats = kbn.getUnitFormats(); - - this.xAxisModes = { - 'Time': 'time', - 'Series': 'series', - 'Table': 'table', - 'Json': 'json' - }; - - this.xAxisStatOptions = [ - {text: 'Avg', value: 'avg'}, - {text: 'Min', value: 'min'}, - {text: 'Max', value: 'min'}, - {text: 'Total', value: 'total'}, - {text: 'Count', value: 'count'}, - ]; - this.subTabIndex = 0; } @@ -175,11 +144,6 @@ class GraphCtrl extends MetricsPanelCtrl { actions.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'}); } - setUnitFormat(axis, subItem) { - axis.format = subItem.value; - this.render(); - } - issueQueries(datasource) { this.annotationsPromise = this.annotationsSrv.getAnnotations({ dashboard: this.dashboard, @@ -328,32 +292,6 @@ class GraphCtrl extends MetricsPanelCtrl { fileExport.exportSeriesListToCsvColumns(this.seriesList); } - xAxisOptionChanged() { - switch (this.panel.xaxis.mode) { - case 'time': { - this.panel.tooltip.shared = true; - this.panel.xaxis.values = []; - this.onDataReceived(this.dataList); - break; - } - case 'series': { - this.panel.tooltip.shared = false; - this.processor.validateXAxisSeriesValue(); - this.onDataReceived(this.dataList); - break; - } - } - } - - getXAxisNameOptions() { - return this.$q.when([ - {text: 'Avg', value: 'avg'} - ]); - } - - getXAxisValueOptions() { - return this.$q.when(this.processor.getXAxisValueOptions({dataList: this.dataList})); - } } export {GraphCtrl, GraphCtrl as PanelCtrl} diff --git a/public/app/plugins/panel/table/options.html b/public/app/plugins/panel/table/options.html deleted file mode 100644 index d43ff958c5d..00000000000 --- a/public/app/plugins/panel/table/options.html +++ /dev/null @@ -1,2 +0,0 @@ - - From f2f3115749f94864615a1eb6fcd142e80113df2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 16:47:37 +0200 Subject: [PATCH 38/74] feat(graph panel): more progress on graph panel and non time series data support --- public/app/core/directives/metric_segment.js | 14 +++------- public/app/plugins/panel/graph/axes_editor.ts | 12 +++++++-- .../app/plugins/panel/graph/data_processor.ts | 27 +++++++++++++------ public/app/plugins/panel/graph/graph.js | 3 --- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 98921753997..d51260395de 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -170,7 +170,6 @@ function (_, $, coreModule) { }, link: { pre: function postLink($scope, elem, attrs) { - var cachedOptions; $scope.valueToSegment = function(value) { var option = _.find($scope.options, {value: value}); @@ -190,20 +189,13 @@ function (_, $, coreModule) { }); return $q.when(optionSegments); } else { - return $scope.getOptions().then(function(options) { - cachedOptions = options; - return _.map(options, function(option) { - return uiSegmentSrv.newSegment({value: option.text}); - }); - }); + return $scope.getOptions(); } }; $scope.onSegmentChange = function() { - var options = $scope.options || cachedOptions; - - if (options) { - var option = _.find(options, {text: $scope.segment.value}); + if ($scope.options) { + var option = _.find($scope.options, {text: $scope.segment.value}); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index e25fe1b3a8e..c6b60121009 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -30,8 +30,7 @@ export class AxesEditorCtrl { this.xAxisModes = { 'Time': 'time', 'Series': 'series', - 'Table': 'table', - 'Json': 'json' + 'Custom': 'custom' }; this.xAxisStatOptions = [ @@ -55,12 +54,21 @@ export class AxesEditorCtrl { xAxisOptionChanged() { switch (this.panel.xaxis.mode) { case 'time': { + this.panel.bars = false; + this.panel.lines = true; + this.panel.points = false; + this.panel.legend.show = true; this.panel.tooltip.shared = true; this.panel.xaxis.values = []; this.panelCtrl.onDataReceived(this.panelCtrl.dataList); break; } case 'series': { + this.panel.bars = true; + this.panel.lines = false; + this.panel.points = false; + this.panel.stack = false; + this.panel.legend.show = false; this.panel.tooltip.shared = false; this.panelCtrl.processor.validateXAxisSeriesValue(); this.panelCtrl.onDataReceived(this.panelCtrl.dataList); diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 71a6875c858..8c3fc927adb 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -11,20 +11,26 @@ export class DataProcessor { } getSeriesList(options) { + if (!options.dataList || options.dataList.length === 0) { + return []; + } + + // auto detect xaxis mode + var firstItem; + if (options.dataList && options.dataList.length > 0) { + firstItem = options.dataList[0]; + if (firstItem.type === 'docs') { + this.panel.xaxis.mode = 'custom'; + } + } switch (this.panel.xaxis.mode) { case 'series': case 'time': { return options.dataList.map(this.timeSeriesHandler.bind(this)); } - case 'table': { - // Table panel uses only first enabled target, so we can use dataList[0] - // dataList.splice(1, dataList.length - 1); - // dataHandler = this.tableHandler; - break; - } - case 'json': { - break; + case 'custom': { + return this.customHandler(firstItem); } } } @@ -56,6 +62,11 @@ export class DataProcessor { return this.seriesHandler(seriesData, index, datapoints, alias); } + customHandler(dataItem) { + console.log('custom', dataItem); + return []; + } + tableHandler(seriesData, index) { var xColumnIndex = Number(this.panel.xaxis.columnIndex); var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index da2c0dae536..08809d95964 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -262,9 +262,6 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) { if (data.length) { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; - options.series.bars.show = true; - options.series.points.show = false; - options.series.lines.show = false; } addXSeriesAxis(options); From a49c21df3a93a368e2859aac9dbe92add77c02d6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 22 Sep 2016 19:24:18 +0200 Subject: [PATCH 39/74] fix(png-renderer): increase timeouts --- pkg/services/alerting/notifier.go | 4 ++-- pkg/services/notifications/webhook.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index bb48f71cdcd..61cc0bc55d3 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -72,8 +72,8 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error { Url: imageUrl, Width: "800", Height: "400", - SessionId: "123", - Timeout: "10", + SessionId: "cef0256d482b4293", + Timeout: "30", } if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index 31f00baebd3..67ffa43900a 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -44,7 +44,7 @@ func sendWebRequest(webhook *Webhook) error { webhookLog.Debug("Sending webhook", "url", webhook.Url) client := http.Client{ - Timeout: time.Duration(3 * time.Second), + Timeout: time.Duration(10 * time.Second), } request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body))) From 2c3dd84ebb82f59090b6cdc1b54c56cfea8f1da1 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 22 Sep 2016 13:46:11 -0400 Subject: [PATCH 40/74] allow non-admin users to view plugin readme --- pkg/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 71331acda9f..b7f767975f1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -202,9 +202,9 @@ func Register(r *macaron.Macaron) { r.Get("/plugins", wrap(GetPluginList)) r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) + r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme)) r.Group("/plugins", func() { - r.Get("/:pluginId/readme", wrap(GetPluginReadme)) r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards)) r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) }, reqOrgAdmin) From b063cf0a6ecc6cbcd9743868357ee402168c8c79 Mon Sep 17 00:00:00 2001 From: Mauro Stettler Date: Thu, 22 Sep 2016 20:20:32 +0100 Subject: [PATCH 41/74] fix typo --- pkg/cmd/grafana-server/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index d3d291a74bf..d38c1acd894 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -56,7 +56,7 @@ func main() { setting.BuildCommit = commit setting.BuildStamp = buildstampInt64 - go listenToSystemSignels() + go listenToSystemSignals() flag.Parse() writePIDFile() @@ -116,7 +116,7 @@ func writePIDFile() { } } -func listenToSystemSignels() { +func listenToSystemSignals() { signalChan := make(chan os.Signal, 1) code := 0 From e36cdac594367a39db7eda96d5128242d8e40d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Sep 2016 21:49:41 +0200 Subject: [PATCH 42/74] fix(templating): fixed issue with templating when initalizing variables without any existing value --- public/app/features/templating/templateSrv.js | 4 ++++ public/app/features/templating/variable_srv.ts | 18 ++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index f7784e2cb50..dadb8f23a89 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -43,6 +43,10 @@ function (angular, _, kbn) { } }; + this.variableInitialized = function(variable) { + this._index[variable.name] = variable; + }; + this.getAdhocFilters = function(datasourceName) { var variable = this._adhocVariables[datasourceName]; if (variable) { diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index cdbc0c5e7c1..bb6f4f7cde3 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -8,7 +8,6 @@ import {Variable, variableTypes} from './variable'; export class VariableSrv { dashboard: any; variables: any; - variableLock: any; /** @ngInject */ constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) { @@ -18,7 +17,6 @@ export class VariableSrv { } init(dashboard) { - this.variableLock = {}; this.dashboard = dashboard; // create working class models representing variables @@ -30,7 +28,7 @@ export class VariableSrv { // init variables for (let variable of this.variables) { - this.variableLock[variable.name] = this.$q.defer(); + variable.initLock = this.$q.defer(); } var queryParams = this.$location.search(); @@ -61,27 +59,27 @@ export class VariableSrv { processVariable(variable, queryParams) { var dependencies = []; - var lock = this.variableLock[variable.name]; for (let otherVariable of this.variables) { if (variable.dependsOn(otherVariable)) { - dependencies.push(this.variableLock[otherVariable.name].promise); + dependencies.push(otherVariable.initLock.promise); } } return this.$q.all(dependencies).then(() => { var urlValue = queryParams['var-' + variable.name]; if (urlValue !== void 0) { - return variable.setValueFromUrl(urlValue).then(lock.resolve); + return variable.setValueFromUrl(urlValue).then(variable.initLock.resolve); } if (variable.refresh === 1 || variable.refresh === 2) { - return variable.updateOptions().then(lock.resolve); + return variable.updateOptions().then(variable.initLock.resolve); } - lock.resolve(); + variable.initLock.resolve(); }).finally(() => { - delete this.variableLock[variable.name]; + this.templateSrv.variableInitialized(variable); + delete variable.initLock; }); } @@ -113,7 +111,7 @@ export class VariableSrv { variableUpdated(variable) { // if there is a variable lock ignore cascading update because we are in a boot up scenario - if (this.variableLock[variable.name]) { + if (variable.initLock) { return this.$q.when(); } From cb0f19f8d4cff3453ac51bf47fdf89ed89f2a2e9 Mon Sep 17 00:00:00 2001 From: Mauro Stettler Date: Thu, 22 Sep 2016 21:13:08 +0100 Subject: [PATCH 43/74] fix context dependency --- pkg/tsdb/prometheus/prometheus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 781eb91dd8f..1df51bd1ffe 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -1,7 +1,6 @@ package prometheus import ( - "context" "fmt" "net/http" "regexp" @@ -11,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" "github.com/prometheus/client_golang/api/prometheus" + "golang.org/x/net/context" pmodel "github.com/prometheus/common/model" ) From 521b5cf0142abb701765d076d5adcced7f3065ce Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 23 Sep 2016 06:51:33 +0200 Subject: [PATCH 44/74] tech(build): make sure build.go setup works fine ref #6113 --- scripts/circle-test.sh | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 scripts/circle-test.sh diff --git a/scripts/circle-test.sh b/scripts/circle-test.sh old mode 100644 new mode 100755 index b00bf7459ad..8918de82948 --- a/scripts/circle-test.sh +++ b/scripts/circle-test.sh @@ -25,6 +25,7 @@ exit_if_fail npm run coveralls test -z "$(gofmt -s -l ./pkg/... | tee /dev/stderr)" +exit_if_fail go run build.go setup exit_if_fail go run build.go build exit_if_fail go vet ./pkg/... From e5c64732f156cbfe626aa284fe41934ca31ad47a Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 23 Sep 2016 08:07:14 +0200 Subject: [PATCH 45/74] fix(sql): Add boolstr to all dialects closes #6116 --- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/alert_notification.go | 3 ++- pkg/services/sqlstore/annotation.go | 2 +- pkg/services/sqlstore/migrator/dialect.go | 1 + pkg/services/sqlstore/migrator/mysql_dialect.go | 4 ++++ pkg/services/sqlstore/migrator/postgres_dialect.go | 4 ++++ pkg/services/sqlstore/migrator/sqlite_dialect.go | 7 +++++++ 7 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 5a430238823..64a4a6b3d8a 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -92,7 +92,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { params = append(params, query.Limit) } - sql.WriteString("ORDER BY name ASC") + sql.WriteString(" ORDER BY name ASC") alerts := make([]*m.Alert, 0) if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 5105ca39eff..5acb53c3c09 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -66,7 +66,8 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro sql.WriteString(` WHERE alert_notification.org_id = ?`) params = append(params, query.OrgId) - sql.WriteString(` AND ((alert_notification.is_default = 1)`) + sql.WriteString(` AND ((alert_notification.is_default = ?)`) + params = append(params, dialect.BooleanStr(true)) if len(query.Ids) > 0 { sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") for _, v := range query.Ids { diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 1b0f02fce09..3ea8647d3fa 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -75,7 +75,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I query.Limit = 10 } - sql.WriteString(fmt.Sprintf("ORDER BY epoch DESC LIMIT %v", query.Limit)) + sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit)) items := make([]*annotations.Item, 0) if err := x.Sql(sql.String(), params...).Find(&items); err != nil { diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index 0c94eb82234..4473b560428 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -18,6 +18,7 @@ type Dialect interface { SupportEngine() bool LikeStr() string Default(col *Column) string + BooleanStr(bool) string CreateIndexSql(tableName string, index *Index) string CreateTableSql(table *Table) string diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index 195d52d1934..fc64842bd07 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -29,6 +29,10 @@ func (db *Mysql) AutoIncrStr() string { return "AUTO_INCREMENT" } +func (db *Mysql) BooleanStr(value bool) string { + return strconv.FormatBool(value) +} + func (db *Mysql) SqlType(c *Column) string { var res string switch c.Type { diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index 826a00a1410..5500b9f1684 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -36,6 +36,10 @@ func (db *Postgres) AutoIncrStr() string { return "" } +func (db *Postgres) BooleanStr(value bool) string { + return strconv.FormatBool(value) +} + func (b *Postgres) Default(col *Column) string { if col.Type == DB_Bool { if col.Default == "0" { diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go index 8555754ab92..fe1e781c8df 100644 --- a/pkg/services/sqlstore/migrator/sqlite_dialect.go +++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go @@ -29,6 +29,13 @@ func (db *Sqlite3) AutoIncrStr() string { return "AUTOINCREMENT" } +func (db *Sqlite3) BooleanStr(value bool) string { + if value { + return "1" + } + return "0" +} + func (db *Sqlite3) SqlType(c *Column) string { switch c.Type { case DB_Date, DB_DateTime, DB_TimeStamp, DB_Time: From 99c77e7df87bd65bfdb460cebd391a57790c9f47 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 23 Sep 2016 09:56:42 +0200 Subject: [PATCH 46/74] tech(plugins): increase timeout --- pkg/log/log.go | 18 ++++++++++++++++-- pkg/plugins/update_checker.go | 10 ++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pkg/log/log.go b/pkg/log/log.go index 34a2aed4762..fd18e9c65bf 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -32,11 +32,25 @@ func New(logger string, ctx ...interface{}) Logger { } func Trace(format string, v ...interface{}) { - Root.Debug(fmt.Sprintf(format, v)) + var message string + if len(v) > 0 { + message = fmt.Sprintf(format, v) + } else { + message = format + } + + Root.Debug(message) } func Debug(format string, v ...interface{}) { - Root.Debug(fmt.Sprintf(format, v)) + var message string + if len(v) > 0 { + message = fmt.Sprintf(format, v) + } else { + message = format + } + + Root.Debug(message) } func Debug2(message string, v ...interface{}) { diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index 980c813888f..79e6c061e5d 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -11,6 +11,10 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +var ( + httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)} +) + type GrafanaNetPlugin struct { Slug string `json:"slug"` Version string `json:"version"` @@ -54,10 +58,8 @@ func getAllExternalPluginSlugs() string { func checkForUpdates() { log.Trace("Checking for updates") - client := http.Client{Timeout: time.Duration(5 * time.Second)} - pluginSlugs := getAllExternalPluginSlugs() - resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) + resp, err := httpClient.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) if err != nil { log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error()) @@ -88,7 +90,7 @@ func checkForUpdates() { } } - resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json") + 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()) return From 175c651e65a853874a1a272ecf22afeedb3d13d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Sep 2016 12:29:53 +0200 Subject: [PATCH 47/74] fix(server side rendering): Fixed issues with server side rendering for alerting & for auth proxy scenarios, fixes #6115, fixes #5906 --- CHANGELOG.md | 1 + pkg/api/frontendsettings.go | 2 +- pkg/api/index.go | 15 ++++++- pkg/api/render.go | 24 +++-------- pkg/components/renderer/renderer.go | 31 ++++++++++---- pkg/middleware/middleware.go | 28 ++----------- pkg/middleware/render_auth.go | 55 +++++++++++++++++++++++++ pkg/middleware/session.go | 1 - pkg/services/alerting/notifier.go | 10 ++--- public/app/core/services/backend_srv.ts | 4 ++ vendor/phantomjs/render.js | 10 ++--- 11 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 pkg/middleware/render_auth.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c842b3b031a..3fc33b0c0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) * **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769) * **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887) +* **PNG Rendering**: Fix for server side rendering when using auth proxy, fixes [#5906](https://github.com/grafana/grafana/pull/5906) # 3.1.2 (unreleased) * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3a019e80c49..5a324aa1331 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro url := ds.Url if ds.Access == m.DS_ACCESS_PROXY { - url = setting.AppSubUrl + "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10) + url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10) } var dsMap = map[string]interface{}{ diff --git a/pkg/api/index.go b/pkg/api/index.go index e9d784cb652..063e91ef5da 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "strings" "github.com/grafana/grafana/pkg/api/dtos" @@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { locale = parts[0] } + appUrl := setting.AppUrl + appSubUrl := setting.AppSubUrl + + // special case when doing localhost call from phantomjs + if c.IsRenderCall { + appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) + appSubUrl = "" + settings["appSubUrl"] = "" + } + var data = dtos.IndexViewData{ User: &dtos.CurrentUser{ Id: c.UserId, @@ -49,8 +60,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Locale: locale, }, Settings: settings, - AppUrl: setting.AppUrl, - AppSubUrl: setting.AppSubUrl, + AppUrl: appUrl, + AppSubUrl: appSubUrl, GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, diff --git a/pkg/api/render.go b/pkg/api/render.go index 65c1499d0c5..ab794e7ce3e 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -6,35 +6,21 @@ import ( "github.com/grafana/grafana/pkg/components/renderer" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) func RenderToPng(c *middleware.Context) { queryReader := util.NewUrlQueryReader(c.Req.URL) queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) - sessionId := c.Session.ID() - - // Handle api calls authenticated without session - if sessionId == "" && c.ApiKeyId != 0 { - c.Session.Start(c) - c.Session.Set(middleware.SESS_KEY_APIKEY, c.ApiKeyId) - // release will make sure the new session is persisted before - // we spin up phantomjs - c.Session.Release() - // cleanup session after render is complete - defer func() { c.Session.Destory(c) }() - } renderOpts := &renderer.RenderOpts{ - Url: c.Params("*") + queryParams, - Width: queryReader.Get("width", "800"), - Height: queryReader.Get("height", "400"), - SessionId: c.Session.ID(), - Timeout: queryReader.Get("timeout", "30"), + Url: c.Params("*") + queryParams, + Width: queryReader.Get("width", "800"), + Height: queryReader.Get("height", "400"), + OrgId: c.OrgId, + Timeout: queryReader.Get("timeout", "30"), } - renderOpts.Url = setting.ToAbsUrl(renderOpts.Url) pngPath, err := renderer.RenderToPng(renderOpts) if err != nil { diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index ad8f76e03aa..87791bbb1a5 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -12,16 +12,17 @@ import ( "strconv" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) type RenderOpts struct { - Url string - Width string - Height string - SessionId string - Timeout string + Url string + Width string + Height string + Timeout string + OrgId int64 } var rendererLog log.Logger = log.New("png-renderer") @@ -34,14 +35,28 @@ func RenderToPng(params *RenderOpts) (string, error) { executable = executable + ".exe" } + params.Url = fmt.Sprintf("%s://localhost:%s/%s", setting.Protocol, setting.HttpPort, params.Url) + binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable)) scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath = pngPath + ".png" - cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width, - "height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName, - "domain="+setting.Domain, "sessionid="+params.SessionId) + renderKey := middleware.AddRenderAuthKey(params.OrgId) + defer middleware.RemoveRenderAuthKey(renderKey) + + cmdArgs := []string{ + "--ignore-ssl-errors=true", + scriptPath, + "url=" + params.Url, + "width=" + params.Width, + "height=" + params.Height, + "png=" + pngPath, + "domain=" + setting.Domain, + "renderKey=" + renderKey, + } + + cmd := exec.Command(binPath, cmdArgs...) stdout, err := cmd.StdoutPipe() if err != nil { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index df1768e1c3a..cb3f4480821 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -22,6 +22,7 @@ type Context struct { Session SessionStore IsSignedIn bool + IsRenderCall bool AllowAnonymous bool Logger log.Logger } @@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler { // then init session and look for userId in session // then look for api key in session (special case for render calls via api) // then test if anonymous access is enabled - if initContextWithApiKey(ctx) || + if initContextWithRenderAuth(ctx) || + initContextWithApiKey(ctx) || initContextWithBasicAuth(ctx) || initContextWithAuthProxy(ctx) || initContextWithUserSessionCookie(ctx) || - initContextWithApiKeyFromSession(ctx) || initContextWithAnonymousUser(ctx) { } @@ -176,29 +177,6 @@ func initContextWithBasicAuth(ctx *Context) bool { } } -// special case for panel render calls with api key -func initContextWithApiKeyFromSession(ctx *Context) bool { - keyId := ctx.Session.Get(SESS_KEY_APIKEY) - if keyId == nil { - return false - } - - keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)} - if err := bus.Dispatch(&keyQuery); err != nil { - ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err) - return false - } else { - apikey := keyQuery.Result - - ctx.IsSignedIn = true - ctx.SignedInUser = &m.SignedInUser{} - ctx.OrgRole = apikey.Role - ctx.ApiKeyId = apikey.Id - ctx.OrgId = apikey.OrgId - return true - } -} - // Handle handles and logs error by given status. func (ctx *Context) Handle(status int, title string, err error) { if err != nil { diff --git a/pkg/middleware/render_auth.go b/pkg/middleware/render_auth.go new file mode 100644 index 00000000000..3a57660c9bf --- /dev/null +++ b/pkg/middleware/render_auth.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "sync" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" +) + +var renderKeysLock sync.Mutex +var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser) + +func initContextWithRenderAuth(ctx *Context) bool { + key := ctx.GetCookie("renderKey") + if key == "" { + return false + } + + renderKeysLock.Lock() + defer renderKeysLock.Unlock() + + if renderUser, exists := renderKeys[key]; !exists { + ctx.JsonApiErr(401, "Invalid Render Key", nil) + return true + } else { + + ctx.IsSignedIn = true + ctx.SignedInUser = renderUser + ctx.IsRenderCall = true + return true + } +} + +type renderContextFunc func(key string) (string, error) + +func AddRenderAuthKey(orgId int64) string { + renderKeysLock.Lock() + + key := util.GetRandomString(32) + + renderKeys[key] = &m.SignedInUser{ + OrgId: orgId, + OrgRole: m.ROLE_VIEWER, + } + + renderKeysLock.Unlock() + + return key +} + +func RemoveRenderAuthKey(key string) { + renderKeysLock.Lock() + delete(renderKeys, key) + renderKeysLock.Unlock() +} diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 583c57b85a5..ee6462be37a 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -13,7 +13,6 @@ import ( const ( SESS_KEY_USERID = "uid" - SESS_KEY_APIKEY = "apikey_id" // used fror render requests with api keys ) var sessionManager *session.Manager diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 61cc0bc55d3..06828356eaf 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -69,11 +69,11 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error { } renderOpts := &renderer.RenderOpts{ - Url: imageUrl, - Width: "800", - Height: "400", - SessionId: "cef0256d482b4293", - Timeout: "30", + Url: imageUrl, + Width: "800", + Height: "400", + Timeout: "30", + OrgId: context.Rule.OrgId, } if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index fdc2b6cb974..1e620e88216 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -114,6 +114,10 @@ export class BackendSrv { var requestIsLocal = options.url.indexOf('/') === 0; var firstAttempt = options.retry === 0; + if (requestIsLocal && !options.hasSubUrl && options.retry === 0) { + options.url = config.appSubUrl + options.url; + } + if (requestIsLocal && options.headers && options.headers.Authorization) { options.headers['X-DS-Authorization'] = options.headers.Authorization; delete options.headers.Authorization; diff --git a/vendor/phantomjs/render.js b/vendor/phantomjs/render.js index 3e10ee852f9..2f62bfce955 100644 --- a/vendor/phantomjs/render.js +++ b/vendor/phantomjs/render.js @@ -12,17 +12,17 @@ params[parts[1]] = parts[2]; }); - var usage = "url= png= width= height= cookiename= sessionid= domain="; + var usage = "url= png= width= height= renderKey="; - if (!params.url || !params.png || !params.cookiename || ! params.sessionid || !params.domain) { + if (!params.url || !params.png || !params.renderKey || !params.domain) { console.log(usage); phantom.exit(); } phantom.addCookie({ - 'name': params.cookiename, - 'value': params.sessionid, - 'domain': params.domain + 'name': 'renderKey', + 'value': params.renderKey, + 'domain': 'localhost', }); page.viewportSize = { From 501e67a6577577f819bff7db21d58a3090dea5f1 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 23 Sep 2016 08:38:59 -0400 Subject: [PATCH 48/74] use semver when comparing grafana and plugin versions (#6108) --- pkg/plugins/update_checker.go | 18 +++++++++++++++++- public/views/index.html | 8 ++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index 79e6c061e5d..76c566803ac 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" + "github.com/hashicorp/go-version" ) var ( @@ -85,7 +86,15 @@ func checkForUpdates() { for _, gplug := range gNetPlugins { if gplug.Slug == plug.Id { plug.GrafanaNetVersion = gplug.Version - plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion + + plugVersion, err1 := version.NewVersion(plug.Info.Version) + gplugVersion, err2 := version.NewVersion(gplug.Version) + + if err1 != nil || err2 != nil { + plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion + } else { + plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion) + } } } } @@ -117,4 +126,11 @@ func checkForUpdates() { GrafanaLatestVersion = githubLatest.Stable GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion } + + currVersion, err1 := version.NewVersion(setting.BuildVersion) + latestVersion, err2 := version.NewVersion(GrafanaLatestVersion) + + if err1 == nil && err2 == nil { + GrafanaHasUpdate = currVersion.LessThan(latestVersion) + } } diff --git a/public/views/index.html b/public/views/index.html index a54d2c0c166..8d231ed2b68 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -64,13 +64,13 @@ Grafana v[[.BuildVersion]] (commit: [[.BuildCommit]]) -
  • - [[if .NewGrafanaVersionExists]] + [[if .NewGrafanaVersionExists]] +
  • New version available! - [[end]] -
  • + + [[end]]
    From cd270f14a2059d6e4aa0930c2ae96ec9433738d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Sep 2016 17:12:10 +0200 Subject: [PATCH 49/74] feat(graph): more work on graph panel and support for non time series --- public/app/core/directives/metric_segment.js | 14 ++- .../app/plugins/panel/graph/axes_editor.html | 4 +- public/app/plugins/panel/graph/axes_editor.ts | 24 ++-- .../app/plugins/panel/graph/data_processor.ts | 106 ++++++++++++------ .../panel/graph/specs/data_processor_specs.ts | 38 +++++++ .../panel/graph/specs/graph_ctrl_specs.ts | 8 +- 6 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 public/app/plugins/panel/graph/specs/data_processor_specs.ts diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index d51260395de..98921753997 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -170,6 +170,7 @@ function (_, $, coreModule) { }, link: { pre: function postLink($scope, elem, attrs) { + var cachedOptions; $scope.valueToSegment = function(value) { var option = _.find($scope.options, {value: value}); @@ -189,13 +190,20 @@ function (_, $, coreModule) { }); return $q.when(optionSegments); } else { - return $scope.getOptions(); + return $scope.getOptions().then(function(options) { + cachedOptions = options; + return _.map(options, function(option) { + return uiSegmentSrv.newSegment({value: option.text}); + }); + }); } }; $scope.onSegmentChange = function() { - if ($scope.options) { - var option = _.find($scope.options, {text: $scope.segment.value}); + var options = $scope.options || cachedOptions; + + if (options) { + var option = _.find(options, {text: $scope.segment.value}); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index f139e88de5a..74af39aa4ed 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -49,9 +49,9 @@ -
    +
    - +
    diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index c6b60121009..d943d2c2c1f 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -40,6 +40,12 @@ export class AxesEditorCtrl { {text: 'Total', value: 'total'}, {text: 'Count', value: 'count'}, ]; + + if (this.panel.xaxis.mode === 'custom') { + if (!this.panel.xaxis.name) { + this.panel.xaxis.name = 'specify field'; + } + } } setUnitFormat(axis, subItem) { @@ -77,16 +83,14 @@ export class AxesEditorCtrl { } } - getXAxisNameOptions() { - return this.$q.when([ - {text: 'Avg', value: 'avg'} - ]); - } + getDataPropertyNames() { + var props = this.panelCtrl.processor.getDocProperties(this.panelCtrl.dataList); + var items = props.map(prop => { + return {text: prop}; + }); + console.log(items); - getXAxisValueOptions() { - return this.$q.when(this.panelCtrl.processor.getXAxisValueOptions({ - dataList: this.panelCtrl.dataList - })); + return this.$q.when(items); } } @@ -97,7 +101,7 @@ export function axesEditorComponent() { return { restrict: 'E', scope: true, - templateUrl: 'public/app/plugins/panel/graph/tab_axes.html', + templateUrl: 'public/app/plugins/panel/graph/axes_editor.html', controller: AxesEditorCtrl, }; } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 8c3fc927adb..a61e7fa7ffa 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -5,6 +5,8 @@ import _ from 'lodash'; import TimeSeries from 'app/core/time_series2'; import {colors} from 'app/core/core'; + + export class DataProcessor { constructor(private panel) { @@ -64,6 +66,26 @@ export class DataProcessor { customHandler(dataItem) { console.log('custom', dataItem); + let nameField = this.panel.xaxis.name; + if (!nameField) { + throw {message: 'No field name specified to use for x-axis, check your axes settings'}; + } + + // let valueField = this.panel.xaxis.esValueField; + // let datapoints = _.map(seriesData.datapoints, (doc) => { + // return [ + // pluckDeep(doc, valueField), // Y value + // pluckDeep(doc, xField) // X value + // ]; + // }); + // + // // Remove empty points + // datapoints = _.filter(datapoints, (point) => { + // return point[0] !== undefined; + // }); + // + // var alias = valueField; + // re return []; } @@ -120,6 +142,21 @@ export class DataProcessor { } } + getDocProperties(dataList) { + if (dataList.length === 0) { + return []; + } + + var firstItem = dataList[0]; + if (firstItem.type === 'docs'){ + if (firstItem.datapoints.length === 0) { + return []; + } + + return this.getPropertiesFromDoc(firstItem.datapoints[0]); + } + } + getXAxisValueOptions(options) { switch (this.panel.xaxis.mode) { case 'time': { @@ -136,40 +173,41 @@ export class DataProcessor { } } } + + getPropertiesFromDoc(doc) { + let props = []; + let propParts = []; + + function getPropertiesRecursive(obj) { + _.forEach(obj, (value, key) => { + if (_.isObject(value)) { + propParts.push(key); + getPropertiesRecursive(value); + } else { + let field = propParts.concat(key).join('.'); + props.push(field); + } + }); + propParts.pop(); + } + + getPropertiesRecursive(doc); + return props; + } + + pluckDeep(obj: any, property: string) { + let propertyParts = property.split('.'); + let value = obj; + for (let i = 0; i < propertyParts.length; ++i) { + if (value[propertyParts[i]]) { + value = value[propertyParts[i]]; + } else { + return undefined; + } + } + return value; + } + } -// function getFieldsFromESDoc(doc) { -// let fields = []; -// let fieldNameParts = []; -// -// function getFieldsRecursive(obj) { -// _.forEach(obj, (value, key) => { -// if (_.isObject(value)) { -// fieldNameParts.push(key); -// getFieldsRecursive(value); -// } else { -// let field = fieldNameParts.concat(key).join('.'); -// fields.push(field); -// } -// }); -// fieldNameParts.pop(); -// } -// -// getFieldsRecursive(doc); -// return fields; -// } -// -// function pluckDeep(obj: any, property: string) { -// let propertyParts = property.split('.'); -// let value = obj; -// for (let i = 0; i < propertyParts.length; ++i) { -// if (value[propertyParts[i]]) { -// value = value[propertyParts[i]]; -// } else { -// return undefined; -// } -// } -// return value; -// } - diff --git a/public/app/plugins/panel/graph/specs/data_processor_specs.ts b/public/app/plugins/panel/graph/specs/data_processor_specs.ts new file mode 100644 index 00000000000..6d6e28b3629 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/data_processor_specs.ts @@ -0,0 +1,38 @@ +/// + +import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common'; + +import {DataProcessor} from '../data_processor'; + +describe('Graph DataProcessor', function() { + var panel: any = { + xaxis: {} + }; + var processor = new DataProcessor(panel); + var seriesList; + + describe('Given default xaxis options and query that returns docs', () => { + + beforeEach(() => { + panel.xaxis.mode = 'time'; + panel.xaxis.name = 'hostname'; + panel.xaxis.values = []; + + seriesList = processor.getSeriesList({ + dataList: [ + { + type: 'docs', + datapoints: [{hostname: "server1", avg: 10}] + } + ] + }); + }); + + it('Should automatically set xaxis mode to custom', () => { + expect(panel.xaxis.mode).to.be('custom'); + }); + + }); + +}); + diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts index d00c90ae6a1..c7d981f78d4 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts @@ -19,7 +19,7 @@ describe('GraphCtrl', function() { ctx.ctrl.updateTimeRange(); }); - describe('msResolution with second resolution timestamps', function() { + describe.skip('msResolution with second resolution timestamps', function() { beforeEach(function() { var data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]}, @@ -34,7 +34,7 @@ describe('GraphCtrl', function() { }); }); - describe('msResolution with millisecond resolution timestamps', function() { + describe.skip('msResolution with millisecond resolution timestamps', function() { beforeEach(function() { var data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, @@ -49,7 +49,7 @@ describe('GraphCtrl', function() { }); }); - describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { + describe.skip('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { beforeEach(function() { var data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, @@ -64,7 +64,7 @@ describe('GraphCtrl', function() { }); }); - describe('msResolution with millisecond resolution timestamps in one of the series', function() { + describe.skip('msResolution with millisecond resolution timestamps in one of the series', function() { beforeEach(function() { var data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, From 5682520603dfa886e78a1cf4d2caa968334d6b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 24 Sep 2016 13:08:58 +0200 Subject: [PATCH 50/74] feat(graph): more work on graph panel and support for non time series --- public/app/core/directives/metric_segment.js | 1 + .../app/plugins/panel/graph/axes_editor.html | 10 +- public/app/plugins/panel/graph/axes_editor.ts | 34 +---- .../app/plugins/panel/graph/data_processor.ts | 120 ++++++++++++------ public/app/plugins/panel/graph/graph.js | 29 ++--- .../panel/graph/specs/data_processor_specs.ts | 30 ++++- 6 files changed, 134 insertions(+), 90 deletions(-) diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 98921753997..381f9110c65 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -180,6 +180,7 @@ function (_, $, coreModule) { value: option ? option.text : value, selectMode: attrs.selectMode, }; + return uiSegmentSrv.newSegment(segment); }; diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 74af39aa4ed..4c89b5b8b73 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -49,9 +49,15 @@
    -
    +
    - + +
    + + +
    + +
    diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index d943d2c2c1f..591cfba4f17 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -30,7 +30,7 @@ export class AxesEditorCtrl { this.xAxisModes = { 'Time': 'time', 'Series': 'series', - 'Custom': 'custom' + 'Data field': 'field', }; this.xAxisStatOptions = [ @@ -58,37 +58,15 @@ export class AxesEditorCtrl { } xAxisOptionChanged() { - switch (this.panel.xaxis.mode) { - case 'time': { - this.panel.bars = false; - this.panel.lines = true; - this.panel.points = false; - this.panel.legend.show = true; - this.panel.tooltip.shared = true; - this.panel.xaxis.values = []; - this.panelCtrl.onDataReceived(this.panelCtrl.dataList); - break; - } - case 'series': { - this.panel.bars = true; - this.panel.lines = false; - this.panel.points = false; - this.panel.stack = false; - this.panel.legend.show = false; - this.panel.tooltip.shared = false; - this.panelCtrl.processor.validateXAxisSeriesValue(); - this.panelCtrl.onDataReceived(this.panelCtrl.dataList); - break; - } - } + this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode(); + this.panelCtrl.onDataReceived(this.panelCtrl.dataList); } - getDataPropertyNames() { - var props = this.panelCtrl.processor.getDocProperties(this.panelCtrl.dataList); + getDataFieldNames(onlyNumbers) { + var props = this.panelCtrl.processor.getDataFieldNames(this.panelCtrl.dataList, onlyNumbers); var items = props.map(prop => { - return {text: prop}; + return {text: prop, value: prop}; }); - console.log(items); return this.$q.when(items); } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index a61e7fa7ffa..b4ff3968fcb 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -21,22 +21,60 @@ export class DataProcessor { var firstItem; if (options.dataList && options.dataList.length > 0) { firstItem = options.dataList[0]; - if (firstItem.type === 'docs') { - this.panel.xaxis.mode = 'custom'; + let autoDetectMode = this.getAutoDetectXAxisMode(firstItem); + if (this.panel.xaxis.mode !== autoDetectMode) { + this.panel.xaxis.mode = autoDetectMode; + this.setPanelDefaultsForNewXAxisMode(); } } switch (this.panel.xaxis.mode) { case 'series': - case 'time': { + case 'time': { return options.dataList.map(this.timeSeriesHandler.bind(this)); } - case 'custom': { + case 'field': { return this.customHandler(firstItem); } } } + getAutoDetectXAxisMode(firstItem) { + switch (firstItem.type) { + case 'docs': return 'field'; + case 'table': return 'field'; + default: { + if (this.panel.xaxis.mode === 'series') { + return 'series'; + } + return 'time'; + } + } + } + + setPanelDefaultsForNewXAxisMode() { + switch (this.panel.xaxis.mode) { + case 'time': { + this.panel.bars = false; + this.panel.lines = true; + this.panel.points = false; + this.panel.legend.show = true; + this.panel.tooltip.shared = true; + this.panel.xaxis.values = []; + break; + } + case 'series': { + this.panel.bars = true; + this.panel.lines = false; + this.panel.points = false; + this.panel.stack = false; + this.panel.legend.show = false; + this.panel.tooltip.shared = false; + break; + } + } + } + seriesHandler(seriesData, index, datapoints, alias) { var colorIndex = index % colors.length; var color = this.panel.aliasColors[alias] || colors[colorIndex]; @@ -71,21 +109,21 @@ export class DataProcessor { throw {message: 'No field name specified to use for x-axis, check your axes settings'}; } - // let valueField = this.panel.xaxis.esValueField; - // let datapoints = _.map(seriesData.datapoints, (doc) => { - // return [ - // pluckDeep(doc, valueField), // Y value - // pluckDeep(doc, xField) // X value - // ]; - // }); - // - // // Remove empty points - // datapoints = _.filter(datapoints, (point) => { - // return point[0] !== undefined; - // }); - // - // var alias = valueField; - // re + // let valueField = this.panel.xaxis.esValueField; + // let datapoints = _.map(seriesData.datapoints, (doc) => { + // return [ + // pluckDeep(doc, valueField), // Y value + // pluckDeep(doc, xField) // X value + // ]; + // }); + // + // // Remove empty points + // datapoints = _.filter(datapoints, (point) => { + // return point[0] !== undefined; + // }); + // + // var alias = valueField; + // re return []; } @@ -142,18 +180,37 @@ export class DataProcessor { } } - getDocProperties(dataList) { + getDataFieldNames(dataList, onlyNumbers) { if (dataList.length === 0) { return []; } + let fields = []; var firstItem = dataList[0]; if (firstItem.type === 'docs'){ if (firstItem.datapoints.length === 0) { return []; } - return this.getPropertiesFromDoc(firstItem.datapoints[0]); + let fieldParts = []; + + function getPropertiesRecursive(obj) { + _.forEach(obj, (value, key) => { + if (_.isObject(value)) { + fieldParts.push(key); + getPropertiesRecursive(value); + } else { + if (!onlyNumbers || _.isNumber(value)) { + let field = fieldParts.concat(key).join('.'); + fields.push(field); + } + } + }); + fieldParts.pop(); + } + + getPropertiesRecursive(firstItem.datapoints[0]); + return fields; } } @@ -174,27 +231,6 @@ export class DataProcessor { } } - getPropertiesFromDoc(doc) { - let props = []; - let propParts = []; - - function getPropertiesRecursive(obj) { - _.forEach(obj, (value, key) => { - if (_.isObject(value)) { - propParts.push(key); - getPropertiesRecursive(value); - } else { - let field = propParts.concat(key).join('.'); - props.push(field); - } - }); - propParts.pop(); - } - - getPropertiesRecursive(doc); - return props; - } - pluckDeep(obj: any, property: string) { let propertyParts = property.split('.'); let value = obj; diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 08809d95964..2253798be5d 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -258,29 +258,26 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) { } } - if (panel.xaxis.mode === 'series') { - if (data.length) { + switch(panel.xaxis.mode) { + case 'series': { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; + addXSeriesAxis(options); + break; } - - addXSeriesAxis(options); - - } else if (panel.xaxis.mode === 'table' || - panel.xaxis.mode === 'elastic') { - if (data.length) { + case 'table': { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; + addXTableAxis(options); + break; } - - addXTableAxis(options); - - } else { - if (data.length && data[0].stats.timeStep) { - options.series.bars.barWidth = data[0].stats.timeStep / 1.5; + default: { + if (data.length && data[0].stats.timeStep) { + options.series.bars.barWidth = data[0].stats.timeStep / 1.5; + } + addTimeAxis(options); + break; } - - addTimeAxis(options); } thresholdManager.addPlotOptions(options, panel); diff --git a/public/app/plugins/panel/graph/specs/data_processor_specs.ts b/public/app/plugins/panel/graph/specs/data_processor_specs.ts index 6d6e28b3629..bdc1943e9fb 100644 --- a/public/app/plugins/panel/graph/specs/data_processor_specs.ts +++ b/public/app/plugins/panel/graph/specs/data_processor_specs.ts @@ -28,11 +28,37 @@ describe('Graph DataProcessor', function() { }); }); - it('Should automatically set xaxis mode to custom', () => { - expect(panel.xaxis.mode).to.be('custom'); + it('Should automatically set xaxis mode to field', () => { + expect(panel.xaxis.mode).to.be('field'); }); }); + describe('getDataFieldNames(', () => { + var dataList = [{ + type: 'docs', datapoints: [ + { + hostname: "server1", + valueField: 11, + nested: { + prop1: 'server2', value2: 23} + } + ] + }]; + + it('Should return all field names', () => { + var fields = processor.getDataFieldNames(dataList, false); + expect(fields).to.contain('hostname'); + expect(fields).to.contain('valueField'); + expect(fields).to.contain('nested.prop1'); + expect(fields).to.contain('nested.value2'); + }); + + it('Should return all number fields', () => { + var fields = processor.getDataFieldNames(dataList, true); + expect(fields).to.contain('valueField'); + expect(fields).to.contain('nested.value2'); + }); + }); }); From 8cd9225eb69a6fd2e11a4e9dbe583c48f7f21336 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 26 Sep 2016 08:45:55 +0200 Subject: [PATCH 51/74] feat(alerting): increase timeout to 15s --- pkg/services/alerting/eval_handler.go | 2 +- pkg/tsdb/graphite/graphite.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index ab4c377197b..a5599b96d2c 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -20,7 +20,7 @@ type DefaultEvalHandler struct { func NewEvalHandler() *DefaultEvalHandler { return &DefaultEvalHandler{ log: log.New("alerting.evalHandler"), - alertJobTimeout: time.Second * 10, + alertJobTimeout: time.Second * 15, } } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 32e1ab4fa76..b0fdbcadb93 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -38,7 +38,7 @@ func init() { } HttpClient = http.Client{ - Timeout: time.Duration(10 * time.Second), + Timeout: time.Duration(15 * time.Second), Transport: tr, } } From 887e236bce0b86658a1f726d2526e774a4c1cdcd Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 26 Sep 2016 10:01:33 +0200 Subject: [PATCH 52/74] fix(rule): fixes rule reading bug --- pkg/services/alerting/conditions/query.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 15db31838b0..fc5316870bd 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -184,5 +184,6 @@ func validateToValue(to string) error { } } - return fmt.Errorf("cannot parse to value %s", to) + _, err := time.ParseDuration(to) + return err } From effd2098ee86d0af116908bce42a4097cf395840 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 26 Sep 2016 10:51:45 +0200 Subject: [PATCH 53/74] feat(alerting): fixes broken image renderer --- pkg/services/alerting/eval_context.go | 13 ++----------- pkg/services/alerting/notifier.go | 15 ++++++++------- pkg/services/alerting/notifiers/webhook.go | 5 ++--- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 13067c25f08..a76ed8d519f 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -71,7 +71,7 @@ func (c *EvalContext) GetNotificationTitle() string { return "[" + c.GetStateModel().Text + "] " + c.Rule.Name } -func (c *EvalContext) getDashboardSlug() (string, error) { +func (c *EvalContext) GetDashboardSlug() (string, error) { if c.dashboardSlug != "" { return c.dashboardSlug, nil } @@ -86,7 +86,7 @@ func (c *EvalContext) getDashboardSlug() (string, error) { } func (c *EvalContext) GetRuleUrl() (string, error) { - if slug, err := c.getDashboardSlug(); err != nil { + if slug, err := c.GetDashboardSlug(); err != nil { return "", err } else { ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId) @@ -94,15 +94,6 @@ func (c *EvalContext) GetRuleUrl() (string, error) { } } -func (c *EvalContext) GetImageUrl() (string, error) { - if slug, err := c.getDashboardSlug(); err != nil { - return "", err - } else { - ruleUrl := fmt.Sprintf("%sdashboard-solo/db/%s?&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId) - return ruleUrl, nil - } -} - func NewEvalContext(rule *Rule) *EvalContext { return &EvalContext{ StartTime: time.Now(), diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 06828356eaf..68366d1abf3 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -2,6 +2,7 @@ package alerting import ( "errors" + "fmt" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" @@ -60,22 +61,22 @@ func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalCont } } -func (n *RootNotifier) uploadImage(context *EvalContext) error { +func (n *RootNotifier) uploadImage(context *EvalContext) (err error) { uploader, _ := imguploader.NewImageUploader() - imageUrl, err := context.GetImageUrl() - if err != nil { - return err - } - renderOpts := &renderer.RenderOpts{ - Url: imageUrl, Width: "800", Height: "400", Timeout: "30", OrgId: context.Rule.OrgId, } + if slug, err := context.GetDashboardSlug(); err != nil { + return err + } else { + renderOpts.Url = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId) + } + if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { return err } else { diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 7e28b35cd0a..320f273eddc 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -52,9 +52,8 @@ func (this *WebhookNotifier) Notify(context *alerting.EvalContext) { bodyJSON.Set("rule_url", ruleUrl) } - imageUrl, err := context.GetImageUrl() - if err == nil { - bodyJSON.Set("image_url", imageUrl) + if context.ImagePublicUrl != "" { + bodyJSON.Set("image_url", context.ImagePublicUrl) } body, _ := bodyJSON.MarshalJSON() From 1a32ab64b6ff2b1a5e77f8e6f13f50ad6297aebb Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 26 Sep 2016 11:07:36 +0200 Subject: [PATCH 54/74] tech(renderer): improve renderOpts names --- pkg/api/render.go | 2 +- pkg/components/renderer/renderer.go | 8 ++++---- pkg/services/alerting/notifier.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/api/render.go b/pkg/api/render.go index ab794e7ce3e..6018656badb 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -14,7 +14,7 @@ func RenderToPng(c *middleware.Context) { queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) renderOpts := &renderer.RenderOpts{ - Url: c.Params("*") + queryParams, + Path: c.Params("*") + queryParams, Width: queryReader.Get("width", "800"), Height: queryReader.Get("height", "400"), OrgId: c.OrgId, diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index 87791bbb1a5..a55ba5e0ab5 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -18,7 +18,7 @@ import ( ) type RenderOpts struct { - Url string + Path string Width string Height string Timeout string @@ -28,14 +28,14 @@ type RenderOpts struct { var rendererLog log.Logger = log.New("png-renderer") func RenderToPng(params *RenderOpts) (string, error) { - rendererLog.Info("Rendering", "url", params.Url) + rendererLog.Info("Rendering", "path", params.Path) var executable = "phantomjs" if runtime.GOOS == "windows" { executable = executable + ".exe" } - params.Url = fmt.Sprintf("%s://localhost:%s/%s", setting.Protocol, setting.HttpPort, params.Url) + url := fmt.Sprintf("%s://localhost:%s/%s", setting.Protocol, setting.HttpPort, params.Path) binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable)) scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) @@ -48,7 +48,7 @@ func RenderToPng(params *RenderOpts) (string, error) { cmdArgs := []string{ "--ignore-ssl-errors=true", scriptPath, - "url=" + params.Url, + "url=" + url, "width=" + params.Width, "height=" + params.Height, "png=" + pngPath, diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 68366d1abf3..52d4075ae6e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -74,7 +74,7 @@ func (n *RootNotifier) uploadImage(context *EvalContext) (err error) { if slug, err := context.GetDashboardSlug(); err != nil { return err } else { - renderOpts.Url = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId) + renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId) } if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { From acfde82c0a656a952b75bc9c71b4ec0f37498c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 26 Sep 2016 13:55:42 +0200 Subject: [PATCH 55/74] refactor(graph): js -> typescript refactoring --- .../app/plugins/panel/graph/data_processor.ts | 2 - public/app/plugins/panel/graph/graph.js | 601 ----------------- public/app/plugins/panel/graph/graph.ts | 602 ++++++++++++++++++ .../plugins/panel/graph/specs/graph_specs.ts | 282 ++++---- 4 files changed, 743 insertions(+), 744 deletions(-) delete mode 100755 public/app/plugins/panel/graph/graph.js create mode 100755 public/app/plugins/panel/graph/graph.ts diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index b4ff3968fcb..0b80e543e2a 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -5,8 +5,6 @@ import _ from 'lodash'; import TimeSeries from 'app/core/time_series2'; import {colors} from 'app/core/core'; - - export class DataProcessor { constructor(private panel) { diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js deleted file mode 100755 index 2253798be5d..00000000000 --- a/public/app/plugins/panel/graph/graph.js +++ /dev/null @@ -1,601 +0,0 @@ -define([ - 'angular', - 'jquery', - 'moment', - 'lodash', - 'app/core/utils/kbn', - './graph_tooltip', - './threshold_manager', - 'jquery.flot', - 'jquery.flot.selection', - 'jquery.flot.time', - 'jquery.flot.stack', - 'jquery.flot.stackpercent', - 'jquery.flot.fillbelow', - 'jquery.flot.crosshair', - './jquery.flot.events', -], -function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) { - 'use strict'; - - var module = angular.module('grafana.directives'); - var labelWidthCache = {}; - - module.directive('grafanaGraph', function($rootScope, timeSrv) { - return { - restrict: 'A', - template: '
    ', - link: function(scope, elem) { - var ctrl = scope.ctrl; - var dashboard = ctrl.dashboard; - var panel = ctrl.panel; - var data, annotations; - var sortedSeries; - var legendSideLastValue = null; - var rootScope = scope.$root; - var panelWidth = 0; - var thresholdManager = new thresholdManExports.ThresholdManager(ctrl); - - rootScope.onAppEvent('setCrosshair', function(event, info) { - // do not need to to this if event is from this panel - if (info.scope === scope) { - return; - } - - if(dashboard.sharedCrosshair) { - var plot = elem.data().plot; - if (plot) { - plot.setCrosshair({ x: info.pos.x, y: info.pos.y }); - } - } - }, scope); - - rootScope.onAppEvent('clearCrosshair', function() { - var plot = elem.data().plot; - if (plot) { - plot.clearCrosshair(); - } - }, scope); - - // Receive render events - ctrl.events.on('render', function(renderData) { - data = renderData || data; - if (!data) { - return; - } - annotations = data.annotations || annotations; - render_panel(); - }); - - function getLegendHeight(panelHeight) { - if (!panel.legend.show || panel.legend.rightSide) { - return 0; - } - - if (panel.legend.alignAsTable) { - var legendSeries = _.filter(data, function(series) { - return series.hideFromLegend(panel.legend) === false; - }); - var total = 23 + (21 * legendSeries.length); - return Math.min(total, Math.floor(panelHeight/2)); - } else { - return 26; - } - } - - function setElementHeight() { - try { - var height = ctrl.height - getLegendHeight(ctrl.height); - elem.css('height', height + 'px'); - - return true; - } catch(e) { // IE throws errors sometimes - console.log(e); - return false; - } - } - - function shouldAbortRender() { - if (!data) { - return true; - } - - if (!setElementHeight()) { return true; } - - if (panelWidth === 0) { - return true; - } - } - - function getLabelWidth(text, elem) { - var labelWidth = labelWidthCache[text]; - - if (!labelWidth) { - labelWidth = labelWidthCache[text] = elem.width(); - } - - return labelWidth; - } - - function drawHook(plot) { - // Update legend values - var yaxis = plot.getYAxes(); - for (var i = 0; i < data.length; i++) { - var series = data[i]; - var axis = yaxis[series.yaxis - 1]; - var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; - - // decimal override - if (_.isNumber(panel.decimals)) { - series.updateLegendValues(formater, panel.decimals, null); - } else { - // auto decimals - // legend and tooltip gets one more decimal precision - // than graph legend ticks - var tickDecimals = (axis.tickDecimals || -1) + 1; - series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2); - } - - if(!rootScope.$$phase) { scope.$digest(); } - } - - // add left axis labels - if (panel.yaxes[0].label) { - var yaxisLabel = $("
    ") - .text(panel.yaxes[0].label) - .appendTo(elem); - - yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px'; - } - - // add right axis labels - if (panel.yaxes[1].label) { - var rightLabel = $("
    ") - .text(panel.yaxes[1].label) - .appendTo(elem); - - rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px'; - } - - thresholdManager.draw(plot); - } - - function processOffsetHook(plot, gridMargin) { - var left = panel.yaxes[0]; - var right = panel.yaxes[1]; - if (left.show && left.label) { gridMargin.left = 20; } - if (right.show && right.label) { gridMargin.right = 20; } - } - - // Function for rendering panel - function render_panel() { - panelWidth = elem.width(); - - if (shouldAbortRender()) { - return; - } - - // give space to alert editing - thresholdManager.prepare(elem, data); - - var stack = panel.stack ? true : null; - - // Populate element - var options = { - hooks: { - draw: [drawHook], - processOffset: [processOffsetHook], - }, - legend: { show: false }, - series: { - stackpercent: panel.stack ? panel.percentage : false, - stack: panel.percentage ? null : stack, - lines: { - show: panel.lines, - zero: false, - fill: translateFillOption(panel.fill), - lineWidth: panel.linewidth, - steps: panel.steppedLine - }, - bars: { - show: panel.bars, - fill: 1, - barWidth: 1, - zero: false, - lineWidth: 0 - }, - points: { - show: panel.points, - fill: 1, - fillColor: false, - radius: panel.points ? panel.pointradius : 2 - }, - shadowSize: 0 - }, - yaxes: [], - xaxis: {}, - grid: { - minBorderMargin: 0, - markings: [], - backgroundColor: null, - borderWidth: 0, - hoverable: true, - color: '#c8c8c8', - margin: { left: 0, right: 0 }, - }, - selection: { - mode: "x", - color: '#666' - }, - crosshair: { - mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null - } - }; - - for (var i = 0; i < data.length; i++) { - var series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - - if (panel.xaxis.mode === 'series') { - series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; - } else if (panel.xaxis.mode === 'table' || - panel.xaxis.mode === 'elastic') { - series.data = []; - for (var j = 0; j < series.datapoints.length; j++) { - var dataIndex = i * series.datapoints.length + j; - series.datapoints[j]; - series.data.push([ - dataIndex + 1, - series.datapoints[j][0] - ]); - } - } - - // if hidden remove points and disable stack - if (ctrl.hiddenSeries[series.alias]) { - series.data = []; - series.stack = false; - } - } - - switch(panel.xaxis.mode) { - case 'series': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - addXSeriesAxis(options); - break; - } - case 'table': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - addXTableAxis(options); - break; - } - default: { - if (data.length && data[0].stats.timeStep) { - options.series.bars.barWidth = data[0].stats.timeStep / 1.5; - } - addTimeAxis(options); - break; - } - } - - thresholdManager.addPlotOptions(options, panel); - addAnnotations(options); - configureAxisOptions(data, options); - - sortedSeries = _.sortBy(data, function(series) { return series.zindex; }); - - function callPlot(incrementRenderCounter) { - try { - $.plot(elem, sortedSeries, options); - if (ctrl.renderError) { - delete ctrl.error; - delete ctrl.inspector; - } - } catch (e) { - console.log('flotcharts error', e); - ctrl.error = e.message || "Render Error"; - ctrl.renderError = true; - ctrl.inspector = {error: e}; - } - - if (incrementRenderCounter) { - ctrl.renderingCompleted(); - } - } - - if (shouldDelayDraw(panel)) { - // temp fix for legends on the side, need to render twice to get dimensions right - callPlot(false); - setTimeout(function() { callPlot(true); }, 50); - legendSideLastValue = panel.legend.rightSide; - } - else { - callPlot(true); - } - } - - function translateFillOption(fill) { - return fill === 0 ? 0.001 : fill/10; - } - - function shouldDelayDraw(panel) { - if (panel.legend.rightSide) { - return true; - } - if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) { - return true; - } - } - - function addTimeAxis(options) { - var ticks = panelWidth / 100; - var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf(); - var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf(); - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: "time", - min: min, - max: max, - label: "Datetime", - ticks: ticks, - timeformat: time_format(ticks, min, max), - }; - } - - function addXSeriesAxis(options) { - var ticks = _.map(data, function(series, index) { - return [index + 1, series.alias]; - }); - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: null, - min: 0, - max: ticks.length + 1, - label: "Datetime", - ticks: ticks - }; - } - - function addXTableAxis(options) { - var ticks = _.map(data, function(series, seriesIndex) { - return _.map(series.datapoints, function(point, pointIndex) { - var tickIndex = seriesIndex * series.datapoints.length + pointIndex; - return [tickIndex + 1, point[1]]; - }); - }); - ticks = _.flatten(ticks, true); - - options.xaxis = { - timezone: dashboard.getTimezone(), - show: panel.xaxis.show, - mode: null, - min: 0, - max: ticks.length + 1, - label: "Datetime", - ticks: ticks - }; - } - - function addAnnotations(options) { - if(!annotations || annotations.length === 0) { - return; - } - - var types = {}; - for (var i = 0; i < annotations.length; i++) { - var item = annotations[i]; - - if (!types[item.source.name]) { - types[item.source.name] = { - color: item.source.iconColor, - position: 'BOTTOM', - markerSize: 5, - }; - } - } - - options.events = { - levels: _.keys(types).length + 1, - data: annotations, - types: types, - }; - } - - //Override min/max to provide more flexible autoscaling - function autoscaleSpanOverride(yaxis, data, options) { - var expr; - if (yaxis.min != null && data != null) { - expr = parseThresholdExpr(yaxis.min); - options.min = autoscaleYAxisMin(expr, data.stats); - } - if (yaxis.max != null && data != null) { - expr = parseThresholdExpr(yaxis.max); - options.max = autoscaleYAxisMax(expr, data.stats); - } - } - - function parseThresholdExpr(expr) { - var match, operator, value, precision; - expr = String(expr); - match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/); - if (match) { - operator = match[1]; - value = parseFloat(match[2]); - //Precision based on input - precision = match[3] ? match[3].length - 1 : 0; - return { - operator: operator, - value: value, - precision: precision - }; - } else { - return undefined; - } - } - - function autoscaleYAxisMax(expr, dataStats) { - var operator = expr.operator, - value = expr.value, - precision = expr.precision; - if (operator === ">") { - return dataStats.max < value ? value : null; - } else if (operator === "<") { - return dataStats.max > value ? value : null; - } else if (operator === "~") { - return kbn.roundValue(dataStats.avg + value, precision); - } else if (operator === "=") { - return kbn.roundValue(dataStats.current + value, precision); - } else if (!operator && !isNaN(value)) { - return kbn.roundValue(value, precision); - } else { - return null; - } - } - - function autoscaleYAxisMin(expr, dataStats) { - var operator = expr.operator, - value = expr.value, - precision = expr.precision; - if (operator === ">") { - return dataStats.min < value ? value : null; - } else if (operator === "<") { - return dataStats.min > value ? value : null; - } else if (operator === "~") { - return kbn.roundValue(dataStats.avg - value, precision); - } else if (operator === "=") { - return kbn.roundValue(dataStats.current - value, precision); - } else if (!operator && !isNaN(value)) { - return kbn.roundValue(value, precision); - } else { - return null; - } - } - - function configureAxisOptions(data, options) { - var defaults = { - position: 'left', - show: panel.yaxes[0].show, - min: panel.yaxes[0].min, - index: 1, - logBase: panel.yaxes[0].logBase || 1, - max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max, - }; - - autoscaleSpanOverride(panel.yaxes[0], data[0], defaults); - options.yaxes.push(defaults); - - if (_.find(data, {yaxis: 2})) { - var secondY = _.clone(defaults); - secondY.index = 2, - secondY.show = panel.yaxes[1].show; - secondY.logBase = panel.yaxes[1].logBase || 1, - secondY.position = 'right'; - secondY.min = panel.yaxes[1].min; - secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max; - autoscaleSpanOverride(panel.yaxes[1], data[1], secondY); - options.yaxes.push(secondY); - - applyLogScale(options.yaxes[1], data); - configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format); - } - - applyLogScale(options.yaxes[0], data); - configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format); - } - - function applyLogScale(axis, data) { - if (axis.logBase === 1) { - return; - } - - var series, i; - var max = axis.max; - - if (max === null) { - for (i = 0; i < data.length; i++) { - series = data[i]; - if (series.yaxis === axis.index) { - if (max < series.stats.max) { - max = series.stats.max; - } - } - } - if (max === void 0) { - max = Number.MAX_VALUE; - } - } - - axis.min = axis.min !== null ? axis.min : 0; - axis.ticks = [0, 1]; - var nextTick = 1; - - while (true) { - nextTick = nextTick * axis.logBase; - axis.ticks.push(nextTick); - if (nextTick > max) { - break; - } - } - - if (axis.logBase === 10) { - axis.transform = function(v) { return Math.log(v+0.1); }; - axis.inverseTransform = function (v) { return Math.pow(10,v); }; - } else { - axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); }; - axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); }; - } - } - - function configureAxisMode(axis, format) { - axis.tickFormatter = function(val, axis) { - return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); - }; - } - - function time_format(ticks, min, max) { - if (min && max && ticks) { - var range = max - min; - var secPerTick = (range/ticks) / 1000; - var oneDay = 86400000; - var 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"; - } - - new GraphTooltip(elem, dashboard, scope, function() { - return sortedSeries; - }); - - elem.bind("plotselected", function (event, ranges) { - scope.$apply(function() { - timeSrv.setTime({ - from : moment.utc(ranges.xaxis.from), - to : moment.utc(ranges.xaxis.to), - }); - }); - }); - } - }; - }); -}); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts new file mode 100755 index 00000000000..29da2562bf5 --- /dev/null +++ b/public/app/plugins/panel/graph/graph.ts @@ -0,0 +1,602 @@ +/// + +import 'jquery.flot'; +import 'jquery.flot.selection'; +import 'jquery.flot.time'; +import 'jquery.flot.stack'; +import 'jquery.flot.stackpercent'; +import 'jquery.flot.fillbelow'; +import 'jquery.flot.crosshair'; +import './jquery.flot.events'; + +import angular from 'angular'; +import $ from 'jquery'; +import moment from 'moment'; +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; +import GraphTooltip from './graph_tooltip'; +import {ThresholdManager} from './threshold_manager'; + +var module = angular.module('grafana.directives'); +var labelWidthCache = {}; + +module.directive('grafanaGraph', function($rootScope, timeSrv) { + return { + restrict: 'A', + template: '
    ', + link: function(scope, elem) { + var ctrl = scope.ctrl; + var dashboard = ctrl.dashboard; + var panel = ctrl.panel; + var data, annotations; + var sortedSeries; + var legendSideLastValue = null; + var rootScope = scope.$root; + var panelWidth = 0; + var thresholdManager = new ThresholdManager(ctrl); + + rootScope.onAppEvent('setCrosshair', function(event, info) { + // do not need to to this if event is from this panel + if (info.scope === scope) { + return; + } + + if (dashboard.sharedCrosshair) { + var plot = elem.data().plot; + if (plot) { + plot.setCrosshair({ x: info.pos.x, y: info.pos.y }); + } + } + }, scope); + + rootScope.onAppEvent('clearCrosshair', function() { + var plot = elem.data().plot; + if (plot) { + plot.clearCrosshair(); + } + }, scope); + + // Receive render events + ctrl.events.on('render', function(renderData) { + data = renderData || data; + if (!data) { + return; + } + annotations = data.annotations || annotations; + render_panel(); + }); + + function getLegendHeight(panelHeight) { + if (!panel.legend.show || panel.legend.rightSide) { + return 0; + } + + if (panel.legend.alignAsTable) { + var legendSeries = _.filter(data, function(series) { + return series.hideFromLegend(panel.legend) === false; + }); + var total = 23 + (21 * legendSeries.length); + return Math.min(total, Math.floor(panelHeight/2)); + } else { + return 26; + } + } + + function setElementHeight() { + try { + var height = ctrl.height - getLegendHeight(ctrl.height); + elem.css('height', height + 'px'); + + return true; + } catch (e) { // IE throws errors sometimes + console.log(e); + return false; + } + } + + function shouldAbortRender() { + if (!data) { + return true; + } + + if (!setElementHeight()) { return true; } + + if (panelWidth === 0) { + return true; + } + } + + function getLabelWidth(text, elem) { + var labelWidth = labelWidthCache[text]; + + if (!labelWidth) { + labelWidth = labelWidthCache[text] = elem.width(); + } + + return labelWidth; + } + + function drawHook(plot) { + // Update legend values + var yaxis = plot.getYAxes(); + for (var i = 0; i < data.length; i++) { + var series = data[i]; + var axis = yaxis[series.yaxis - 1]; + var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; + + // decimal override + if (_.isNumber(panel.decimals)) { + series.updateLegendValues(formater, panel.decimals, null); + } else { + // auto decimals + // legend and tooltip gets one more decimal precision + // than graph legend ticks + var tickDecimals = (axis.tickDecimals || -1) + 1; + series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2); + } + + if (!rootScope.$$phase) { scope.$digest(); } + } + + // add left axis labels + if (panel.yaxes[0].label) { + var yaxisLabel = $("
    ") + .text(panel.yaxes[0].label) + .appendTo(elem); + + yaxisLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[0].label, yaxisLabel) / 2) + 'px'; + } + + // add right axis labels + if (panel.yaxes[1].label) { + var rightLabel = $("
    ") + .text(panel.yaxes[1].label) + .appendTo(elem); + + rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px'; + } + + thresholdManager.draw(plot); + } + + function processOffsetHook(plot, gridMargin) { + var left = panel.yaxes[0]; + var right = panel.yaxes[1]; + if (left.show && left.label) { gridMargin.left = 20; } + if (right.show && right.label) { gridMargin.right = 20; } + } + + function processDatapoints(plot) { + console.log('processDatapoints'); + } + + // Function for rendering panel + function render_panel() { + panelWidth = elem.width(); + + if (shouldAbortRender()) { + return; + } + + // give space to alert editing + thresholdManager.prepare(elem, data); + + var stack = panel.stack ? true : null; + + // Populate element + var options: any = { + hooks: { + draw: [drawHook], + processOffset: [processOffsetHook], + processDatapoints: [processDatapoints], + }, + legend: { show: false }, + series: { + stackpercent: panel.stack ? panel.percentage : false, + stack: panel.percentage ? null : stack, + lines: { + show: panel.lines, + zero: false, + fill: translateFillOption(panel.fill), + lineWidth: panel.linewidth, + steps: panel.steppedLine + }, + bars: { + show: panel.bars, + fill: 1, + barWidth: 1, + zero: false, + lineWidth: 0 + }, + points: { + show: panel.points, + fill: 1, + fillColor: false, + radius: panel.points ? panel.pointradius : 2 + }, + shadowSize: 0 + }, + yaxes: [], + xaxis: {}, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + hoverable: true, + color: '#c8c8c8', + margin: { left: 0, right: 0 }, + }, + selection: { + mode: "x", + color: '#666' + }, + crosshair: { + mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null + } + }; + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); + + if (panel.xaxis.mode === 'series') { + series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; + } else if (panel.xaxis.mode === 'table' || panel.xaxis.mode === 'elastic') { + series.data = []; + for (var j = 0; j < series.datapoints.length; j++) { + var dataIndex = i * series.datapoints.length + j; + series.datapoints[j]; + series.data.push([ + dataIndex + 1, + series.datapoints[j][0] + ]); + } + } + + // if hidden remove points and disable stack + if (ctrl.hiddenSeries[series.alias]) { + series.data = []; + series.stack = false; + } + } + + switch (panel.xaxis.mode) { + case 'series': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + addXSeriesAxis(options); + break; + } + case 'table': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + addXTableAxis(options); + break; + } + default: { + if (data.length && data[0].stats.timeStep) { + options.series.bars.barWidth = data[0].stats.timeStep / 1.5; + } + addTimeAxis(options); + break; + } + } + + thresholdManager.addPlotOptions(options, panel); + addAnnotations(options); + configureAxisOptions(data, options); + + sortedSeries = _.sortBy(data, function(series) { return series.zindex; }); + + function callPlot(incrementRenderCounter) { + try { + $.plot(elem, sortedSeries, options); + if (ctrl.renderError) { + delete ctrl.error; + delete ctrl.inspector; + } + } catch (e) { + console.log('flotcharts error', e); + ctrl.error = e.message || "Render Error"; + ctrl.renderError = true; + ctrl.inspector = {error: e}; + } + + if (incrementRenderCounter) { + ctrl.renderingCompleted(); + } + } + + if (shouldDelayDraw(panel)) { + // temp fix for legends on the side, need to render twice to get dimensions right + callPlot(false); + setTimeout(function() { callPlot(true); }, 50); + legendSideLastValue = panel.legend.rightSide; + } else { + callPlot(true); + } + } + + function translateFillOption(fill) { + return fill === 0 ? 0.001 : fill/10; + } + + function shouldDelayDraw(panel) { + if (panel.legend.rightSide) { + return true; + } + if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) { + return true; + } + } + + function addTimeAxis(options) { + var ticks = panelWidth / 100; + var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf(); + var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf(); + + options.xaxis = { + timezone: dashboard.getTimezone(), + show: panel.xaxis.show, + mode: "time", + min: min, + max: max, + label: "Datetime", + ticks: ticks, + timeformat: time_format(ticks, min, max), + }; + } + + function addXSeriesAxis(options) { + var ticks = _.map(data, function(series, index) { + return [index + 1, series.alias]; + }); + + options.xaxis = { + timezone: dashboard.getTimezone(), + show: panel.xaxis.show, + mode: null, + min: 0, + max: ticks.length + 1, + label: "Datetime", + ticks: ticks + }; + } + + function addXTableAxis(options) { + var ticks = _.map(data, function(series, seriesIndex) { + return _.map(series.datapoints, function(point, pointIndex) { + var tickIndex = seriesIndex * series.datapoints.length + pointIndex; + return [tickIndex + 1, point[1]]; + }); + }); + ticks = _.flatten(ticks, true); + + options.xaxis = { + timezone: dashboard.getTimezone(), + show: panel.xaxis.show, + mode: null, + min: 0, + max: ticks.length + 1, + label: "Datetime", + ticks: ticks + }; + } + + function addAnnotations(options) { + if (!annotations || annotations.length === 0) { + return; + } + + var types = {}; + for (var i = 0; i < annotations.length; i++) { + var item = annotations[i]; + + if (!types[item.source.name]) { + types[item.source.name] = { + color: item.source.iconColor, + position: 'BOTTOM', + markerSize: 5, + }; + } + } + + options.events = { + levels: _.keys(types).length + 1, + data: annotations, + types: types, + }; + } + + //Override min/max to provide more flexible autoscaling + function autoscaleSpanOverride(yaxis, data, options) { + var expr; + if (yaxis.min != null && data != null) { + expr = parseThresholdExpr(yaxis.min); + options.min = autoscaleYAxisMin(expr, data.stats); + } + if (yaxis.max != null && data != null) { + expr = parseThresholdExpr(yaxis.max); + options.max = autoscaleYAxisMax(expr, data.stats); + } + } + + function parseThresholdExpr(expr) { + var match, operator, value, precision; + expr = String(expr); + match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/); + if (match) { + operator = match[1]; + value = parseFloat(match[2]); + //Precision based on input + precision = match[3] ? match[3].length - 1 : 0; + return { + operator: operator, + value: value, + precision: precision + }; + } else { + return undefined; + } + } + + function autoscaleYAxisMax(expr, dataStats) { + var operator = expr.operator, + value = expr.value, + precision = expr.precision; + if (operator === ">") { + return dataStats.max < value ? value : null; + } else if (operator === "<") { + return dataStats.max > value ? value : null; + } else if (operator === "~") { + return kbn.roundValue(dataStats.avg + value, precision); + } else if (operator === "=") { + return kbn.roundValue(dataStats.current + value, precision); + } else if (!operator && !isNaN(value)) { + return kbn.roundValue(value, precision); + } else { + return null; + } + } + + function autoscaleYAxisMin(expr, dataStats) { + var operator = expr.operator, + value = expr.value, + precision = expr.precision; + if (operator === ">") { + return dataStats.min < value ? value : null; + } else if (operator === "<") { + return dataStats.min > value ? value : null; + } else if (operator === "~") { + return kbn.roundValue(dataStats.avg - value, precision); + } else if (operator === "=") { + return kbn.roundValue(dataStats.current - value, precision); + } else if (!operator && !isNaN(value)) { + return kbn.roundValue(value, precision); + } else { + return null; + } + } + + function configureAxisOptions(data, options) { + var defaults = { + position: 'left', + show: panel.yaxes[0].show, + // min: panel.yaxes[0].min, + index: 1, + logBase: panel.yaxes[0].logBase || 1, + max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max, + }; + + // autoscaleSpanOverride(panel.yaxes[0], data[0], defaults); + options.yaxes.push(defaults); + + if (_.find(data, {yaxis: 2})) { + var secondY = _.clone(defaults); + secondY.index = 2, + secondY.show = panel.yaxes[1].show; + secondY.logBase = panel.yaxes[1].logBase || 1, + secondY.position = 'right'; + // secondY.min = panel.yaxes[1].min; + secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max; + // autoscaleSpanOverride(panel.yaxes[1], data[1], secondY); + options.yaxes.push(secondY); + + applyLogScale(options.yaxes[1], data); + configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format); + } + + applyLogScale(options.yaxes[0], data); + configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format); + } + + function applyLogScale(axis, data) { + if (axis.logBase === 1) { + return; + } + + var series, i; + var max = axis.max; + + if (max === null) { + for (i = 0; i < data.length; i++) { + series = data[i]; + if (series.yaxis === axis.index) { + if (max < series.stats.max) { + max = series.stats.max; + } + } + } + if (max === void 0) { + max = Number.MAX_VALUE; + } + } + + axis.min = axis.min !== null ? axis.min : 0; + axis.ticks = [0, 1]; + var nextTick = 1; + + while (true) { + nextTick = nextTick * axis.logBase; + axis.ticks.push(nextTick); + if (nextTick > max) { + break; + } + } + + if (axis.logBase === 10) { + axis.transform = function(v) { return Math.log(v+0.1); }; + axis.inverseTransform = function (v) { return Math.pow(10,v); }; + } else { + axis.transform = function(v) { return Math.log(v+0.1) / Math.log(axis.logBase); }; + axis.inverseTransform = function (v) { return Math.pow(axis.logBase,v); }; + } + } + + function configureAxisMode(axis, format) { + axis.tickFormatter = function(val, axis) { + return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); + }; + } + + function time_format(ticks, min, max) { + if (min && max && ticks) { + var range = max - min; + var secPerTick = (range/ticks) / 1000; + var oneDay = 86400000; + var 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"; + } + + new GraphTooltip(elem, dashboard, scope, function() { + return sortedSeries; + }); + + elem.bind("plotselected", function (event, ranges) { + scope.$apply(function() { + timeSrv.setTime({ + from : moment.utc(ranges.xaxis.from), + to : moment.utc(ranges.xaxis.to), + }); + }); + }); + } + }; +}); diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 2065bffb130..9f8d91ca9de 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -219,145 +219,145 @@ describe('grafanaGraph', function() { }, 10); - graphScenario('when using flexible Y-Min and Y-Max settings', function(ctx) { - describe('and Y-Min is <100 and Y-Max is >200 and values within range', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '<100'; - ctrl.panel.yaxes[0].max = '>200'; - data[0] = new TimeSeries({ - datapoints: [[120,10],[160,20]], - alias: 'series1', - }); - }); - - it('should set min to 100 and max to 200', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(100); - expect(ctx.plotOptions.yaxes[0].max).to.be(200); - }); - }); - describe('and Y-Min is <100 and Y-Max is >200 and values outside range', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '<100'; - ctrl.panel.yaxes[0].max = '>200'; - data[0] = new TimeSeries({ - datapoints: [[99,10],[201,20]], - alias: 'series1', - }); - }); - - it('should set min to auto and max to auto', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(null); - expect(ctx.plotOptions.yaxes[0].max).to.be(null); - }); - }); - describe('and Y-Min is =10.5 and Y-Max is =10.5', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '=10.5'; - ctrl.panel.yaxes[0].max = '=10.5'; - data[0] = new TimeSeries({ - datapoints: [[100,10],[120,20], [110,30]], - alias: 'series1', - }); - }); - - it('should set min to last value + 10.5 and max to last value + 10.5', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(99.5); - expect(ctx.plotOptions.yaxes[0].max).to.be(120.5); - }); - }); - describe('and Y-Min is ~10.5 and Y-Max is ~10.5', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '~10.5'; - ctrl.panel.yaxes[0].max = '~10.5'; - data[0] = new TimeSeries({ - datapoints: [[102,10],[104,20], [110,30]], //Also checks precision - alias: 'series1', - }); - }); - - it('should set min to average value + 10.5 and max to average value + 10.5', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(94.8); - expect(ctx.plotOptions.yaxes[0].max).to.be(115.8); - }); - }); - }); - graphScenario('when using regular Y-Min and Y-Max settings', function(ctx) { - describe('and Y-Min is 100 and Y-Max is 200', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '100'; - ctrl.panel.yaxes[0].max = '200'; - data[0] = new TimeSeries({ - datapoints: [[120,10],[160,20]], - alias: 'series1', - }); - }); - - it('should set min to 100 and max to 200', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(100); - expect(ctx.plotOptions.yaxes[0].max).to.be(200); - }); - }); - describe('and Y-Min is 0 and Y-Max is 0', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '0'; - ctrl.panel.yaxes[0].max = '0'; - data[0] = new TimeSeries({ - datapoints: [[120,10],[160,20]], - alias: 'series1', - }); - }); - - it('should set min to 0 and max to 0', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(0); - expect(ctx.plotOptions.yaxes[0].max).to.be(0); - }); - }); - describe('and negative values used', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = '-10'; - ctrl.panel.yaxes[0].max = '-13.14'; - data[0] = new TimeSeries({ - datapoints: [[120,10],[160,20]], - alias: 'series1', - }); - }); - - it('should set min and max to negative', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(-10); - expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14); - }); - }); - }); - graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) { - describe('and Y-Min is 0 and Y-Max is 100', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = 0; - ctrl.panel.yaxes[0].max = 100; - data[0] = new TimeSeries({ - datapoints: [[120,10],[160,20]], - alias: 'series1', - }); - }); - - it('should set min to 0 and max to 100', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(0); - expect(ctx.plotOptions.yaxes[0].max).to.be(100); - }); - }); - describe('and Y-Min is -100 and Y-Max is -10.5', function() { - ctx.setup(function(ctrl, data) { - ctrl.panel.yaxes[0].min = -100; - ctrl.panel.yaxes[0].max = -10.5; - data[0] = new TimeSeries({ - datapoints: [[120,10],[160,20]], - alias: 'series1', - }); - }); - - it('should set min to -100 and max to -10.5', function() { - expect(ctx.plotOptions.yaxes[0].min).to.be(-100); - expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5); - }); - }); - }); + // graphScenario('when using flexible Y-Min and Y-Max settings', function(ctx) { + // describe('and Y-Min is <100 and Y-Max is >200 and values within range', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '<100'; + // ctrl.panel.yaxes[0].max = '>200'; + // data[0] = new TimeSeries({ + // datapoints: [[120,10],[160,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to 100 and max to 200', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(100); + // expect(ctx.plotOptions.yaxes[0].max).to.be(200); + // }); + // }); + // describe('and Y-Min is <100 and Y-Max is >200 and values outside range', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '<100'; + // ctrl.panel.yaxes[0].max = '>200'; + // data[0] = new TimeSeries({ + // datapoints: [[99,10],[201,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to auto and max to auto', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(null); + // expect(ctx.plotOptions.yaxes[0].max).to.be(null); + // }); + // }); + // describe('and Y-Min is =10.5 and Y-Max is =10.5', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '=10.5'; + // ctrl.panel.yaxes[0].max = '=10.5'; + // data[0] = new TimeSeries({ + // datapoints: [[100,10],[120,20], [110,30]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to last value + 10.5 and max to last value + 10.5', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(99.5); + // expect(ctx.plotOptions.yaxes[0].max).to.be(120.5); + // }); + // }); + // describe('and Y-Min is ~10.5 and Y-Max is ~10.5', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '~10.5'; + // ctrl.panel.yaxes[0].max = '~10.5'; + // data[0] = new TimeSeries({ + // datapoints: [[102,10],[104,20], [110,30]], //Also checks precision + // alias: 'series1', + // }); + // }); + // + // it('should set min to average value + 10.5 and max to average value + 10.5', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(94.8); + // expect(ctx.plotOptions.yaxes[0].max).to.be(115.8); + // }); + // }); + // }); + // graphScenario('when using regular Y-Min and Y-Max settings', function(ctx) { + // describe('and Y-Min is 100 and Y-Max is 200', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '100'; + // ctrl.panel.yaxes[0].max = '200'; + // data[0] = new TimeSeries({ + // datapoints: [[120,10],[160,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to 100 and max to 200', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(100); + // expect(ctx.plotOptions.yaxes[0].max).to.be(200); + // }); + // }); + // describe('and Y-Min is 0 and Y-Max is 0', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '0'; + // ctrl.panel.yaxes[0].max = '0'; + // data[0] = new TimeSeries({ + // datapoints: [[120,10],[160,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to 0 and max to 0', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(0); + // expect(ctx.plotOptions.yaxes[0].max).to.be(0); + // }); + // }); + // describe('and negative values used', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = '-10'; + // ctrl.panel.yaxes[0].max = '-13.14'; + // data[0] = new TimeSeries({ + // datapoints: [[120,10],[160,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min and max to negative', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(-10); + // expect(ctx.plotOptions.yaxes[0].max).to.be(-13.14); + // }); + // }); + // }); + // graphScenario('when using Y-Min and Y-Max settings stored as number', function(ctx) { + // describe('and Y-Min is 0 and Y-Max is 100', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = 0; + // ctrl.panel.yaxes[0].max = 100; + // data[0] = new TimeSeries({ + // datapoints: [[120,10],[160,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to 0 and max to 100', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(0); + // expect(ctx.plotOptions.yaxes[0].max).to.be(100); + // }); + // }); + // describe('and Y-Min is -100 and Y-Max is -10.5', function() { + // ctx.setup(function(ctrl, data) { + // ctrl.panel.yaxes[0].min = -100; + // ctrl.panel.yaxes[0].max = -10.5; + // data[0] = new TimeSeries({ + // datapoints: [[120,10],[160,20]], + // alias: 'series1', + // }); + // }); + // + // it('should set min to -100 and max to -10.5', function() { + // expect(ctx.plotOptions.yaxes[0].min).to.be(-100); + // expect(ctx.plotOptions.yaxes[0].max).to.be(-10.5); + // }); + // }); + // }); }); From 40e6317b7b7ed3a443226ebc987f911ae9dd2bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 26 Sep 2016 14:04:13 +0200 Subject: [PATCH 56/74] feat(graph): refactoring --- public/app/plugins/panel/graph/axes_editor.ts | 2 +- .../app/plugins/panel/graph/data_processor.ts | 1 + public/app/plugins/panel/graph/graph.ts | 34 +++++++------------ 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 591cfba4f17..502af07ff79 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -30,7 +30,7 @@ export class AxesEditorCtrl { this.xAxisModes = { 'Time': 'time', 'Series': 'series', - 'Data field': 'field', + // 'Data field': 'field', }; this.xAxisStatOptions = [ diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 0b80e543e2a..b22d9f391d0 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -68,6 +68,7 @@ export class DataProcessor { this.panel.stack = false; this.panel.legend.show = false; this.panel.tooltip.shared = false; + this.panel.xaxis.values = ['total']; break; } } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 29da2562bf5..f5f943be715 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -236,24 +236,10 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { } }; - for (var i = 0; i < data.length; i++) { + for (let i = 0; i < data.length; i++) { var series = data[i]; series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - if (panel.xaxis.mode === 'series') { - series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; - } else if (panel.xaxis.mode === 'table' || panel.xaxis.mode === 'elastic') { - series.data = []; - for (var j = 0; j < series.datapoints.length; j++) { - var dataIndex = i * series.datapoints.length + j; - series.datapoints[j]; - series.data.push([ - dataIndex + 1, - series.datapoints[j][0] - ]); - } - } - // if hidden remove points and disable stack if (ctrl.hiddenSeries[series.alias]) { series.data = []; @@ -265,6 +251,12 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { case 'series': { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; + + for (let i = 0; i < data.length; i++) { + var series = data[i]; + series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; + } + addXSeriesAxis(options); break; } @@ -483,7 +475,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { var defaults = { position: 'left', show: panel.yaxes[0].show, - // min: panel.yaxes[0].min, + min: panel.yaxes[0].min, index: 1, logBase: panel.yaxes[0].logBase || 1, max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max, @@ -494,11 +486,11 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { if (_.find(data, {yaxis: 2})) { var secondY = _.clone(defaults); - secondY.index = 2, - secondY.show = panel.yaxes[1].show; - secondY.logBase = panel.yaxes[1].logBase || 1, - secondY.position = 'right'; - // secondY.min = panel.yaxes[1].min; + secondY.index = 2; + secondY.show = panel.yaxes[1].show; + secondY.logBase = panel.yaxes[1].logBase || 1; + secondY.position = 'right'; + secondY.min = panel.yaxes[1].min; secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max; // autoscaleSpanOverride(panel.yaxes[1], data[1], secondY); options.yaxes.push(secondY); From dbb7852f212171297ed5a58c2bdb26a639e4d705 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 23 Sep 2016 16:56:12 +0200 Subject: [PATCH 57/74] feat: purge old files and snapshots closes #4087 closes #2172 --- conf/defaults.ini | 9 +++++ conf/sample.ini | 6 +++ pkg/cmd/grafana-server/main.go | 3 +- pkg/models/timer.go | 7 ++++ .../backgroundtasks/background_tasks.go | 39 +++++++++++++++++++ .../backgroundtasks/remove_tmp_images.go | 38 ++++++++++++++++++ pkg/services/sqlstore/dashboard_snapshot.go | 27 +++++++++++++ pkg/setting/setting.go | 18 ++++++--- 8 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 pkg/models/timer.go create mode 100644 pkg/services/backgroundtasks/background_tasks.go create mode 100644 pkg/services/backgroundtasks/remove_tmp_images.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 49329a0a4ac..750502fb663 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -161,6 +161,12 @@ external_enabled = true external_snapshot_url = https://snapshots-origin.raintank.io external_snapshot_name = Publish to snapshot.raintank.io +# remove expired snapshot +snapshot_remove_expired = true + +# remove snapshots after 90 days +snapshot_TTL_days = 90 + #################################### Users #################################### [users] # disable user signup / registration @@ -267,6 +273,9 @@ from_address = admin@grafana.localhost welcome_email_on_sign_up = false templates_pattern = emails/*.html +[tmp.files] +rendered_image_ttl_days = 14 + #################################### Logging ########################## [log] # Either "console", "file", "syslog". Default is console and file diff --git a/conf/sample.ini b/conf/sample.ini index 2c428ea775f..d6e33153919 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -149,6 +149,12 @@ check_for_updates = true ;external_snapshot_url = https://snapshots-origin.raintank.io ;external_snapshot_name = Publish to snapshot.raintank.io +# remove expired snapshot +;snapshot_remove_expired = true + +# remove snapshots after 90 days +;snapshot_TTL_days = 90 + #################################### Users #################################### [users] # disable user signup / registration diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index d38c1acd894..42c8dfedacf 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/plugins" alertingInit "github.com/grafana/grafana/pkg/services/alerting/init" + "github.com/grafana/grafana/pkg/services/backgroundtasks" "github.com/grafana/grafana/pkg/services/eventpublisher" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/search" @@ -62,13 +63,13 @@ func main() { writePIDFile() initRuntime() metrics.Init() - search.Init() login.Init() social.NewOAuthService() eventpublisher.Init() plugins.Init() alertingInit.Init() + backgroundtasks.Init() if err := notifications.Init(); err != nil { log.Fatal(3, "Notification service failed to initialize", err) diff --git a/pkg/models/timer.go b/pkg/models/timer.go new file mode 100644 index 00000000000..6cbd7ed29d5 --- /dev/null +++ b/pkg/models/timer.go @@ -0,0 +1,7 @@ +package models + +import "time" + +type HourCommand struct { + Time time.Time +} diff --git a/pkg/services/backgroundtasks/background_tasks.go b/pkg/services/backgroundtasks/background_tasks.go new file mode 100644 index 00000000000..5c4a7d197a8 --- /dev/null +++ b/pkg/services/backgroundtasks/background_tasks.go @@ -0,0 +1,39 @@ +//"I want to be a cleaner, just like you," said Mathilda +//"Okay," replied Leon + +package backgroundtasks + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" +) + +var ( + tlog log.Logger = log.New("ticker") +) + +func Init() { + go start() +} + +func start() { + go cleanup(time.Now()) + + ticker := time.NewTicker(time.Hour * 1) + for { + select { + case tick := <-ticker.C: + go cleanup(tick) + } + } +} + +func cleanup(now time.Time) { + err := bus.Publish(&models.HourCommand{Time: now}) + if err != nil { + tlog.Error("Cleanup job failed", "error", err) + } +} diff --git a/pkg/services/backgroundtasks/remove_tmp_images.go b/pkg/services/backgroundtasks/remove_tmp_images.go new file mode 100644 index 00000000000..d6048f09523 --- /dev/null +++ b/pkg/services/backgroundtasks/remove_tmp_images.go @@ -0,0 +1,38 @@ +package backgroundtasks + +import ( + "io/ioutil" + "os" + "path" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +func init() { + bus.AddEventListener(CleanTmpFiles) +} + +func CleanTmpFiles(cmd *models.HourCommand) error { + files, err := ioutil.ReadDir(setting.ImagesDir) + + var toDelete []os.FileInfo + for _, file := range files { + if file.ModTime().AddDate(0, 0, setting.RenderedImageTTLDays).Before(cmd.Time) { + toDelete = append(toDelete, file) + } + } + + for _, file := range toDelete { + fullPath := path.Join(setting.ImagesDir, file.Name()) + err := os.Remove(fullPath) + if err != nil { + return err + } + } + + tlog.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files)) + + return err +} diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index fc94a91cce5..50a7ece05f3 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -5,7 +5,9 @@ import ( "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) func init() { @@ -13,6 +15,31 @@ func init() { bus.AddHandler("sql", GetDashboardSnapshot) bus.AddHandler("sql", DeleteDashboardSnapshot) bus.AddHandler("sql", SearchDashboardSnapshots) + bus.AddEventListener(DeleteExpiredSnapshots) +} + +func DeleteExpiredSnapshots(cmd *m.HourCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var expiredCount int64 = 0 + var oldCount int64 = 0 + + if setting.SnapShotRemoveExpired { + deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?" + expiredResponse, err := x.Exec(deleteExpiredSql, cmd.Time) + if err != nil { + return err + } + expiredCount, _ = expiredResponse.RowsAffected() + } + + oldSnapshotsSql := "DELETE FROM dashboard_snapshot WHERE created < ?" + oldResponse, err := x.Exec(oldSnapshotsSql, cmd.Time.AddDate(0, 0, setting.SnapShotTTLDays*-1)) + oldCount, _ = oldResponse.RowsAffected() + + log.Debug2("Deleted old/expired snaphots", "to old", oldCount, "expired", expiredCount) + + return err + }) } func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 33b026713a1..79e61dd0114 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -78,9 +78,11 @@ var ( DataProxyWhiteList map[string]bool // Snapshots - ExternalSnapshotUrl string - ExternalSnapshotName string - ExternalEnabled bool + ExternalSnapshotUrl string + ExternalSnapshotName string + ExternalEnabled bool + SnapShotTTLDays int + SnapShotRemoveExpired bool // User settings AllowUserSignUp bool @@ -118,8 +120,9 @@ var ( IsWindows bool // PhantomJs Rendering - ImagesDir string - PhantomDir string + ImagesDir string + PhantomDir string + RenderedImageTTLDays int // for logging purposes configFiles []string @@ -495,6 +498,8 @@ func NewConfigContext(args *CommandLineArgs) error { ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String() ExternalSnapshotName = snapshots.Key("external_snapshot_name").String() ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) + SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true) + SnapShotTTLDays = snapshots.Key("snapshot_TTL_days").MustInt(90) // read data source proxy white list DataProxyWhiteList = make(map[string]bool) @@ -535,6 +540,9 @@ func NewConfigContext(args *CommandLineArgs) error { ImagesDir = filepath.Join(DataPath, "png") PhantomDir = filepath.Join(HomePath, "vendor/phantomjs") + tmpFilesSection := Cfg.Section("tmp.files") + RenderedImageTTLDays = tmpFilesSection.Key("rendered_image_ttl_days").MustInt(14) + analytics := Cfg.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) CheckForUpdates = analytics.Key("check_for_updates").MustBool(true) From 7585e9a9309f5017c68e2c72f31949548df4c936 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 26 Sep 2016 14:41:13 +0200 Subject: [PATCH 58/74] docs(changelog): add note about closing #4087 and #2172 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc33b0c0ae..5baddec644d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ * **OAuth**: Add support for generic oauth, closes [#4718](https://github.com/grafana/grafana/pull/4718) * **Cloudwatch**: Add support to expand multi select template variable, closes [#5003](https://github.com/grafana/grafana/pull/5003) * **Graph Panel**: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR [#5720](https://github.com/grafana/grafana/pull/5720) +* **Background Tasks**: Now support automatic purging of old snapshots, closes [#4087](https://github.com/grafana/grafana/issues/4087) +* **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172) ### Breaking changes * **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971) From 6a699d13c415b2a7eeb1eabb4ca99207e13b13ed Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 26 Sep 2016 14:46:03 +0200 Subject: [PATCH 59/74] docs(configuration): add note about snapshot configuration --- docs/sources/installation/configuration.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4c7f63d53ae..d8f29dd7029 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -525,3 +525,9 @@ Set root url to a Grafana instance where you want to publish external snapshots ### external_snapshot_name Set name for external snapshot button. Defaults to `Publish to snapshot.raintank.io` + +### remove expired snapshot +Enabled to automatically remove expired snapshots + +### remove snapshots after 90 days +Time to live for snapshots. From 81cb4a740b775c485cd6c873677ebbe0bb69f813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 26 Sep 2016 17:25:29 +0200 Subject: [PATCH 60/74] refactor(graph): progress on graph panel work --- public/app/plugins/panel/graph/graph.ts | 93 +++---------------------- 1 file changed, 10 insertions(+), 83 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index f5f943be715..36c50f4e703 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -23,7 +23,7 @@ var labelWidthCache = {}; module.directive('grafanaGraph', function($rootScope, timeSrv) { return { restrict: 'A', - template: '
    ', + template: '', link: function(scope, elem) { var ctrl = scope.ctrl; var dashboard = ctrl.dashboard; @@ -164,10 +164,15 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { var right = panel.yaxes[1]; if (left.show && left.label) { gridMargin.left = 20; } if (right.show && right.label) { gridMargin.right = 20; } - } - function processDatapoints(plot) { - console.log('processDatapoints'); + // apply y-axis min/max options + var yaxis = plot.getYAxes(); + for (var i = 0; i < yaxis.length; i++) { + var axis = yaxis[i]; + var panelOptions = panel.yaxes[i]; + axis.options.max = panelOptions.max; + axis.options.min = panelOptions.min; + } } // Function for rendering panel @@ -188,7 +193,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { hooks: { draw: [drawHook], processOffset: [processOffsetHook], - processDatapoints: [processDatapoints], }, legend: { show: false }, series: { @@ -401,87 +405,15 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { }; } - //Override min/max to provide more flexible autoscaling - function autoscaleSpanOverride(yaxis, data, options) { - var expr; - if (yaxis.min != null && data != null) { - expr = parseThresholdExpr(yaxis.min); - options.min = autoscaleYAxisMin(expr, data.stats); - } - if (yaxis.max != null && data != null) { - expr = parseThresholdExpr(yaxis.max); - options.max = autoscaleYAxisMax(expr, data.stats); - } - } - - function parseThresholdExpr(expr) { - var match, operator, value, precision; - expr = String(expr); - match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/); - if (match) { - operator = match[1]; - value = parseFloat(match[2]); - //Precision based on input - precision = match[3] ? match[3].length - 1 : 0; - return { - operator: operator, - value: value, - precision: precision - }; - } else { - return undefined; - } - } - - function autoscaleYAxisMax(expr, dataStats) { - var operator = expr.operator, - value = expr.value, - precision = expr.precision; - if (operator === ">") { - return dataStats.max < value ? value : null; - } else if (operator === "<") { - return dataStats.max > value ? value : null; - } else if (operator === "~") { - return kbn.roundValue(dataStats.avg + value, precision); - } else if (operator === "=") { - return kbn.roundValue(dataStats.current + value, precision); - } else if (!operator && !isNaN(value)) { - return kbn.roundValue(value, precision); - } else { - return null; - } - } - - function autoscaleYAxisMin(expr, dataStats) { - var operator = expr.operator, - value = expr.value, - precision = expr.precision; - if (operator === ">") { - return dataStats.min < value ? value : null; - } else if (operator === "<") { - return dataStats.min > value ? value : null; - } else if (operator === "~") { - return kbn.roundValue(dataStats.avg - value, precision); - } else if (operator === "=") { - return kbn.roundValue(dataStats.current - value, precision); - } else if (!operator && !isNaN(value)) { - return kbn.roundValue(value, precision); - } else { - return null; - } - } - function configureAxisOptions(data, options) { var defaults = { position: 'left', show: panel.yaxes[0].show, - min: panel.yaxes[0].min, index: 1, logBase: panel.yaxes[0].logBase || 1, - max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max, + max: 100, // correct later }; - // autoscaleSpanOverride(panel.yaxes[0], data[0], defaults); options.yaxes.push(defaults); if (_.find(data, {yaxis: 2})) { @@ -490,12 +422,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { secondY.show = panel.yaxes[1].show; secondY.logBase = panel.yaxes[1].logBase || 1; secondY.position = 'right'; - secondY.min = panel.yaxes[1].min; - secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max; - // autoscaleSpanOverride(panel.yaxes[1], data[1], secondY); options.yaxes.push(secondY); - - applyLogScale(options.yaxes[1], data); configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format); } From 46d4f817e3f6e0e7ca39027b155d084d14dd2d8a Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 27 Sep 2016 10:09:56 +0200 Subject: [PATCH 61/74] tech(graphite): return error if statuscode is not ok --- pkg/tsdb/graphite/graphite.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index b0fdbcadb93..5ae2b13b3f2 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -102,9 +102,9 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT return nil, err } - if res.StatusCode == http.StatusUnauthorized { - glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body)) - return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body)) + if res.StatusCode/100 != 200 { + glog.Info("Request failed", "status", res.Status, "body", string(body)) + return nil, fmt.Errorf("Request failed status: %v", res.Status) } var data []TargetResponseDTO From 071f2205e15f74f69ca77946a6d3632224feaece Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 27 Sep 2016 11:13:13 +0200 Subject: [PATCH 62/74] tech(tsdb): ops --- pkg/tsdb/graphite/graphite.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 5ae2b13b3f2..def7039ac91 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -102,7 +102,7 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT return nil, err } - if res.StatusCode/100 != 200 { + if res.StatusCode/100 != 2 { glog.Info("Request failed", "status", res.Status, "body", string(body)) return nil, fmt.Errorf("Request failed status: %v", res.Status) } From 262e7193a324f86c2a55a1b2b9d0b3faaba662e8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 27 Sep 2016 11:16:34 +0200 Subject: [PATCH 63/74] feat(alerting): keep proccessing results even if one response panics --- pkg/services/alerting/engine.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 7abfe32425c..19befe87ed8 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -93,14 +93,18 @@ func (e *Engine) executeJob(job *Job) { } func (e *Engine) resultDispatcher() { + for result := range e.resultQueue { + go e.handleResponse(result) + } +} + +func (e *Engine) handleResponse(result *EvalContext) { defer func() { if err := recover(); err != nil { e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1)) } }() - for result := range e.resultQueue { - e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing) - e.resultHandler.Handle(result) - } + e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing) + e.resultHandler.Handle(result) } From 34f15d92d0befdebd5e78a9e49422e7294aac82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 27 Sep 2016 14:39:51 +0200 Subject: [PATCH 64/74] feat(testdata): worked on testdata app --- pkg/api/dtos/models.go | 7 +- pkg/api/index.go | 2 +- pkg/api/metrics.go | 48 ++++++++----- pkg/plugins/frontend_plugin.go | 7 +- pkg/services/alerting/conditions/query.go | 8 +-- pkg/services/alerting/init/init.go | 1 + pkg/tsdb/models.go | 21 +++--- pkg/tsdb/testdata/testdata.go | 54 ++++++++++++++ pkg/tsdb/time_range.go | 11 ++- pkg/tsdb/time_range_test.go | 17 +++++ public/app/core/time_series2.ts | 3 + public/app/core/utils/kbn.js | 10 ++- .../app/features/panel/metrics_panel_ctrl.ts | 19 ++++- .../features/templating/interval_variable.ts | 4 +- .../testdata/dashboards/graph_last_1h.json | 5 ++ .../app/testdata/datasource/datasource.ts | 45 ++++++++++++ .../plugins/app/testdata/datasource/module.ts | 22 ++++++ .../app/testdata/datasource/plugin.json | 19 +++++ .../app/testdata/datasource/query_ctrl.ts | 24 +++++++ public/app/plugins/app/testdata/module.ts | 36 ++++++++++ .../app/testdata/partials/query.editor.html | 22 ++++++ public/app/plugins/app/testdata/plugin.json | 27 +++++++ .../app/plugins/panel/graph/data_processor.ts | 68 +++++++++--------- .../app/plugins/panel/graph/graph_tooltip.js | 14 ++-- public/app/plugins/panel/graph/module.ts | 16 +++-- .../panel/graph/specs/graph_ctrl_specs.ts | 58 +++++++-------- public/app/plugins/panel/graph/template.ts | 9 ++- .../app/plugins/panel/pluginlist/plugin.json | 2 +- public/test/core/time_series_specs.js | 32 +++++++++ public/test/core/utils/kbn_specs.js | 70 ++++++++++--------- 30 files changed, 512 insertions(+), 169 deletions(-) create mode 100644 pkg/tsdb/testdata/testdata.go create mode 100644 public/app/plugins/app/testdata/dashboards/graph_last_1h.json create mode 100644 public/app/plugins/app/testdata/datasource/datasource.ts create mode 100644 public/app/plugins/app/testdata/datasource/module.ts create mode 100644 public/app/plugins/app/testdata/datasource/plugin.json create mode 100644 public/app/plugins/app/testdata/datasource/query_ctrl.ts create mode 100644 public/app/plugins/app/testdata/module.ts create mode 100644 public/app/plugins/app/testdata/partials/query.editor.html create mode 100644 public/app/plugins/app/testdata/plugin.json diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 8bfc9f9138d..143ee5b98d5 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -97,12 +97,7 @@ func (slice DataSourceList) Swap(i, j int) { } type MetricQueryResultDto struct { - Data []MetricQueryResultDataDto `json:"data"` -} - -type MetricQueryResultDataDto struct { - Target string `json:"target"` - DataPoints [][2]float64 `json:"datapoints"` + Data []interface{} `json:"data"` } type UserStars struct { diff --git a/pkg/api/index.go b/pkg/api/index.go index 063e91ef5da..385810b942e 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -165,7 +165,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } } - if c.OrgRole == m.ROLE_ADMIN { + if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN { appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 154f863af53..c36f2108581 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -2,39 +2,49 @@ package api import ( "encoding/json" - "math/rand" "net/http" - "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/util" ) func GetTestMetrics(c *middleware.Context) Response { - from := c.QueryInt64("from") - to := c.QueryInt64("to") - maxDataPoints := c.QueryInt64("maxDataPoints") - stepInSeconds := (to - from) / maxDataPoints + + timeRange := tsdb.NewTimeRange(c.Query("from"), c.Query("to")) + + req := &tsdb.Request{ + TimeRange: timeRange, + Queries: []*tsdb.Query{ + { + RefId: "A", + MaxDataPoints: c.QueryInt64("maxDataPoints"), + IntervalMs: c.QueryInt64("intervalMs"), + DataSource: &tsdb.DataSourceInfo{ + Name: "Grafana TestDataDB", + PluginId: "grafana-testdata-datasource", + }, + }, + }, + } + + resp, err := tsdb.HandleRequest(req) + if err != nil { + return ApiError(500, "Metric request error", err) + } result := dtos.MetricQueryResultDto{} - result.Data = make([]dtos.MetricQueryResultDataDto, 1) - for seriesIndex := range result.Data { - points := make([][2]float64, maxDataPoints) - walker := rand.Float64() * 100 - time := from - - for i := range points { - points[i][0] = walker - points[i][1] = float64(time) - walker += rand.Float64() - 0.5 - time += stepInSeconds + for _, v := range resp.Results { + if v.Error != nil { + return ApiError(500, "tsdb.HandleRequest() response error", v.Error) } - result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex) - result.Data[seriesIndex].DataPoints = points + for _, series := range v.Series { + result.Data = append(result.Data, series) + } } return Json(200, &result) diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index 974559001d1..8db480f947d 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1) fp.IncludedInAppId = app.Id fp.BaseUrl = app.BaseUrl - fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module" + + if isExternalPlugin(app.PluginDir) { + fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module" + } else { + fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module" + } } func (fp *FrontendPluginBase) handleModuleDefaults() { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 15db31838b0..e808d77a182 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -34,8 +34,8 @@ type AlertQuery struct { } func (c *QueryCondition) Eval(context *alerting.EvalContext) { - timerange := tsdb.NewTimerange(c.Query.From, c.Query.To) - seriesList, err := c.executeQuery(context, timerange) + timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To) + seriesList, err := c.executeQuery(context, timeRange) if err != nil { context.Error = err return @@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { context.Firing = len(context.EvalMatches) > 0 } -func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { +func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ Id: c.Query.DatasourceId, OrgId: context.Rule.OrgId, @@ -79,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timerange t return nil, fmt.Errorf("Could not find datasource") } - req := c.getRequestForAlertRule(getDsInfo.Result, timerange) + req := c.getRequestForAlertRule(getDsInfo.Result, timeRange) result := make(tsdb.TimeSeriesSlice, 0) resp, err := c.HandleRequest(req) diff --git a/pkg/services/alerting/init/init.go b/pkg/services/alerting/init/init.go index b9cba2fd353..94f97a41905 100644 --- a/pkg/services/alerting/init/init.go +++ b/pkg/services/alerting/init/init.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/setting" _ "github.com/grafana/grafana/pkg/tsdb/graphite" _ "github.com/grafana/grafana/pkg/tsdb/prometheus" + _ "github.com/grafana/grafana/pkg/tsdb/testdata" ) var engine *alerting.Engine diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 262be5cbd24..b8c55f1e60d 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -3,21 +3,22 @@ package tsdb import "github.com/grafana/grafana/pkg/components/simplejson" type Query struct { - RefId string - Query string - Model *simplejson.Json - Depends []string - DataSource *DataSourceInfo - Results []*TimeSeries - Exclude bool + RefId string + Query string + Model *simplejson.Json + Depends []string + DataSource *DataSourceInfo + Results []*TimeSeries + Exclude bool + MaxDataPoints int64 + IntervalMs int64 } type QuerySlice []*Query type Request struct { - TimeRange TimeRange - MaxDataPoints int - Queries QuerySlice + TimeRange TimeRange + Queries QuerySlice } type Response struct { diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go new file mode 100644 index 00000000000..b1eca4cb46a --- /dev/null +++ b/pkg/tsdb/testdata/testdata.go @@ -0,0 +1,54 @@ +package testdata + +import ( + "math/rand" + + "github.com/grafana/grafana/pkg/tsdb" +) + +type TestDataExecutor struct { + *tsdb.DataSourceInfo +} + +func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { + return &TestDataExecutor{dsInfo} +} + +func init() { + tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor) +} + +func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { + result := &tsdb.BatchResult{} + result.QueryResults = make(map[string]*tsdb.QueryResult) + + from, _ := context.TimeRange.FromTime() + to, _ := context.TimeRange.ToTime() + + queryRes := &tsdb.QueryResult{} + + for _, query := range queries { + // scenario := query.Model.Get("scenario").MustString("random_walk") + series := &tsdb.TimeSeries{Name: "test-series-0"} + + stepInSeconds := (to.Unix() - from.Unix()) / query.MaxDataPoints + points := make([][2]*float64, 0) + walker := rand.Float64() * 100 + time := from.Unix() + + for i := int64(0); i < query.MaxDataPoints; i++ { + timestamp := float64(time) + val := float64(walker) + points = append(points, [2]*float64{&val, ×tamp}) + + walker += rand.Float64() - 0.5 + time += stepInSeconds + } + + series.Points = points + queryRes.Series = append(queryRes.Series, series) + } + + result.QueryResults["A"] = queryRes + return result +} diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index 8e1a1c66e3d..dee3e683516 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -2,11 +2,12 @@ package tsdb import ( "fmt" + "strconv" "strings" "time" ) -func NewTimerange(from, to string) TimeRange { +func NewTimeRange(from, to string) TimeRange { return TimeRange{ From: from, To: to, @@ -21,6 +22,10 @@ type TimeRange struct { } func (tr TimeRange) FromTime() (time.Time, error) { + if val, err := strconv.ParseInt(tr.From, 10, 64); err == nil { + return time.Unix(val, 0), nil + } + fromRaw := strings.Replace(tr.From, "now-", "", 1) diff, err := time.ParseDuration("-" + fromRaw) @@ -45,5 +50,9 @@ func (tr TimeRange) ToTime() (time.Time, error) { return tr.Now.Add(diff), nil } + if val, err := strconv.ParseInt(tr.To, 10, 64); err == nil { + return time.Unix(val, 0), nil + } + return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To) } diff --git a/pkg/tsdb/time_range_test.go b/pkg/tsdb/time_range_test.go index 56ea9d24490..f4acb5e6d80 100644 --- a/pkg/tsdb/time_range_test.go +++ b/pkg/tsdb/time_range_test.go @@ -60,6 +60,23 @@ func TestTimeRange(t *testing.T) { }) }) + Convey("can parse unix epocs", func() { + var err error + tr := TimeRange{ + From: "1474973725473", + To: "1474975757930", + Now: now, + } + + res, err := tr.FromTime() + So(err, ShouldBeNil) + So(res.Unix(), ShouldEqual, 1474973725473) + + res, err = tr.ToTime() + So(err, ShouldBeNil) + So(res.Unix(), ShouldEqual, 1474975757930) + }) + Convey("Cannot parse asdf", func() { var err error tr := TimeRange{ diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index dfae26fb48b..d672e0dd0dc 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -31,6 +31,8 @@ export default class TimeSeries { allIsZero: boolean; decimals: number; scaledDecimals: number; + hasMsResolution: boolean; + isOutsideRange: boolean; lines: any; bars: any; @@ -54,6 +56,7 @@ export default class TimeSeries { this.stats = {}; this.legend = true; this.unit = opts.unit; + this.hasMsResolution = this.isMsResolutionNeeded(); } applySeriesOverrides(overrides) { diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index cf80d671d71..a807a249235 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -174,7 +174,10 @@ function($, _, moment) { lowLimitMs = kbn.interval_to_ms(lowLimitInterval); } else { - return userInterval; + return { + intervalMs: kbn.interval_to_ms(userInterval), + interval: userInterval, + }; } } @@ -183,7 +186,10 @@ function($, _, moment) { intervalMs = lowLimitMs; } - return kbn.secondsToHms(intervalMs / 1000); + return { + intervalMs: intervalMs, + interval: kbn.secondsToHms(intervalMs / 1000), + }; }; kbn.describe_interval = function (string) { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 62cece44acf..f6f2d730cd3 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -25,6 +25,7 @@ class MetricsPanelCtrl extends PanelCtrl { range: any; rangeRaw: any; interval: any; + intervalMs: any; resolution: any; timeInfo: any; skipDataOnInit: boolean; @@ -123,11 +124,22 @@ class MetricsPanelCtrl extends PanelCtrl { this.resolution = Math.ceil($(window).width() * (this.panel.span / 12)); } - var panelInterval = this.panel.interval; - var datasourceInterval = (this.datasource || {}).interval; - this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval); + this.calculateInterval(); }; + calculateInterval() { + var intervalOverride = this.panel.interval; + + // if no panel interval check datasource + if (!intervalOverride && this.datasource && this.datasource.interval) { + intervalOverride = this.datasource.interval; + } + + var res = kbn.calculateInterval(this.range, this.resolution, intervalOverride); + this.interval = res.interval; + this.intervalMs = res.intervalMs; + } + applyPanelTimeOverrides() { this.timeInfo = ''; @@ -183,6 +195,7 @@ class MetricsPanelCtrl extends PanelCtrl { range: this.range, rangeRaw: this.rangeRaw, interval: this.interval, + intervalMs: this.intervalMs, targets: this.panel.targets, format: this.panel.renderer === 'png' ? 'png' : 'json', maxDataPoints: this.resolution, diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index 30b056e1c60..a1cfbf324c0 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -54,8 +54,8 @@ export class IntervalVariable implements Variable { this.options.unshift({ text: 'auto', value: '$__auto_interval' }); } - var interval = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null)); - this.templateSrv.setGrafanaVariable('$__auto_interval', interval); + var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null)); + this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval); } updateOptions() { diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json new file mode 100644 index 00000000000..7533a44760b --- /dev/null +++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json @@ -0,0 +1,5 @@ +{ + "title": "TestData - Graph Panel Last 1h", + "tags": ["testdata"], + "revision": 1 +} diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/app/testdata/datasource/datasource.ts new file mode 100644 index 00000000000..75b43bd34c9 --- /dev/null +++ b/public/app/plugins/app/testdata/datasource/datasource.ts @@ -0,0 +1,45 @@ +/// + +import _ from 'lodash'; + +class TestDataDatasource { + + /** @ngInject */ + constructor(private backendSrv, private $q) {} + + query(options) { + var queries = _.filter(options.targets, item => { + return item.hide !== true; + }); + + if (queries.length === 0) { + return this.$q.when({data: []}); + } + + return this.backendSrv.get('/api/metrics/test', { + from: options.range.from.valueOf(), + to: options.range.to.valueOf(), + scenario: options.targets[0].scenario, + interval: options.intervalMs, + maxDataPoints: options.maxDataPoints, + }).then(res => { + res.data = res.data.map(item => { + return {target: item.name, datapoints: item.points}; + }); + + return res; + }); + } + + annotationQuery(options) { + return this.backendSrv.get('/api/annotations', { + from: options.range.from.valueOf(), + to: options.range.to.valueOf(), + limit: options.limit, + type: options.type, + }); + } + +} + +export {TestDataDatasource}; diff --git a/public/app/plugins/app/testdata/datasource/module.ts b/public/app/plugins/app/testdata/datasource/module.ts new file mode 100644 index 00000000000..309b7443836 --- /dev/null +++ b/public/app/plugins/app/testdata/datasource/module.ts @@ -0,0 +1,22 @@ +/// + +import {TestDataDatasource} from './datasource'; +import {TestDataQueryCtrl} from './query_ctrl'; + +class TestDataAnnotationsQueryCtrl { + annotation: any; + + constructor() { + } + + static template = '

    test data

    '; +} + + +export { + TestDataDatasource, + TestDataDatasource as Datasource, + TestDataQueryCtrl as QueryCtrl, + TestDataAnnotationsQueryCtrl as AnnotationsQueryCtrl, +}; + diff --git a/public/app/plugins/app/testdata/datasource/plugin.json b/public/app/plugins/app/testdata/datasource/plugin.json new file mode 100644 index 00000000000..0ad87a15081 --- /dev/null +++ b/public/app/plugins/app/testdata/datasource/plugin.json @@ -0,0 +1,19 @@ +{ + "type": "datasource", + "name": "Grafana TestDataDB", + "id": "grafana-testdata-datasource", + + "metrics": true, + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "", + "large": "" + } + } +} diff --git a/public/app/plugins/app/testdata/datasource/query_ctrl.ts b/public/app/plugins/app/testdata/datasource/query_ctrl.ts new file mode 100644 index 00000000000..44a62fd1a11 --- /dev/null +++ b/public/app/plugins/app/testdata/datasource/query_ctrl.ts @@ -0,0 +1,24 @@ +/// + +import {TestDataDatasource} from './datasource'; +import {QueryCtrl} from 'app/plugins/sdk'; + +export class TestDataQueryCtrl extends QueryCtrl { + static templateUrl = 'partials/query.editor.html'; + + scenarioDefs: any; + + /** @ngInject **/ + constructor($scope, $injector) { + super($scope, $injector); + + this.target.scenario = this.target.scenario || 'random_walk'; + + this.scenarioDefs = { + 'random_walk': {text: 'Random Walk'}, + 'no_datapoints': {text: 'No Datapoints'}, + 'data_outside_range': {text: 'Data Outside Range'}, + }; + } +} + diff --git a/public/app/plugins/app/testdata/module.ts b/public/app/plugins/app/testdata/module.ts new file mode 100644 index 00000000000..dee1679637a --- /dev/null +++ b/public/app/plugins/app/testdata/module.ts @@ -0,0 +1,36 @@ +/// + +export class ConfigCtrl { + static template = ''; + + appEditCtrl: any; + + constructor(private backendSrv) { + this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this)); + } + + initDatasource() { + return this.backendSrv.get('/api/datasources').then(res => { + var found = false; + for (let ds of res) { + if (ds.type === "grafana-testdata-datasource") { + found = true; + } + } + + if (!found) { + var dsInstance = { + name: 'Grafana TestData', + type: 'grafana-testdata-datasource', + access: 'direct', + jsonData: {} + }; + + return this.backendSrv.post('/api/datasources', dsInstance); + } + + return Promise.resolve(); + }); + } +} + diff --git a/public/app/plugins/app/testdata/partials/query.editor.html b/public/app/plugins/app/testdata/partials/query.editor.html new file mode 100644 index 00000000000..d9068dfda49 --- /dev/null +++ b/public/app/plugins/app/testdata/partials/query.editor.html @@ -0,0 +1,22 @@ + +
    +
    + +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    + diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json new file mode 100644 index 00000000000..47ab291409b --- /dev/null +++ b/public/app/plugins/app/testdata/plugin.json @@ -0,0 +1,27 @@ +{ + "type": "app", + "name": "Grafana TestData", + "id": "testdata", + + "info": { + "description": "Grafana test data app", + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "version": "1.0.5", + "updated": "2016-09-26" + }, + + "includes": [ + { + "type": "dashboard", + "name": "TestData - Graph Last 1h", + "path": "dashboards/graph_last_1h.json" + } + ], + + "dependencies": { + "grafanaVersion": "4.x.x" + } +} diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index b22d9f391d0..404b5a90aee 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -2,6 +2,7 @@ import kbn from 'app/core/utils/kbn'; import _ from 'lodash'; +import moment from 'moment'; import TimeSeries from 'app/core/time_series2'; import {colors} from 'app/core/core'; @@ -28,8 +29,10 @@ export class DataProcessor { switch (this.panel.xaxis.mode) { case 'series': - case 'time': { - return options.dataList.map(this.timeSeriesHandler.bind(this)); + case 'time': { + return options.dataList.map((item, index) => { + return this.timeSeriesHandler(item, index, options); + }); } case 'field': { return this.customHandler(firstItem); @@ -74,33 +77,26 @@ export class DataProcessor { } } - seriesHandler(seriesData, index, datapoints, alias) { + timeSeriesHandler(seriesData, index, options) { + var datapoints = seriesData.datapoints; + var alias = seriesData.target; + var colorIndex = index % colors.length; var color = this.panel.aliasColors[alias] || colors[colorIndex]; var series = new TimeSeries({datapoints: datapoints, alias: alias, color: color, unit: seriesData.unit}); - // if (datapoints && datapoints.length > 0) { - // var last = moment.utc(datapoints[datapoints.length - 1][1]); - // var from = moment.utc(this.range.from); - // if (last - from < -10000) { - // this.datapointsOutside = true; - // } - // - // this.datapointsCount += datapoints.length; - // this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); - // } + if (datapoints && datapoints.length > 0) { + var last = datapoints[datapoints.length - 1][1]; + var from = options.range.from; + if (last - from < -10000) { + series.isOutsideRange = true; + } + } return series; } - timeSeriesHandler(seriesData, index) { - var datapoints = seriesData.datapoints; - var alias = seriesData.target; - - return this.seriesHandler(seriesData, index, datapoints, alias); - } - customHandler(dataItem) { console.log('custom', dataItem); let nameField = this.panel.xaxis.name; @@ -126,21 +122,21 @@ export class DataProcessor { return []; } - tableHandler(seriesData, index) { - var xColumnIndex = Number(this.panel.xaxis.columnIndex); - var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); - var datapoints = _.map(seriesData.rows, (row) => { - var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); - return [ - value, // Y value - row[xColumnIndex] // X value - ]; - }); - - var alias = seriesData.columns[valueColumnIndex].text; - - return this.seriesHandler(seriesData, index, datapoints, alias); - } + // tableHandler(seriesData, index) { + // var xColumnIndex = Number(this.panel.xaxis.columnIndex); + // var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); + // var datapoints = _.map(seriesData.rows, (row) => { + // var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); + // return [ + // value, // Y value + // row[xColumnIndex] // X value + // ]; + // }); + // + // var alias = seriesData.columns[valueColumnIndex].text; + // + // return this.seriesHandler(seriesData, index, datapoints, alias); + // } // esRawDocHandler(seriesData, index) { // let xField = this.panel.xaxis.esField; @@ -160,7 +156,7 @@ export class DataProcessor { // var alias = valueField; // return this.seriesHandler(seriesData, index, datapoints, alias); // } - // + validateXAxisSeriesValue() { switch (this.panel.xaxis.mode) { case 'series': { diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 70eef7c5fe3..cd3bddf41ef 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -121,20 +121,20 @@ function ($, _) { var seriesList = getSeriesFn(); var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; - if (panel.tooltip.msResolution) { - tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS'; - } else { - tooltipFormat = 'YYYY-MM-DD HH:mm:ss'; - } - if (dashboard.sharedCrosshair) { - ctrl.publishAppEvent('setCrosshair', { pos: pos, scope: scope }); + ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope}); } if (seriesList.length === 0) { return; } + if (seriesList[0].hasMsResolution) { + tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS'; + } else { + tooltipFormat = 'YYYY-MM-DD HH:mm:ss'; + } + if (panel.tooltip.shared) { plot.unhighlight(); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 9edcb7fe1ff..f6d77636845 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -25,7 +25,6 @@ class GraphCtrl extends MetricsPanelCtrl { annotationsPromise: any; datapointsCount: number; datapointsOutside: boolean; - datapointsWarning: boolean; colors: any = []; subTabIndex: number; processor: DataProcessor; @@ -172,13 +171,20 @@ class GraphCtrl extends MetricsPanelCtrl { } onDataReceived(dataList) { - this.datapointsWarning = false; - this.datapointsCount = 0; - this.datapointsOutside = false; this.dataList = dataList; this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range}); - this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside; + + this.datapointsCount = this.seriesList.reduce((prev, series) => { + return prev + series.datapoints.length; + }, 0); + + this.datapointsOutside = false; + for (let series of this.seriesList) { + if (series.isOutsideRange) { + this.datapointsOutside = true; + } + } this.annotationsPromise.then(annotations => { this.loading = false; diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts index c7d981f78d4..cac69807eab 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts @@ -3,6 +3,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common'; import angular from 'angular'; +import moment from 'moment'; import {GraphCtrl} from '../module'; import helpers from '../../../../../test/specs/helpers'; @@ -19,64 +20,53 @@ describe('GraphCtrl', function() { ctx.ctrl.updateTimeRange(); }); - describe.skip('msResolution with second resolution timestamps', function() { + describe('when time series are outside range', function() { + beforeEach(function() { var data = [ - { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]}, - { target: 'test.cpu2', datapoints: [[55, 1236547890], [90, 1234456709]]} + {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]}, ]; - ctx.ctrl.panel.tooltip.msResolution = false; + + ctx.ctrl.range = {from: moment().valueOf(), to: moment().valueOf()}; ctx.ctrl.onDataReceived(data); }); - it('should not show millisecond resolution tooltip', function() { - expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false); + it('should set datapointsOutside', function() { + expect(ctx.ctrl.datapointsOutside).to.be(true); }); }); - describe.skip('msResolution with millisecond resolution timestamps', function() { + describe('when time series are inside range', function() { beforeEach(function() { + var range = { + from: moment().subtract(1, 'days').valueOf(), + to: moment().valueOf() + }; + var data = [ - { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, - { target: 'test.cpu2', datapoints: [[55, 1236547890001], [90, 1234456709000]]} + {target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]]}, ]; - ctx.ctrl.panel.tooltip.msResolution = false; + + ctx.ctrl.range = range; ctx.ctrl.onDataReceived(data); }); - it('should show millisecond resolution tooltip', function() { - expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true); + it('should set datapointsOutside', function() { + expect(ctx.ctrl.datapointsOutside).to.be(false); }); }); - describe.skip('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { + describe('datapointsCount given 2 series', function() { beforeEach(function() { var data = [ - { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, - { target: 'test.cpu2', datapoints: [[55, 1236547890000], [90, 1234456709000]]} + {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]}, + {target: 'test.cpu2', datapoints: [[45, 1234567890]]}, ]; - ctx.ctrl.panel.tooltip.msResolution = false; ctx.ctrl.onDataReceived(data); }); - it('should not show millisecond resolution tooltip', function() { - expect(ctx.ctrl.panel.tooltip.msResolution).to.be(false); - }); - }); - - describe.skip('msResolution with millisecond resolution timestamps in one of the series', function() { - beforeEach(function() { - var data = [ - { target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]}, - { target: 'test.cpu2', datapoints: [[55, 1236547890010], [90, 1234456709000]]}, - { target: 'test.cpu3', datapoints: [[65, 1236547890000], [120, 1234456709000]]} - ]; - ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.onDataReceived(data); - }); - - it('should show millisecond resolution tooltip', function() { - expect(ctx.ctrl.panel.tooltip.msResolution).to.be(true); + it('should set datapointsCount to sum of datapoints', function() { + expect(ctx.ctrl.datapointsCount).to.be(3); }); }); diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts index fc989e659c7..ec6cd8d0907 100644 --- a/public/app/plugins/panel/graph/template.ts +++ b/public/app/plugins/panel/graph/template.ts @@ -2,11 +2,14 @@ var template = `
    -
    - +
    + No datapoints No datapoints returned from metric query - +
    + +
    + Datapoints outside time range Can be caused by timezone mismatch between browser and graphite server diff --git a/public/app/plugins/panel/pluginlist/plugin.json b/public/app/plugins/panel/pluginlist/plugin.json index be6ae9a5985..72f5ea06d25 100644 --- a/public/app/plugins/panel/pluginlist/plugin.json +++ b/public/app/plugins/panel/pluginlist/plugin.json @@ -7,7 +7,7 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" -}, + }, "logos": { "small": "img/icn-dashlist-panel.svg", "large": "img/icn-dashlist-panel.svg" diff --git a/public/test/core/time_series_specs.js b/public/test/core/time_series_specs.js index 034e872e2f1..2b325cf6d46 100644 --- a/public/test/core/time_series_specs.js +++ b/public/test/core/time_series_specs.js @@ -56,6 +56,38 @@ define([ }); }); + describe('When checking if ms resolution is needed', function() { + describe('msResolution with second resolution timestamps', function() { + beforeEach(function() { + series = new TimeSeries({datapoints: [[45, 1234567890], [60, 1234567899]]}); + }); + + it('should set hasMsResolution to false', function() { + expect(series.hasMsResolution).to.be(false); + }); + }); + + describe('msResolution with millisecond resolution timestamps', function() { + beforeEach(function() { + series = new TimeSeries({datapoints: [[55, 1236547890001], [90, 1234456709000]]}); + }); + + it('should show millisecond resolution tooltip', function() { + expect(series.hasMsResolution).to.be(true); + }); + }); + + describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { + beforeEach(function() { + series = new TimeSeries({datapoints: [[45, 1234567890000], [60, 1234567899000]]}); + }); + + it('should not show millisecond resolution tooltip', function() { + expect(series.hasMsResolution).to.be(false); + }); + }); + }); + describe('can detect if series contains ms precision', function() { var fakedata; diff --git a/public/test/core/utils/kbn_specs.js b/public/test/core/utils/kbn_specs.js index 959b176b06c..95bef57ef1a 100644 --- a/public/test/core/utils/kbn_specs.js +++ b/public/test/core/utils/kbn_specs.js @@ -132,62 +132,64 @@ define([ describe('calculateInterval', function() { it('1h 100 resultion', function() { var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; - var str = kbn.calculateInterval(range, 100, null); - expect(str).to.be('30s'); + var res = kbn.calculateInterval(range, 100, null); + expect(res.interval).to.be('30s'); }); it('10m 1600 resolution', function() { var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - var str = kbn.calculateInterval(range, 1600, null); - expect(str).to.be('500ms'); + var res = kbn.calculateInterval(range, 1600, null); + expect(res.interval).to.be('500ms'); + expect(res.intervalMs).to.be(500); }); it('fixed user interval', function() { var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - var str = kbn.calculateInterval(range, 1600, '10s'); - expect(str).to.be('10s'); + var res = kbn.calculateInterval(range, 1600, '10s'); + expect(res.interval).to.be('10s'); + expect(res.intervalMs).to.be(10000); }); it('short time range and user low limit', function() { var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - var str = kbn.calculateInterval(range, 1600, '>10s'); - expect(str).to.be('10s'); + var res = kbn.calculateInterval(range, 1600, '>10s'); + expect(res.interval).to.be('10s'); }); it('large time range and user low limit', function() { - var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; - var str = kbn.calculateInterval(range, 1000, '>10s'); - expect(str).to.be('20m'); + var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')}; + var res = kbn.calculateInterval(range, 1000, '>10s'); + expect(res.interval).to.be('20m'); }); - + it('10s 900 resolution and user low limit in ms', function() { var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; - var str = kbn.calculateInterval(range, 900, '>15ms'); - expect(str).to.be('15ms'); + var res = kbn.calculateInterval(range, 900, '>15ms'); + expect(res.interval).to.be('15ms'); }); }); describe('hex', function() { - it('positive integer', function() { - var str = kbn.valueFormats.hex(100, 0); - expect(str).to.be('64'); - }); - it('negative integer', function() { - var str = kbn.valueFormats.hex(-100, 0); - expect(str).to.be('-64'); - }); - it('null', function() { - var str = kbn.valueFormats.hex(null, 0); - expect(str).to.be(''); - }); - it('positive float', function() { - var str = kbn.valueFormats.hex(50.52, 1); - expect(str).to.be('32.8'); - }); - it('negative float', function() { - var str = kbn.valueFormats.hex(-50.333, 2); - expect(str).to.be('-32.547AE147AE14'); - }); + it('positive integer', function() { + var str = kbn.valueFormats.hex(100, 0); + expect(str).to.be('64'); + }); + it('negative integer', function() { + var str = kbn.valueFormats.hex(-100, 0); + expect(str).to.be('-64'); + }); + it('null', function() { + var str = kbn.valueFormats.hex(null, 0); + expect(str).to.be(''); + }); + it('positive float', function() { + var str = kbn.valueFormats.hex(50.52, 1); + expect(str).to.be('32.8'); + }); + it('negative float', function() { + var str = kbn.valueFormats.hex(-50.333, 2); + expect(str).to.be('-32.547AE147AE14'); + }); }); describe('hex 0x', function() { From ade8aa5b923318f7a446e80230d96542bb758b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 27 Sep 2016 14:47:04 +0200 Subject: [PATCH 65/74] feat(graph): refactorings --- pkg/tsdb/models.go | 4 +- .../app/testdata/datasource/datasource.ts | 6 --- .../plugins/datasource/grafana/datasource.ts | 2 + .../app/plugins/panel/graph/data_processor.ts | 52 ------------------- 4 files changed, 4 insertions(+), 60 deletions(-) diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index b8c55f1e60d..0060f459d7b 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -56,8 +56,8 @@ type QueryResult struct { } type TimeSeries struct { - Name string `json:"name"` - Points [][2]*float64 `json:"points"` + Name string `json:"target"` + Points [][2]*float64 `json:"datapoints"` } type TimeSeriesSlice []*TimeSeries diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/app/testdata/datasource/datasource.ts index 75b43bd34c9..32edfb59755 100644 --- a/public/app/plugins/app/testdata/datasource/datasource.ts +++ b/public/app/plugins/app/testdata/datasource/datasource.ts @@ -22,12 +22,6 @@ class TestDataDatasource { scenario: options.targets[0].scenario, interval: options.intervalMs, maxDataPoints: options.maxDataPoints, - }).then(res => { - res.data = res.data.map(item => { - return {target: item.name, datapoints: item.points}; - }); - - return res; }); } diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 58799778acd..3ae030e4423 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -9,6 +9,8 @@ class GrafanaDatasource { return this.backendSrv.get('/api/metrics/test', { from: options.range.from.valueOf(), to: options.range.to.valueOf(), + scenario: 'random_walk', + interval: options.intervalMs, maxDataPoints: options.maxDataPoints }); } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 404b5a90aee..bb8af84cf00 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -98,65 +98,13 @@ export class DataProcessor { } customHandler(dataItem) { - console.log('custom', dataItem); let nameField = this.panel.xaxis.name; if (!nameField) { throw {message: 'No field name specified to use for x-axis, check your axes settings'}; } - - // let valueField = this.panel.xaxis.esValueField; - // let datapoints = _.map(seriesData.datapoints, (doc) => { - // return [ - // pluckDeep(doc, valueField), // Y value - // pluckDeep(doc, xField) // X value - // ]; - // }); - // - // // Remove empty points - // datapoints = _.filter(datapoints, (point) => { - // return point[0] !== undefined; - // }); - // - // var alias = valueField; - // re return []; } - // tableHandler(seriesData, index) { - // var xColumnIndex = Number(this.panel.xaxis.columnIndex); - // var valueColumnIndex = Number(this.panel.xaxis.valueColumnIndex); - // var datapoints = _.map(seriesData.rows, (row) => { - // var value = valueColumnIndex ? row[valueColumnIndex] : _.last(row); - // return [ - // value, // Y value - // row[xColumnIndex] // X value - // ]; - // }); - // - // var alias = seriesData.columns[valueColumnIndex].text; - // - // return this.seriesHandler(seriesData, index, datapoints, alias); - // } - - // esRawDocHandler(seriesData, index) { - // let xField = this.panel.xaxis.esField; - // let valueField = this.panel.xaxis.esValueField; - // let datapoints = _.map(seriesData.datapoints, (doc) => { - // return [ - // pluckDeep(doc, valueField), // Y value - // pluckDeep(doc, xField) // X value - // ]; - // }); - // - // // Remove empty points - // datapoints = _.filter(datapoints, (point) => { - // return point[0] !== undefined; - // }); - // - // var alias = valueField; - // return this.seriesHandler(seriesData, index, datapoints, alias); - // } - validateXAxisSeriesValue() { switch (this.panel.xaxis.mode) { case 'series': { From 3ecd96e68225c3b8ae8cdae10adec1c2f3c3b00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 27 Sep 2016 18:17:39 +0200 Subject: [PATCH 66/74] feat(testdata): lots of work on new test data data source and scenarios --- pkg/api/api.go | 3 +- pkg/api/dtos/models.go | 6 +- pkg/api/metrics.go | 50 +++++----- pkg/services/alerting/conditions/query.go | 6 +- pkg/tsdb/models.go | 17 ++-- pkg/tsdb/prometheus/prometheus.go | 6 +- pkg/tsdb/query_context.go | 4 +- pkg/tsdb/testdata/scenarios.go | 98 +++++++++++++++++++ pkg/tsdb/testdata/testdata.go | 39 +++----- pkg/tsdb/time_range.go | 42 ++++++-- pkg/tsdb/time_range_test.go | 20 ++-- pkg/tsdb/tsdb_test.go | 30 +++--- .../app/testdata/datasource/datasource.ts | 32 ++++-- .../app/testdata/datasource/query_ctrl.ts | 17 ++-- .../app/testdata/partials/query.editor.html | 8 +- .../app/plugins/panel/graph/data_processor.ts | 2 +- 16 files changed, 257 insertions(+), 123 deletions(-) create mode 100644 pkg/tsdb/testdata/scenarios.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 38e299d19c8..bac3db429d2 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -244,7 +244,8 @@ func Register(r *macaron.Macaron) { r.Get("/search/", Search) // metrics - r.Get("/metrics/test", wrap(GetTestMetrics)) + r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics)) + r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios)) // metrics r.Get("/metrics", wrap(GetInternalMetrics)) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 143ee5b98d5..170a5a868fc 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -96,8 +96,10 @@ func (slice DataSourceList) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } -type MetricQueryResultDto struct { - Data []interface{} `json:"data"` +type MetricRequest struct { + From string `json:"from"` + To string `json:"to"` + Queries []*simplejson.Json `json:"queries"` } type UserStars struct { diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index c36f2108581..c3bd9062737 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -8,43 +8,47 @@ import ( "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/tsdb" + "github.com/grafana/grafana/pkg/tsdb/testdata" "github.com/grafana/grafana/pkg/util" ) -func GetTestMetrics(c *middleware.Context) Response { +// POST /api/tsdb/query +func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { + timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To) - timeRange := tsdb.NewTimeRange(c.Query("from"), c.Query("to")) + request := &tsdb.Request{TimeRange: timeRange} - req := &tsdb.Request{ - TimeRange: timeRange, - Queries: []*tsdb.Query{ - { - RefId: "A", - MaxDataPoints: c.QueryInt64("maxDataPoints"), - IntervalMs: c.QueryInt64("intervalMs"), - DataSource: &tsdb.DataSourceInfo{ - Name: "Grafana TestDataDB", - PluginId: "grafana-testdata-datasource", - }, + for _, query := range reqDto.Queries { + request.Queries = append(request.Queries, &tsdb.Query{ + RefId: query.Get("refId").MustString("A"), + MaxDataPoints: query.Get("maxDataPoints").MustInt64(100), + IntervalMs: query.Get("intervalMs").MustInt64(1000), + Model: query, + DataSource: &tsdb.DataSourceInfo{ + Name: "Grafana TestDataDB", + PluginId: "grafana-testdata-datasource", }, - }, + }) } - resp, err := tsdb.HandleRequest(req) + resp, err := tsdb.HandleRequest(request) if err != nil { return ApiError(500, "Metric request error", err) } - result := dtos.MetricQueryResultDto{} + return Json(200, &resp) +} - for _, v := range resp.Results { - if v.Error != nil { - return ApiError(500, "tsdb.HandleRequest() response error", v.Error) - } +// GET /api/tsdb/testdata/scenarios +func GetTestDataScenarios(c *middleware.Context) Response { + result := make([]interface{}, 0) - for _, series := range v.Series { - result.Data = append(result.Data, series) - } + for _, scenario := range testdata.ScenarioRegistry { + result = append(result, map[string]interface{}{ + "id": scenario.Id, + "name": scenario.Name, + "description": scenario.Description, + }) } return Json(200, &result) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index e808d77a182..d32c42f27b0 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -69,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { context.Firing = len(context.EvalMatches) > 0 } -func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { +func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ Id: c.Query.DatasourceId, OrgId: context.Rule.OrgId, @@ -105,9 +105,9 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange t return result, nil } -func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timerange tsdb.TimeRange) *tsdb.Request { +func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request { req := &tsdb.Request{ - TimeRange: timerange, + TimeRange: timeRange, Queries: []*tsdb.Query{ { RefId: "A", diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 0060f459d7b..fc66a47f981 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -4,7 +4,6 @@ import "github.com/grafana/grafana/pkg/components/simplejson" type Query struct { RefId string - Query string Model *simplejson.Json Depends []string DataSource *DataSourceInfo @@ -17,13 +16,13 @@ type Query struct { type QuerySlice []*Query type Request struct { - TimeRange TimeRange + TimeRange *TimeRange Queries QuerySlice } type Response struct { - BatchTimings []*BatchTiming - Results map[string]*QueryResult + BatchTimings []*BatchTiming `json:"timings"` + Results map[string]*QueryResult `json:"results"` } type DataSourceInfo struct { @@ -50,14 +49,14 @@ type BatchResult struct { } type QueryResult struct { - Error error - RefId string - Series TimeSeriesSlice + Error error `json:"error"` + RefId string `json:"refId"` + Series TimeSeriesSlice `json:"series"` } type TimeSeries struct { - Name string `json:"target"` - Points [][2]*float64 `json:"datapoints"` + Name string `json:"name"` + Points [][2]*float64 `json:"points"` } type TimeSeriesSlice []*TimeSeries diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 1df51bd1ffe..4d6ab03cede 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" "github.com/prometheus/client_golang/api/prometheus" - "golang.org/x/net/context" pmodel "github.com/prometheus/common/model" + "golang.org/x/net/context" ) type PrometheusExecutor struct { @@ -111,12 +111,12 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom return nil, err } - start, err := queryContext.TimeRange.FromTime() + start, err := queryContext.TimeRange.ParseFrom() if err != nil { return nil, err } - end, err := queryContext.TimeRange.ToTime() + end, err := queryContext.TimeRange.ParseTo() if err != nil { return nil, err } diff --git a/pkg/tsdb/query_context.go b/pkg/tsdb/query_context.go index a1fc4c9bcb5..db40ba6253c 100644 --- a/pkg/tsdb/query_context.go +++ b/pkg/tsdb/query_context.go @@ -3,7 +3,7 @@ package tsdb import "sync" type QueryContext struct { - TimeRange TimeRange + TimeRange *TimeRange Queries QuerySlice Results map[string]*QueryResult ResultsChan chan *BatchResult @@ -11,7 +11,7 @@ type QueryContext struct { BatchWaits sync.WaitGroup } -func NewQueryContext(queries QuerySlice, timeRange TimeRange) *QueryContext { +func NewQueryContext(queries QuerySlice, timeRange *TimeRange) *QueryContext { return &QueryContext{ TimeRange: timeRange, Queries: queries, diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go new file mode 100644 index 00000000000..d54cbb05638 --- /dev/null +++ b/pkg/tsdb/testdata/scenarios.go @@ -0,0 +1,98 @@ +package testdata + +import ( + "math/rand" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/tsdb" +) + +type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult + +type Scenario struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Handler ScenarioHandler `json:"-"` +} + +var ScenarioRegistry map[string]*Scenario + +func init() { + ScenarioRegistry = make(map[string]*Scenario) + logger := log.New("tsdb.testdata") + + registerScenario(&Scenario{ + Id: "random_walk", + Name: "Random Walk", + + Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + timeWalkerMs := context.TimeRange.MustGetFrom().Unix() * 1000 + to := context.TimeRange.MustGetTo().Unix() * 1000 + + series := newSeriesForQuery(query) + + points := make([][2]*float64, 0) + walker := rand.Float64() * 100 + + for i := int64(0); i < 10000 && timeWalkerMs < to; i++ { + timestamp := float64(timeWalkerMs) + val := float64(walker) + points = append(points, [2]*float64{&val, ×tamp}) + + walker += rand.Float64() - 0.5 + timeWalkerMs += query.IntervalMs + } + + series.Points = points + + queryRes := &tsdb.QueryResult{} + queryRes.Series = append(queryRes.Series, series) + return queryRes + }, + }) + + registerScenario(&Scenario{ + Id: "no_data_points", + Name: "No Data Points", + Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + return &tsdb.QueryResult{ + Series: make(tsdb.TimeSeriesSlice, 0), + } + }, + }) + + registerScenario(&Scenario{ + Id: "datapoints_outside_range", + Name: "Datapoints Outside Range", + Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + queryRes := &tsdb.QueryResult{} + + series := newSeriesForQuery(query) + outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000 + + timestamp := float64(outsideTime) + logger.Info("time", "from", timestamp) + val := float64(10) + + series.Points = append(series.Points, [2]*float64{&val, ×tamp}) + queryRes.Series = append(queryRes.Series, series) + return queryRes + }, + }) + +} + +func registerScenario(scenario *Scenario) { + ScenarioRegistry[scenario.Id] = scenario +} + +func newSeriesForQuery(query *tsdb.Query) *tsdb.TimeSeries { + alias := query.Model.Get("alias").MustString("") + if alias == "" { + alias = query.RefId + "-series" + } + + return &tsdb.TimeSeries{Name: alias} +} diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go index b1eca4cb46a..5b40bb6de5a 100644 --- a/pkg/tsdb/testdata/testdata.go +++ b/pkg/tsdb/testdata/testdata.go @@ -1,17 +1,20 @@ package testdata import ( - "math/rand" - + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" ) type TestDataExecutor struct { *tsdb.DataSourceInfo + log log.Logger } func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { - return &TestDataExecutor{dsInfo} + return &TestDataExecutor{ + DataSourceInfo: dsInfo, + log: log.New("tsdb.testdata"), + } } func init() { @@ -22,33 +25,15 @@ func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result := &tsdb.BatchResult{} result.QueryResults = make(map[string]*tsdb.QueryResult) - from, _ := context.TimeRange.FromTime() - to, _ := context.TimeRange.ToTime() - - queryRes := &tsdb.QueryResult{} - for _, query := range queries { - // scenario := query.Model.Get("scenario").MustString("random_walk") - series := &tsdb.TimeSeries{Name: "test-series-0"} - - stepInSeconds := (to.Unix() - from.Unix()) / query.MaxDataPoints - points := make([][2]*float64, 0) - walker := rand.Float64() * 100 - time := from.Unix() - - for i := int64(0); i < query.MaxDataPoints; i++ { - timestamp := float64(time) - val := float64(walker) - points = append(points, [2]*float64{&val, ×tamp}) - - walker += rand.Float64() - 0.5 - time += stepInSeconds + scenarioId := query.Model.Get("scenarioId").MustString("random_walk") + if scenario, exist := ScenarioRegistry[scenarioId]; exist { + result.QueryResults[query.RefId] = scenario.Handler(query, context) + result.QueryResults[query.RefId].RefId = query.RefId + } else { + e.log.Error("Scenario not found", "scenarioId", scenarioId) } - - series.Points = points - queryRes.Series = append(queryRes.Series, series) } - result.QueryResults["A"] = queryRes return result } diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index dee3e683516..67227b834dc 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -7,8 +7,8 @@ import ( "time" ) -func NewTimeRange(from, to string) TimeRange { - return TimeRange{ +func NewTimeRange(from, to string) *TimeRange { + return &TimeRange{ From: from, To: to, Now: time.Now(), @@ -21,13 +21,37 @@ type TimeRange struct { Now time.Time } -func (tr TimeRange) FromTime() (time.Time, error) { - if val, err := strconv.ParseInt(tr.From, 10, 64); err == nil { - return time.Unix(val, 0), nil +func (tr *TimeRange) MustGetFrom() time.Time { + if res, err := tr.ParseFrom(); err != nil { + return time.Unix(0, 0) + } else { + return res + } +} + +func (tr *TimeRange) MustGetTo() time.Time { + if res, err := tr.ParseTo(); err != nil { + return time.Unix(0, 0) + } else { + return res + } +} + +func tryParseUnixMsEpoch(val string) (time.Time, bool) { + if val, err := strconv.ParseInt(val, 10, 64); err == nil { + seconds := val / 1000 + nano := (val - seconds*1000) * 1000000 + return time.Unix(seconds, nano), true + } + return time.Time{}, false +} + +func (tr *TimeRange) ParseFrom() (time.Time, error) { + if res, ok := tryParseUnixMsEpoch(tr.From); ok { + return res, nil } fromRaw := strings.Replace(tr.From, "now-", "", 1) - diff, err := time.ParseDuration("-" + fromRaw) if err != nil { return time.Time{}, err @@ -36,7 +60,7 @@ func (tr TimeRange) FromTime() (time.Time, error) { return tr.Now.Add(diff), nil } -func (tr TimeRange) ToTime() (time.Time, error) { +func (tr *TimeRange) ParseTo() (time.Time, error) { if tr.To == "now" { return tr.Now, nil } else if strings.HasPrefix(tr.To, "now-") { @@ -50,8 +74,8 @@ func (tr TimeRange) ToTime() (time.Time, error) { return tr.Now.Add(diff), nil } - if val, err := strconv.ParseInt(tr.To, 10, 64); err == nil { - return time.Unix(val, 0), nil + if res, ok := tryParseUnixMsEpoch(tr.To); ok { + return res, nil } return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To) diff --git a/pkg/tsdb/time_range_test.go b/pkg/tsdb/time_range_test.go index f4acb5e6d80..5412d0d05f3 100644 --- a/pkg/tsdb/time_range_test.go +++ b/pkg/tsdb/time_range_test.go @@ -23,13 +23,13 @@ func TestTimeRange(t *testing.T) { fiveMinAgo, _ := time.ParseDuration("-5m") expected := now.Add(fiveMinAgo) - res, err := tr.FromTime() + res, err := tr.ParseFrom() So(err, ShouldBeNil) So(res.Unix(), ShouldEqual, expected.Unix()) }) Convey("now ", func() { - res, err := tr.ToTime() + res, err := tr.ParseTo() So(err, ShouldBeNil) So(res.Unix(), ShouldEqual, now.Unix()) }) @@ -46,7 +46,7 @@ func TestTimeRange(t *testing.T) { fiveHourAgo, _ := time.ParseDuration("-5h") expected := now.Add(fiveHourAgo) - res, err := tr.FromTime() + res, err := tr.ParseFrom() So(err, ShouldBeNil) So(res.Unix(), ShouldEqual, expected.Unix()) }) @@ -54,7 +54,7 @@ func TestTimeRange(t *testing.T) { Convey("now-10m ", func() { fiveMinAgo, _ := time.ParseDuration("-10m") expected := now.Add(fiveMinAgo) - res, err := tr.ToTime() + res, err := tr.ParseTo() So(err, ShouldBeNil) So(res.Unix(), ShouldEqual, expected.Unix()) }) @@ -68,13 +68,13 @@ func TestTimeRange(t *testing.T) { Now: now, } - res, err := tr.FromTime() + res, err := tr.ParseFrom() So(err, ShouldBeNil) - So(res.Unix(), ShouldEqual, 1474973725473) + So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474973725473) - res, err = tr.ToTime() + res, err = tr.ParseTo() So(err, ShouldBeNil) - So(res.Unix(), ShouldEqual, 1474975757930) + So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474975757930) }) Convey("Cannot parse asdf", func() { @@ -85,10 +85,10 @@ func TestTimeRange(t *testing.T) { Now: now, } - _, err = tr.FromTime() + _, err = tr.ParseFrom() So(err, ShouldNotBeNil) - _, err = tr.ToTime() + _, err = tr.ParseTo() So(err, ShouldNotBeNil) }) }) diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go index 24d84a27c74..429dd01d6ba 100644 --- a/pkg/tsdb/tsdb_test.go +++ b/pkg/tsdb/tsdb_test.go @@ -14,9 +14,9 @@ func TestMetricQuery(t *testing.T) { Convey("Given 3 queries for 2 data sources", func() { request := &Request{ Queries: QuerySlice{ - {RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}}, - {RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1}}, - {RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2}}, + {RefId: "A", DataSource: &DataSourceInfo{Id: 1}}, + {RefId: "B", DataSource: &DataSourceInfo{Id: 1}}, + {RefId: "C", DataSource: &DataSourceInfo{Id: 2}}, }, } @@ -31,9 +31,9 @@ func TestMetricQuery(t *testing.T) { Convey("Given query 2 depends on query 1", func() { request := &Request{ Queries: QuerySlice{ - {RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}}, - {RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 2}}, - {RefId: "C", Query: "#A / #B", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}}, + {RefId: "A", DataSource: &DataSourceInfo{Id: 1}}, + {RefId: "B", DataSource: &DataSourceInfo{Id: 2}}, + {RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}}, }, } @@ -55,7 +55,7 @@ func TestMetricQuery(t *testing.T) { Convey("When executing request with one query", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, }, } @@ -74,8 +74,8 @@ func TestMetricQuery(t *testing.T) { Convey("When executing one request with two queries from same data source", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, - {RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, }, } @@ -100,9 +100,9 @@ func TestMetricQuery(t *testing.T) { Convey("When executing one request with three queries from different datasources", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, - {RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, - {RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}}, + {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}}, }, } @@ -117,7 +117,7 @@ func TestMetricQuery(t *testing.T) { Convey("When query uses data source of unknown type", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}}, + {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}}, }, } @@ -129,10 +129,10 @@ func TestMetricQuery(t *testing.T) { req := &Request{ Queries: QuerySlice{ { - RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}, + RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}, }, { - RefId: "B", Query: "#A / 2", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"}, + RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"}, }, }, } diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/app/testdata/datasource/datasource.ts index 32edfb59755..a06daa19262 100644 --- a/public/app/plugins/app/testdata/datasource/datasource.ts +++ b/public/app/plugins/app/testdata/datasource/datasource.ts @@ -10,18 +10,38 @@ class TestDataDatasource { query(options) { var queries = _.filter(options.targets, item => { return item.hide !== true; + }).map(item => { + return { + refId: item.refId, + scenarioId: item.scenarioId, + intervalMs: options.intervalMs, + maxDataPoints: options.maxDataPoints, + }; }); if (queries.length === 0) { return this.$q.when({data: []}); } - return this.backendSrv.get('/api/metrics/test', { - from: options.range.from.valueOf(), - to: options.range.to.valueOf(), - scenario: options.targets[0].scenario, - interval: options.intervalMs, - maxDataPoints: options.maxDataPoints, + return this.backendSrv.post('/api/tsdb/query', { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries: queries, + }).then(res => { + var data = []; + + if (res.results) { + _.forEach(res.results, queryRes => { + for (let series of queryRes.series) { + data.push({ + target: series.name, + datapoints: series.points + }); + } + }); + } + + return {data: data}; }); } diff --git a/public/app/plugins/app/testdata/datasource/query_ctrl.ts b/public/app/plugins/app/testdata/datasource/query_ctrl.ts index 44a62fd1a11..f22ef8948ad 100644 --- a/public/app/plugins/app/testdata/datasource/query_ctrl.ts +++ b/public/app/plugins/app/testdata/datasource/query_ctrl.ts @@ -6,19 +6,20 @@ import {QueryCtrl} from 'app/plugins/sdk'; export class TestDataQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; - scenarioDefs: any; + scenarioList: any; /** @ngInject **/ - constructor($scope, $injector) { + constructor($scope, $injector, private backendSrv) { super($scope, $injector); - this.target.scenario = this.target.scenario || 'random_walk'; + this.target.scenarioId = this.target.scenarioId || 'random_walk'; + this.scenarioList = []; + } - this.scenarioDefs = { - 'random_walk': {text: 'Random Walk'}, - 'no_datapoints': {text: 'No Datapoints'}, - 'data_outside_range': {text: 'Data Outside Range'}, - }; + $onInit() { + return this.backendSrv.get('/api/tsdb/testdata/scenarios').then(res => { + this.scenarioList = res; + }); } } diff --git a/public/app/plugins/app/testdata/partials/query.editor.html b/public/app/plugins/app/testdata/partials/query.editor.html index d9068dfda49..53f84309661 100644 --- a/public/app/plugins/app/testdata/partials/query.editor.html +++ b/public/app/plugins/app/testdata/partials/query.editor.html @@ -2,17 +2,17 @@
    -
    - +
    +
    - +
    - +
    diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index bb8af84cf00..6233ac345c9 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -78,7 +78,7 @@ export class DataProcessor { } timeSeriesHandler(seriesData, index, options) { - var datapoints = seriesData.datapoints; + var datapoints = seriesData.datapoints || []; var alias = seriesData.target; var colorIndex = index % colors.length; From 22e8885690bb93b6b10bf371fcf5d0afad8accde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 27 Sep 2016 18:36:00 +0200 Subject: [PATCH 67/74] feat(testdata): progress on test data stuff --- pkg/plugins/datasource_plugin.go | 1 + .../app/features/alerting/alert_tab_ctrl.ts | 4 +- .../app/features/dashboard/dashboard_srv.ts | 2 + .../testdata/dashboards/graph_last_1h.json | 290 +++++++++++++++++- .../app/testdata/datasource/plugin.json | 1 + .../plugins/datasource/graphite/plugin.json | 3 +- .../plugins/datasource/prometheus/plugin.json | 1 + 7 files changed, 297 insertions(+), 5 deletions(-) diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index b8c79f22998..aa092c2bc20 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -6,6 +6,7 @@ type DataSourcePlugin struct { FrontendPluginBase Annotations bool `json:"annotations"` Metrics bool `json:"metrics"` + Alerting bool `json:"alerting"` BuiltIn bool `json:"builtIn"` Mixed bool `json:"mixed"` App string `json:"app"` diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index ac4f6af5e38..ec0386ba5a9 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -227,8 +227,8 @@ export class AlertTabCtrl { var datasourceName = foundTarget.datasource || this.panel.datasource; this.datasourceSrv.get(datasourceName).then(ds => { - if (ds.meta.id !== 'graphite' && ds.meta.id !== 'prometheus') { - this.error = 'You datsource does not support alerting queries'; + if (!ds.meta.alerting) { + this.error = 'The datasource does not support alerting queries'; } else if (this.templateSrv.variableExists(foundTarget.target)) { this.error = 'Template variables are not supported in alert queries'; } else { diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index 289e3a841f5..92cfa4dc925 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -30,6 +30,7 @@ export class DashboardModel { snapshot: any; schemaVersion: number; version: number; + revision: number; links: any; gnetId: any; meta: any; @@ -42,6 +43,7 @@ export class DashboardModel { this.events = new Emitter(); this.id = data.id || null; + this.revision = data.revision; this.title = data.title || 'No Title'; this.autoUpdate = data.autoUpdate; this.description = data.description; diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json index 7533a44760b..4669bb8cd60 100644 --- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json +++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json @@ -1,5 +1,291 @@ { + "revision": 2, "title": "TestData - Graph Panel Last 1h", - "tags": ["testdata"], - "revision": 1 + "tags": [ + "grafana-test" + ], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 1, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "target": "", + "scenarioId": "no_data_points" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "No Data Points Warning", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "links": [] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 2, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "target": "", + "scenarioId": "datapoints_outside_range" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Datapoints Outside Range Warning", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "links": [] + } + ], + "title": "New row" + }, + { + "title": "New row", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 8, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "target": "", + "scenarioId": "random_walk" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Random walk series", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "links": [] + } + ] + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "schemaVersion": 13, + "version": 4, + "links": [], + "gnetId": null } diff --git a/public/app/plugins/app/testdata/datasource/plugin.json b/public/app/plugins/app/testdata/datasource/plugin.json index 0ad87a15081..4d66253d78e 100644 --- a/public/app/plugins/app/testdata/datasource/plugin.json +++ b/public/app/plugins/app/testdata/datasource/plugin.json @@ -4,6 +4,7 @@ "id": "grafana-testdata-datasource", "metrics": true, + "alerting": true, "annotations": true, "info": { diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index c47c49e05fa..76242fd883c 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -8,6 +8,7 @@ ], "metrics": true, + "alerting": true, "annotations": true, "info": { @@ -20,4 +21,4 @@ "large": "img/graphite_logo.png" } } -} \ No newline at end of file +} diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index f39f8691661..54fd1129b8b 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -8,6 +8,7 @@ ], "metrics": true, + "alerting": true, "annotations": true, "info": { From 460160cfa4917626c96f5c135c1b7e2c450d68f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Sep 2016 08:08:46 +0200 Subject: [PATCH 68/74] feat(testdata): added test case for ms resolution data --- pkg/tsdb/testdata/scenarios.go | 5 +- .../testdata/dashboards/graph_last_1h.json | 127 ++++++++++++++---- public/app/plugins/app/testdata/plugin.json | 2 +- 3 files changed, 104 insertions(+), 30 deletions(-) diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index d54cbb05638..c7df7d7bffb 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -28,8 +28,8 @@ func init() { Name: "Random Walk", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { - timeWalkerMs := context.TimeRange.MustGetFrom().Unix() * 1000 - to := context.TimeRange.MustGetTo().Unix() * 1000 + to := context.TimeRange.MustGetTo().UnixNano() / int64(time.Millisecond) + timeWalkerMs := context.TimeRange.MustGetFrom().UnixNano() / int64(time.Millisecond) series := newSeriesForQuery(query) @@ -81,7 +81,6 @@ func init() { return queryRes }, }) - } func registerScenario(scenario *Scenario) { diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json index 4669bb8cd60..0cca7b6c331 100644 --- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json +++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json @@ -1,5 +1,5 @@ { - "revision": 2, + "revision": 3, "title": "TestData - Graph Panel Last 1h", "tags": [ "grafana-test" @@ -35,6 +35,7 @@ }, "lines": true, "linewidth": 2, + "links": [], "nullPointMode": "connected", "percentage": false, "pointradius": 5, @@ -48,8 +49,8 @@ { "refId": "A", "scenario": "random_walk", - "target": "", - "scenarioId": "no_data_points" + "scenarioId": "no_data_points", + "target": "" } ], "thresholds": [], @@ -86,8 +87,7 @@ "min": null, "show": true } - ], - "links": [] + ] }, { "aliasColors": {}, @@ -109,6 +109,7 @@ }, "lines": true, "linewidth": 2, + "links": [], "nullPointMode": "connected", "percentage": false, "pointradius": 5, @@ -122,8 +123,8 @@ { "refId": "A", "scenario": "random_walk", - "target": "", - "scenarioId": "datapoints_outside_range" + "scenarioId": "datapoints_outside_range", + "target": "" } ], "thresholds": [], @@ -160,18 +161,8 @@ "min": null, "show": true } - ], - "links": [] - } - ], - "title": "New row" - }, - { - "title": "New row", - "height": "250px", - "editable": true, - "collapse": false, - "panels": [ + ] + }, { "aliasColors": {}, "bars": false, @@ -192,21 +183,22 @@ }, "lines": true, "linewidth": 2, + "links": [], "nullPointMode": "connected", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], - "span": 8, + "span": 4, "stack": false, "steppedLine": false, "targets": [ { "refId": "A", "scenario": "random_walk", - "target": "", - "scenarioId": "random_walk" + "scenarioId": "random_walk", + "target": "" } ], "thresholds": [], @@ -243,14 +235,96 @@ "min": null, "show": true } - ], - "links": [] + ] } - ] + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 4, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 8, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": "5d", + "timeShift": null, + "title": "Millisecond res x-axis and tooltip", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "title": "New row" } ], "time": { - "from": "now-6h", + "from": "now-1h", "to": "now" }, "timepicker": { @@ -284,6 +358,7 @@ "annotations": { "list": [] }, + "refresh": false, "schemaVersion": 13, "version": 4, "links": [], diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json index 47ab291409b..0bd65a0735d 100644 --- a/public/app/plugins/app/testdata/plugin.json +++ b/public/app/plugins/app/testdata/plugin.json @@ -9,7 +9,7 @@ "name": "Grafana Project", "url": "http://grafana.org" }, - "version": "1.0.5", + "version": "1.0.6", "updated": "2016-09-26" }, From 8d5857661eeb1bd061b85c5c5eb9a9c7bba01b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Sep 2016 09:15:48 +0200 Subject: [PATCH 69/74] refactor(tsdb): changed tsdb time series model to use null.Float instead of pointers --- pkg/services/alerting/conditions/evaluator.go | 25 +++++++------- .../alerting/conditions/evaluator_test.go | 6 ++-- pkg/services/alerting/conditions/query.go | 6 ++-- .../alerting/conditions/query_test.go | 33 ++++++++----------- pkg/services/alerting/conditions/reducer.go | 31 ++++++++--------- .../alerting/conditions/reducer_test.go | 25 +++++++------- pkg/tsdb/graphite/graphite.go | 1 + pkg/tsdb/graphite/types.go | 6 ++-- pkg/tsdb/models.go | 27 ++++++++++++--- pkg/tsdb/prometheus/prometheus.go | 16 ++++----- pkg/tsdb/testdata/scenarios.go | 16 +++------ 11 files changed, 102 insertions(+), 90 deletions(-) diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go index 18a2bf35262..1c154e17ec2 100644 --- a/pkg/services/alerting/conditions/evaluator.go +++ b/pkg/services/alerting/conditions/evaluator.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/alerting" + "gopkg.in/guregu/null.v3" ) var ( @@ -13,13 +14,13 @@ var ( ) type AlertEvaluator interface { - Eval(reducedValue *float64) bool + Eval(reducedValue null.Float) bool } type NoDataEvaluator struct{} -func (e *NoDataEvaluator) Eval(reducedValue *float64) bool { - return reducedValue == nil +func (e *NoDataEvaluator) Eval(reducedValue null.Float) bool { + return reducedValue.Valid == false } type ThresholdEvaluator struct { @@ -43,16 +44,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu return defaultEval, nil } -func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool { - if reducedValue == nil { +func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool { + if reducedValue.Valid == false { return false } switch e.Type { case "gt": - return *reducedValue > e.Threshold + return reducedValue.Float64 > e.Threshold case "lt": - return *reducedValue < e.Threshold + return reducedValue.Float64 < e.Threshold } return false @@ -86,16 +87,18 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e return rangedEval, nil } -func (e *RangedEvaluator) Eval(reducedValue *float64) bool { - if reducedValue == nil { +func (e *RangedEvaluator) Eval(reducedValue null.Float) bool { + if reducedValue.Valid == false { return false } + floatValue := reducedValue.Float64 + switch e.Type { case "within_range": - return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue) + return (e.Lower < floatValue && e.Upper > floatValue) || (e.Upper < floatValue && e.Lower > floatValue) case "outside_range": - return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue) + return (e.Upper < floatValue && e.Lower < floatValue) || (e.Upper > floatValue && e.Lower > floatValue) } return false diff --git a/pkg/services/alerting/conditions/evaluator_test.go b/pkg/services/alerting/conditions/evaluator_test.go index d2919f37d9d..24c5cfacea4 100644 --- a/pkg/services/alerting/conditions/evaluator_test.go +++ b/pkg/services/alerting/conditions/evaluator_test.go @@ -3,6 +3,8 @@ package conditions import ( "testing" + "gopkg.in/guregu/null.v3" + "github.com/grafana/grafana/pkg/components/simplejson" . "github.com/smartystreets/goconvey/convey" ) @@ -14,7 +16,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64) evaluator, err := NewAlertEvaluator(jsonModel) So(err, ShouldBeNil) - return evaluator.Eval(&reducedValue) + return evaluator.Eval(null.FloatFrom(reducedValue)) } func TestEvalutors(t *testing.T) { @@ -51,6 +53,6 @@ func TestEvalutors(t *testing.T) { evaluator, err := NewAlertEvaluator(jsonModel) So(err, ShouldBeNil) - So(evaluator.Eval(nil), ShouldBeTrue) + So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue) }) } diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index c84fdab574d..b5300a261a3 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -46,21 +46,21 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { reducedValue := c.Reducer.Reduce(series) evalMatch := c.Evaluator.Eval(reducedValue) - if reducedValue == nil { + if reducedValue.Valid == false { emptySerieCount++ continue } if context.IsTestRun { context.Logs = append(context.Logs, &alerting.ResultLogEntry{ - Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue), + Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64), }) } if evalMatch { context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{ Metric: series.Name, - Value: *reducedValue, + Value: reducedValue.Float64, }) } } diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 983e75c4c1b..51c4226f81c 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -3,6 +3,8 @@ package conditions import ( "testing" + null "gopkg.in/guregu/null.v3" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -41,9 +43,8 @@ func TestQueryCondition(t *testing.T) { }) Convey("should fire when avg is above 100", func() { - one := float64(120) - two := float64(0) - ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})} + points := tsdb.NewTimeSeriesPointsFromArgs(120, 0) + ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} ctx.exec() So(ctx.result.Error, ShouldBeNil) @@ -51,9 +52,8 @@ func TestQueryCondition(t *testing.T) { }) Convey("Should not fire when avg is below 100", func() { - one := float64(90) - two := float64(0) - ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})} + points := tsdb.NewTimeSeriesPointsFromArgs(90, 0) + ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} ctx.exec() So(ctx.result.Error, ShouldBeNil) @@ -61,11 +61,9 @@ func TestQueryCondition(t *testing.T) { }) Convey("Should fire if only first serie matches", func() { - one := float64(120) - two := float64(0) ctx.series = tsdb.TimeSeriesSlice{ - tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}}), - tsdb.NewTimeSeries("test2", [][2]*float64{{&two, &two}}), + tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), + tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)), } ctx.exec() @@ -76,8 +74,8 @@ func TestQueryCondition(t *testing.T) { Convey("Empty series", func() { Convey("Should set NoDataFound both series are empty", func() { ctx.series = tsdb.TimeSeriesSlice{ - tsdb.NewTimeSeries("test1", [][2]*float64{}), - tsdb.NewTimeSeries("test2", [][2]*float64{}), + tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), + tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()), } ctx.exec() @@ -86,10 +84,9 @@ func TestQueryCondition(t *testing.T) { }) Convey("Should set NoDataFound both series contains null", func() { - one := float64(120) ctx.series = tsdb.TimeSeriesSlice{ - tsdb.NewTimeSeries("test1", [][2]*float64{{nil, &one}}), - tsdb.NewTimeSeries("test2", [][2]*float64{{nil, &one}}), + tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), + tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), } ctx.exec() @@ -98,11 +95,9 @@ func TestQueryCondition(t *testing.T) { }) Convey("Should not set NoDataFound if one serie is empty", func() { - one := float64(120) - two := float64(0) ctx.series = tsdb.TimeSeriesSlice{ - tsdb.NewTimeSeries("test1", [][2]*float64{}), - tsdb.NewTimeSeries("test2", [][2]*float64{{&one, &two}}), + tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), + tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), } ctx.exec() diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 2bb4cec00be..a982fa63d33 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -4,19 +4,20 @@ import ( "math" "github.com/grafana/grafana/pkg/tsdb" + "gopkg.in/guregu/null.v3" ) type QueryReducer interface { - Reduce(timeSeries *tsdb.TimeSeries) *float64 + Reduce(timeSeries *tsdb.TimeSeries) null.Float } type SimpleReducer struct { Type string } -func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 { +func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { if len(series.Points) == 0 { - return nil + return null.FloatFromPtr(nil) } value := float64(0) @@ -25,36 +26,36 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 { switch s.Type { case "avg": for _, point := range series.Points { - if point[0] != nil { - value += *point[0] + if point[0].Valid { + value += point[0].Float64 allNull = false } } value = value / float64(len(series.Points)) case "sum": for _, point := range series.Points { - if point[0] != nil { - value += *point[0] + if point[0].Valid { + value += point[0].Float64 allNull = false } } case "min": value = math.MaxFloat64 for _, point := range series.Points { - if point[0] != nil { + if point[0].Valid { allNull = false - if value > *point[0] { - value = *point[0] + if value > point[0].Float64 { + value = point[0].Float64 } } } case "max": value = -math.MaxFloat64 for _, point := range series.Points { - if point[0] != nil { + if point[0].Valid { allNull = false - if value < *point[0] { - value = *point[0] + if value < point[0].Float64 { + value = point[0].Float64 } } } @@ -64,10 +65,10 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 { } if allNull { - return nil + return null.FloatFromPtr(nil) } - return &value + return null.FloatFrom(value) } func NewSimpleReducer(typ string) *SimpleReducer { diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index f60154bc98d..67765f9c310 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -10,44 +10,41 @@ import ( func TestSimpleReducer(t *testing.T) { Convey("Test simple reducer by calculating", t, func() { Convey("avg", func() { - result := *testReducer("avg", 1, 2, 3) + result := testReducer("avg", 1, 2, 3) So(result, ShouldEqual, float64(2)) }) Convey("sum", func() { - result := *testReducer("sum", 1, 2, 3) + result := testReducer("sum", 1, 2, 3) So(result, ShouldEqual, float64(6)) }) Convey("min", func() { - result := *testReducer("min", 3, 2, 1) + result := testReducer("min", 3, 2, 1) So(result, ShouldEqual, float64(1)) }) Convey("max", func() { - result := *testReducer("max", 1, 2, 3) + result := testReducer("max", 1, 2, 3) So(result, ShouldEqual, float64(3)) }) Convey("count", func() { - result := *testReducer("count", 1, 2, 3000) + result := testReducer("count", 1, 2, 3000) So(result, ShouldEqual, float64(3)) }) }) } -func testReducer(typ string, datapoints ...float64) *float64 { +func testReducer(typ string, datapoints ...float64) float64 { reducer := NewSimpleReducer(typ) - var timeserie [][2]*float64 - dummieTimestamp := float64(521452145) + series := &tsdb.TimeSeries{ + Name: "test time serie", + } for idx := range datapoints { - timeserie = append(timeserie, [2]*float64{&datapoints[idx], &dummieTimestamp}) + series.Points = append(series.Points, tsdb.NewTimePoint(datapoints[idx], 1234134)) } - tsdb := &tsdb.TimeSeries{ - Name: "test time serie", - Points: timeserie, - } - return reducer.Reduce(tsdb) + return reducer.Reduce(series).Float64 } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index def7039ac91..5f14f0302f7 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -80,6 +80,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result.QueryResults = make(map[string]*tsdb.QueryResult) queryRes := &tsdb.QueryResult{} + for _, series := range data { queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ Name: series.Target, diff --git a/pkg/tsdb/graphite/types.go b/pkg/tsdb/graphite/types.go index 085b1fb2b94..8bd13aec4f2 100644 --- a/pkg/tsdb/graphite/types.go +++ b/pkg/tsdb/graphite/types.go @@ -1,6 +1,8 @@ package graphite +import "github.com/grafana/grafana/pkg/tsdb" + type TargetResponseDTO struct { - Target string `json:"target"` - DataPoints [][2]*float64 `json:"datapoints"` + Target string `json:"target"` + DataPoints tsdb.TimeSeriesPoints `json:"datapoints"` } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index fc66a47f981..008be3efa28 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -1,6 +1,9 @@ package tsdb -import "github.com/grafana/grafana/pkg/components/simplejson" +import ( + "github.com/grafana/grafana/pkg/components/simplejson" + "gopkg.in/guregu/null.v3" +) type Query struct { RefId string @@ -55,13 +58,29 @@ type QueryResult struct { } type TimeSeries struct { - Name string `json:"name"` - Points [][2]*float64 `json:"points"` + Name string `json:"name"` + Points TimeSeriesPoints `json:"points"` } +type TimePoint [2]null.Float +type TimeSeriesPoints []TimePoint type TimeSeriesSlice []*TimeSeries -func NewTimeSeries(name string, points [][2]*float64) *TimeSeries { +func NewTimePoint(value float64, timestamp float64) TimePoint { + return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)} +} + +func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints { + points := make(TimeSeriesPoints, 0) + + for i := 0; i < len(values); i += 2 { + points = append(points, NewTimePoint(values[i], values[i+1])) + } + + return points +} + +func NewTimeSeries(name string, points TimeSeriesPoints) *TimeSeries { return &TimeSeries{ Name: name, Points: points, diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 4d6ab03cede..eba558ddaa7 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -140,17 +140,15 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb } for _, v := range data { - var points [][2]*float64 - for _, k := range v.Values { - timestamp := float64(k.Timestamp) - val := float64(k.Value) - points = append(points, [2]*float64{&val, ×tamp}) + series := tsdb.TimeSeries{ + Name: formatLegend(v.Metric, query), } - queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ - Name: formatLegend(v.Metric, query), - Points: points, - }) + for _, k := range v.Values { + series.Points = append(series.Points, tsdb.NewTimePoint(float64(k.Value), float64(k.Timestamp.Unix()*1000))) + } + + queryRes.Series = append(queryRes.Series, &series) } queryResults["A"] = queryRes diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index c7df7d7bffb..f34db7ac931 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -4,7 +4,6 @@ import ( "math/rand" "time" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" ) @@ -21,7 +20,7 @@ var ScenarioRegistry map[string]*Scenario func init() { ScenarioRegistry = make(map[string]*Scenario) - logger := log.New("tsdb.testdata") + //logger := log.New("tsdb.testdata") registerScenario(&Scenario{ Id: "random_walk", @@ -33,13 +32,11 @@ func init() { series := newSeriesForQuery(query) - points := make([][2]*float64, 0) + points := make(tsdb.TimeSeriesPoints, 0) walker := rand.Float64() * 100 for i := int64(0); i < 10000 && timeWalkerMs < to; i++ { - timestamp := float64(timeWalkerMs) - val := float64(walker) - points = append(points, [2]*float64{&val, ×tamp}) + points = append(points, tsdb.NewTimePoint(walker, float64(timeWalkerMs))) walker += rand.Float64() - 0.5 timeWalkerMs += query.IntervalMs @@ -72,12 +69,9 @@ func init() { series := newSeriesForQuery(query) outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000 - timestamp := float64(outsideTime) - logger.Info("time", "from", timestamp) - val := float64(10) - - series.Points = append(series.Points, [2]*float64{&val, ×tamp}) + series.Points = append(series.Points, tsdb.NewTimePoint(10, float64(outsideTime))) queryRes.Series = append(queryRes.Series, series) + return queryRes }, }) From a4648607bb7006aec0db9041100adfea38f5cc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Sep 2016 10:37:30 +0200 Subject: [PATCH 70/74] feat(testdata): added csv test data scenario --- pkg/api/metrics.go | 1 + pkg/tsdb/graphite/graphite.go | 2 +- pkg/tsdb/models.go | 6 + pkg/tsdb/prometheus/prometheus.go | 2 +- pkg/tsdb/testdata/scenarios.go | 55 +++- pkg/tsdb/time_range.go | 8 + .../app/testdata/dashboards/alerts.json | 286 ++++++++++++++++++ .../testdata/dashboards/graph_last_1h.json | 123 +++++++- .../app/testdata/datasource/datasource.ts | 3 + .../app/testdata/datasource/query_ctrl.ts | 10 + .../app/testdata/partials/query.editor.html | 12 +- public/app/plugins/app/testdata/plugin.json | 2 +- 12 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 public/app/plugins/app/testdata/dashboards/alerts.json diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index c3bd9062737..0fa6003d67a 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -48,6 +48,7 @@ func GetTestDataScenarios(c *middleware.Context) Response { "id": scenario.Id, "name": scenario.Name, "description": scenario.Description, + "stringInput": scenario.StringInput, }) } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 5f14f0302f7..78685d52371 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -79,7 +79,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC } result.QueryResults = make(map[string]*tsdb.QueryResult) - queryRes := &tsdb.QueryResult{} + queryRes := tsdb.NewQueryResult() for _, series := range data { queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{ diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 008be3efa28..bbf7bba7ac7 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -66,6 +66,12 @@ type TimePoint [2]null.Float type TimeSeriesPoints []TimePoint type TimeSeriesSlice []*TimeSeries +func NewQueryResult() *QueryResult { + return &QueryResult{ + Series: make(TimeSeriesSlice, 0), + } +} + func NewTimePoint(value float64, timestamp float64) TimePoint { return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)} } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index eba558ddaa7..f7e68662efa 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -132,7 +132,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) { queryResults := make(map[string]*tsdb.QueryResult) - queryRes := &tsdb.QueryResult{} + queryRes := tsdb.NewQueryResult() data, ok := value.(pmodel.Matrix) if !ok { diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index f34db7ac931..e90b0d4df79 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -2,8 +2,11 @@ package testdata import ( "math/rand" + "strconv" + "strings" "time" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" ) @@ -12,6 +15,7 @@ type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.Q type Scenario struct { Id string `json:"id"` Name string `json:"name"` + StringInput string `json:"stringOption"` Description string `json:"description"` Handler ScenarioHandler `json:"-"` } @@ -20,15 +24,17 @@ var ScenarioRegistry map[string]*Scenario func init() { ScenarioRegistry = make(map[string]*Scenario) - //logger := log.New("tsdb.testdata") + logger := log.New("tsdb.testdata") + + logger.Debug("Initializing TestData Scenario") registerScenario(&Scenario{ Id: "random_walk", Name: "Random Walk", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { - to := context.TimeRange.MustGetTo().UnixNano() / int64(time.Millisecond) - timeWalkerMs := context.TimeRange.MustGetFrom().UnixNano() / int64(time.Millisecond) + timeWalkerMs := context.TimeRange.GetFromAsMsEpoch() + to := context.TimeRange.GetToAsMsEpoch() series := newSeriesForQuery(query) @@ -44,7 +50,7 @@ func init() { series.Points = points - queryRes := &tsdb.QueryResult{} + queryRes := tsdb.NewQueryResult() queryRes.Series = append(queryRes.Series, series) return queryRes }, @@ -54,9 +60,7 @@ func init() { Id: "no_data_points", Name: "No Data Points", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { - return &tsdb.QueryResult{ - Series: make(tsdb.TimeSeriesSlice, 0), - } + return tsdb.NewQueryResult() }, }) @@ -64,7 +68,7 @@ func init() { Id: "datapoints_outside_range", Name: "Datapoints Outside Range", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { - queryRes := &tsdb.QueryResult{} + queryRes := tsdb.NewQueryResult() series := newSeriesForQuery(query) outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000 @@ -75,6 +79,41 @@ func init() { return queryRes }, }) + + registerScenario(&Scenario{ + Id: "csv_metric_values", + Name: "CSV Metric Values", + StringInput: "1,20,90,30,5,0", + Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + queryRes := tsdb.NewQueryResult() + + stringInput := query.Model.Get("stringInput").MustString() + values := []float64{} + for _, strVal := range strings.Split(stringInput, ",") { + if val, err := strconv.ParseFloat(strVal, 64); err == nil { + values = append(values, val) + } + } + + if len(values) == 0 { + return queryRes + } + + series := newSeriesForQuery(query) + startTime := context.TimeRange.GetFromAsMsEpoch() + endTime := context.TimeRange.GetToAsMsEpoch() + step := (endTime - startTime) / int64(len(values)-1) + + for _, val := range values { + series.Points = append(series.Points, tsdb.NewTimePoint(val, float64(startTime))) + startTime += step + } + + queryRes.Series = append(queryRes.Series, series) + + return queryRes + }, + }) } func registerScenario(scenario *Scenario) { diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index 67227b834dc..cf6bc6a5048 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -21,6 +21,14 @@ type TimeRange struct { Now time.Time } +func (tr *TimeRange) GetFromAsMsEpoch() int64 { + return tr.MustGetFrom().UnixNano() / int64(time.Millisecond) +} + +func (tr *TimeRange) GetToAsMsEpoch() int64 { + return tr.MustGetTo().UnixNano() / int64(time.Millisecond) +} + func (tr *TimeRange) MustGetFrom() time.Time { if res, err := tr.ParseFrom(); err != nil { return time.Unix(0, 0) diff --git a/public/app/plugins/app/testdata/dashboards/alerts.json b/public/app/plugins/app/testdata/dashboards/alerts.json new file mode 100644 index 00000000000..c510ec220ea --- /dev/null +++ b/public/app/plugins/app/testdata/dashboards/alerts.json @@ -0,0 +1,286 @@ +{ + "title": "TestData - Alerts", + "tags": [ + "grafana-test" + ], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": 255.625, + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 111 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "frequency": "60s", + "handler": 1, + "name": "Should always be green", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "target": "", + "stringInput": "1,20,90,30,5,0" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 111 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always OK", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": "125", + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "frequency": "60s", + "handler": 1, + "name": "Always Alerting", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 4, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "target": "", + "stringInput": "200,445,100,150,200,220,190" + } + ], + "thresholds": [ + { + "value": 177, + "op": "gt", + "fill": true, + "line": true, + "colorMode": "critical" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always Alerting", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "title": "New row" + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "schemaVersion": 13, + "version": 10, + "links": [], + "gnetId": null +} diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json index 0cca7b6c331..757dd48e50f 100644 --- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json +++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json @@ -1,5 +1,5 @@ { - "revision": 3, + "revision": 4, "title": "TestData - Graph Panel Last 1h", "tags": [ "grafana-test" @@ -284,7 +284,7 @@ } ], "thresholds": [], - "timeFrom": "5d", + "timeFrom": "2s", "timeShift": null, "title": "Millisecond res x-axis and tooltip", "tooltip": { @@ -318,9 +318,126 @@ "show": true } ] + }, + { + "title": "", + "error": false, + "span": 4, + "editable": true, + "type": "text", + "isNew": true, + "id": 6, + "mode": "markdown", + "content": "Just verify that the tooltip time has millisecond resolution ", + "links": [] } ], "title": "New row" + }, + { + "title": "New row", + "height": 336, + "editable": true, + "collapse": false, + "panels": [ + { + "title": "2 yaxis and axis lables", + "error": false, + "span": 7.99561403508772, + "editable": true, + "type": "graph", + "isNew": true, + "id": 5, + "targets": [ + { + "target": "", + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + }, + { + "target": "", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "2000,3000,4000,1000,3000,10000" + } + ], + "datasource": "Grafana TestData", + "renderer": "flot", + "yaxes": [ + { + "label": "Perecent", + "show": true, + "logBase": 1, + "min": null, + "max": null, + "format": "percent" + }, + { + "label": "Pressure", + "show": true, + "logBase": 1, + "min": null, + "max": null, + "format": "short" + } + ], + "xaxis": { + "show": true, + "mode": "time", + "name": null, + "values": [] + }, + "lines": true, + "fill": 1, + "linewidth": 2, + "points": false, + "pointradius": 5, + "bars": false, + "stack": false, + "percentage": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "shared": true, + "sort": 0, + "msResolution": false + }, + "timeFrom": null, + "timeShift": null, + "aliasColors": {}, + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "thresholds": [], + "links": [] + }, + { + "title": "", + "error": false, + "span": 4.00438596491228, + "editable": true, + "type": "text", + "isNew": true, + "id": 7, + "mode": "markdown", + "content": "Verify that axis labels look ok", + "links": [] + } + ] } ], "time": { @@ -360,7 +477,7 @@ }, "refresh": false, "schemaVersion": 13, - "version": 4, + "version": 3, "links": [], "gnetId": null } diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/app/testdata/datasource/datasource.ts index a06daa19262..e0846d99ab6 100644 --- a/public/app/plugins/app/testdata/datasource/datasource.ts +++ b/public/app/plugins/app/testdata/datasource/datasource.ts @@ -1,6 +1,7 @@ /// import _ from 'lodash'; +import angular from 'angular'; class TestDataDatasource { @@ -16,6 +17,8 @@ class TestDataDatasource { scenarioId: item.scenarioId, intervalMs: options.intervalMs, maxDataPoints: options.maxDataPoints, + stringInput: item.stringInput, + jsonInput: angular.fromJson(item.jsonInput), }; }); diff --git a/public/app/plugins/app/testdata/datasource/query_ctrl.ts b/public/app/plugins/app/testdata/datasource/query_ctrl.ts index f22ef8948ad..6b0ad93f26c 100644 --- a/public/app/plugins/app/testdata/datasource/query_ctrl.ts +++ b/public/app/plugins/app/testdata/datasource/query_ctrl.ts @@ -1,5 +1,7 @@ /// +import _ from 'lodash'; + import {TestDataDatasource} from './datasource'; import {QueryCtrl} from 'app/plugins/sdk'; @@ -7,6 +9,7 @@ export class TestDataQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; scenarioList: any; + scenario: any; /** @ngInject **/ constructor($scope, $injector, private backendSrv) { @@ -19,7 +22,14 @@ export class TestDataQueryCtrl extends QueryCtrl { $onInit() { return this.backendSrv.get('/api/tsdb/testdata/scenarios').then(res => { this.scenarioList = res; + this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId}); }); } + + scenarioChanged() { + this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId}); + this.target.stringInput = this.scenario.stringInput; + this.refresh(); + } } diff --git a/public/app/plugins/app/testdata/partials/query.editor.html b/public/app/plugins/app/testdata/partials/query.editor.html index 53f84309661..a39582d5397 100644 --- a/public/app/plugins/app/testdata/partials/query.editor.html +++ b/public/app/plugins/app/testdata/partials/query.editor.html @@ -2,17 +2,17 @@
    -
    - +
    +
    -
    - - +
    + +
    - +
    diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json index 0bd65a0735d..efd4e6bb739 100644 --- a/public/app/plugins/app/testdata/plugin.json +++ b/public/app/plugins/app/testdata/plugin.json @@ -9,7 +9,7 @@ "name": "Grafana Project", "url": "http://grafana.org" }, - "version": "1.0.6", + "version": "1.0.7", "updated": "2016-09-26" }, From 68370ba2bcc999a47048b137d709e239ccea8d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Sep 2016 10:44:21 +0200 Subject: [PATCH 71/74] feat(testdata): added alert dashboard --- .../app/testdata/dashboards/alerts.json | 29 ++++++++++--------- public/app/plugins/app/testdata/plugin.json | 7 ++++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/public/app/plugins/app/testdata/dashboards/alerts.json b/public/app/plugins/app/testdata/dashboards/alerts.json index c510ec220ea..159df0f458b 100644 --- a/public/app/plugins/app/testdata/dashboards/alerts.json +++ b/public/app/plugins/app/testdata/dashboards/alerts.json @@ -1,4 +1,5 @@ { + "revision": 2, "title": "TestData - Alerts", "tags": [ "grafana-test" @@ -20,7 +21,7 @@ { "evaluator": { "params": [ - 111 + 60 ], "type": "gt" }, @@ -41,7 +42,7 @@ "enabled": true, "frequency": "60s", "handler": 1, - "name": "Should always be green", + "name": "TestData - Always OK", "noDataState": "no_data", "notifications": [] }, @@ -79,17 +80,17 @@ "refId": "A", "scenario": "random_walk", "scenarioId": "csv_metric_values", - "target": "", - "stringInput": "1,20,90,30,5,0" + "stringInput": "1,20,90,30,5,0", + "target": "" } ], "thresholds": [ { - "colorMode": "critical", + "value": 60, + "op": "gt", "fill": true, "line": true, - "op": "gt", - "value": 111 + "colorMode": "critical" } ], "timeFrom": null, @@ -154,7 +155,7 @@ "enabled": true, "frequency": "60s", "handler": 1, - "name": "Always Alerting", + "name": "TestData - Always Alerting", "noDataState": "no_data", "notifications": [] }, @@ -192,17 +193,17 @@ "refId": "A", "scenario": "random_walk", "scenarioId": "csv_metric_values", - "target": "", - "stringInput": "200,445,100,150,200,220,190" + "stringInput": "200,445,100,150,200,220,190", + "target": "" } ], "thresholds": [ { - "value": 177, - "op": "gt", + "colorMode": "critical", "fill": true, "line": true, - "colorMode": "critical" + "op": "gt", + "value": 177 } ], "timeFrom": null, @@ -280,7 +281,7 @@ "list": [] }, "schemaVersion": 13, - "version": 10, + "version": 4, "links": [], "gnetId": null } diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json index efd4e6bb739..6742ad04ecb 100644 --- a/public/app/plugins/app/testdata/plugin.json +++ b/public/app/plugins/app/testdata/plugin.json @@ -9,7 +9,7 @@ "name": "Grafana Project", "url": "http://grafana.org" }, - "version": "1.0.7", + "version": "1.0.13", "updated": "2016-09-26" }, @@ -18,6 +18,11 @@ "type": "dashboard", "name": "TestData - Graph Last 1h", "path": "dashboards/graph_last_1h.json" + }, + { + "type": "dashboard", + "name": "TestData - Alerts", + "path": "dashboards/alerts.json" } ], From 593863fb9eb65b25ad7627e1ff77bbd79af4c883 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 28 Sep 2016 18:54:25 +0900 Subject: [PATCH 72/74] (prometheus) check time range (#6137) --- public/app/plugins/datasource/prometheus/datasource.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index e2677b4b8f5..941591791ef 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -123,6 +123,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS }; this.performTimeSeriesQuery = function(query, start, end) { + if (start > end) { + 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); }; From 599fe49944cbf28db1483801e99d02a6ca48ef98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Sep 2016 12:12:38 +0200 Subject: [PATCH 73/74] fix(templating): fix to datasource variable, was not updated on dashboard load, added unit test for case as well --- .../templating/datasource_variable.ts | 3 +++ .../specs/variable_srv_init_specs.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index 3a297002900..d43c0dd486d 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -10,6 +10,7 @@ export class DatasourceVariable implements Variable { query: string; options: any; current: any; + refresh: any; defaults = { type: 'datasource', @@ -20,11 +21,13 @@ export class DatasourceVariable implements Variable { regex: '', options: [], query: '', + refresh: 1, }; /** @ngInject **/ constructor(private model, private datasourceSrv, private variableSrv) { assignModelProperties(this, model, this.defaults); + this.refresh = 1; } getModel() { diff --git a/public/app/features/templating/specs/variable_srv_init_specs.ts b/public/app/features/templating/specs/variable_srv_init_specs.ts index 8cac63135ca..533c70dfc25 100644 --- a/public/app/features/templating/specs/variable_srv_init_specs.ts +++ b/public/app/features/templating/specs/variable_srv_init_specs.ts @@ -62,6 +62,7 @@ describe('VariableSrv init', function() { options: [{text: "test", value: "test"}] }]; scenario.urlParams["var-apps"] = "new"; + scenario.metricSources = []; }); it('should update current value', () => { @@ -110,6 +111,30 @@ describe('VariableSrv init', function() { }); }); + describeInitScenario('when datasource variable is initialized', scenario => { + scenario.setup(() => { + scenario.variables = [{ + type: 'datasource', + query: 'graphite', + name: 'test', + current: {value: 'backend4_pee', text: 'backend4_pee'}, + regex: '/pee$/' + } + ]; + scenario.metricSources = [ + {name: 'backend1', meta: {id: 'influx'}}, + {name: 'backend2_pee', meta: {id: 'graphite'}}, + {name: 'backend3', meta: {id: 'graphite'}}, + {name: 'backend4_pee', meta: {id: 'graphite'}}, + ]; + }); + + it('should update current value', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.options.length).to.be(2); + }); + }); + describeInitScenario('when template variable is present in url multiple times', scenario => { scenario.setup(() => { scenario.variables = [{ From 15299c2ac009c7624aff4e2b43b0baac0f36b97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Sep 2016 12:15:47 +0200 Subject: [PATCH 74/74] fix(govendor): added new null package --- vendor/gopkg.in/guregu/null.v3/LICENSE | 10 ++ vendor/gopkg.in/guregu/null.v3/README.md | 75 +++++++++++++ vendor/gopkg.in/guregu/null.v3/bool.go | 129 ++++++++++++++++++++++ vendor/gopkg.in/guregu/null.v3/float.go | 117 ++++++++++++++++++++ vendor/gopkg.in/guregu/null.v3/int.go | 118 ++++++++++++++++++++ vendor/gopkg.in/guregu/null.v3/string.go | 110 ++++++++++++++++++ vendor/gopkg.in/guregu/null.v3/time.go | 135 +++++++++++++++++++++++ vendor/vendor.json | 18 ++- 8 files changed, 706 insertions(+), 6 deletions(-) create mode 100644 vendor/gopkg.in/guregu/null.v3/LICENSE create mode 100644 vendor/gopkg.in/guregu/null.v3/README.md create mode 100644 vendor/gopkg.in/guregu/null.v3/bool.go create mode 100644 vendor/gopkg.in/guregu/null.v3/float.go create mode 100644 vendor/gopkg.in/guregu/null.v3/int.go create mode 100644 vendor/gopkg.in/guregu/null.v3/string.go create mode 100644 vendor/gopkg.in/guregu/null.v3/time.go diff --git a/vendor/gopkg.in/guregu/null.v3/LICENSE b/vendor/gopkg.in/guregu/null.v3/LICENSE new file mode 100644 index 00000000000..69062b45b16 --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2014, Greg Roseberry +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/gopkg.in/guregu/null.v3/README.md b/vendor/gopkg.in/guregu/null.v3/README.md new file mode 100644 index 00000000000..62bf48be93e --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/README.md @@ -0,0 +1,75 @@ +## null [![GoDoc](https://godoc.org/github.com/guregu/null?status.svg)](https://godoc.org/github.com/guregu/null) [![Coverage](http://gocover.io/_badge/github.com/guregu/null)](http://gocover.io/github.com/guregu/null) +`import "gopkg.in/guregu/null.v3"` + +null is a library with reasonable options for dealing with nullable SQL and JSON values + +There are two packages: `null` and its subpackage `zero`. + +Types in `null` will only be considered null on null input, and will JSON encode to `null`. If you need zero and null be considered separate values, use these. + +Types in `zero` are treated like zero values in Go: blank string input will produce a null `zero.String`, and null Strings will JSON encode to `""`. Zero values of these types will be considered null to SQL. If you need zero and null treated the same, use these. + +All types implement `sql.Scanner` and `driver.Valuer`, so you can use this library in place of `sql.NullXXX`. All types also implement: `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, `json.Marshaler`, and `json.Unmarshaler`. + +### null package + +`import "gopkg.in/guregu/null.v3"` + +#### null.String +Nullable string. + +Marshals to JSON null if SQL source data is null. Zero (blank) input will not produce a null String. Can unmarshal from `sql.NullString` JSON input or string input. + +#### null.Int +Nullable int64. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Int. Can unmarshal from `sql.NullInt64` JSON input. + +#### null.Float +Nullable float64. + +Marshals to JSON null if SQL source data is null. Zero input will not produce a null Float. Can unmarshal from `sql.NullFloat64` JSON input. + +#### null.Bool +Nullable bool. + +Marshals to JSON null if SQL source data is null. False input will not produce a null Bool. Can unmarshal from `sql.NullBool` JSON input. + +#### null.Time + +Marshals to JSON null if SQL source data is null. Uses `time.Time`'s marshaler. Can unmarshal from `pq.NullTime` and similar JSON input. + +### zero package + +`import "gopkg.in/guregu/null.v3/zero"` + +#### zero.String +Nullable string. + +Will marshal to a blank string if null. Blank string input produces a null String. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullString` JSON input. + +#### zero.Int +Nullable int64. + +Will marshal to 0 if null. 0 produces a null Int. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullInt64` JSON input. + +#### zero.Float +Nullable float64. + +Will marshal to 0 if null. 0.0 produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullFloat64` JSON input. + +#### zero.Bool +Nullable bool. + +Will marshal to false if null. `false` produces a null Float. Null values and zero values are considered equivalent. Can unmarshal from `sql.NullBool` JSON input. + +#### zero.Time + +Will marshal to the zero time if null. Uses `time.Time`'s marshaler. Can unmarshal from `pq.NullTime` and similar JSON input. + + +### Bugs +`json`'s `",omitempty"` struct tag does not work correctly right now. It will never omit a null or empty String. This might be [fixed eventually](https://github.com/golang/go/issues/4357). + +### License +BSD diff --git a/vendor/gopkg.in/guregu/null.v3/bool.go b/vendor/gopkg.in/guregu/null.v3/bool.go new file mode 100644 index 00000000000..1ec13961bfe --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/bool.go @@ -0,0 +1,129 @@ +package null + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "reflect" +) + +// Bool is a nullable bool. +// It does not consider false values to be null. +// It will decode to null, not false, if null. +type Bool struct { + sql.NullBool +} + +// NewBool creates a new Bool +func NewBool(b bool, valid bool) Bool { + return Bool{ + NullBool: sql.NullBool{ + Bool: b, + Valid: valid, + }, + } +} + +// BoolFrom creates a new Bool that will always be valid. +func BoolFrom(b bool) Bool { + return NewBool(b, true) +} + +// BoolFromPtr creates a new Bool that will be null if f is nil. +func BoolFromPtr(b *bool) Bool { + if b == nil { + return NewBool(false, false) + } + return NewBool(*b, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Bool. +// It also supports unmarshalling a sql.NullBool. +func (b *Bool) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch x := v.(type) { + case bool: + b.Bool = x + case map[string]interface{}: + err = json.Unmarshal(data, &b.NullBool) + case nil: + b.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Bool", reflect.TypeOf(v).Name()) + } + b.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Bool if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (b *Bool) UnmarshalText(text []byte) error { + str := string(text) + switch str { + case "", "null": + b.Valid = false + return nil + case "true": + b.Bool = true + case "false": + b.Bool = false + default: + b.Valid = false + return errors.New("invalid input:" + str) + } + b.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Bool is null. +func (b Bool) MarshalJSON() ([]byte, error) { + if !b.Valid { + return []byte("null"), nil + } + if !b.Bool { + return []byte("false"), nil + } + return []byte("true"), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Bool is null. +func (b Bool) MarshalText() ([]byte, error) { + if !b.Valid { + return []byte{}, nil + } + if !b.Bool { + return []byte("false"), nil + } + return []byte("true"), nil +} + +// SetValid changes this Bool's value and also sets it to be non-null. +func (b *Bool) SetValid(v bool) { + b.Bool = v + b.Valid = true +} + +// Ptr returns a pointer to this Bool's value, or a nil pointer if this Bool is null. +func (b Bool) Ptr() *bool { + if !b.Valid { + return nil + } + return &b.Bool +} + +// IsZero returns true for invalid Bools, for future omitempty support (Go 1.4?) +// A non-null Bool with a 0 value will not be considered zero. +func (b Bool) IsZero() bool { + return !b.Valid +} diff --git a/vendor/gopkg.in/guregu/null.v3/float.go b/vendor/gopkg.in/guregu/null.v3/float.go new file mode 100644 index 00000000000..1f57b959ab7 --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/float.go @@ -0,0 +1,117 @@ +package null + +import ( + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// Float is a nullable float64. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Float struct { + sql.NullFloat64 +} + +// NewFloat creates a new Float +func NewFloat(f float64, valid bool) Float { + return Float{ + NullFloat64: sql.NullFloat64{ + Float64: f, + Valid: valid, + }, + } +} + +// FloatFrom creates a new Float that will always be valid. +func FloatFrom(f float64) Float { + return NewFloat(f, true) +} + +// FloatFromPtr creates a new Float that be null if f is nil. +func FloatFromPtr(f *float64) Float { + if f == nil { + return NewFloat(0, false) + } + return NewFloat(*f, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Float. +// It also supports unmarshalling a sql.NullFloat64. +func (f *Float) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch x := v.(type) { + case float64: + f.Float64 = float64(x) + case map[string]interface{}: + err = json.Unmarshal(data, &f.NullFloat64) + case nil: + f.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name()) + } + f.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Float if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (f *Float) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + f.Valid = false + return nil + } + var err error + f.Float64, err = strconv.ParseFloat(string(text), 64) + f.Valid = err == nil + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Float is null. +func (f Float) MarshalJSON() ([]byte, error) { + if !f.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Float is null. +func (f Float) MarshalText() ([]byte, error) { + if !f.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil +} + +// SetValid changes this Float's value and also sets it to be non-null. +func (f *Float) SetValid(n float64) { + f.Float64 = n + f.Valid = true +} + +// Ptr returns a pointer to this Float's value, or a nil pointer if this Float is null. +func (f Float) Ptr() *float64 { + if !f.Valid { + return nil + } + return &f.Float64 +} + +// IsZero returns true for invalid Floats, for future omitempty support (Go 1.4?) +// A non-null Float with a 0 value will not be considered zero. +func (f Float) IsZero() bool { + return !f.Valid +} diff --git a/vendor/gopkg.in/guregu/null.v3/int.go b/vendor/gopkg.in/guregu/null.v3/int.go new file mode 100644 index 00000000000..981d17b09aa --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/int.go @@ -0,0 +1,118 @@ +package null + +import ( + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// Int is an nullable int64. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Int struct { + sql.NullInt64 +} + +// NewInt creates a new Int +func NewInt(i int64, valid bool) Int { + return Int{ + NullInt64: sql.NullInt64{ + Int64: i, + Valid: valid, + }, + } +} + +// IntFrom creates a new Int that will always be valid. +func IntFrom(i int64) Int { + return NewInt(i, true) +} + +// IntFromPtr creates a new Int that be null if i is nil. +func IntFromPtr(i *int64) Int { + if i == nil { + return NewInt(0, false) + } + return NewInt(*i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int. +// It also supports unmarshalling a sql.NullInt64. +func (i *Int) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch v.(type) { + case float64: + // Unmarshal again, directly to int64, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Int64) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullInt64) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Int if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Int) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + i.Int64, err = strconv.ParseInt(string(text), 10, 64) + i.Valid = err == nil + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Int is null. +func (i Int) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatInt(i.Int64, 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Int is null. +func (i Int) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatInt(i.Int64, 10)), nil +} + +// SetValid changes this Int's value and also sets it to be non-null. +func (i *Int) SetValid(n int64) { + i.Int64 = n + i.Valid = true +} + +// Ptr returns a pointer to this Int's value, or a nil pointer if this Int is null. +func (i Int) Ptr() *int64 { + if !i.Valid { + return nil + } + return &i.Int64 +} + +// IsZero returns true for invalid Ints, for future omitempty support (Go 1.4?) +// A non-null Int with a 0 value will not be considered zero. +func (i Int) IsZero() bool { + return !i.Valid +} diff --git a/vendor/gopkg.in/guregu/null.v3/string.go b/vendor/gopkg.in/guregu/null.v3/string.go new file mode 100644 index 00000000000..554aac820e3 --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/string.go @@ -0,0 +1,110 @@ +// Package null contains SQL types that consider zero input and null input as separate values, +// with convenient support for JSON and text marshaling. +// Types in this package will always encode to their null value if null. +// Use the zero subpackage if you want zero values and null to be treated the same. +package null + +import ( + "database/sql" + "encoding/json" + "fmt" + "reflect" +) + +// String is a nullable string. It supports SQL and JSON serialization. +// It will marshal to null if null. Blank string input will be considered null. +type String struct { + sql.NullString +} + +// StringFrom creates a new String that will never be blank. +func StringFrom(s string) String { + return NewString(s, true) +} + +// StringFromPtr creates a new String that be null if s is nil. +func StringFromPtr(s *string) String { + if s == nil { + return NewString("", false) + } + return NewString(*s, true) +} + +// NewString creates a new String +func NewString(s string, valid bool) String { + return String{ + NullString: sql.NullString{ + String: s, + Valid: valid, + }, + } +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports string and null input. Blank string input does not produce a null String. +// It also supports unmarshalling a sql.NullString. +func (s *String) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch x := v.(type) { + case string: + s.String = x + case map[string]interface{}: + err = json.Unmarshal(data, &s.NullString) + case nil: + s.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.String", reflect.TypeOf(v).Name()) + } + s.Valid = err == nil + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this String is null. +func (s String) MarshalJSON() ([]byte, error) { + if !s.Valid { + return []byte("null"), nil + } + return json.Marshal(s.String) +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string when this String is null. +func (s String) MarshalText() ([]byte, error) { + if !s.Valid { + return []byte{}, nil + } + return []byte(s.String), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null String if the input is a blank string. +func (s *String) UnmarshalText(text []byte) error { + s.String = string(text) + s.Valid = s.String != "" + return nil +} + +// SetValid changes this String's value and also sets it to be non-null. +func (s *String) SetValid(v string) { + s.String = v + s.Valid = true +} + +// Ptr returns a pointer to this String's value, or a nil pointer if this String is null. +func (s String) Ptr() *string { + if !s.Valid { + return nil + } + return &s.String +} + +// IsZero returns true for null strings, for potential future omitempty support. +func (s String) IsZero() bool { + return !s.Valid +} diff --git a/vendor/gopkg.in/guregu/null.v3/time.go b/vendor/gopkg.in/guregu/null.v3/time.go new file mode 100644 index 00000000000..a4d843920b4 --- /dev/null +++ b/vendor/gopkg.in/guregu/null.v3/time.go @@ -0,0 +1,135 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "time" +) + +// Time is a nullable time.Time. It supports SQL and JSON serialization. +// It will marshal to null if null. +type Time struct { + Time time.Time + Valid bool +} + +// Scan implements the Scanner interface. +func (t *Time) Scan(value interface{}) error { + var err error + switch x := value.(type) { + case time.Time: + t.Time = x + case nil: + t.Valid = false + return nil + default: + err = fmt.Errorf("null: cannot scan type %T into null.Time: %v", value, value) + } + t.Valid = err == nil + return err +} + +// Value implements the driver Valuer interface. +func (t Time) Value() (driver.Value, error) { + if !t.Valid { + return nil, nil + } + return t.Time, nil +} + +// NewTime creates a new Time. +func NewTime(t time.Time, valid bool) Time { + return Time{ + Time: t, + Valid: valid, + } +} + +// TimeFrom creates a new Time that will always be valid. +func TimeFrom(t time.Time) Time { + return NewTime(t, true) +} + +// TimeFromPtr creates a new Time that will be null if t is nil. +func TimeFromPtr(t *time.Time) Time { + if t == nil { + return NewTime(time.Time{}, false) + } + return NewTime(*t, true) +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this time is null. +func (t Time) MarshalJSON() ([]byte, error) { + if !t.Valid { + return []byte("null"), nil + } + return t.Time.MarshalJSON() +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports string, object (e.g. pq.NullTime and friends) +// and null input. +func (t *Time) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch x := v.(type) { + case string: + err = t.Time.UnmarshalJSON(data) + case map[string]interface{}: + ti, tiOK := x["Time"].(string) + valid, validOK := x["Valid"].(bool) + if !tiOK || !validOK { + return fmt.Errorf(`json: unmarshalling object into Go value of type null.Time requires key "Time" to be of type string and key "Valid" to be of type bool; found %T and %T, respectively`, x["Time"], x["Valid"]) + } + err = t.Time.UnmarshalText([]byte(ti)) + t.Valid = valid + return err + case nil: + t.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Time", reflect.TypeOf(v).Name()) + } + t.Valid = err == nil + return err +} + +func (t Time) MarshalText() ([]byte, error) { + if !t.Valid { + return []byte("null"), nil + } + return t.Time.MarshalText() +} + +func (t *Time) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + t.Valid = false + return nil + } + if err := t.Time.UnmarshalText(text); err != nil { + return err + } + t.Valid = true + return nil +} + +// SetValid changes this Time's value and sets it to be non-null. +func (t *Time) SetValid(v time.Time) { + t.Time = v + t.Valid = true +} + +// Ptr returns a pointer to this Time's value, or a nil pointer if this Time is null. +func (t Time) Ptr() *time.Time { + if !t.Valid { + return nil + } + return &t.Time +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 46aec8bd61b..05396911094 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -15,12 +15,6 @@ "revisionTime": "2016-09-17T18:44:01Z" }, { - "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=", - "path": "golang.org/x/net/context/ctxhttp", - "revision": "71a035914f99bb58fe82eac0f1289f10963d876c", - "revisionTime": "2016-09-12T21:59:12Z" - }, - { "checksumSHA1": "6AYg4fjEvFuAVN3wHakGApjhZAM=", "path": "github.com/smartystreets/assertions", "revision": "2063fd1cc7c975db70502811a34b06ad034ccdf2", @@ -55,6 +49,18 @@ "path": "github.com/smartystreets/goconvey/convey/reporting", "revision": "5db88ed452e937f2fd557de6f4f1af7f2eabed0b", "revisionTime": "2016-08-23T18:01:44Z" + }, + { + "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=", + "path": "golang.org/x/net/context/ctxhttp", + "revision": "71a035914f99bb58fe82eac0f1289f10963d876c", + "revisionTime": "2016-09-12T21:59:12Z" + }, + { + "checksumSHA1": "PoHLopxwkiXxa3uVhezeq/qJ/Vo=", + "path": "gopkg.in/guregu/null.v3", + "revision": "41961cea0328defc5f95c1c473f89ebf0d1813f6", + "revisionTime": "2016-02-28T00:53:16Z" } ], "rootPath": "github.com/grafana/grafana"