Add support for :autoGroupBy: template variable
This adds support for dynamic template variables that compute something about themselves given some additional context.pull/4934/head
parent
4282f3e566
commit
45402f476d
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue