From 7588ee974d8113b5229a881a7518533deab84b8e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 14 Aug 2016 17:33:18 +0300 Subject: [PATCH 001/149] 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 002/149] 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 003/149] 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 004/149] 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 005/149] 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 006/149] 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 007/149] 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 008/149] 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 009/149] 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 010/149] 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 011/149] 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 012/149] 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 0a44add6c96cee16f71bf4afec50143b7e313783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Sep 2016 11:20:58 +0200 Subject: [PATCH 013/149] feat(adhoc fiters): began work on ad-hoc filters, refactored submenu filters to use gf-form styles, #6038 --- .../app/features/dashboard/submenu/submenu.html | 17 +++++++++++++---- public/app/features/templating/editorCtrl.js | 1 + .../features/templating/templateValuesSrv.js | 7 ++++++- public/app/partials/valueSelectDropdown.html | 4 ++-- public/sass/components/_submenu.scss | 5 +---- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index 464d8c4cecf..f02b3cfc881 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -1,10 +1,19 @@ -
@@ -215,26 +205,26 @@
-
-
+ + -
-
+ + + + +
-
Options
+
Options
Data source @@ -242,58 +232,58 @@
- + -
-
Selection Options
-
- - - - -
-
- Custom all value - -
-
+
+
Selection Options
+
+ + + + +
+
+ Custom all value + +
+
-
-
Value groups/tags (Experimental feature)
-
- -
-
- Tags query - -
-
-
  • Tag values query
  • - -
    -
    +
    +
    Value groups/tags (Experimental feature)
    +
    + +
    +
    + Tags query + +
    +
    +
  • Tag values query
  • + +
    +
    -
    -
    Preview of values (shows max 20)
    -
    -
    - {{option.text}} -
    -
    -
    - +
    +
    Preview of values (shows max 20)
    +
    +
    + {{option.text}} +
    +
    +
    + -
    - +
    +
    diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 82aaa2e5b8f..5d6492849f4 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -2,8 +2,8 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; -import {Variable, containsVariable, assignModelProperties} from './variable'; -import {VariableSrv, variableConstructorMap} from './variable_srv'; +import {Variable, containsVariable, assignModelProperties, variableTypes} from './variable'; +import {VariableSrv} from './variable_srv'; function getNoneOption() { return { text: 'None', value: '', isNone: true }; @@ -37,8 +37,6 @@ export class QueryVariable implements Variable { current: {text: '', value: ''}, }; - supportsMulti = true; - constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) { // copy model properties to this instance assignModelProperties(this, model, this.defaults); @@ -151,4 +149,9 @@ export class QueryVariable implements Variable { } } -variableConstructorMap['query'] = QueryVariable; +variableTypes['query'] = { + name: 'Query', + ctor: QueryVariable, + description: 'Variable values are fetched from a datasource query', + supportsMulti: true, +}; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index b9441b55840..9a478b50840 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -11,6 +11,7 @@ export interface Variable { getModel(); } +export var variableTypes = {}; export function assignModelProperties(target, source, defaults) { _.forEach(defaults, function(value, key) { diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index f31ce7515e0..d2efdeb971e 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -3,9 +3,7 @@ import angular from 'angular'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; -import {Variable} from './variable'; - -export var variableConstructorMap: any = {}; +import {Variable, variableTypes} from './variable'; export class VariableSrv { dashboard: any; @@ -85,7 +83,7 @@ export class VariableSrv { } createVariableFromModel(model) { - var ctor = variableConstructorMap[model.type]; + var ctor = variableTypes[model.type].ctor; if (!ctor) { throw "Unable to find variable constructor for " + model.type; } From cb522d58cd896b316994c56707215a83acc17c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 19 Sep 2016 18:41:42 +0200 Subject: [PATCH 031/149] feat(templating): back to be able to continue work on ad hoc filters, #6048 --- .../app/features/templating/adhoc_variable.ts | 52 +++++++++++++++++++ public/app/features/templating/all.ts | 2 + 2 files changed, 54 insertions(+) create mode 100644 public/app/features/templating/adhoc_variable.ts diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts new file mode 100644 index 00000000000..579b0268efa --- /dev/null +++ b/public/app/features/templating/adhoc_variable.ts @@ -0,0 +1,52 @@ +/// + +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; +import {Variable, assignModelProperties, variableTypes} from './variable'; +import {VariableSrv} from './variable_srv'; + +export class AdhocVariable implements Variable { + + defaults = { + type: 'adhoc', + name: '', + label: '', + hide: 0, + datasource: null, + options: [], + current: {}, + tags: {}, + }; + + /** @ngInject **/ + constructor(private model, private timeSrv, private templateSrv, private variableSrv) { + assignModelProperties(this, model, this.defaults); + } + + setValue(option) { + return Promise.resolve(); + } + + getModel() { + assignModelProperties(this.model, this, this.defaults); + return this.model; + } + + updateOptions() { + return Promise.resolve(); + } + + dependsOn(variable) { + return false; + } + + setValueFromUrl(urlValue) { + return Promise.resolve(); + } +} + +variableTypes['adhoc'] = { + name: 'Ad hoc', + ctor: AdhocVariable, + description: 'Ad hoc filters', +}; diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index 3b36f18b9b0..7205da52d19 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -7,6 +7,7 @@ import {QueryVariable} from './query_variable'; import {DatasourceVariable} from './datasource_variable'; import {CustomVariable} from './custom_variable'; import {ConstantVariable} from './constant_variable'; +import {AdhocVariable} from './adhoc_variable'; export { VariableSrv, @@ -15,4 +16,5 @@ export { DatasourceVariable, CustomVariable, ConstantVariable, + AdhocVariable, } From fc17ed351c859004dabf377b0e1a1b7d55362d44 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 19 Sep 2016 16:48:07 -0400 Subject: [PATCH 032/149] support logging in with grafana.net credentials --- pkg/api/login.go | 1 + pkg/models/models.go | 1 + pkg/setting/setting_oauth.go | 2 +- pkg/social/grafananet_oauth.go | 112 ++++++++++++++++++++++ pkg/social/social.go | 30 +++++- public/app/core/controllers/login_ctrl.js | 8 +- public/app/partials/login.html | 7 +- public/sass/components/_search.scss | 2 +- public/sass/pages/_login.scss | 15 ++- 9 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 pkg/social/grafananet_oauth.go diff --git a/pkg/api/login.go b/pkg/api/login.go index 789765ee01e..d1c571d8e0f 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -27,6 +27,7 @@ func LoginView(c *middleware.Context) { viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub + viewData.Settings["grafanaNetAuthEnabled"] = setting.OAuthService.GrafanaNet viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp diff --git a/pkg/models/models.go b/pkg/models/models.go index 5a53cfdabb3..3f4b27ed6ab 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -7,4 +7,5 @@ const ( GOOGLE TWITTER GENERIC + GRAFANANET ) diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index 71c4ade1468..540b32ad83e 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -11,7 +11,7 @@ type OAuthInfo struct { } type OAuther struct { - GitHub, Google, Twitter, Generic bool + GitHub, Google, Twitter, Generic, GrafanaNet bool OAuthInfos map[string]*OAuthInfo OAuthProviderName string } diff --git a/pkg/social/grafananet_oauth.go b/pkg/social/grafananet_oauth.go new file mode 100644 index 00000000000..05cc7c1f397 --- /dev/null +++ b/pkg/social/grafananet_oauth.go @@ -0,0 +1,112 @@ +package social + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGrafanaNet struct { + *oauth2.Config + url string + allowedOrganizations []string + allowSignup bool +} + +func (s *SocialGrafanaNet) Type() int { + return int(models.GRAFANANET) +} + +func (s *SocialGrafanaNet) IsEmailAllowed(email string) bool { + return true +} + +func (s *SocialGrafanaNet) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool { + if len(s.allowedOrganizations) == 0 { + return true + } + + organizations, err := s.FetchOrganizations(client) + if err != nil { + return false + } + + for _, allowedOrganization := range s.allowedOrganizations { + for _, organization := range organizations { + if organization == allowedOrganization { + return true + } + } + } + + return false +} + +func (s *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) { + type Record struct { + Login string `json:"login"` + } + + url := fmt.Sprintf(s.url + "/api/oauth2/user/orgs") + r, err := client.Get(url) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var logins = make([]string, len(records)) + for i, record := range records { + logins[i] = record.Login + } + + return logins, nil +} + +func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { + var data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + } + + var err error + client := s.Client(oauth2.NoContext, token) + r, err := client.Get(s.url + "/api/oauth2/user") + if err != nil { + return nil, err + } + + defer r.Body.Close() + + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + + userInfo := &BasicUserInfo{ + Identity: strconv.Itoa(data.Id), + Name: data.Name, + Email: data.Email, + } + + if !s.IsOrganizationMember(client) { + return nil, ErrMissingOrganizationMembership + } + + return userInfo, nil +} diff --git a/pkg/social/social.go b/pkg/social/social.go index 66d0f5fa778..83e5aa19b43 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -36,7 +36,7 @@ func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "google", "generic_oauth"} + allOauthes := []string{"github", "google", "generic_oauth", "grafananet"} for _, name := range allOauthes { sec := setting.Cfg.Section("auth." + name) @@ -108,5 +108,33 @@ func NewOAuthService() { allowedOrganizations: allowedOrganizations, } } + + if name == "grafananet" { + setting.OAuthService.GrafanaNet = true + allowedOrganizations := sec.Key("allowed_organizations").Strings(" ") + + url := sec.Key("url").String() + if url == "" { + url = "https://grafana.net" + } + + config := oauth2.Config{ + ClientID: info.ClientId, + ClientSecret: info.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: url + "/oauth2/authorize", + TokenURL: url + "/api/oauth2/token", + }, + RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name, + Scopes: info.Scopes, + } + + SocialMap["grafananet"] = &SocialGrafanaNet{ + Config: &config, + url: url, + allowSignup: info.AllowSignup, + allowedOrganizations: allowedOrganizations, + } + } } } diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 3f31407f454..ee5a1832420 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -17,7 +17,13 @@ function (angular, coreModule, config) { $scope.googleAuthEnabled = config.googleAuthEnabled; $scope.githubAuthEnabled = config.githubAuthEnabled; - $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled; + $scope.grafanaNetAuthEnabled = config.grafanaNetAuthEnabled; + $scope.oauthEnabled = ( + config.githubAuthEnabled + || config.googleAuthEnabled + || config.grafanaNetAuthEnabled + || config.genericOAuthEnabled + ); $scope.allowUserPassLogin = config.allowUserPassLogin; $scope.genericOAuthEnabled = config.genericOAuthEnabled; $scope.oauthProviderName = config.oauthProviderName; diff --git a/public/app/partials/login.html b/public/app/partials/login.html index f4f5fb26d7e..d9bf7ecdb18 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -59,10 +59,13 @@ with Github + + with Grafana.net + - with {{oauthProviderName || "OAuth 2"}} - + with {{oauthProviderName || "OAuth 2"}} + diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 563f89919e5..2eeb34ec0b4 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -111,7 +111,7 @@ font-size: $font-size-sm; padding-right: 7rem; background: url(../img/grafana_net_logo.svg); - background-size: 6.5rem 3rem; + background-size: 6.5rem; background-repeat: no-repeat; background-position: right; position: relative; diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 8f376dfae71..8641aabe1b1 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -112,6 +112,19 @@ background: #555; color: white; } + + .btn-grafana-net { + background: url(../img/grafana_net_logo.svg); + background-size: 10rem; + background-repeat: no-repeat; + background-position: right 35%; + overflow: hidden; + padding-right: 10.5rem; + + span { + display: none; + } + } } .password-recovery { @@ -157,7 +170,7 @@ .invite-box { text-align: center; border: 1px solid $tight-form-func-bg; - background-color: $panel-bg; + background-color: $panel-bg; max-width: 800px; margin-left: auto; margin-right: auto; From 490141da8252aaf80c6bd0660ee253cecdbbfda1 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Sat, 19 Mar 2016 23:02:42 +0900 Subject: [PATCH 033/149] (cloudwatch) expand multi select template variable --- .../datasource/cloudwatch/datasource.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index d9fc6464491..c98c7e09b2b 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -23,6 +23,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { var queries = []; options = angular.copy(options); + options.targets = this.expandTemplateVariable(options.targets); _.each(options.targets, function(target) { if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) { return; @@ -337,6 +338,36 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); } + this.expandTemplateVariable = function(targets) { + return _.chain(targets) + .map(function(target) { + var dimensionKey = null; + var variableName = null; + _.each(target.dimensions, function(v, k) { + if (templateSrv.variableExists(v)) { + dimensionKey = k; + variableName = v; + } + }); + if (dimensionKey) { + var variable = _.find(templateSrv.variables, function(variable) { + return templateSrv.containsVariable(variableName, variable.name); + }); + return _.chain(variable.options) + .filter(function(v) { + return v.selected; + }) + .map(function(v) { + var t = angular.copy(target); + t.dimensions[dimensionKey] = v.value; + return t; + }).value(); + } else { + return [target]; + } + }).flatten().value(); + }; + this.convertToCloudWatchTime = function(date, roundUp) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); From 540436e9d5f9f068882e952ae65498e09a9748a7 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 20 Apr 2016 21:06:53 +0900 Subject: [PATCH 034/149] inject templateSrv --- public/app/plugins/datasource/cloudwatch/datasource.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index c98c7e09b2b..1a3d990eac6 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -23,7 +23,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { var queries = []; options = angular.copy(options); - options.targets = this.expandTemplateVariable(options.targets); + options.targets = this.expandTemplateVariable(options.targets, templateSrv); _.each(options.targets, function(target) { if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) { return; @@ -338,7 +338,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); } - this.expandTemplateVariable = function(targets) { + this.expandTemplateVariable = function(targets, templateSrv) { return _.chain(targets) .map(function(target) { var dimensionKey = null; From 481cc012423c4d667babeb96ef0800b327526fcb Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 19 Apr 2016 23:27:18 +0900 Subject: [PATCH 035/149] (cloudwatch) add test for expand templater variables --- .../cloudwatch/specs/datasource_specs.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index 86e085b3f6f..0b9b3b53fb6 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts @@ -98,6 +98,38 @@ describe('CloudWatchDatasource', function() { }); ctx.$rootScope.$apply(); }); + + it('should generate the correct targets by expanding template variables', function() { + var templateSrv = { + variables: [ + { + name: 'instance_id', + options: [ + { value: 'i-23456789', selected: false }, + { value: 'i-34567890', selected: true } + ] + } + ], + variableExists: function (e) { return true; }, + containsVariable: function (str, variableName) { return str.indexOf('$' + variableName) !== -1; } + }; + + var targets = [ + { + region: 'us-east-1', + namespace: 'AWS/EC2', + metricName: 'CPUUtilization', + dimensions: { + InstanceId: '$instance_id' + }, + statistics: ['Average'], + period: 300 + } + ]; + + var result = ctx.ds.expandTemplateVariable(targets, templateSrv); + expect(result[0].dimensions.InstanceId).to.be('i-34567890'); + }); }); function describeMetricFindQuery(query, func) { From 07cce5f6bd06a5a5e36b63da39efb2b24b2f5b04 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 6 Sep 2016 01:22:39 +0900 Subject: [PATCH 036/149] refactor --- .../datasource/cloudwatch/datasource.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 1a3d990eac6..10bf4e1295c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -338,7 +338,20 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); } + this.getExpandedVariables = function(target, dimensionKey, variable) { + return _.chain(variable.options) + .filter(function(v) { + return v.selected; + }) + .map(function(v) { + var t = angular.copy(target); + t.dimensions[dimensionKey] = v.value; + return t; + }).value(); + }; + this.expandTemplateVariable = function(targets, templateSrv) { + var self = this; return _.chain(targets) .map(function(target) { var dimensionKey = null; @@ -353,15 +366,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { var variable = _.find(templateSrv.variables, function(variable) { return templateSrv.containsVariable(variableName, variable.name); }); - return _.chain(variable.options) - .filter(function(v) { - return v.selected; - }) - .map(function(v) { - var t = angular.copy(target); - t.dimensions[dimensionKey] = v.value; - return t; - }).value(); + return self.getExpandedVariables(target, dimensionKey, variable); } else { return [target]; } From 71b5007ec7722d3a4ab4e6fb09694a15e6f67146 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 6 Sep 2016 01:51:10 +0900 Subject: [PATCH 037/149] refactor --- .../app/plugins/datasource/cloudwatch/datasource.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 10bf4e1295c..4365c2e2596 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -354,17 +354,13 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { var self = this; return _.chain(targets) .map(function(target) { - var dimensionKey = null; - var variableName = null; - _.each(target.dimensions, function(v, k) { - if (templateSrv.variableExists(v)) { - dimensionKey = k; - variableName = v; - } + var dimensionKey = _.findKey(target.dimensions, function(v) { + return templateSrv.variableExists(v); }); + if (dimensionKey) { var variable = _.find(templateSrv.variables, function(variable) { - return templateSrv.containsVariable(variableName, variable.name); + return templateSrv.containsVariable(target.dimensions[dimensionKey], variable.name); }); return self.getExpandedVariables(target, dimensionKey, variable); } else { From 46866add7e0f9bef50a8a72ea808743730a0dc97 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 20 Sep 2016 08:13:53 +0200 Subject: [PATCH 038/149] docs(changelog): add note about closing #5003 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 505a5f433bb..92f4ddfc586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613) * **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827) * **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) ### Breaking changes * **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971) From ec452dd70432fa675866afa0a65e6ebc2766d415 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 20 Sep 2016 08:18:15 +0200 Subject: [PATCH 039/149] docs(conf): remove internal options since its not working atm --- conf/defaults.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 53a034bfeaa..49329a0a4ac 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -404,7 +404,7 @@ url = https://grafana.net #################################### External image storage ########################## [external_image_storage] -# You can choose between (s3, webdav or internal) +# You can choose between (s3, webdav) provider = s3 [external_image_storage.s3] From d2fb660557bb1bc1d60faa5cee5a401c7659c00e Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 20 Sep 2016 10:10:27 +0200 Subject: [PATCH 040/149] fix(cli): remove unused logging --- pkg/cmd/grafana-cli/services/services.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index ed1a69d50a8..e2ccce8418f 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -141,8 +141,6 @@ func createRequest(repoUrl string, subPaths ...string) ([]byte, error) { req, err := http.NewRequest(http.MethodGet, u.String(), nil) - logger.Info("grafanaVersion ", grafanaVersion) - req.Header.Set("grafana-version", grafanaVersion) req.Header.Set("User-Agent", "grafana "+grafanaVersion) From e926b01185f63c5934b72b1e605e0b3659323150 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 20 Sep 2016 12:23:31 +0300 Subject: [PATCH 041/149] Bug fixes for flexible Y-Min and Y-Max settings (#6066) * fix(flexible_y-min/max): fixed negative values support and added tests for this. * fix(flexible_y-min/max): fixed issue with Y-Min and Y-Max values stored as numbers. Issue: panels with configured Y-Min and Y-Max don't display anything after upgrade. --- public/app/plugins/panel/graph/graph.js | 3 +- .../plugins/panel/graph/specs/graph_specs.ts | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 69154ffed65..2e7353b6048 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -354,7 +354,8 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) { function parseThresholdExpr(expr) { var match, operator, value, precision; - match = expr.match(/\s*([<=>~]*)\W*(\d+(\.\d+)?)/); + expr = String(expr); + match = expr.match(/\s*([<=>~]*)\s*(\-?\d+(\.\d+)?)/); if (match) { operator = match[1]; value = parseFloat(match[2]); diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 10c81b7212d..2065bffb130 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -312,5 +312,52 @@ describe('grafanaGraph', function() { 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 20f7eee8ccf84a44c03c41986817af2f025f6b38 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 20 Sep 2016 12:38:18 +0300 Subject: [PATCH 042/149] fix(graph): increase Y min and max range when series values are the same, (#6082) fixes #6070, #6050. --- public/vendor/flot/jquery.flot.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 45d76b440c3..7159bcee30a 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1663,8 +1663,10 @@ Licensed under the MIT license. delta = max - min; if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; + // Grafana fix: wide Y min and max using increased wideFactor + // when all series values are the same + var wideFactor = 0.25; + var widen = max == 0 ? 1 : max * wideFactor; if (opts.min == null) min -= widen; From 6e429b8575edc2a6ec7c27bb276534676da21bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 11:57:43 +0200 Subject: [PATCH 043/149] feat(adhoc filters): good progress on ad hoc filters and sync from to url, #6038 --- .../app/features/dashboard/ad_hoc_filters.ts | 36 ++- public/app/features/dashboard/viewStateSrv.js | 20 -- .../app/features/templating/adhoc_variable.ts | 28 ++- .../features/templating/constant_variable.ts | 7 + .../features/templating/custom_variable.ts | 11 +- .../templating/datasource_variable.ts | 7 +- .../features/templating/interval_variable.ts | 7 +- .../app/features/templating/query_variable.ts | 12 +- .../templating/specs/template_srv_specs.ts | 237 ++++++++++++++++++ public/app/features/templating/templateSrv.js | 13 +- public/app/features/templating/variable.ts | 1 + .../app/features/templating/variable_srv.ts | 18 ++ .../plugins/datasource/influxdb/datasource.ts | 6 +- public/test/specs/templateSrv-specs.js | 235 ----------------- 14 files changed, 351 insertions(+), 287 deletions(-) create mode 100644 public/app/features/templating/specs/template_srv_specs.ts delete mode 100644 public/test/specs/templateSrv-specs.js diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index 426f6162467..f962f8ca2f4 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -21,7 +21,7 @@ export class AdHocFiltersCtrl { if (this.variable.value && !_.isArray(this.variable.value)) { } - for (let tag of this.variable.tags) { + for (let tag of this.variable.filters) { if (this.segments.length > 0) { this.segments.push(this.uiSegmentSrv.newCondition('AND')); } @@ -38,7 +38,11 @@ export class AdHocFiltersCtrl { getOptions(segment, index) { if (segment.type === 'operator') { - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>', '=~', '!~'])); + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '>', '=~', '!~'])); + } + + if (segment.type === 'condition') { + return this.$q.when([this.uiSegmentSrv.newSegment('AND')]); } return this.datasourceSrv.get(this.variable.datasource).then(ds => { @@ -100,37 +104,45 @@ export class AdHocFiltersCtrl { } updateVariableModel() { - var tags = []; - var tagIndex = -1; - var tagOperator = ""; + var filters = []; + var filterIndex = -1; + var operator = ""; + var hasFakes = false; - this.segments.forEach((segment, index) => { - if (segment.fake) { + this.segments.forEach(segment => { + if (segment.type === 'value' && segment.fake) { + hasFakes = true; return; } switch (segment.type) { case 'key': { - tags.push({key: segment.value}); - tagIndex += 1; + filters.push({key: segment.value}); + filterIndex += 1; break; } case 'value': { - tags[tagIndex].value = segment.value; + filters[filterIndex].value = segment.value; break; } case 'operator': { - tags[tagIndex].operator = segment.value; + filters[filterIndex].operator = segment.value; break; } case 'condition': { + filters[filterIndex].condition = segment.value; break; } } }); + if (hasFakes) { + return; + } + + this.variable.setFilters(filters); + this.$rootScope.$emit('template-variable-value-updated'); this.$rootScope.$broadcast('refresh'); - this.variable.tags = tags; } } diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 758d8ab7067..73f0c038c7f 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -34,10 +34,6 @@ function (angular, _, $) { $location.search(urlParams); }); - $scope.onAppEvent('template-variable-value-updated', function() { - self.updateUrlParamsWithCurrentVariables(); - }); - $scope.onAppEvent('$routeUpdate', function() { var urlState = self.getQueryStringState(); if (self.needsSync(urlState)) { @@ -57,22 +53,6 @@ function (angular, _, $) { this.expandRowForPanel(); } - DashboardViewState.prototype.updateUrlParamsWithCurrentVariables = function() { - // update url - var params = $location.search(); - // remove variable params - _.each(params, function(value, key) { - if (key.indexOf('var-') === 0) { - delete params[key]; - } - }); - - // add new values - templateSrv.fillVariableValuesForUrl(params); - // update url - $location.search(params); - }; - DashboardViewState.prototype.expandRowForPanel = function() { if (!this.state.panelId) { return; } diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index 579b0268efa..ea942beaef3 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -6,6 +6,7 @@ import {Variable, assignModelProperties, variableTypes} from './variable'; import {VariableSrv} from './variable_srv'; export class AdhocVariable implements Variable { + filters: any[]; defaults = { type: 'adhoc', @@ -13,9 +14,7 @@ export class AdhocVariable implements Variable { label: '', hide: 0, datasource: null, - options: [], - current: {}, - tags: {}, + filters: [], }; /** @ngInject **/ @@ -41,8 +40,31 @@ export class AdhocVariable implements Variable { } setValueFromUrl(urlValue) { + if (!_.isArray(urlValue)) { + urlValue = [urlValue]; + } + + this.filters = urlValue.map(item => { + var values = item.split('|'); + return { + key: values[0], + operator: values[1], + value: values[2], + }; + }); + return Promise.resolve(); } + + getValueForUrl() { + return this.filters.map(filter => { + return filter.key + '|' + filter.operator + '|' + filter.value; + }); + } + + setFilters(filters: any[]) { + this.filters = filters; + } } variableTypes['adhoc'] = { diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index f0a659e0c20..9fe2acfdfb2 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -7,6 +7,7 @@ import {VariableSrv} from './variable_srv'; export class ConstantVariable implements Variable { query: string; options: any[]; + current: any; defaults = { type: 'constant', @@ -14,6 +15,7 @@ export class ConstantVariable implements Variable { hide: 2, label: '', query: '', + current: {}, }; /** @ngInject */ @@ -43,6 +45,11 @@ export class ConstantVariable implements Variable { setValueFromUrl(urlValue) { return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + return this.current.value; + } + } variableTypes['constant'] = { diff --git a/public/app/features/templating/custom_variable.ts b/public/app/features/templating/custom_variable.ts index 77dfd57cae8..90ce08cf9e4 100644 --- a/public/app/features/templating/custom_variable.ts +++ b/public/app/features/templating/custom_variable.ts @@ -10,6 +10,7 @@ export class CustomVariable implements Variable { options: any; includeAll: boolean; multi: boolean; + current: any; defaults = { type: 'custom', @@ -17,10 +18,11 @@ export class CustomVariable implements Variable { label: '', hide: 0, options: [], - current: {text: '', value: ''}, + current: {}, query: '', includeAll: false, multi: false, + allValue: null, }; /** @ngInject **/ @@ -61,6 +63,13 @@ export class CustomVariable implements Variable { setValueFromUrl(urlValue) { return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + if (this.current.text === 'All') { + return 'All'; + } + return this.current.value; + } } variableTypes['custom'] = { diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index b23b5e04e42..96776c31163 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -9,13 +9,14 @@ export class DatasourceVariable implements Variable { regex: any; query: string; options: any; + current: any; defaults = { type: 'datasource', name: '', hide: 0, label: '', - current: {text: '', value: ''}, + current: {}, regex: '', options: [], query: '', @@ -73,6 +74,10 @@ export class DatasourceVariable implements Variable { setValueFromUrl(urlValue) { return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + return this.current.value; + } } variableTypes['datasource'] = { diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index 94bb608e7e3..d53e44ae533 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -12,6 +12,7 @@ export class IntervalVariable implements Variable { auto: boolean; query: string; refresh: number; + current: any; defaults = { type: 'interval', @@ -20,7 +21,7 @@ export class IntervalVariable implements Variable { label: '', refresh: 2, options: [], - current: {text: '', value: ''}, + current: {}, query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d', auto: false, auto_min: '10s', @@ -75,6 +76,10 @@ export class IntervalVariable implements Variable { this.updateAutoValue(); return this.variableSrv.setOptionFromUrl(this, urlValue); } + + getValueForUrl() { + return this.current.value; + } } variableTypes['interval'] = { diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 5d6492849f4..a9ac6aa02b9 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -33,8 +33,11 @@ export class QueryVariable implements Variable { name: '', multi: false, includeAll: false, + allValue: null, options: [], - current: {text: '', value: ''}, + current: {}, + tagsQuery: null, + tagValuesQuery: null, }; constructor(private model, private datasourceSrv, private templateSrv, private variableSrv, private $q) { @@ -56,6 +59,13 @@ export class QueryVariable implements Variable { return this.variableSrv.setOptionFromUrl(this, urlValue); } + getValueForUrl() { + if (this.current.text === 'All') { + return 'All'; + } + return this.current.value; + } + updateOptions() { return this.datasourceSrv.get(this.datasource) .then(this.updateOptionsFromMetricFindQuery.bind(this)) diff --git a/public/app/features/templating/specs/template_srv_specs.ts b/public/app/features/templating/specs/template_srv_specs.ts new file mode 100644 index 00000000000..94b1e211293 --- /dev/null +++ b/public/app/features/templating/specs/template_srv_specs.ts @@ -0,0 +1,237 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; + +import '../all'; +import {Emitter} from 'app/core/core'; + +describe('templateSrv', function() { + var _templateSrv, _variableSrv; + + beforeEach(angularMocks.module('grafana.core')); + beforeEach(angularMocks.module('grafana.services')); + + beforeEach(angularMocks.inject(function(variableSrv, templateSrv) { + _templateSrv = templateSrv; + _variableSrv = variableSrv; + })); + + function initTemplateSrv(variables) { + _variableSrv.init({ + templating: {list: variables}, + events: new Emitter(), + }); + } + + describe('init', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle'}}]); + }); + + it('should initialize template data', function() { + var target = _templateSrv.replace('this.[[test]].filters'); + expect(target).to.be('this.oogle.filters'); + }); + }); + + describe('replace can pass scoped vars', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: {value: 'oogle' }}]); + }); + + it('should replace $test with scoped value', function() { + var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}}); + expect(target).to.be('this.mupp.filters'); + }); + + it('should replace $test with scoped text', function() { + var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}}); + expect(target).to.be('this.asd.filters'); + }); + }); + + describe('replace can pass multi / all format', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: {value: ['value1', 'value2'] }}]); + }); + + it('should replace $test with globbed value', function() { + var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + expect(target).to.be('this.{value1,value2}.filters'); + }); + + it('should replace $test with piped value', function() { + var target = _templateSrv.replace('this=$test', {}, 'pipe'); + expect(target).to.be('this=value1|value2'); + }); + + it('should replace $test with piped value', function() { + var target = _templateSrv.replace('this=$test', {}, 'pipe'); + expect(target).to.be('this=value1|value2'); + }); + }); + + describe('variable with all option', function() { + beforeEach(function() { + initTemplateSrv([{ + type: 'query', + name: 'test', + current: {value: '$__all' }, + options: [ + {value: '$__all'}, {value: 'value1'}, {value: 'value2'} + ] + }]); + }); + + it('should replace $test with formatted all value', function() { + var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + expect(target).to.be('this.{value1,value2}.filters'); + }); + }); + + describe('variable with all option and custom value', function() { + beforeEach(function() { + initTemplateSrv([{ + type: 'query', + name: 'test', + current: {value: '$__all' }, + allValue: '*', + options: [ + {value: 'value1'}, {value: 'value2'} + ] + }]); + }); + + it('should replace $test with formatted all value', function() { + var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + expect(target).to.be('this.*.filters'); + }); + + it('should not escape custom all value', function() { + var target = _templateSrv.replace('this.$test', {}, 'regex'); + expect(target).to.be('this.*'); + }); + }); + + describe('lucene format', function() { + it('should properly escape $test with lucene escape sequences', function() { + initTemplateSrv([{type: 'query', name: 'test', current: {value: 'value/4' }}]); + var target = _templateSrv.replace('this:$test', {}, 'lucene'); + expect(target).to.be("this:value\\\/4"); + }); + }); + + describe('format variable to string values', function() { + it('single value should return value', function() { + var result = _templateSrv.formatValue('test'); + expect(result).to.be('test'); + }); + + it('multi value and glob format should render glob string', function() { + var result = _templateSrv.formatValue(['test','test2'], 'glob'); + expect(result).to.be('{test,test2}'); + }); + + it('multi value and lucene should render as lucene expr', function() { + var result = _templateSrv.formatValue(['test','test2'], 'lucene'); + expect(result).to.be('("test" OR "test2")'); + }); + + it('multi value and regex format should render regex string', function() { + var result = _templateSrv.formatValue(['test.','test2'], 'regex'); + expect(result).to.be('(test\\.|test2)'); + }); + + it('multi value and pipe should render pipe string', function() { + var result = _templateSrv.formatValue(['test','test2'], 'pipe'); + expect(result).to.be('test|test2'); + }); + + it('slash should be properly escaped in regex format', function() { + var result = _templateSrv.formatValue('Gi3/14', 'regex'); + expect(result).to.be('Gi3\\/14'); + }); + + }); + + describe('can check if variable exists', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]); + }); + + it('should return true if exists', function() { + var result = _templateSrv.variableExists('$test'); + expect(result).to.be(true); + }); + }); + + describe('can hightlight variables in string', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: { value: 'oogle' } }]); + }); + + it('should insert html', function() { + var result = _templateSrv.highlightVariablesAsHtml('$test'); + expect(result).to.be('$test'); + }); + + it('should insert html anywhere in string', function() { + var result = _templateSrv.highlightVariablesAsHtml('this $test ok'); + expect(result).to.be('this $test ok'); + }); + + it('should ignore if variables does not exist', function() { + var result = _templateSrv.highlightVariablesAsHtml('this $google ok'); + expect(result).to.be('this $google ok'); + }); + }); + + describe('updateTemplateData with simple value', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: { value: 'muuuu' } }]); + }); + + it('should set current value and update template data', function() { + var target = _templateSrv.replace('this.[[test]].filters'); + expect(target).to.be('this.muuuu.filters'); + }); + }); + + describe('fillVariableValuesForUrl with multi value', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]); + }); + + it('should set multiple url params', function() { + var params = {}; + _templateSrv.fillVariableValuesForUrl(params); + expect(params['var-test']).to.eql(['val1', 'val2']); + }); + }); + + describe('fillVariableValuesForUrl with multi value and scopedVars', function() { + beforeEach(function() { + initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]); + }); + + it('should set scoped value as url params', function() { + var params = {}; + _templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}}); + expect(params['var-test']).to.eql('val1'); + }); + }); + + describe('replaceWithText', function() { + beforeEach(function() { + initTemplateSrv([ + {type: 'query', name: 'server', current: { value: '{asd,asd2}', text: 'All' } }, + {type: 'interval', name: 'period', current: { value: '$__auto_interval', text: 'auto' } } + ]); + _templateSrv.setGrafanaVariable('$__auto_interval', '13m'); + _templateSrv.updateTemplateData(); + }); + + it('should replace with text except for grafanaVariables', function() { + var target = _templateSrv.replaceWithText('Server: $server, period: $period'); + expect(target).to.be('Server: All, period: 13m'); + }); + }); +}); diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index 7615aa7da0f..bca814d29a4 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -180,18 +180,11 @@ function (angular, _, kbn) { this.fillVariableValuesForUrl = function(params, scopedVars) { _.each(this.variables, function(variable) { - var current = variable.current; - var value = current.value; - - if (current.text === 'All') { - value = 'All'; - } - if (scopedVars && scopedVars[variable.name] !== void 0) { - value = scopedVars[variable.name].value; + params['var-' + variable.name] = scopedVars[variable.name].value; + } else { + params['var-' + variable.name] = variable.getValueForUrl(); } - - params['var-' + variable.name] = value; }); }; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 9a478b50840..3e12b65ec16 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -8,6 +8,7 @@ export interface Variable { updateOptions(); dependsOn(variable); setValueFromUrl(urlValue); + getValueForUrl(); getModel(); } diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index d2efdeb971e..b7013d517f4 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -14,6 +14,7 @@ export class VariableSrv { constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) { // update time variant variables $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope); + $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope); } init(dashboard) { @@ -210,6 +211,23 @@ export class VariableSrv { this.selectOptionsForCurrentValue(variable); return this.variableUpdated(variable); } + + updateUrlParamsWithCurrentVariables() { + // update url + var params = this.$location.search(); + + // remove variable params + _.each(params, function(value, key) { + if (key.indexOf('var-') === 0) { + delete params[key]; + } + }); + + // add new values + this.templateSrv.fillVariableValuesForUrl(params); + // update url + this.$location.search(params); + } } coreModule.service('variableSrv', VariableSrv); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 4bb183474fb..81cffd18364 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -56,9 +56,9 @@ export default class InfluxDatasource { // apply add hoc filters for (let variable of this.templateSrv.variables) { if (variable.type === 'adhoc' && variable.datasource === this.name) { - for (let tag of variable.tags) { - if (tag.key !== undefined && tag.value !== undefined) { - target.tags.push({key: tag.key, value: tag.value, condition: 'AND'}); + for (let filter of variable.filters) { + if (filter.key !== undefined && filter.value !== undefined) { + target.tags.push({key: filter.key, value: filter.value, condition: filter.condition, operator: filter.operator}); } } } diff --git a/public/test/specs/templateSrv-specs.js b/public/test/specs/templateSrv-specs.js deleted file mode 100644 index 3dac01fbdf2..00000000000 --- a/public/test/specs/templateSrv-specs.js +++ /dev/null @@ -1,235 +0,0 @@ -define([ - '../mocks/dashboard-mock', - 'lodash', - 'app/features/templating/templateSrv' -], function(dashboardMock) { - 'use strict'; - - describe('templateSrv', function() { - var _templateSrv; - var _dashboard; - - beforeEach(module('grafana.services')); - beforeEach(module(function() { - _dashboard = dashboardMock.create(); - })); - - beforeEach(inject(function(templateSrv) { - _templateSrv = templateSrv; - })); - - describe('init', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); - }); - - it('should initialize template data', function() { - var target = _templateSrv.replace('this.[[test]].filters'); - expect(target).to.be('this.oogle.filters'); - }); - }); - - describe('replace can pass scoped vars', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); - }); - - it('should replace $test with scoped value', function() { - var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}}); - expect(target).to.be('this.mupp.filters'); - }); - - it('should replace $test with scoped text', function() { - var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}}); - expect(target).to.be('this.asd.filters'); - }); - }); - - describe('replace can pass multi / all format', function() { - beforeEach(function() { - _templateSrv.init([{name: 'test', current: {value: ['value1', 'value2'] }}]); - }); - - it('should replace $test with globbed value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); - expect(target).to.be('this.{value1,value2}.filters'); - }); - - it('should replace $test with piped value', function() { - var target = _templateSrv.replace('this=$test', {}, 'pipe'); - expect(target).to.be('this=value1|value2'); - }); - - it('should replace $test with piped value', function() { - var target = _templateSrv.replace('this=$test', {}, 'pipe'); - expect(target).to.be('this=value1|value2'); - }); - }); - - describe('variable with all option', function() { - beforeEach(function() { - _templateSrv.init([{ - name: 'test', - current: {value: '$__all' }, - options: [ - {value: '$__all'}, {value: 'value1'}, {value: 'value2'} - ] - }]); - }); - - it('should replace $test with formatted all value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); - expect(target).to.be('this.{value1,value2}.filters'); - }); - }); - - describe('variable with all option and custom value', function() { - beforeEach(function() { - _templateSrv.init([{ - name: 'test', - current: {value: '$__all' }, - allValue: '*', - options: [ - {value: 'value1'}, {value: 'value2'} - ] - }]); - }); - - it('should replace $test with formatted all value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); - expect(target).to.be('this.*.filters'); - }); - - it('should not escape custom all value', function() { - var target = _templateSrv.replace('this.$test', {}, 'regex'); - expect(target).to.be('this.*'); - }); - }); - - describe('lucene format', function() { - it('should properly escape $test with lucene escape sequences', function() { - _templateSrv.init([{name: 'test', current: {value: 'value/4' }}]); - var target = _templateSrv.replace('this:$test', {}, 'lucene'); - expect(target).to.be("this:value\\\/4"); - }); - }); - - describe('format variable to string values', function() { - it('single value should return value', function() { - var result = _templateSrv.formatValue('test'); - expect(result).to.be('test'); - }); - - it('multi value and glob format should render glob string', function() { - var result = _templateSrv.formatValue(['test','test2'], 'glob'); - expect(result).to.be('{test,test2}'); - }); - - it('multi value and lucene should render as lucene expr', function() { - var result = _templateSrv.formatValue(['test','test2'], 'lucene'); - expect(result).to.be('("test" OR "test2")'); - }); - - it('multi value and regex format should render regex string', function() { - var result = _templateSrv.formatValue(['test.','test2'], 'regex'); - expect(result).to.be('(test\\.|test2)'); - }); - - it('multi value and pipe should render pipe string', function() { - var result = _templateSrv.formatValue(['test','test2'], 'pipe'); - expect(result).to.be('test|test2'); - }); - - it('slash should be properly escaped in regex format', function() { - var result = _templateSrv.formatValue('Gi3/14', 'regex'); - expect(result).to.be('Gi3\\/14'); - }); - - }); - - describe('can check if variable exists', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); - }); - - it('should return true if exists', function() { - var result = _templateSrv.variableExists('$test'); - expect(result).to.be(true); - }); - }); - - describe('can hightlight variables in string', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); - }); - - it('should insert html', function() { - var result = _templateSrv.highlightVariablesAsHtml('$test'); - expect(result).to.be('$test'); - }); - - it('should insert html anywhere in string', function() { - var result = _templateSrv.highlightVariablesAsHtml('this $test ok'); - expect(result).to.be('this $test ok'); - }); - - it('should ignore if variables does not exist', function() { - var result = _templateSrv.highlightVariablesAsHtml('this $google ok'); - expect(result).to.be('this $google ok'); - }); - }); - - describe('updateTemplateData with simple value', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]); - }); - - it('should set current value and update template data', function() { - var target = _templateSrv.replace('this.[[test]].filters'); - expect(target).to.be('this.muuuu.filters'); - }); - }); - - describe('fillVariableValuesForUrl with multi value', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]); - }); - - it('should set multiple url params', function() { - var params = {}; - _templateSrv.fillVariableValuesForUrl(params); - expect(params['var-test']).to.eql(['val1', 'val2']); - }); - }); - - describe('fillVariableValuesForUrl with multi value and scopedVars', function() { - beforeEach(function() { - _templateSrv.init([{ name: 'test', current: { value: ['val1', 'val2'] }}]); - }); - - it('should set multiple url params', function() { - var params = {}; - _templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}}); - expect(params['var-test']).to.eql('val1'); - }); - }); - - describe('replaceWithText', function() { - beforeEach(function() { - _templateSrv.init([ - { name: 'server', current: { value: '{asd,asd2}', text: 'All' } }, - { name: 'period', current: { value: '$__auto_interval', text: 'auto' } } - ]); - _templateSrv.setGrafanaVariable('$__auto_interval', '13m'); - _templateSrv.updateTemplateData(); - }); - - it('should replace with text except for grafanaVariables', function() { - var target = _templateSrv.replaceWithText('Server: $server, period: $period'); - expect(target).to.be('Server: All, period: 13m'); - }); - }); - - }); - -}); From 159a8bf7346abb9e98219a6724b39099b3e3904f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 20 Sep 2016 16:29:10 +0300 Subject: [PATCH 044/149] fix(singlestat): update singlestat options tab styles. (#6084) --- .../app/plugins/panel/singlestat/editor.html | 351 +++++++++--------- 1 file changed, 168 insertions(+), 183 deletions(-) diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index c0c522abac9..a4fd27bd080 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -1,207 +1,192 @@
    -
    -
    -
      -
    • - Big value -
    • -
    • - Prefix -
    • -
    • - -
    • -
    • - Value -
    • -
    • - -
    • -
    • - Postfix -
    • -
    • - -
    • -
    -
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    -
    -
      -
    • - Font size -
    • -
    • - Prefix -
    • -
    • - -
    • -
    • - Value -
    • -
    • - -
    • -
    • - Postfix -
    • -
    • - -
    • -
    -
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    -
    -
      -
    • - Unit -
    • - -
    • Decimals
    • -
    • - -
    • -
    -
    +
    +
    +
    + + +
    -
    -
    -
      -
    • - Coloring -
    • -
    • - Background  - - -
    • -
    • - Value  - - -
    • -
    • - ThresholdsDefine two threshold values<br /> 50,80 will produce: <50 = Green, 50:80 = Yellow, >80 = Red -
    • -
    • - -
    • -
    • - Colors -
    • -
    • - - - -
    • -
    • - invert order -
    • -
    -
    +
    Coloring
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + + + + +
    + +
    +
    -
    -
    -
      -
    • - Spark lines -
    • -
    • - Show  - - -
    • -
    • - Background mode  - - -
    • -
    • - Line Color -
    • -
    • - -
    • -
    • - Fill Color -
    • -
    • - -
    • -
    -
    +
    Spark lines
    +
    +
    + + +
    + + + + +
    +
    + + + + +
    -
    -
    -
      -
    • - Gauge -
    • -
    • - Show  - - -
    • -
    • - Min -
    • -
    • - -
    • -
    • - Max -
    • -
    • - - -   - Min value is bigger than max. -   - -
    • -
    -
    +
    Gauge
    +
    +
    + +
    + + +
    +
    + + +
    +
    + +
    -
    -
  • - Threshold labels  - - -
  • -
  • - Threshold markers  - - -
  • -
    +
    + +
    From dcd96c90e2128bf62f9933c3e3b439df640360ac Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 20 Sep 2016 10:09:20 -0400 Subject: [PATCH 045/149] add grafana.net auth section to defaults.ini, normalize section heading line lengths --- conf/defaults.ini | 49 ++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 53a034bfeaa..92a0cf10b8c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -9,7 +9,7 @@ app_mode = production # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty instance_name = ${HOSTNAME} -#################################### Paths #################################### +#################################### Paths ############################### [paths] # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) # @@ -23,7 +23,7 @@ logs = data/log # plugins = data/plugins -#################################### Server #################################### +#################################### Server ############################## [server] # Protocol (http or https) protocol = http @@ -57,7 +57,7 @@ enable_gzip = false cert_file = cert_key = -#################################### Database #################################### +#################################### Database ############################ [database] # You can configure the database connection by specifying type, host, name, user and password # as seperate properties or as on string using the url propertie. @@ -84,7 +84,7 @@ server_cert_name = # For "sqlite3" only, path relative to data_path setting path = grafana.db -#################################### Session #################################### +#################################### Session ############################# [session] # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" provider = file @@ -112,7 +112,7 @@ cookie_secure = false session_life_time = 86400 gc_interval_time = 86400 -#################################### Analytics #################################### +#################################### Analytics ########################### [analytics] # Server reporting, sends usage counters to stats.grafana.org every 24 hours. # No ip addresses are being tracked, only simple counters to track @@ -133,7 +133,7 @@ google_analytics_ua_id = # Google Tag Manager ID, only enabled if you specify an id here google_tag_manager_id = -#################################### Security #################################### +#################################### Security ############################ [security] # default admin user, created on startup admin_user = admin @@ -161,7 +161,7 @@ external_enabled = true external_snapshot_url = https://snapshots-origin.raintank.io external_snapshot_name = Publish to snapshot.raintank.io -#################################### Users #################################### +#################################### Users ############################### [users] # disable user signup / registration allow_sign_up = true @@ -187,7 +187,7 @@ default_theme = dark # Allow users to sign in using username and password allow_user_pass_login = true -#################################### Anonymous Auth ########################## +#################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access enabled = false @@ -198,7 +198,7 @@ org_name = Main Org. # specify role for unauthenticated users org_role = Viewer -#################################### Github Auth ########################## +#################################### Github Auth ######################### [auth.github] enabled = false allow_sign_up = false @@ -211,7 +211,7 @@ api_url = https://api.github.com/user team_ids = allowed_organizations = -#################################### Google Auth ########################## +#################################### Google Auth ######################### [auth.google] enabled = false allow_sign_up = false @@ -223,7 +223,16 @@ token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = -#################################### Generic OAuth ########################## +#################################### Grafana.net Auth #################### +[auth.grafananet] +enabled = false +allow_sign_up = false +client_id = some_id +client_secret = some_secret +scopes = user:email +allowed_organizations = + +#################################### Generic OAuth ####################### [auth.generic_oauth] enabled = false allow_sign_up = false @@ -247,12 +256,12 @@ header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true -#################################### Auth LDAP ########################## +#################################### Auth LDAP ########################### [auth.ldap] enabled = false config_file = /etc/grafana/ldap.toml -#################################### SMTP / Emailing ########################## +#################################### SMTP / Emailing ##################### [smtp] enabled = false host = localhost:25 @@ -267,7 +276,7 @@ from_address = admin@grafana.localhost welcome_email_on_sign_up = false templates_pattern = emails/*.html -#################################### Logging ########################## +#################################### Logging ############################# [log] # Either "console", "file", "syslog". Default is console and file # Use space to separate multiple modes, e.g. "console file" @@ -322,18 +331,18 @@ facility = tag = -#################################### AMQP Event Publisher ########################## +#################################### AMQP Event Publisher ################ [event_publisher] enabled = false rabbitmq_url = amqp://localhost/ exchange = grafana_events -#################################### Dashboard JSON files ########################## +#################################### Dashboard JSON files ################ [dashboards.json] enabled = false path = /var/lib/grafana/dashboards -#################################### Usage Quotas ########################## +#################################### Usage Quotas ######################## [quota] enabled = false @@ -368,7 +377,7 @@ global_api_key = -1 # global limit on number of logged in users. global_session = -1 -#################################### Alerting ###################################### +#################################### Alerting ############################ # docs about alerting can be found in /docs/sources/alerting/ # __.-/| # \`o_O' @@ -387,7 +396,7 @@ global_session = -1 [alerting] enabled = true -#################################### Internal Grafana Metrics ########################## +#################################### Internal Grafana Metrics ############ # Metrics available at HTTP API Url /api/metrics [metrics] enabled = true @@ -402,7 +411,7 @@ prefix = prod.grafana.%(instance_name)s. [grafana_net] url = https://grafana.net -#################################### External image storage ########################## +#################################### External Image Storage ############## [external_image_storage] # You can choose between (s3, webdav or internal) provider = s3 From dfe0f911053705e394457bab2919f659eada7021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 16:29:48 +0200 Subject: [PATCH 046/149] feat(adhoc): adhoc filters progress --- .../app/features/templating/adhoc_variable.ts | 6 +-- .../templating/specs/adhoc_variable_specs.ts | 40 +++++++++++++++++++ public/app/features/templating/templateSrv.js | 18 +++++++++ .../plugins/datasource/influxdb/datasource.ts | 33 +++++++-------- .../datasource/influxdb/influx_query.ts | 7 ++++ .../influxdb/specs/influx_query_specs.ts | 13 ++++++ 6 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 public/app/features/templating/specs/adhoc_variable_specs.ts diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index ea942beaef3..5ebffb0b0e6 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -18,7 +18,7 @@ export class AdhocVariable implements Variable { }; /** @ngInject **/ - constructor(private model, private timeSrv, private templateSrv, private variableSrv) { + constructor(private model) { assignModelProperties(this, model, this.defaults); } @@ -68,7 +68,7 @@ export class AdhocVariable implements Variable { } variableTypes['adhoc'] = { - name: 'Ad hoc', + name: 'Ad hoc filters', ctor: AdhocVariable, - description: 'Ad hoc filters', + description: 'Add key/value filters on the fly', }; diff --git a/public/app/features/templating/specs/adhoc_variable_specs.ts b/public/app/features/templating/specs/adhoc_variable_specs.ts new file mode 100644 index 00000000000..15856940540 --- /dev/null +++ b/public/app/features/templating/specs/adhoc_variable_specs.ts @@ -0,0 +1,40 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; + +import {AdhocVariable} from '../adhoc_variable'; + +describe('AdhocVariable', function() { + + describe('when serializing to url', function() { + + it('should set return key value and op seperated by pipe', function() { + var variable = new AdhocVariable({ + filters: [ + {key: 'key1', operator: '=', value: 'value1'}, + {key: 'key2', operator: '!=', value: 'value2'}, + ] + }); + var urlValue = variable.getValueForUrl(); + expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2"]); + }); + + }); + + describe('when deserializing from url', function() { + + it('should restore filters', function() { + var variable = new AdhocVariable({}); + variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2"]); + + expect(variable.filters[0].key).to.be('key1'); + expect(variable.filters[0].operator).to.be('='); + expect(variable.filters[0].value).to.be('value1'); + + expect(variable.filters[1].key).to.be('key2'); + expect(variable.filters[1].operator).to.be('!='); + expect(variable.filters[1].value).to.be('value2'); + }); + + }); + +}); + diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index bca814d29a4..e5ef5a0f6b7 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -15,6 +15,7 @@ function (angular, _, kbn) { this._index = {}; this._texts = {}; this._grafanaVariables = {}; + this._adhocVariables = {}; this.init = function(variables) { this.variables = variables; @@ -23,16 +24,33 @@ function (angular, _, kbn) { this.updateTemplateData = function() { this._index = {}; + this._filters = {}; for (var i = 0; i < this.variables.length; i++) { var variable = this.variables[i]; + + // add adhoc filters to it's own index + if (variable.type === 'adhoc') { + this._adhocVariables[variable.datasource] = variable; + continue; + } + if (!variable.current || !variable.current.isNone && !variable.current.value) { continue; } + this._index[variable.name] = variable; } }; + this.getAdhocFilters = function(datasourceName) { + var variable = this._adhocVariables[datasourceName]; + if (variable) { + return variable.filters || []; + } + return [] + }; + function luceneEscape(value) { return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1"); } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 81cffd18364..dd35f42f02e 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -44,36 +44,23 @@ export default class InfluxDatasource { query(options) { var timeFilter = this.getTimeFilter(options); - var scopedVars = _.extend({}, options.scopedVars); + var scopedVars = options.scopedVars ? _.cloneDeep(options.scopedVars) : {}; var targets = _.cloneDeep(options.targets); var queryTargets = []; + var queryModel; var i, y; var allQueries = _.map(targets, target => { if (target.hide) { return ""; } - if (!target.rawQuery) { - // apply add hoc filters - for (let variable of this.templateSrv.variables) { - if (variable.type === 'adhoc' && variable.datasource === this.name) { - for (let filter of variable.filters) { - if (filter.key !== undefined && filter.value !== undefined) { - target.tags.push({key: filter.key, value: filter.value, condition: filter.condition, operator: filter.operator}); - } - } - } - } - } - queryTargets.push(target); // build query scopedVars.interval = {value: target.interval || options.interval}; - var queryModel = new InfluxQuery(target, this.templateSrv, scopedVars); - var query = queryModel.render(true); + queryModel = new InfluxQuery(target, this.templateSrv, scopedVars); + return queryModel.render(true); - return query; }).reduce((acc, current) => { if (current !== "") { acc += ";" + current; @@ -81,6 +68,16 @@ export default class InfluxDatasource { return acc; }); + if (allQueries === '') { + return this.$q.when({data: []}); + } + + // add global adhoc filters to timeFilter + var adhocFilters = this.templateSrv.getAdhocFilters(this.name); + if (adhocFilters.length > 0 ) { + timeFilter += ' AND ' + queryModel.renderAdhocFilters(adhocFilters); + } + // replace grafana variables scopedVars.timeFilter = {value: timeFilter}; @@ -120,7 +117,7 @@ export default class InfluxDatasource { } } - return { data: seriesList }; + return {data: seriesList}; }); }; diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index fddb452767a..2f03f37a0a1 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -251,4 +251,11 @@ export default class InfluxQuery { return query; } + + renderAdhocFilters(filters) { + var conditions = _.map(filters, (tag, index) => { + return this.renderTagCondition(tag, index, false); + }); + return conditions.join(' '); + } } diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts index 557c626a7cc..52beed1d080 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts @@ -237,6 +237,19 @@ describe('InfluxQuery', function() { expect(query.target.select[0][2].type).to.be('math'); }); + describe('when render adhoc filters', function() { + it('should generate correct query segment', function() { + var query = new InfluxQuery({measurement: 'cpu', }, templateSrv, {}); + + var queryText = query.renderAdhocFilters([ + {key: 'key1', operator: '=', value: 'value1'}, + {key: 'key2', operator: '!=', value: 'value2'}, + ]); + + expect(queryText).to.be('"key1" = \'value1\' AND "key2" != \'value2\''); + }); + }); + }); }); From 15423e6e51313e071ac64df8a313f813e397c610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 17:34:38 +0200 Subject: [PATCH 047/149] feat(adhoc filters): initial base mvp for adhoc filters are donecloses #6038 --- public/app/features/templating/editor_ctrl.ts | 22 ++++++-- .../features/templating/partials/editor.html | 51 ++++++++++--------- .../plugins/datasource/influxdb/datasource.ts | 2 + 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 25ddec62cce..81bf2d4d71c 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -9,6 +9,7 @@ export class VariableEditorCtrl { /** @ngInject */ constructor(private $scope, private datasourceSrv, private variableSrv, templateSrv) { $scope.variableTypes = variableTypes; + $scope.ctrl = {}; $scope.refreshOptions = [ {value: 0, text: "Never"}, @@ -60,9 +61,8 @@ export class VariableEditorCtrl { }; $scope.isValid = function() { - if (!$scope.current.name) { - $scope.appEvent('alert-warning', ['Validation', 'Template variable requires a name']); - return false; + if (!$scope.ctrl.form.$valid) { + return; } if (!$scope.current.name.match(/^\w+$/)) { @@ -79,6 +79,18 @@ export class VariableEditorCtrl { return true; }; + $scope.validate = function() { + $scope.infoText = ''; + if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) { + $scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource'; + datasourceSrv.get($scope.current.datasource).then(ds => { + if (!ds.supportAdhocFilters) { + $scope.infoText = 'This datasource does not support adhoc filters yet.'; + } + }); + } + }; + $scope.runQuery = function() { return variableSrv.updateOptions($scope.current).then(null, function(err) { if (err.data && err.data.message) { err.message = err.data.message; } @@ -90,6 +102,7 @@ export class VariableEditorCtrl { $scope.current = variable; $scope.currentIsNew = false; $scope.mode = 'edit'; + $scope.validate(); }; $scope.duplicate = function(variable) { @@ -126,6 +139,8 @@ export class VariableEditorCtrl { if (oldIndex !== -1) { this.variables[oldIndex] = $scope.current; } + + $scope.validate(); }; $scope.removeVariable = function(variable) { @@ -133,7 +148,6 @@ export class VariableEditorCtrl { $scope.variables.splice(index, 1); $scope.updateSubmenuVisibility(); }; - } } diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index c74245ae6be..e485072eed0 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -70,13 +70,13 @@
    -
    +
    Variable
    Name - +
    @@ -102,15 +102,14 @@
    -
    -
    +
    Interval Options
    Values - +
    Auto option @@ -134,15 +133,15 @@
    -
    +
    Custom Options
    Values separated by comma - +
    -
    +
    Constant options
    Value @@ -150,14 +149,14 @@
    -
    +
    Query Options
    Data source
    - +
    @@ -174,7 +173,7 @@
    Query - +
    @@ -223,19 +222,18 @@
    -
    +
    Options
    -
    Data source
    - +
    -
    +
    -
    -
    Selection Options
    +
    +
    Selection Options
    -
    +
    Preview of values (shows max 20)
    @@ -280,12 +278,17 @@
    -
    -
    - - -
    -
    +
    + {{infoText}} +
    + +
    + + +
    + + +
    diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index dd35f42f02e..4c707dd59d1 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -21,6 +21,7 @@ export default class InfluxDatasource { interval: any; supportAnnotations: boolean; supportMetrics: boolean; + supportAdhocFilters: boolean; responseParser: any; /** @ngInject */ @@ -39,6 +40,7 @@ export default class InfluxDatasource { this.interval = (instanceSettings.jsonData || {}).timeInterval; this.supportAnnotations = true; this.supportMetrics = true; + this.supportAdhocFilters = true; this.responseParser = new ResponseParser(); } From da95a23080963a39bcb28c1513e385b0fef6e910 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 20 Sep 2016 11:36:13 -0400 Subject: [PATCH 048/149] remove 'Github' from oauth login error messages --- pkg/api/login_oauth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 6512a827341..eab8d869951 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -46,9 +46,9 @@ func OAuthLogin(ctx *middleware.Context) { userInfo, err := connect.UserInfo(token) if err != nil { if err == social.ErrMissingTeamMembership { - ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled")) + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required team membership not fulfilled")) } else if err == social.ErrMissingOrganizationMembership { - ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled")) + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required organization membership not fulfilled")) } else { ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) } From 1b02632de83c8cf04e3be40f52d980c1baa0ba75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Sep 2016 17:36:20 +0200 Subject: [PATCH 049/149] fix(jslint): fixed lint issue --- public/app/features/templating/templateSrv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index e5ef5a0f6b7..f7784e2cb50 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -48,7 +48,7 @@ function (angular, _, kbn) { if (variable) { return variable.filters || []; } - return [] + return []; }; function luceneEscape(value) { From 630a8ed8aa2508adc14453fab8b7cf04aeab7e59 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 20 Sep 2016 12:36:36 -0400 Subject: [PATCH 050/149] support setting default org role when adding user via grafana.net auth --- pkg/api/login_oauth.go | 9 +++++---- pkg/models/user.go | 19 ++++++++++--------- pkg/services/sqlstore/user.go | 6 +++++- pkg/social/grafananet_oauth.go | 2 ++ pkg/social/social.go | 1 + 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index eab8d869951..07cf70a96e3 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -83,10 +83,11 @@ func OAuthLogin(ctx *middleware.Context) { return } cmd := m.CreateUserCommand{ - Login: userInfo.Email, - Email: userInfo.Email, - Name: userInfo.Name, - Company: userInfo.Company, + Login: userInfo.Email, + Email: userInfo.Email, + Name: userInfo.Name, + Company: userInfo.Company, + DefaultOrgRole: userInfo.Role, } if err = bus.Dispatch(&cmd); err != nil { diff --git a/pkg/models/user.go b/pkg/models/user.go index a231156b7b0..d2dcdf0a5c9 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -44,15 +44,16 @@ func (u *User) NameOrFallback() string { // COMMANDS type CreateUserCommand struct { - Email string - Login string - Name string - Company string - OrgName string - Password string - EmailVerified bool - IsAdmin bool - SkipOrgSetup bool + Email string + Login string + Name string + Company string + OrgName string + Password string + EmailVerified bool + IsAdmin bool + SkipOrgSetup bool + DefaultOrgRole string Result User } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 3dc685cd7e5..bbf21296519 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -128,7 +128,11 @@ func CreateUser(cmd *m.CreateUserCommand) error { } if setting.AutoAssignOrg && !user.IsAdmin { - orgUser.Role = m.RoleType(setting.AutoAssignOrgRole) + if len(cmd.DefaultOrgRole) > 0 { + orgUser.Role = m.RoleType(cmd.DefaultOrgRole) + } else { + orgUser.Role = m.RoleType(setting.AutoAssignOrgRole) + } } if _, err = sess.Insert(&orgUser); err != nil { diff --git a/pkg/social/grafananet_oauth.go b/pkg/social/grafananet_oauth.go index 05cc7c1f397..80c1aaedb45 100644 --- a/pkg/social/grafananet_oauth.go +++ b/pkg/social/grafananet_oauth.go @@ -83,6 +83,7 @@ func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) Id int `json:"id"` Name string `json:"login"` Email string `json:"email"` + Role string `json:"role"` } var err error @@ -102,6 +103,7 @@ func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) Identity: strconv.Itoa(data.Id), Name: data.Name, Email: data.Email, + Role: data.Role, } if !s.IsOrganizationMember(client) { diff --git a/pkg/social/social.go b/pkg/social/social.go index 83e5aa19b43..fc29fe9c5d2 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -15,6 +15,7 @@ type BasicUserInfo struct { Email string Login string Company string + Role string } type SocialConnector interface { From 43d8bd5a254733582baf3c1bc209a9b2db3e0866 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 07:01:53 +0200 Subject: [PATCH 051/149] 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 392976981c836ca5c697910f1d59aebc5774e5b7 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Sep 2016 08:40:12 +0300 Subject: [PATCH 052/149] fix(table panel): update Table Panel options tab styles. (#6087) --- public/app/plugins/panel/table/editor.html | 294 ++++++++++----------- 1 file changed, 145 insertions(+), 149 deletions(-) diff --git a/public/app/plugins/panel/table/editor.html b/public/app/plugins/panel/table/editor.html index d5565aa7ba1..f2f0f3e7394 100644 --- a/public/app/plugins/panel/table/editor.html +++ b/public/app/plugins/panel/table/editor.html @@ -1,173 +1,169 @@
    -
    -
    Data
    -
    -
    -
      -
    • - To Table Transform -
    • -
    • - -
    • -
    -
    +
    -
    -
      -
    • - Columns -
    • -
    • - - - {{column.text}} - -
    • -
    • - -
    • -
    -
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    -
    -
    Table Display
    -
    -
    -
      -
    • - Pagination (Page size) -
    • -
    • - -
    • -
    • - -
    • -
    • - Font size -
    • -
    • - -
    • -
    -
    +
    +
    Table Display
    +
    +
    + + +
    + +
    + +
    + +
    -
    -
    Column Styles
    - -
    +
    +
    +
    Column Styles
    -
    -
      -
    • - -
    • -
    - -
      -
    • - Name or regex -
    • -
    • - -
    • -
    • - Type -
    • -
    • - -
    • -
    -
      -
    • - Format -
    • -
    • - -
    • -
    -
      -
    • - -
    • -
    -
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    + +
    +
    +
    +
    + +
    -
    -
      -
    • - Coloring -
    • -
    • - -
    • -
    • - ThresholdsComma separated values -
    • -
    • - -
    • -
    • - Colors -
    • -
    • - - - -
    • -
    • +
      +
      + +
      + +
      +
      +
      + + +
      +
      + + + + + + + + + + + invert order -
    • -
    -
    + +
    +
    -
    -
      -
    • - Unit -
    • - -
    • - Decimals -
    • -
    • - -
    • -
    -
    +
    +
    +
    + + +
    +
    +
    +
    -
    - - +
    + +
    - From a01836e86d20d2012b03bfe722a16a85bdc9d33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Sep 2016 08:46:59 +0200 Subject: [PATCH 053/149] feat(adhoc filters): basic implementation for ad hoc filters for elasticsearch, #6038 --- public/app/features/templating/editor_ctrl.ts | 2 +- .../datasource/elasticsearch/datasource.js | 15 ++++++++++++-- .../datasource/elasticsearch/query_builder.js | 20 ++++++++++++++++++- .../specs/query_builder_specs.ts | 12 +++++++++++ .../plugins/datasource/influxdb/datasource.ts | 3 +-- public/test/specs/helpers.js | 1 + 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 81bf2d4d71c..3c0410e1316 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -84,7 +84,7 @@ export class VariableEditorCtrl { if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) { $scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource'; datasourceSrv.get($scope.current.datasource).then(ds => { - if (!ds.supportAdhocFilters) { + if (!ds.getTagKeys) { $scope.infoText = 'This datasource does not support adhoc filters yet.'; } }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 8cfe7d6129a..0889c078082 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -177,11 +177,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes var target; var sentTargets = []; + // add global adhoc filters to timeFilter + var adhocFilters = templateSrv.getAdhocFilters(this.name); + for (var i = 0; i < options.targets.length; i++) { target = options.targets[i]; if (target.hide) {continue;} - var queryObj = this.queryBuilder.build(target); + var queryObj = this.queryBuilder.build(target, adhocFilters); var esQuery = angular.toJson(queryObj); var luceneQuery = target.query || '*'; luceneQuery = templateSrv.replace(luceneQuery, options.scopedVars, 'lucene'); @@ -247,7 +250,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes // Hide meta-fields and check field type if (key[0] !== '_' && (!query.type || - query.type && typeMap[subObj.type] === query.type)) { + query.type && typeMap[subObj.type] === query.type)) { fields[fieldName] = { text: fieldName, @@ -314,6 +317,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes return this.getTerms(query); } }; + + this.getTagKeys = function() { + return this.getFields({}); + }; + + this.getTagValues = function(options) { + return this.getTerms({field: options.key, query: '*'}); + }; } return { diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index 9c8217102aa..d256c6d1438 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -98,7 +98,23 @@ function (queryDef) { return query; }; - ElasticQueryBuilder.prototype.build = function(target) { + ElasticQueryBuilder.prototype.addAdhocFilters = function(query, adhocFilters) { + if (!adhocFilters) { + return; + } + + var i, filter, condition; + var must = query.query.filtered.filter.bool.must; + + for (i = 0; i < adhocFilters.length; i++) { + filter = adhocFilters[i]; + condition = {}; + condition[filter.key] = filter.value; + must.push({"term": condition}); + } + }; + + ElasticQueryBuilder.prototype.build = function(target, adhocFilters) { // make sure query has defaults; target.metrics = target.metrics || [{ type: 'count', id: '1' }]; target.dsType = 'elasticsearch'; @@ -125,6 +141,8 @@ function (queryDef) { } }; + this.addAdhocFilters(query, adhocFilters); + // handle document query if (target.bucketAggs.length === 0) { metric = target.metrics[0]; diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts index bbd5711fd1f..d9174e73969 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts @@ -238,4 +238,16 @@ describe('ElasticQueryBuilder', function() { expect(firstLevel.aggs["2"].derivative.buckets_path).to.be("3"); }); + it('with adhoc filters', function() { + var query = builder.build({ + metrics: [{type: 'Count', id: '0'}], + timeField: '@timestamp', + bucketAggs: [{type: 'date_histogram', field: '@timestamp', id: '3'}], + }, [ + {key: 'key1', operator: '=', value: 'value1'} + ]); + + expect(query.query.filtered.filter.bool.must[1].term["key1"]).to.be("value1"); + }); + }); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 4c707dd59d1..76a66d18e3c 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -9,6 +9,7 @@ import InfluxQuery from './influx_query'; import ResponseParser from './response_parser'; import InfluxQueryBuilder from './query_builder'; + export default class InfluxDatasource { type: string; urls: any; @@ -21,7 +22,6 @@ export default class InfluxDatasource { interval: any; supportAnnotations: boolean; supportMetrics: boolean; - supportAdhocFilters: boolean; responseParser: any; /** @ngInject */ @@ -40,7 +40,6 @@ export default class InfluxDatasource { this.interval = (instanceSettings.jsonData || {}).timeInterval; this.supportAnnotations = true; this.supportMetrics = true; - this.supportAdhocFilters = true; this.responseParser = new ResponseParser(); } diff --git a/public/test/specs/helpers.js b/public/test/specs/helpers.js index 0e5f20a853c..424b190b6dd 100644 --- a/public/test/specs/helpers.js +++ b/public/test/specs/helpers.js @@ -158,6 +158,7 @@ define([ return _.template(text, this.templateSettings)(this.data); }; this.init = function() {}; + this.getAdhocFilters = function() { return []; }; this.fillVariableValuesForUrl = function() {}; this.updateTemplateData = function() { }; this.variableExists = function() { return false; }; From 7b7ba46f123cff0517d110594f045bc6727a101d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Sep 2016 09:10:25 +0200 Subject: [PATCH 054/149] fix(styleguide): fixed theme switching in style guide --- public/app/features/styleguide/styleguide.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/app/features/styleguide/styleguide.ts b/public/app/features/styleguide/styleguide.ts index 52ccd20ac59..c297f12ab4c 100644 --- a/public/app/features/styleguide/styleguide.ts +++ b/public/app/features/styleguide/styleguide.ts @@ -13,7 +13,7 @@ class StyleGuideCtrl { pages = ['colors', 'buttons']; /** @ngInject **/ - constructor(private $http, $routeParams) { + constructor(private $http, private $routeParams, private $location) { this.theme = config.bootData.user.lightTheme ? 'light': 'dark'; this.page = {}; @@ -37,8 +37,11 @@ class StyleGuideCtrl { } switchTheme() { - var other = this.theme === 'dark' ? 'light' : 'dark'; - window.location.href = window.location.href + '?theme=' + other; + this.$routeParams.theme = this.theme === 'dark' ? 'light' : 'dark'; + this.$location.search(this.$routeParams); + setTimeout(() => { + window.location.href = window.location.href; + }); } } From 7c10af012eb40bcc6a7e7c3e8d8fec70accc7464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Sep 2016 10:56:17 +0200 Subject: [PATCH 055/149] feat(singlestat): slight layout changes to options tab --- .../app/plugins/panel/singlestat/editor.html | 307 +++++++----------- 1 file changed, 120 insertions(+), 187 deletions(-) diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index a4fd27bd080..e4e8446934a 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -1,192 +1,125 @@
    -
    -
    -
    - -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    -
    -
    - -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    +
    +
    Value
    -
    -
    Coloring
    -
    -
    - - -
    -
    -
    - - -
    -
    - - - - - - - - - - -
    - -
    -
    -
    -
    -
    +
    +
    + +
    + +
    + +
    + +
    +
    +
    -
    -
    Spark lines
    -
    -
    - - -
    - - - - -
    -
    - - - - -
    -
    -
    -
    +
    +
    + + + +
    + +
    +
    +
    -
    -
    Gauge
    -
    -
    - -
    - - -
    -
    - - -
    -
    - -
    -
    -
    - - -
    -
    +
    + + + +
    + +
    +
    +
    + +
    +
    Unit
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    Coloring
    +
    + + +
    +
    +
    + + +
    +
    +
    + + + + + + + + + + + + + Invert + + +
    +
    + +
    +
    Spark lines
    + +
    + +
    + + + + +
    +
    + + + + +
    +
    +
    + +
    +
    Gauge
    + +
    +
    + + + +
    +
    + + +
    + + +
    +
    From 4c88db3e43e1772ef6a193574a5ad38d9a310962 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Sep 2016 11:17:29 +0200 Subject: [PATCH 056/149] 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 8b90eedb35f57a226d9f8ca90cca8cb4282c5848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Sep 2016 11:23:39 +0200 Subject: [PATCH 057/149] feat(table): table options tab form layout changes --- public/app/plugins/panel/table/editor.html | 172 +++++++++------------ 1 file changed, 71 insertions(+), 101 deletions(-) diff --git a/public/app/plugins/panel/table/editor.html b/public/app/plugins/panel/table/editor.html index f2f0f3e7394..6e7281dd78c 100644 --- a/public/app/plugins/panel/table/editor.html +++ b/public/app/plugins/panel/table/editor.html @@ -1,24 +1,16 @@
    Data
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    - -
    +
    + +
    + +
    +
    +
    +
    + +