diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 094f98cbe53..50d155131fd 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -2,7 +2,9 @@ package influxdb import ( "fmt" + "strconv" "strings" + "time" "regexp" @@ -15,24 +17,53 @@ var ( ) func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { + var res string + if query.UseRawQuery && query.RawQuery != "" { - q := query.RawQuery - - q = strings.Replace(q, "$timeFilter", query.renderTimeFilter(queryContext), 1) - q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1) - - return q, nil + res = query.RawQuery + } else { + res = query.renderSelectors(queryContext) + res += query.renderMeasurement() + res += query.renderWhereClause() + res += query.renderTimeFilter(queryContext) + res += query.renderGroupBy(queryContext) } - res := query.renderSelectors(queryContext) - res += query.renderMeasurement() - res += query.renderWhereClause() - res += query.renderTimeFilter(queryContext) - res += query.renderGroupBy(queryContext) + interval, err := getDefinedInterval(query, queryContext) + if err != nil { + return "", err + } + res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), 1) + res = strings.Replace(res, "$interval", interval.Text, 1) + res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), 1) + res = strings.Replace(res, "$__interval", interval.Text, 1) return res, nil } +func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) (*tsdb.Interval, error) { + defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange) + + if query.Interval == "" { + return &defaultInterval, nil + } + + setInterval := strings.Replace(strings.Replace(query.Interval, "<", "", 1), ">", "", 1) + parsedSetInterval, err := time.ParseDuration(setInterval) + + if err != nil { + return nil, err + } + + if strings.Contains(query.Interval, ">") { + if defaultInterval.Value > parsedSetInterval { + return &defaultInterval, nil + } + } + + return &tsdb.Interval{Value: parsedSetInterval, Text: setInterval}, nil +} + func (query *Query) renderTags() []string { var res []string for i, tag := range query.Tags { diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 222e0b054ee..3145ff1b333 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -3,7 +3,6 @@ package influxdb import ( "fmt" "strings" - "time" "github.com/grafana/grafana/pkg/tsdb" ) @@ -93,30 +92,10 @@ func fieldRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPar return fmt.Sprintf(`"%s"`, part.Params[0]) } -func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) string { - setInterval := strings.Replace(strings.Replace(query.Interval, "<", "", 1), ">", "", 1) - defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange) - - if strings.Contains(query.Interval, ">") { - parsedDefaultInterval, err := time.ParseDuration(defaultInterval) - parsedSetInterval, err2 := time.ParseDuration(setInterval) - - if err == nil && err2 == nil && parsedDefaultInterval > parsedSetInterval { - return defaultInterval - } - } - - return setInterval -} - func functionRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { for i, param := range part.Params { - if param == "$interval" || param == "auto" { - if query.Interval != "" { - part.Params[i] = getDefinedInterval(query, queryContext) - } else { - part.Params[i] = tsdb.CalculateInterval(queryContext.TimeRange) - } + if part.Type == "time" && param == "auto" { + part.Params[i] = "$__interval" } } diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index a083754574a..b5bae2fdf37 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -42,7 +42,7 @@ func TestInfluxdbQueryPart(t *testing.T) { So(err, ShouldBeNil) res := part.Render(query, queryContext, "") - So(res, ShouldEqual, "time(200ms)") + So(res, ShouldEqual, "time($interval)") }) Convey("render time with auto", func() { @@ -50,28 +50,7 @@ func TestInfluxdbQueryPart(t *testing.T) { So(err, ShouldBeNil) res := part.Render(query, queryContext, "") - So(res, ShouldEqual, "time(200ms)") - }) - - Convey("render time interval >10s", func() { - part, err := NewQueryPart("time", []string{"$interval"}) - So(err, ShouldBeNil) - - query.Interval = ">10s" - - res := part.Render(query, queryContext, "") - So(res, ShouldEqual, "time(10s)") - }) - - Convey("render time interval >1s and higher interval calculation", func() { - part, err := NewQueryPart("time", []string{"$interval"}) - queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("1y", "now")} - So(err, ShouldBeNil) - - query.Interval = ">1s" - - res := part.Render(query, queryContext, "") - So(res, ShouldEqual, "time(168h)") + So(res, ShouldEqual, "time($__interval)") }) Convey("render spread", func() { diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index b6af67e5e42..dd2a3a5687a 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -16,10 +16,15 @@ func TestInfluxdbQueryBuilder(t *testing.T) { qp1, _ := NewQueryPart("field", []string{"value"}) qp2, _ := NewQueryPart("mean", []string{}) - groupBy1, _ := NewQueryPart("time", []string{"$interval"}) + mathPartDivideBy100, _ := NewQueryPart("math", []string{"/ 100"}) + mathPartDivideByIntervalMs, _ := NewQueryPart("math", []string{"/ $__interval_ms"}) + + groupBy1, _ := NewQueryPart("time", []string{"$__interval"}) groupBy2, _ := NewQueryPart("tag", []string{"datacenter"}) groupBy3, _ := NewQueryPart("fill", []string{"null"}) + groupByOldInterval, _ := NewQueryPart("time", []string{"$interval"}) + tag1 := &Tag{Key: "hostname", Value: "server1", Operator: "="} tag2 := &Tag{Key: "hostname", Value: "server2", Operator: "=", Condition: "OR"} @@ -55,6 +60,43 @@ func TestInfluxdbQueryBuilder(t *testing.T) { So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`) }) + Convey("can build query with math part", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2, *mathPartDivideBy100}}, + Measurement: "cpu", + Interval: "5s", + } + + rawQuery, err := query.Build(queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("value") / 100 FROM "cpu" WHERE time > now() - 5m`) + }) + + Convey("can build query with math part using $__interval_ms variable", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2, *mathPartDivideByIntervalMs}}, + Measurement: "cpu", + Interval: "5s", + } + + rawQuery, err := query.Build(queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("value") / 5000 FROM "cpu" WHERE time > now() - 5m`) + }) + + Convey("can build query with old $interval variable", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2}}, + Measurement: "cpu", + Policy: "", + GroupBy: []*QueryPart{groupByOldInterval}, + } + + rawQuery, err := query.Build(queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(200ms)`) + }) + Convey("can render time range", func() { query := Query{} Convey("render from: 2h to now-1h", func() { @@ -139,4 +181,5 @@ func TestInfluxdbQueryBuilder(t *testing.T) { So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"./apa/`) }) }) + } diff --git a/pkg/tsdb/interval.go b/pkg/tsdb/interval.go index 71caf122c13..aef6cc4f47b 100644 --- a/pkg/tsdb/interval.go +++ b/pkg/tsdb/interval.go @@ -12,14 +12,19 @@ var ( day time.Duration = time.Hour * 24 * 365 ) -func CalculateInterval(timerange *TimeRange) string { +type Interval struct { + Text string + Value time.Duration +} + +func CalculateInterval(timerange *TimeRange) Interval { interval := time.Duration((timerange.MustGetTo().UnixNano() - timerange.MustGetFrom().UnixNano()) / defaultRes) if interval < minInterval { - return formatDuration(minInterval) + return Interval{Text: formatDuration(minInterval), Value: interval} } - return formatDuration(roundInterval(interval)) + return Interval{Text: formatDuration(roundInterval(interval)), Value: interval} } func formatDuration(inter time.Duration) string { diff --git a/pkg/tsdb/interval_test.go b/pkg/tsdb/interval_test.go index c06e1879668..7b243b4e3ba 100644 --- a/pkg/tsdb/interval_test.go +++ b/pkg/tsdb/interval_test.go @@ -18,28 +18,28 @@ func TestInterval(t *testing.T) { tr := NewTimeRange("5m", "now") interval := CalculateInterval(tr) - So(interval, ShouldEqual, "200ms") + So(interval.Text, ShouldEqual, "200ms") }) Convey("for 15min", func() { tr := NewTimeRange("15m", "now") interval := CalculateInterval(tr) - So(interval, ShouldEqual, "500ms") + So(interval.Text, ShouldEqual, "500ms") }) Convey("for 30min", func() { tr := NewTimeRange("30m", "now") interval := CalculateInterval(tr) - So(interval, ShouldEqual, "1s") + So(interval.Text, ShouldEqual, "1s") }) Convey("for 1h", func() { tr := NewTimeRange("1h", "now") interval := CalculateInterval(tr) - So(interval, ShouldEqual, "2s") + So(interval.Text, ShouldEqual, "2s") }) Convey("Round interval", func() { diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts index fcd337a0a26..cf6780ac182 100644 --- a/public/app/core/components/query_part/query_part.ts +++ b/public/app/core/components/query_part/query_part.ts @@ -89,7 +89,7 @@ export function functionRenderer(part, innerExpr) { var paramType = part.def.params[index]; if (paramType.type === 'time') { if (value === 'auto') { - value = '$interval'; + value = '$__interval'; } } if (paramType.quote === 'single') { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index aba4492401b..66e624911e2 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -191,6 +191,13 @@ class MetricsPanelCtrl extends PanelCtrl { return this.$q.when([]); } + // make shallow copy of scoped vars, + // and add built in variables interval and interval_ms + var scopedVars = Object.assign({}, this.panel.scopedVars, { + "__interval": {text: this.interval, value: this.interval}, + "__interval_ms": {text: this.intervalMs, value: this.intervalMs}, + }); + var metricsQuery = { panelId: this.panel.id, range: this.range, @@ -200,7 +207,7 @@ class MetricsPanelCtrl extends PanelCtrl { targets: this.panel.targets, format: this.panel.renderer === 'png' ? 'png' : 'json', maxDataPoints: this.resolution, - scopedVars: this.panel.scopedVars, + scopedVars: scopedVars, cacheTimeout: this.panel.cacheTimeout }; diff --git a/public/app/features/templating/specs/template_srv_specs.ts b/public/app/features/templating/specs/template_srv_specs.ts index af95b68cb7d..f3e8ab3f58a 100644 --- a/public/app/features/templating/specs/template_srv_specs.ts +++ b/public/app/features/templating/specs/template_srv_specs.ts @@ -244,4 +244,16 @@ describe('templateSrv', function() { expect(target).to.be('Server: All, period: 13m'); }); }); + + describe('built in interval variables', function() { + beforeEach(function() { + initTemplateSrv([]); + }); + + it('should replace $__interval_ms with interval milliseconds', function() { + var target = _templateSrv.replace('10 * $__interval_ms', {"__interval_ms": {text: "100", value: "100"}}); + expect(target).to.be('10 * 100'); + }); + + }); }); diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index de82be2cf66..f47afc81574 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -17,6 +17,11 @@ function (angular, _, kbn) { this._grafanaVariables = {}; this._adhocVariables = {}; + // default built ins + this._builtIns = {}; + this._builtIns['__interval'] = {text: '1s', value: '1s'}; + this._builtIns['__interval_ms'] = {text: '100', value: '100'}; + this.init = function(variables) { this.variables = variables; this.updateTemplateData(); @@ -42,6 +47,7 @@ function (angular, _, kbn) { this._index[variable.name] = variable; } + }; this.variableInitialized = function(variable) { @@ -103,10 +109,10 @@ function (angular, _, kbn) { return this.distributeVariable(value, variable.name); } default: { - if (typeof value === 'string') { - return value; + if (_.isArray(value)) { + return '{' + value.join(',') + '}'; } - return '{' + value.join(',') + '}'; + return value; } } }; @@ -135,7 +141,7 @@ function (angular, _, kbn) { str = _.escape(str); this._regex.lastIndex = 0; return str.replace(this._regex, function(match, g1, g2) { - if (self._index[g1 || g2]) { + if (self._index[g1 || g2] || self._builtIns[g1 || g2]) { return '' + match + ''; } return match; diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 38fd6337f22..94fa75bf2eb 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -197,15 +197,9 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes target = options.targets[i]; if (target.hide) {continue;} - var queryObj = this.queryBuilder.build(target, adhocFilters); + var queryString = templateSrv.replace(target.query || '*', options.scopedVars, 'lucene'); + var queryObj = this.queryBuilder.build(target, adhocFilters, queryString); var esQuery = angular.toJson(queryObj); - var luceneQuery = target.query || '*'; - luceneQuery = templateSrv.replace(luceneQuery, options.scopedVars, 'lucene'); - luceneQuery = angular.toJson(luceneQuery); - - // remove inner quotes - luceneQuery = luceneQuery.substr(1, luceneQuery.length - 2); - esQuery = esQuery.replace("$lucene_query", luceneQuery); var searchType = (queryObj.size === 0 && this.esVersion < 5) ? 'count' : 'query_then_fetch'; var header = this.getQueryHeader(searchType, options.range.from, options.range.to); @@ -219,7 +213,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes return $q.when([]); } - payload = payload.replace(/\$interval/g, options.interval); payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf()); payload = payload.replace(/\$timeTo/g, options.range.to.valueOf()); payload = templateSrv.replace(payload, options.scopedVars); diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index 62f5ed2a994..26e338957f4 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -66,7 +66,7 @@ function (queryDef) { esAgg.format = "epoch_millis"; if (esAgg.interval === 'auto') { - esAgg.interval = "$interval"; + esAgg.interval = "$__interval"; } if (settings.missing) { @@ -121,7 +121,7 @@ function (queryDef) { } }; - ElasticQueryBuilder.prototype.build = function(target, adhocFilters) { + ElasticQueryBuilder.prototype.build = function(target, adhocFilters, queryString) { // make sure query has defaults; target.metrics = target.metrics || [{ type: 'count', id: '1' }]; target.dsType = 'elasticsearch'; @@ -138,7 +138,7 @@ function (queryDef) { { "query_string": { "analyze_wildcard": true, - "query": '$lucene_query' + "query": queryString, } } ] diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 28f13752dbe..dc6aaaf7a03 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -45,7 +45,7 @@ export default class InfluxDatasource { query(options) { var timeFilter = this.getTimeFilter(options); - var scopedVars = options.scopedVars ? _.cloneDeep(options.scopedVars) : {}; + var scopedVars = options.scopedVars; var targets = _.cloneDeep(options.targets); var queryTargets = []; var queryModel; @@ -56,8 +56,8 @@ export default class InfluxDatasource { queryTargets.push(target); - // build query - scopedVars.interval = {value: target.interval || options.interval}; + // backward compatability + scopedVars.interval = scopedVars.__interval; queryModel = new InfluxQuery(target, this.templateSrv, scopedVars); return queryModel.render(true); diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 2f03f37a0a1..065b85cf175 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -23,7 +23,7 @@ export default class InfluxQuery { target.resultFormat = target.resultFormat || 'time_series'; target.tags = target.tags || []; target.groupBy = target.groupBy || [ - {type: 'time', params: ['$interval']}, + {type: 'time', params: ['$__interval']}, {type: 'fill', params: ['null']}, ]; target.select = target.select || [[ 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 52beed1d080..c162c488f82 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts @@ -12,7 +12,7 @@ describe('InfluxQuery', function() { }, templateSrv, {}); var queryText = query.render(); - expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)'); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)'); }); }); @@ -24,7 +24,7 @@ describe('InfluxQuery', function() { }, templateSrv, {}); var queryText = query.render(); - expect(queryText).to.be('SELECT mean("value") FROM "5m_avg"."cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)'); + expect(queryText).to.be('SELECT mean("value") FROM "5m_avg"."cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)'); }); }); @@ -43,7 +43,7 @@ describe('InfluxQuery', function() { }, templateSrv, {}); var queryText = query.render(); - expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)'); + expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)'); }); }); @@ -58,7 +58,7 @@ describe('InfluxQuery', function() { var queryText = query.render(); expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server\\\\1\' AND $timeFilter' - + ' GROUP BY time($interval)'); + + ' GROUP BY time($__interval)'); }); it('should switch regex operator with tag value is regex', function() { @@ -69,7 +69,7 @@ describe('InfluxQuery', function() { }, templateSrv, {}); var queryText = query.render(); - expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval)'); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($__interval)'); }); }); @@ -83,7 +83,7 @@ describe('InfluxQuery', function() { var queryText = query.render(); expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' + - '$timeFilter GROUP BY time($interval)'); + '$timeFilter GROUP BY time($__interval)'); }); }); @@ -97,7 +97,7 @@ describe('InfluxQuery', function() { var queryText = query.render(); expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' + - '$timeFilter GROUP BY time($interval)'); + '$timeFilter GROUP BY time($__interval)'); }); }); @@ -124,7 +124,7 @@ describe('InfluxQuery', function() { var queryText = query.render(); expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter ' + - 'GROUP BY time($interval), "host"'); + 'GROUP BY time($__interval), "host"'); }); }); @@ -148,7 +148,7 @@ describe('InfluxQuery', function() { groupBy: [{type: 'time'}, {type: 'fill', params: ['0']}], }, templateSrv, {}); var queryText = query.render(); - expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(0)'); + expect(queryText).to.be('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(0)'); }); });