186 lines
4.8 KiB
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,
|
|
}
|
|
}
|