Add absolute time range support to :interval:

Previously, users of the :interval: macro were restricted to using a
relative time range in their queries, or the :dashboardTime: macro. This
permits users to also supply an absolute time range in the form of:

time > '2017-01-01T00:00:00Z' and time < '2017-06-01T00:00:00Z'
pull/10616/head
Tim Raymond 2017-08-08 11:05:50 -07:00
parent c307715840
commit 228920fde4
2 changed files with 129 additions and 8 deletions

View File

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"time"
@ -193,32 +194,103 @@ type GroupByVar struct {
// Exec is responsible for extracting the Duration from the query
func (g *GroupByVar) Exec(query string) {
whereClause := "WHERE time > now() - "
whereClause := "WHERE"
start := strings.Index(query, whereClause)
if start == -1 {
// no where clause
return
}
// reposition start to the END of the where clause
// reposition start to after the 'where' keyword
durStr := query[start+len(whereClause):]
// advance to next space
// attempt to parse out a relative time range
dur, err := g.parseRelative(durStr)
if err == nil {
// we parsed relative duration successfully
g.Duration = dur
return
}
dur, err = g.parseAbsolute(durStr)
if err == nil {
// we found an absolute time range
g.Duration = dur
}
}
// parseRelative locates and extracts a duration value from a fragment of an
// InfluxQL query following the "where" keyword. For example, in the fragment
// "time > now() - 180d GROUP BY :interval:", parseRelative would return a
// duration equal to 180d
func (g *GroupByVar) parseRelative(fragment string) (time.Duration, error) {
// locate duration literal start
prefix := "time > now() - "
start := strings.Index(fragment, prefix)
if start == -1 {
return time.Duration(0), errors.New("not a relative duration")
}
// reposition to duration literal
durFragment := fragment[start+len(prefix):]
// init counters
pos := 0
for pos < len(durStr) {
rn, _ := utf8.DecodeRuneInString(durStr[pos:])
// locate end of duration literal
for pos < len(durFragment) {
rn, _ := utf8.DecodeRuneInString(durFragment[pos:])
if unicode.IsSpace(rn) {
break
}
pos++
}
dur, err := influxql.ParseDuration(durStr[:pos])
// attempt to parse what we suspect is a duration literal
dur, err := influxql.ParseDuration(durFragment[:pos])
if err != nil {
return
return dur, err
}
g.Duration = dur
return dur, nil
}
// parseAbsolute will determine the duration between two absolute timestamps
// found within an InfluxQL fragment following the "where" keyword. For
// example, the fragement "time > '1985-10-25T00:01:21-0800 and time <
// '1985-10-25T00:01:22-0800'" would yield a duration of 1m'
func (g *GroupByVar) parseAbsolute(fragment string) (time.Duration, error) {
timePtn := `time\s[>|<]\s'([0-9\-TZ\:]+)'` // Playground: http://gobular.com/x/41a45095-c384-46ea-b73c-54ef91ab93af
re, err := regexp.Compile(timePtn)
if err != nil {
// this is a developer error and should complain loudly
panic("Bad Regex: err:" + err.Error())
}
if !re.Match([]byte(fragment)) {
return time.Duration(0), errors.New("absolute duration not found")
}
// extract at most two times
matches := re.FindAll([]byte(fragment), 2)
// parse out absolute times
durs := make([]time.Time, 0, 2)
for _, match := range matches {
durStr := re.FindSubmatch(match)
if tm, err := time.Parse(time.RFC3339Nano, string(durStr[1])); err == nil {
durs = append(durs, tm)
}
}
// reject more than 2 times found
if len(durs) != 2 {
return time.Duration(0), errors.New("must provide exactly two absolute times")
}
dur := durs[1].Sub(durs[0])
return dur, nil
}
func (g *GroupByVar) String() string {

49
chronograf_test.go Normal file
View File

@ -0,0 +1,49 @@
package chronograf_test
import (
"testing"
"time"
"github.com/influxdata/chronograf"
)
func Test_GroupByVar(t *testing.T) {
gbvTests := []struct {
name string
query string
expected time.Duration
resolution uint // the screen resolution to render queries into
reportingInterval time.Duration
}{
{
"relative time",
"SELECT mean(usage_idle) FROM cpu WHERE time > now() - 180d GROUP BY :interval:",
4320 * time.Hour,
1000,
10 * time.Second,
},
{
"absolute time",
"SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY :interval:",
1 * time.Minute,
1000,
10 * time.Second,
},
}
for _, test := range gbvTests {
t.Run(test.name, func(t *testing.T) {
gbv := chronograf.GroupByVar{
Var: ":interval:",
Resolution: test.resolution,
ReportingInterval: test.reportingInterval,
}
gbv.Exec(test.query)
if gbv.Duration != test.expected {
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", test.name, test.expected, gbv.Duration)
}
})
}
}