diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index 69525931a..11f35474e 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -265,9 +265,9 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { templates := make([]chronograf.Template, len(pb.Templates)) for i, t := range pb.Templates { - vals := make([]chronograf.TemplateValue, len(t.Values)) + vals := make([]chronograf.BasicTemplateValue, len(t.Values)) for j, v := range t.Values { - vals[j] = chronograf.TemplateValue{ + vals[j] = chronograf.BasicTemplateValue{ Selected: v.Selected, Type: v.Type, Value: v.Value, @@ -276,7 +276,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { template := chronograf.Template{ ID: chronograf.TemplateID(t.ID), - TemplateVar: chronograf.TemplateVar{ + BasicTemplateVar: chronograf.BasicTemplateVar{ Var: t.TempVar, Values: vals, }, diff --git a/chronograf.go b/chronograf.go index f3d4796e1..e433a5b66 100644 --- a/chronograf.go +++ b/chronograf.go @@ -2,8 +2,13 @@ package chronograf import ( "context" + "encoding/json" + "errors" + "fmt" "io" "net/http" + "strconv" + "time" ) // General errors. @@ -123,22 +128,31 @@ type Range struct { Lower int64 `json:"lower"` // Lower is the lower bound } +type TemplateVariable interface { + fmt.Stringer + Name() string // returns the variable name +} + // TemplateValue is a value use to replace a template in an InfluxQL query -type TemplateValue struct { +type BasicTemplateValue struct { Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement } // TemplateVar is a named variable within an InfluxQL query to be replaced with Values -type TemplateVar struct { - Var string `json:"tempVar"` // Var is the string to replace within InfluxQL - Values []TemplateValue `json:"values"` // Values are the replacement values within InfluxQL +type BasicTemplateVar struct { + Var string `json:"tempVar"` // Var is the string to replace within InfluxQL + Values []BasicTemplateValue `json:"values"` // Values are the replacement values within InfluxQL +} + +func (t BasicTemplateVar) Name() string { + return t.Var } // String converts the template variable into a correct InfluxQL string based // on its type -func (t TemplateVar) String() string { +func (t BasicTemplateVar) String() string { if len(t.Values) == 0 { return "" } @@ -149,17 +163,36 @@ func (t TemplateVar) String() string { return `'` + t.Values[0].Value + `'` case "csv", "constant": return t.Values[0].Value + case "autoGroupBy": + return "group by time(1555s)" default: return "" } } +type GroupByVar struct { + Var string // the name of the variable as present in the query + Duration time.Duration // the Duration supplied by the query + Resolution uint // the available screen resolution to render the results of this query + ReportingInterval time.Duration // the interval at which data is reported to this series +} + +func (g *GroupByVar) String() string { + //TODO(timraymond): ascertain group by resolution + duration := g.Duration.Nanoseconds() / g.ReportingInterval.Nanoseconds() * int64(g.Resolution) + return "group by time(" + strconv.Itoa(int(duration)/1000000) + "s)" +} + +func (g *GroupByVar) Name() string { + return g.Var +} + // TemplateID is the unique ID used to identify a template type TemplateID string // Template represents a series of choices to replace TemplateVars within InfluxQL type Template struct { - TemplateVar + BasicTemplateVar ID TemplateID `json:"id"` // ID is the unique ID associated with this template Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases Label string `json:"label"` // Label is a user-facing description of the Template @@ -168,14 +201,46 @@ type Template struct { // Query retrieves a Response from a TimeSeries. type Query struct { - Command string `json:"query"` // Command is the query itself - DB string `json:"db,omitempty"` // DB is optional and if empty will not be used. - RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used. - TemplateVars []TemplateVar `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query - Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes - GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags - Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data - Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data + Command string `json:"query"` // Command is the query itself + DB string `json:"db,omitempty"` // DB is optional and if empty will not be used. + RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used. + TemplateVars TemplateVars `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query + Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes + GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags + Resolution uint `json:"resolution,omitempty"` + Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data + Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data +} + +// TemplateVars are a hetergenous collection of different TemplateVariables +// with the capability to decode arbitrary JSON into the appropriate template +// variable type +type TemplateVars []TemplateVariable + +func (t *TemplateVars) UnmarshalJSON(text []byte) error { + var rawVars []interface{} + err := json.Unmarshal(text, &rawVars) + if err != nil { + return err + } + for _, rawVar := range rawVars { + halfBakedVar, ok := rawVar.(map[string]interface{}) + if !ok { + return errors.New("error decoding template variables. Expected a map") + } + + switch halfBakedVar["tempVar"] { + case "autoGroupBy": + (*t) = append(*t, &GroupByVar{ + Duration: 180 * 24 * time.Hour, + Resolution: 1000, + ReportingInterval: 10 * time.Second, + }) + default: + (*t) = append(*t, &BasicTemplateVar{}) + } + } + return nil } // DashboardQuery includes state for the query builder. This is a transition diff --git a/influx/influx.go b/influx/influx.go index c39eb844a..a66176dee 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -69,6 +69,7 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err } req.Header.Set("Content-Type", "application/json") command := q.Command + // TODO(timraymond): move this upper Query() function if len(q.TemplateVars) > 0 { command = TemplateReplace(q.Command, q.TemplateVars) } @@ -84,7 +85,7 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err params.Set("q", command) params.Set("db", q.DB) params.Set("rp", q.RP) - params.Set("epoch", "ms") + params.Set("epoch", "ms") // TODO(timraymond): set this based on analysis req.URL.RawQuery = params.Encode() hc := &http.Client{} diff --git a/influx/influx_test.go b/influx/influx_test.go index 535fd9744..99cdc76ef 100644 --- a/influx/influx_test.go +++ b/influx/influx_test.go @@ -125,10 +125,10 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) { q = "" query = chronograf.Query{ Command: "select $field from cpu", - TemplateVars: []chronograf.TemplateVar{ - { + TemplateVars: chronograf.TemplateVars{ + chronograf.BasicTemplateVar{ Var: "$field", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Value: "usage_user", Type: "fieldKey", diff --git a/influx/templates.go b/influx/templates.go index 7017a3845..27826ca95 100644 --- a/influx/templates.go +++ b/influx/templates.go @@ -7,12 +7,12 @@ import ( ) // TemplateReplace replaces templates with values within the query string -func TemplateReplace(query string, templates []chronograf.TemplateVar) string { +func TemplateReplace(query string, templates chronograf.TemplateVars) string { replacements := []string{} for _, v := range templates { newVal := v.String() if newVal != "" { - replacements = append(replacements, v.Var, newVal) + replacements = append(replacements, v.Name(), newVal) } } diff --git a/influx/templates_test.go b/influx/templates_test.go index b66c8dc2f..e8796c275 100644 --- a/influx/templates_test.go +++ b/influx/templates_test.go @@ -1,7 +1,9 @@ package influx import ( + "encoding/json" "testing" + "time" "github.com/influxdata/chronograf" ) @@ -10,43 +12,43 @@ func TestTemplateReplace(t *testing.T) { tests := []struct { name string query string - vars []chronograf.TemplateVar + vars chronograf.TemplateVars want string }{ { name: "select with parameters", query: "$METHOD field1, $field FROM $measurement WHERE temperature > $temperature", - vars: []chronograf.TemplateVar{ - { + vars: chronograf.TemplateVars{ + chronograf.BasicTemplateVar{ Var: "$temperature", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "csv", Value: "10", }, }, }, - { + chronograf.BasicTemplateVar{ Var: "$field", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "fieldKey", Value: "field2", }, }, }, - { + chronograf.BasicTemplateVar{ Var: "$METHOD", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "csv", Value: "SELECT", }, }, }, - { + chronograf.BasicTemplateVar{ Var: "$measurement", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "csv", Value: `"cpu"`, @@ -59,28 +61,28 @@ func TestTemplateReplace(t *testing.T) { { name: "select with parameters and aggregates", query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`, - vars: []chronograf.TemplateVar{ - { + vars: chronograf.TemplateVars{ + chronograf.BasicTemplateVar{ Var: "$value", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "tagValue", Value: "howdy.com", }, }, }, - { + chronograf.BasicTemplateVar{ Var: "$tag", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "tagKey", Value: "host", }, }, }, - { + chronograf.BasicTemplateVar{ Var: "$field", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "fieldKey", Value: "field", @@ -98,8 +100,8 @@ func TestTemplateReplace(t *testing.T) { { name: "var without a value", query: `SELECT $field FROM "cpu"`, - vars: []chronograf.TemplateVar{ - { + vars: chronograf.TemplateVars{ + chronograf.BasicTemplateVar{ Var: "$field", }, }, @@ -108,10 +110,10 @@ func TestTemplateReplace(t *testing.T) { { name: "var with unknown type", query: `SELECT $field FROM "cpu"`, - vars: []chronograf.TemplateVar{ - { + vars: chronograf.TemplateVars{ + chronograf.BasicTemplateVar{ Var: "$field", - Values: []chronograf.TemplateValue{ + Values: []chronograf.BasicTemplateValue{ { Type: "who knows?", Value: "field", @@ -121,6 +123,19 @@ func TestTemplateReplace(t *testing.T) { }, want: `SELECT $field FROM "cpu"`, }, + { + name: "auto group by", + query: `SELECT mean(usage_idle) from "cpu" where time > now() - 180d :autoGroupBy:`, + vars: chronograf.TemplateVars{ + &chronograf.GroupByVar{ + Var: ":autoGroupBy:", + Duration: 180 * 24 * time.Hour, + Resolution: 1000, + ReportingInterval: 10 * time.Second, + }, + }, + want: `SELECT mean(usage_idle) from "cpu" where time > now() - 180d group by time(1555s)`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -131,3 +146,40 @@ func TestTemplateReplace(t *testing.T) { }) } } + +func Test_TemplateVarsUnmarshalling(t *testing.T) { + req := `[ + { + "tempVar": "autoGroupBy", + "duration": 15552000, + "resolution": 1000, + "reportingInterval": 10 + }, + { + "tempVar": "cpu", + "values": [ + { + "type": "tagValue", + "value": "cpu-total" + } + ] + } + ]` + + expected := []string{ + "group by time(1555s)", + "'cpu-total'", + } + + var tvars chronograf.TemplateVars + err := json.Unmarshal([]byte(req), &tvars) + if err != nil { + t.Fatal("Err unmarshaling:", err) + } + + for idx, tvar := range tvars { + if actual := tvar.String(); expected[idx] != actual { + t.Error("Unexpected tvar. Want:", expected[idx], "Got:", actual) + } + } +} diff --git a/server/queries.go b/server/queries.go index a11fbd594..2c73ff7b4 100644 --- a/server/queries.go +++ b/server/queries.go @@ -28,7 +28,7 @@ type QueryResponse struct { QueryConfig chronograf.QueryConfig `json:"queryConfig"` QueryAST *queries.SelectStatement `json:"queryAST,omitempty"` QueryTemplated *string `json:"queryTemplated,omitempty"` - TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"` + TemplateVars chronograf.TemplateVars `json:"tempVars,omitempty"` } type QueriesResponse struct { diff --git a/server/templates_test.go b/server/templates_test.go index 8a9bec46f..afd220afe 100644 --- a/server/templates_test.go +++ b/server/templates_test.go @@ -16,8 +16,8 @@ func TestValidTemplateRequest(t *testing.T) { name: "Valid Template", template: &chronograf.Template{ Type: "fieldKeys", - TemplateVar: chronograf.TemplateVar{ - Values: []chronograf.TemplateValue{ + BasicTemplateVar: chronograf.BasicTemplateVar{ + Values: []chronograf.BasicTemplateValue{ { Type: "fieldKey", }, @@ -30,8 +30,8 @@ func TestValidTemplateRequest(t *testing.T) { wantErr: true, template: &chronograf.Template{ Type: "Unknown Type", - TemplateVar: chronograf.TemplateVar{ - Values: []chronograf.TemplateValue{ + BasicTemplateVar: chronograf.BasicTemplateVar{ + Values: []chronograf.BasicTemplateValue{ { Type: "fieldKey", }, @@ -44,8 +44,8 @@ func TestValidTemplateRequest(t *testing.T) { wantErr: true, template: &chronograf.Template{ Type: "csv", - TemplateVar: chronograf.TemplateVar{ - Values: []chronograf.TemplateValue{ + BasicTemplateVar: chronograf.BasicTemplateVar{ + Values: []chronograf.BasicTemplateValue{ { Type: "unknown value", },