influxdb/flux/client/request.go

186 lines
4.8 KiB
Go

package client
import (
"context"
"encoding/json"
"fmt"
"time"
"unicode/utf8"
"github.com/influxdata/flux"
"github.com/influxdata/flux/csv"
"github.com/influxdata/flux/lang"
"github.com/influxdata/influxdb/query"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
)
type Controller interface {
Query(ctx context.Context, compiler flux.Compiler) (flux.Query, error)
PrometheusCollectors() []prometheus.Collector
}
// QueryRequest is a flux query request.
type QueryRequest struct {
Type string `json:"type"`
Query string `json:"query"`
// Flux fields
Extern json.RawMessage `json:"extern,omitempty"`
AST json.RawMessage `json:"ast,omitempty"`
Dialect QueryDialect `json:"dialect"`
Now time.Time `json:"now"`
// PreferNoContent specifies if the Response to this request should
// contain any result. This is done for avoiding unnecessary
// bandwidth consumption in certain cases. For example, when the
// query produces side effects and the results do not matter. E.g.:
// from(...) |> ... |> to()
// For example, tasks do not use the results of queries, but only
// care about their side effects.
// To obtain a QueryRequest with no result, add the header
// `Prefer: return-no-content` to the HTTP request.
PreferNoContent bool
// PreferNoContentWithError is the same as above, but it forces the
// Response to contain an error if that is a Flux runtime error encoded
// in the response body.
// To obtain a QueryRequest with no result but runtime errors,
// add the header `Prefer: return-no-content-with-error` to the HTTP request.
PreferNoContentWithError bool
}
// QueryDialect is the formatting options for the query response.
type QueryDialect struct {
Header *bool `json:"header"`
Delimiter string `json:"delimiter"`
CommentPrefix string `json:"commentPrefix"`
DateTimeFormat string `json:"dateTimeFormat"`
Annotations []string `json:"annotations"`
}
// WithDefaults adds default values to the request.
func (r QueryRequest) WithDefaults() QueryRequest {
if r.Type == "" {
r.Type = "flux"
}
if r.Dialect.Delimiter == "" {
r.Dialect.Delimiter = ","
}
if r.Dialect.DateTimeFormat == "" {
r.Dialect.DateTimeFormat = "RFC3339"
}
if r.Dialect.Header == nil {
header := true
r.Dialect.Header = &header
}
return r
}
// Validate checks the query request and returns an error if the request is invalid.
func (r QueryRequest) Validate() error {
if r.Query == "" && r.AST == nil {
return errors.New(`request body requires either query or AST`)
}
if r.Type != "flux" {
return fmt.Errorf(`unknown query type: %s`, r.Type)
}
if len(r.Dialect.CommentPrefix) > 1 {
return fmt.Errorf("invalid dialect comment prefix: must be length 0 or 1")
}
if len(r.Dialect.Delimiter) != 1 {
return fmt.Errorf("invalid dialect delimeter: must be length 1")
}
rn, size := utf8.DecodeRuneInString(r.Dialect.Delimiter)
if rn == utf8.RuneError && size == 1 {
return fmt.Errorf("invalid dialect delimeter character")
}
for _, a := range r.Dialect.Annotations {
switch a {
case "group", "datatype", "default":
default:
return fmt.Errorf(`unknown dialect annotation type: %s`, a)
}
}
switch r.Dialect.DateTimeFormat {
case "RFC3339", "RFC3339Nano":
default:
return fmt.Errorf(`unknown dialect date time format: %s`, r.Dialect.DateTimeFormat)
}
return nil
}
// ProxyRequest specifies a query request and the dialect for the results.
type ProxyRequest struct {
// Compiler converts the query to a specification to run against the data.
Compiler flux.Compiler
// Dialect is the result encoder
Dialect flux.Dialect
}
// ProxyRequest returns a request to proxy from the flux.
func (r QueryRequest) ProxyRequest() *ProxyRequest {
n := r.Now
if n.IsZero() {
n = time.Now()
}
// Query is preferred over spec
var compiler flux.Compiler
if r.Query != "" {
compiler = lang.FluxCompiler{
Query: r.Query,
Extern: r.Extern,
Now: n,
}
} else if len(r.AST) > 0 {
c := lang.ASTCompiler{
Extern: r.Extern,
AST: r.AST,
Now: n,
}
compiler = c
}
delimiter, _ := utf8.DecodeRuneInString(r.Dialect.Delimiter)
noHeader := false
if r.Dialect.Header != nil {
noHeader = !*r.Dialect.Header
}
var dialect flux.Dialect
if r.PreferNoContent {
dialect = &query.NoContentDialect{}
} else {
// TODO(nathanielc): Use commentPrefix and dateTimeFormat
// once they are supported.
encConfig := csv.ResultEncoderConfig{
NoHeader: noHeader,
Delimiter: delimiter,
Annotations: r.Dialect.Annotations,
}
if r.PreferNoContentWithError {
dialect = &query.NoContentWithErrorDialect{
ResultEncoderConfig: encConfig,
}
} else {
dialect = &csv.Dialect{
ResultEncoderConfig: encConfig,
}
}
}
return &ProxyRequest{
Compiler: compiler,
Dialect: dialect,
}
}