diff --git a/chronograf.go b/chronograf.go index 245b00a87e..f1f2a6ca76 100644 --- a/chronograf.go +++ b/chronograf.go @@ -289,6 +289,12 @@ type GroupBy struct { Tags []string `json:"tags"` } +// DurationRange represents the lower and upper durations of the query config +type DurationRange struct { + Upper string `json:"upper"` + Lower string `json:"lower"` +} + // QueryConfig represents UI query from the data explorer type QueryConfig struct { ID string `json:"id,omitempty"` @@ -300,6 +306,7 @@ type QueryConfig struct { GroupBy GroupBy `json:"groupBy"` AreTagsAccepted bool `json:"areTagsAccepted"` RawText *string `json:"rawText"` + Range *DurationRange `json:"range"` } // KapacitorNode adds arguments and properties to an alert diff --git a/influx/query.go b/influx/query.go index dc88932ef0..9592103e85 100644 --- a/influx/query.go +++ b/influx/query.go @@ -2,6 +2,7 @@ package influx import ( "strings" + "time" "github.com/influxdata/chronograf" "github.com/influxdata/influxdb/influxql" @@ -174,6 +175,14 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) { } } + // If the condition has a time range we report back its duration + if dur, ok := hasTimeRange(stmt.Condition); ok { + qc.Range = &chronograf.DurationRange{ + Lower: shortDur(dur), + Upper: "now", + } + } + return qc, nil } @@ -202,31 +211,36 @@ func isNow(exp influxql.Expr) bool { return false } -func isDuration(exp influxql.Expr) bool { +func isDuration(exp influxql.Expr) (time.Duration, bool) { switch e := exp.(type) { case *influxql.ParenExpr: return isDuration(e.Expr) - case *influxql.DurationLiteral, *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral: - return true + case *influxql.DurationLiteral: + return e.Val, true + case *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral: + return 0, false } - return false + return 0, false } -func isPreviousTime(exp influxql.Expr) bool { +func isPreviousTime(exp influxql.Expr) (time.Duration, bool) { if p, ok := exp.(*influxql.ParenExpr); ok { return isPreviousTime(p.Expr) } else if bin, ok := exp.(*influxql.BinaryExpr); ok { now := isNow(bin.LHS) || isNow(bin.RHS) // either side can be now op := bin.Op == influxql.SUB - dur := isDuration(bin.LHS) || isDuration(bin.RHS) // either side can be a isDuration - return now && op && dur + dur, hasDur := isDuration(bin.LHS) + if !hasDur { + dur, hasDur = isDuration(bin.RHS) + } + return dur, now && op && hasDur } else if isNow(exp) { // just comparing to now - return true + return 0, true } - return false + return 0, false } -func isTimeRange(exp influxql.Expr) bool { +func isTimeRange(exp influxql.Expr) (time.Duration, bool) { if p, ok := exp.(*influxql.ParenExpr); ok { return isTimeRange(p.Expr) } else if bin, ok := exp.(*influxql.BinaryExpr); ok { @@ -236,21 +250,28 @@ func isTimeRange(exp influxql.Expr) bool { case influxql.LT, influxql.LTE, influxql.GT, influxql.GTE: op = true } - prev := isPreviousTime(bin.LHS) || isPreviousTime(bin.RHS) - return tm && op && prev + dur, prev := isPreviousTime(bin.LHS) + if !prev { + dur, prev = isPreviousTime(bin.RHS) + } + return dur, tm && op && prev } - return false + return 0, false } -func hasTimeRange(exp influxql.Expr) bool { +func hasTimeRange(exp influxql.Expr) (time.Duration, bool) { if p, ok := exp.(*influxql.ParenExpr); ok { return hasTimeRange(p.Expr) - } else if isTimeRange(exp) { - return true + } else if dur, ok := isTimeRange(exp); ok { + return dur, true } else if bin, ok := exp.(*influxql.BinaryExpr); ok { - return isTimeRange(bin.LHS) || isTimeRange(bin.RHS) + dur, ok := isTimeRange(bin.LHS) + if !ok { + dur, ok = isTimeRange(bin.RHS) + } + return dur, ok } - return false + return 0, false } func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { @@ -258,7 +279,7 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { return isTagLogic(p.Expr) } - if isTimeRange(exp) { + if _, ok := isTimeRange(exp); ok { return nil, true } else if tf, ok := isTagFilter(exp); ok { return []tagFilter{tf}, true @@ -280,7 +301,10 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { return nil, false } - tm := isTimeRange(bin.LHS) || isTimeRange(bin.RHS) + _, tm := isTimeRange(bin.LHS) + if !tm { + _, tm = isTimeRange(bin.RHS) + } tf := lhsOK || rhsOK if tm && tf { if lhsOK { @@ -307,35 +331,6 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { return nil, false } -func hasTagFilter(exp influxql.Expr) bool { - if _, ok := isTagFilter(exp); ok { - return true - } else if p, ok := exp.(*influxql.ParenExpr); ok { - return hasTagFilter(p.Expr) - } else if bin, ok := exp.(*influxql.BinaryExpr); ok { - or := bin.Op == influxql.OR - and := bin.Op == influxql.AND - op := or || and - return op && (hasTagFilter(bin.LHS) || hasTagFilter(bin.RHS)) - } - return false -} - -func singleTagFilter(exp influxql.Expr) (tagFilter, bool) { - if p, ok := exp.(*influxql.ParenExpr); ok { - return singleTagFilter(p.Expr) - } else if tf, ok := isTagFilter(exp); ok { - return tf, true - } else if bin, ok := exp.(*influxql.BinaryExpr); ok && bin.Op == influxql.OR { - lhs, lhsOK := singleTagFilter(bin.LHS) - rhs, rhsOK := singleTagFilter(bin.RHS) - if lhsOK && rhsOK && lhs.Op == rhs.Op && lhs.Tag == rhs.Tag { - return lhs, true - } - } - return tagFilter{}, false -} - func isVarRef(exp influxql.Expr) bool { if p, ok := exp.(*influxql.ParenExpr); ok { return isVarRef(p.Expr) @@ -411,3 +406,15 @@ var supportedFuncs = map[string]bool{ "spread": true, "stddev": true, } + +// shortDur converts duration into the queryConfig duration format +func shortDur(d time.Duration) string { + s := d.String() + if strings.HasSuffix(s, "m0s") { + s = s[:len(s)-2] + } + if strings.HasSuffix(s, "h0m") { + s = s[:len(s)-2] + } + return s +} diff --git a/influx/query_test.go b/influx/query_test.go index 9be203c480..247b4ea1aa 100644 --- a/influx/query_test.go +++ b/influx/query_test.go @@ -39,6 +39,182 @@ func TestConvert(t *testing.T) { }, }, }, + { + name: "Test range", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{"host": []string{"myhost"}}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, + { + name: "Test invalid range", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test range with no duration", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now()`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{"host": []string{"myhost"}}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "0s", + Upper: "now", + }, + }, + }, + { + name: "Test range with no tags", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Tags: map[string][]string{}, + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, + { + name: "Test range with no tags nor duration", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where time`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where time`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test with no time range", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test tags accepted", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" = 'myhost' and time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{"host": []string{"myhost"}}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: true, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, + { + name: "Test mixed tag logic", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' or "this" = 'those') and ("howdy" != 'doody') and time > now() - 15m`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' or "this" = 'those') and ("howdy" != 'doody') and time > now() - 15m`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test tags accepted", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' OR "host" = 'yourhost') and ("these" = 'those') and time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{ + "host": []string{"myhost", "yourhost"}, + "these": []string{"those"}, + }, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: true, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -49,6 +225,11 @@ func TestConvert(t *testing.T) { } if tt.RawText != "" { tt.want.RawText = &tt.RawText + if got.RawText == nil { + t.Errorf("Convert() = nil, want %s", tt.RawText) + } else if *got.RawText != tt.RawText { + t.Errorf("Convert() = %s, want %s", *got.RawText, tt.RawText) + } } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Convert() = %#v, want %#v", got, tt.want) diff --git a/server/dashboards_test.go b/server/dashboards_test.go index 73cd59c32f..d090fd73fc 100644 --- a/server/dashboards_test.go +++ b/server/dashboards_test.go @@ -283,6 +283,10 @@ func Test_newDashboardResponse(t *testing.T) { }, Tags: make(map[string][]string, 0), AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, }, }, }, @@ -299,7 +303,7 @@ func Test_newDashboardResponse(t *testing.T) { } for _, tt := range tests { if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) { - t.Errorf("%q. newDashboardResponse() = \n%+v\n\n, want\n\n%+v", tt.name, got, tt.want) + t.Errorf("%q. newDashboardResponse() = \n%#v\n\n, want\n\n%#v", tt.name, got, tt.want) } } }