mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Expressions: Move GEL into core as expressions (#29072)
* comes from grafana/gel-app * remove transform plugin code * move __expr__ and -100 constants to expr pkg * set OrgID on request plugin context * use gtime for resample duration * in resample, rename "rule" to "window", use gtime for duration, parse duration before exec * remove gel entry from plugins-bundled/external.json which creates an empty array for plugins
This commit is contained in:
parent
f01c3f35e1
commit
0cb29d337a
1
go.mod
1
go.mod
@ -86,6 +86,7 @@ require (
|
|||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||||
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd // indirect
|
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd // indirect
|
||||||
golang.org/x/tools v0.0.0-20201023150057-2f4fa188d925 // indirect
|
golang.org/x/tools v0.0.0-20201023150057-2f4fa188d925 // indirect
|
||||||
|
gonum.org/v1/gonum v0.6.0
|
||||||
google.golang.org/api v0.33.0
|
google.golang.org/api v0.33.0
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5 // indirect
|
google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5 // indirect
|
||||||
|
3
go.sum
3
go.sum
@ -1338,6 +1338,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo=
|
||||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
@ -1623,8 +1624,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
|
|||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
|
gonum.org/v1/gonum v0.6.0 h1:DJy6UzXbahnGUf1ujUNkh/NEtK14qMo2nvlBPs4U5yw=
|
||||||
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
||||||
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
|
@ -5,8 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -29,13 +29,13 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext, reqDto dtos.MetricReq
|
|||||||
User: c.SignedInUser,
|
User: c.SignedInUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
expr := false
|
hasExpr := false
|
||||||
var ds *models.DataSource
|
var ds *models.DataSource
|
||||||
for i, query := range reqDto.Queries {
|
for i, query := range reqDto.Queries {
|
||||||
hs.log.Debug("Processing metrics query", "query", query)
|
hs.log.Debug("Processing metrics query", "query", query)
|
||||||
name := query.Get("datasource").MustString("")
|
name := query.Get("datasource").MustString("")
|
||||||
if name == "__expr__" {
|
if name == expr.DatasourceName {
|
||||||
expr = true
|
hasExpr = true
|
||||||
}
|
}
|
||||||
|
|
||||||
datasourceID, err := query.Get("datasourceId").Int64()
|
datasourceID, err := query.Get("datasourceId").Int64()
|
||||||
@ -44,7 +44,7 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext, reqDto dtos.MetricReq
|
|||||||
return Error(400, "Query missing data source ID", nil)
|
return Error(400, "Query missing data source ID", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if i == 0 && !expr {
|
if i == 0 && !hasExpr {
|
||||||
ds, err = hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
|
ds, err = hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hs.log.Debug("Encountered error getting data source", "err", err)
|
hs.log.Debug("Encountered error getting data source", "err", err)
|
||||||
@ -70,7 +70,7 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext, reqDto dtos.MetricReq
|
|||||||
|
|
||||||
var resp *tsdb.Response
|
var resp *tsdb.Response
|
||||||
var err error
|
var err error
|
||||||
if !expr {
|
if !hasExpr {
|
||||||
resp, err = tsdb.HandleRequest(c.Req.Context(), ds, request)
|
resp, err = tsdb.HandleRequest(c.Req.Context(), ds, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Error(500, "Metric request error", err)
|
return Error(500, "Metric request error", err)
|
||||||
@ -80,7 +80,7 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext, reqDto dtos.MetricReq
|
|||||||
return Error(404, "Expressions feature toggle is not enabled", nil)
|
return Error(404, "Expressions feature toggle is not enabled", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = plugins.Transform.Transform(c.Req.Context(), request)
|
resp, err = expr.WrapTransformData(c.Req.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Error(500, "Transform request error", err)
|
return Error(500, "Transform request error", err)
|
||||||
}
|
}
|
||||||
|
263
pkg/expr/commands.go
Normal file
263
pkg/expr/commands.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/components/gtime"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command is an interface for all expression commands.
|
||||||
|
type Command interface {
|
||||||
|
NeedsVars() []string
|
||||||
|
Execute(c context.Context, vars mathexp.Vars) (mathexp.Results, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MathCommand is a command for a math expression such as "1 + $GA / 2"
|
||||||
|
type MathCommand struct {
|
||||||
|
RawExpression string
|
||||||
|
Expression *mathexp.Expr
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMathCommand creates a new MathCommand. It will return an error
|
||||||
|
// if there is an error parsing expr.
|
||||||
|
func NewMathCommand(expr string) (*MathCommand, error) {
|
||||||
|
parsedExpr, err := mathexp.New(expr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &MathCommand{
|
||||||
|
RawExpression: expr,
|
||||||
|
Expression: parsedExpr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMathCommand creates a MathCommand from Grafana's frontend query.
|
||||||
|
func UnmarshalMathCommand(rn *rawNode) (*MathCommand, error) {
|
||||||
|
rawExpr, ok := rn.Query["expression"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("math command for refId %v is missing an expression", rn.RefID)
|
||||||
|
}
|
||||||
|
exprString, ok := rawExpr.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected math command for refId %v expression to be a string, got %T", rn.RefID, rawExpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
gm, err := NewMathCommand(exprString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid math command type in '%v': %v", rn.RefID, err)
|
||||||
|
}
|
||||||
|
return gm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsVars returns the variable names (refIds) that are dependencies
|
||||||
|
// to execute the command and allows the command to fulfill the Command interface.
|
||||||
|
func (gm *MathCommand) NeedsVars() []string {
|
||||||
|
return gm.Expression.VarNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the command and returns the results or an error if the command
|
||||||
|
// failed to execute.
|
||||||
|
func (gm *MathCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
|
||||||
|
return gm.Expression.Execute(vars)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max.
|
||||||
|
type ReduceCommand struct {
|
||||||
|
Reducer string
|
||||||
|
VarToReduce string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReduceCommand creates a new ReduceCMD.
|
||||||
|
func NewReduceCommand(reducer, varToReduce string) *ReduceCommand {
|
||||||
|
// TODO: validate reducer here, before execution
|
||||||
|
return &ReduceCommand{
|
||||||
|
Reducer: reducer,
|
||||||
|
VarToReduce: varToReduce,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query.
|
||||||
|
func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error) {
|
||||||
|
rawVar, ok := rn.Query["expression"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no variable specified to reduce for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
varToReduce, ok := rawVar.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected reduce variable to be a string, got %T for refId %v", rawVar, rn.RefID)
|
||||||
|
}
|
||||||
|
varToReduce = strings.TrimPrefix(varToReduce, "$")
|
||||||
|
|
||||||
|
rawReducer, ok := rn.Query["reducer"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no reducer specified for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
redFunc, ok := rawReducer.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected reducer to be a string, got %T for refId %v", rawReducer, rn.RefID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewReduceCommand(redFunc, varToReduce), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsVars returns the variable names (refIds) that are dependencies
|
||||||
|
// to execute the command and allows the command to fulfill the Command interface.
|
||||||
|
func (gr *ReduceCommand) NeedsVars() []string {
|
||||||
|
return []string{gr.VarToReduce}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the command and returns the results or an error if the command
|
||||||
|
// failed to execute.
|
||||||
|
func (gr *ReduceCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
|
||||||
|
newRes := mathexp.Results{}
|
||||||
|
for _, val := range vars[gr.VarToReduce].Values {
|
||||||
|
series, ok := val.(mathexp.Series)
|
||||||
|
if !ok {
|
||||||
|
return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type())
|
||||||
|
}
|
||||||
|
num, err := series.Reduce(gr.Reducer)
|
||||||
|
if err != nil {
|
||||||
|
return newRes, err
|
||||||
|
}
|
||||||
|
newRes.Values = append(newRes.Values, num)
|
||||||
|
}
|
||||||
|
return newRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResampleCommand is an expression command for resampling of a timeseries.
|
||||||
|
type ResampleCommand struct {
|
||||||
|
Window time.Duration
|
||||||
|
VarToResample string
|
||||||
|
Downsampler string
|
||||||
|
Upsampler string
|
||||||
|
TimeRange backend.TimeRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResampleCommand creates a new ResampleCMD.
|
||||||
|
func NewResampleCommand(rawWindow, varToResample string, downsampler string, upsampler string, tr backend.TimeRange) (*ResampleCommand, error) {
|
||||||
|
// TODO: validate reducer here, before execution
|
||||||
|
window, err := gtime.ParseDuration(rawWindow)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`failed to parse resample "window" duration field %q: %w`, window, err)
|
||||||
|
}
|
||||||
|
return &ResampleCommand{
|
||||||
|
Window: window,
|
||||||
|
VarToResample: varToResample,
|
||||||
|
Downsampler: downsampler,
|
||||||
|
Upsampler: upsampler,
|
||||||
|
TimeRange: tr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.
|
||||||
|
func UnmarshalResampleCommand(rn *rawNode) (*ResampleCommand, error) {
|
||||||
|
rawVar, ok := rn.Query["expression"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no variable to resample for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
varToReduce, ok := rawVar.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected resample input variable to be type string, but got type %T for refId %v", rawVar, rn.RefID)
|
||||||
|
}
|
||||||
|
varToReduce = strings.TrimPrefix(varToReduce, "$")
|
||||||
|
varToResample := varToReduce
|
||||||
|
|
||||||
|
rawWindow, ok := rn.Query["window"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no time duration specified for the window in resample command for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
window, ok := rawWindow.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected resample window to be a string, got %T for refId %v", rawWindow, rn.RefID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawDownsampler, ok := rn.Query["downsampler"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no downsampler function specified in resample command for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
downsampler, ok := rawDownsampler.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", downsampler, rn.RefID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawUpsampler, ok := rn.Query["upsampler"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no downsampler specified in resample command for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
upsampler, ok := rawUpsampler.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", upsampler, rn.RefID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewResampleCommand(window, varToResample, downsampler, upsampler, rn.TimeRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsVars returns the variable names (refIds) that are dependencies
|
||||||
|
// to execute the command and allows the command to fulfill the Command interface.
|
||||||
|
func (gr *ResampleCommand) NeedsVars() []string {
|
||||||
|
return []string{gr.VarToResample}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the command and returns the results or an error if the command
|
||||||
|
// failed to execute.
|
||||||
|
func (gr *ResampleCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
|
||||||
|
newRes := mathexp.Results{}
|
||||||
|
for _, val := range vars[gr.VarToResample].Values {
|
||||||
|
series, ok := val.(mathexp.Series)
|
||||||
|
if !ok {
|
||||||
|
return newRes, fmt.Errorf("can only resample type series, got type %v", val.Type())
|
||||||
|
}
|
||||||
|
num, err := series.Resample(gr.Window, gr.Downsampler, gr.Upsampler, gr.TimeRange)
|
||||||
|
if err != nil {
|
||||||
|
return newRes, err
|
||||||
|
}
|
||||||
|
newRes.Values = append(newRes.Values, num)
|
||||||
|
}
|
||||||
|
return newRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandType is the type of the expression command.
|
||||||
|
type CommandType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TypeUnknown is the CMDType for an unrecognized expression type.
|
||||||
|
TypeUnknown CommandType = iota
|
||||||
|
// TypeMath is the CMDType for a math expression.
|
||||||
|
TypeMath
|
||||||
|
// TypeReduce is the CMDType for a reduction expression.
|
||||||
|
TypeReduce
|
||||||
|
// TypeResample is the CMDType for a resampling expression.
|
||||||
|
TypeResample
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gt CommandType) String() string {
|
||||||
|
switch gt {
|
||||||
|
case TypeMath:
|
||||||
|
return "math"
|
||||||
|
case TypeReduce:
|
||||||
|
return "reduce"
|
||||||
|
case TypeResample:
|
||||||
|
return "resample"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCommandType returns a CommandType from its string representation.
|
||||||
|
func ParseCommandType(s string) (CommandType, error) {
|
||||||
|
switch s {
|
||||||
|
case "math":
|
||||||
|
return TypeMath, nil
|
||||||
|
case "reduce":
|
||||||
|
return TypeReduce, nil
|
||||||
|
case "resample":
|
||||||
|
return TypeResample, nil
|
||||||
|
default:
|
||||||
|
return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s)
|
||||||
|
}
|
||||||
|
}
|
183
pkg/expr/graph.go
Normal file
183
pkg/expr/graph.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||||
|
|
||||||
|
"gonum.org/v1/gonum/graph"
|
||||||
|
"gonum.org/v1/gonum/graph/simple"
|
||||||
|
"gonum.org/v1/gonum/graph/topo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NodeType is the type of a DPNode. Currently either a expression command or datasource query.
|
||||||
|
type NodeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TypeCMDNode is a NodeType for expression commands.
|
||||||
|
TypeCMDNode NodeType = iota
|
||||||
|
// TypeDatasourceNode is a NodeType for datasource queries.
|
||||||
|
TypeDatasourceNode
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node is a node in a Data Pipeline. Node is either a expression command or a datasource query.
|
||||||
|
type Node interface {
|
||||||
|
ID() int64 // ID() allows the gonum graph node interface to be fulfilled
|
||||||
|
NodeType() NodeType
|
||||||
|
RefID() string
|
||||||
|
Execute(c context.Context, vars mathexp.Vars) (mathexp.Results, error)
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataPipeline is an ordered set of nodes returned from DPGraph processing.
|
||||||
|
type DataPipeline []Node
|
||||||
|
|
||||||
|
// execute runs all the command/datasource requests in the pipeline return a
|
||||||
|
// map of the refId of the of each command
|
||||||
|
func (dp *DataPipeline) execute(c context.Context) (mathexp.Vars, error) {
|
||||||
|
vars := make(mathexp.Vars)
|
||||||
|
for _, node := range *dp {
|
||||||
|
res, err := node.Execute(c, vars)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vars[node.RefID()] = res
|
||||||
|
}
|
||||||
|
return vars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPipeline builds a graph of the nodes, and returns the nodes in an
|
||||||
|
// executable order.
|
||||||
|
func buildPipeline(queries []backend.DataQuery) (DataPipeline, error) {
|
||||||
|
graph, err := buildDependencyGraph(queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := buildExecutionOrder(graph)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDependencyGraph returns a dependency graph for a set of queries.
|
||||||
|
func buildDependencyGraph(queries []backend.DataQuery) (*simple.DirectedGraph, error) {
|
||||||
|
graph, err := buildGraph(queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := buildNodeRegistry(graph)
|
||||||
|
|
||||||
|
if err := buildGraphEdges(graph, registry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildExecutionOrder returns a sequence of nodes ordered by dependency.
|
||||||
|
func buildExecutionOrder(graph *simple.DirectedGraph) ([]Node, error) {
|
||||||
|
sortedNodes, err := topo.Sort(graph)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := make([]Node, len(sortedNodes))
|
||||||
|
for i, v := range sortedNodes {
|
||||||
|
nodes[i] = v.(Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildNodeRegistry returns a lookup table for reference IDs to respective node.
|
||||||
|
func buildNodeRegistry(g *simple.DirectedGraph) map[string]Node {
|
||||||
|
res := make(map[string]Node)
|
||||||
|
|
||||||
|
nodeIt := g.Nodes()
|
||||||
|
|
||||||
|
for nodeIt.Next() {
|
||||||
|
if dpNode, ok := nodeIt.Node().(Node); ok {
|
||||||
|
res[dpNode.RefID()] = dpNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGraph creates a new graph populated with nodes for every query.
|
||||||
|
func buildGraph(queries []backend.DataQuery) (*simple.DirectedGraph, error) {
|
||||||
|
dp := simple.NewDirectedGraph()
|
||||||
|
|
||||||
|
for _, query := range queries {
|
||||||
|
rawQueryProp := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(query.JSON, &rawQueryProp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rn := &rawNode{
|
||||||
|
Query: rawQueryProp,
|
||||||
|
RefID: query.RefID,
|
||||||
|
TimeRange: query.TimeRange,
|
||||||
|
QueryType: query.QueryType,
|
||||||
|
}
|
||||||
|
|
||||||
|
dsName, err := rn.GetDatasourceName()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var node graph.Node
|
||||||
|
switch dsName {
|
||||||
|
case DatasourceName:
|
||||||
|
node, err = buildCMDNode(dp, rn)
|
||||||
|
default: // If it's not an expression query, it's a data source query.
|
||||||
|
node, err = buildDSNode(dp, rn)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dp.AddNode(node)
|
||||||
|
}
|
||||||
|
return dp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGraphEdges generates graph edges based on each node's dependencies.
|
||||||
|
func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
|
||||||
|
nodeIt := dp.Nodes()
|
||||||
|
|
||||||
|
for nodeIt.Next() {
|
||||||
|
node := nodeIt.Node().(Node)
|
||||||
|
|
||||||
|
if node.NodeType() != TypeCMDNode {
|
||||||
|
// datasource node, nothing to do for now. Although if we want expression results to be
|
||||||
|
// used as datasource query params some day this will need change
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdNode := node.(*CMDNode)
|
||||||
|
|
||||||
|
for _, neededVar := range cmdNode.Command.NeedsVars() {
|
||||||
|
neededNode, ok := registry[neededVar]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unable to find dependent node '%v'", neededVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
if neededNode.ID() == cmdNode.ID() {
|
||||||
|
return fmt.Errorf("can not add self referencing node for var '%v' ", neededVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
edge := dp.NewEdge(neededNode, cmdNode)
|
||||||
|
|
||||||
|
dp.SetEdge(edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
536
pkg/expr/mathexp/exp.go
Normal file
536
pkg/expr/mathexp/exp.go
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expr holds a parsed math command expression.
|
||||||
|
type Expr struct {
|
||||||
|
*parse.Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
// State embeds a parsed Expr with variables and their results
|
||||||
|
// so the expression can act on them.
|
||||||
|
type State struct {
|
||||||
|
*Expr
|
||||||
|
Vars Vars
|
||||||
|
// Could hold more properties that change behavior around:
|
||||||
|
// - Unions (How many result A and many Result B in case A + B are joined)
|
||||||
|
// - NaN/Null behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vars holds the results of datasource queries or other expression commands.
|
||||||
|
type Vars map[string]Results
|
||||||
|
|
||||||
|
// New creates a new expression tree
|
||||||
|
func New(expr string, funcs ...map[string]parse.Func) (*Expr, error) {
|
||||||
|
funcs = append(funcs, builtins)
|
||||||
|
t, err := parse.Parse(expr, funcs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e := &Expr{
|
||||||
|
Tree: t,
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute applies a parse expression to the context and executes it
|
||||||
|
func (e *Expr) Execute(vars Vars) (r Results, err error) {
|
||||||
|
s := &State{
|
||||||
|
Expr: e,
|
||||||
|
Vars: vars,
|
||||||
|
}
|
||||||
|
return e.executeState(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Expr) executeState(s *State) (r Results, err error) {
|
||||||
|
defer errRecover(&err, s)
|
||||||
|
r, err = s.walk(e.Tree.Root)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// errRecover is the handler that turns panics into returns from the top
|
||||||
|
// level of Parse.
|
||||||
|
func errRecover(errp *error, s *State) {
|
||||||
|
e := recover()
|
||||||
|
if e != nil {
|
||||||
|
switch err := e.(type) {
|
||||||
|
case runtime.Error:
|
||||||
|
panic(e)
|
||||||
|
case error:
|
||||||
|
*errp = err
|
||||||
|
default:
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk is the top level function to walk a parsed expression
|
||||||
|
// with its associate variables
|
||||||
|
func (e *State) walk(node parse.Node) (res Results, err error) {
|
||||||
|
switch node := node.(type) {
|
||||||
|
case *parse.ScalarNode:
|
||||||
|
res = NewScalarResults(&node.Float64)
|
||||||
|
case *parse.VarNode:
|
||||||
|
res = e.Vars[node.Name]
|
||||||
|
case *parse.BinaryNode:
|
||||||
|
res, err = e.walkBinary(node)
|
||||||
|
case *parse.UnaryNode:
|
||||||
|
res, err = e.walkUnary(node)
|
||||||
|
case *parse.FuncNode:
|
||||||
|
res, err = e.walkFunc(node)
|
||||||
|
default:
|
||||||
|
return res, fmt.Errorf("expr: can not walk node type: %s", node.Type())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *State) walkUnary(node *parse.UnaryNode) (Results, error) {
|
||||||
|
a, err := e.walk(node.Arg)
|
||||||
|
if err != nil {
|
||||||
|
return Results{}, err
|
||||||
|
}
|
||||||
|
newResults := Results{}
|
||||||
|
for _, val := range a.Values {
|
||||||
|
var newVal Value
|
||||||
|
switch rt := val.(type) {
|
||||||
|
case Scalar:
|
||||||
|
newVal = NewScalar(nil)
|
||||||
|
f := rt.GetFloat64Value()
|
||||||
|
if f != nil {
|
||||||
|
newF, err := unaryOp(node.OpStr, *f)
|
||||||
|
if err != nil {
|
||||||
|
return newResults, err
|
||||||
|
}
|
||||||
|
newVal = NewScalar(&newF)
|
||||||
|
}
|
||||||
|
case Number:
|
||||||
|
newVal, err = unaryNumber(rt, node.OpStr)
|
||||||
|
case Series:
|
||||||
|
newVal, err = unarySeries(rt, node.OpStr)
|
||||||
|
default:
|
||||||
|
return newResults, fmt.Errorf("can not perform a unary operation on type %v", rt.Type())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return newResults, err
|
||||||
|
}
|
||||||
|
newResults.Values = append(newResults.Values, newVal)
|
||||||
|
}
|
||||||
|
return newResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unarySeries(s Series, op string) (Series, error) {
|
||||||
|
newSeries := NewSeries(s.GetName(), s.GetLabels(), s.TimeIdx, s.TimeIsNullable, s.ValueIdx, s.ValueIsNullabe, s.Len())
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
t, f := s.GetPoint(i)
|
||||||
|
if f == nil {
|
||||||
|
if err := newSeries.SetPoint(i, t, nil); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newF, err := unaryOp(op, *f)
|
||||||
|
if err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
if err := newSeries.SetPoint(i, t, &newF); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSeries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unaryNumber(n Number, op string) (Number, error) {
|
||||||
|
newNumber := NewNumber(n.GetName(), n.GetLabels())
|
||||||
|
|
||||||
|
f := n.GetFloat64Value()
|
||||||
|
if f != nil {
|
||||||
|
newF, err := unaryOp(op, *f)
|
||||||
|
if err != nil {
|
||||||
|
return newNumber, err
|
||||||
|
}
|
||||||
|
newNumber.SetValue(&newF)
|
||||||
|
}
|
||||||
|
return newNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unaryOp performs a unary operation on a float.
|
||||||
|
func unaryOp(op string, a float64) (r float64, err error) {
|
||||||
|
if math.IsNaN(a) {
|
||||||
|
return math.NaN(), nil
|
||||||
|
}
|
||||||
|
switch op {
|
||||||
|
case "!":
|
||||||
|
if a == 0 {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case "-":
|
||||||
|
r = -a
|
||||||
|
default:
|
||||||
|
return r, fmt.Errorf("expr: unknown unary operator %s", op)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union holds to Values from Two sets where their labels are compatible (TODO: define compatible).
|
||||||
|
// This is a intermediate container for Binary operations such (e.g. A + B).
|
||||||
|
type Union struct {
|
||||||
|
Labels data.Labels
|
||||||
|
A, B Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// union creates Union objects based on the Labels attached to each Series or Number
|
||||||
|
// within a collection of Series or Numbers. The Unions are used with binary
|
||||||
|
// operations. The labels of the Union will the taken from result with a greater
|
||||||
|
// number of tags.
|
||||||
|
func union(aResults, bResults Results) []*Union {
|
||||||
|
unions := []*Union{}
|
||||||
|
if len(aResults.Values) == 0 || len(bResults.Values) == 0 {
|
||||||
|
return unions
|
||||||
|
}
|
||||||
|
for _, a := range aResults.Values {
|
||||||
|
for _, b := range bResults.Values {
|
||||||
|
var labels data.Labels
|
||||||
|
aLabels := a.GetLabels()
|
||||||
|
bLabels := b.GetLabels()
|
||||||
|
switch {
|
||||||
|
case aLabels.Equals(bLabels) || len(aLabels) == 0 || len(bLabels) == 0:
|
||||||
|
l := aLabels
|
||||||
|
if len(aLabels) == 0 {
|
||||||
|
l = bLabels
|
||||||
|
}
|
||||||
|
labels = l
|
||||||
|
case len(aLabels) == len(bLabels):
|
||||||
|
continue // invalid union, drop for now
|
||||||
|
case aLabels.Contains(bLabels):
|
||||||
|
labels = aLabels
|
||||||
|
case bLabels.Contains(aLabels):
|
||||||
|
labels = bLabels
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u := &Union{
|
||||||
|
Labels: labels,
|
||||||
|
A: a,
|
||||||
|
B: b,
|
||||||
|
}
|
||||||
|
unions = append(unions, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(unions) == 0 && len(aResults.Values) == 1 && len(bResults.Values) == 1 {
|
||||||
|
// In the case of only 1 thing on each side of the operator, we combine them
|
||||||
|
// and strip the tags.
|
||||||
|
// This isn't ideal for understanding behavior, but will make more stuff work when
|
||||||
|
// combining different datasources without munging.
|
||||||
|
// This choice is highly questionable in the long term.
|
||||||
|
unions = append(unions, &Union{
|
||||||
|
A: aResults.Values[0],
|
||||||
|
B: bResults.Values[0],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return unions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *State) walkBinary(node *parse.BinaryNode) (Results, error) {
|
||||||
|
res := Results{Values{}}
|
||||||
|
ar, err := e.walk(node.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
br, err := e.walk(node.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
unions := union(ar, br)
|
||||||
|
for _, uni := range unions {
|
||||||
|
name := uni.Labels.String()
|
||||||
|
var value Value
|
||||||
|
switch at := uni.A.(type) {
|
||||||
|
case Scalar:
|
||||||
|
aFloat := at.GetFloat64Value()
|
||||||
|
switch bt := uni.B.(type) {
|
||||||
|
// Scalar op Scalar
|
||||||
|
case Scalar:
|
||||||
|
bFloat := bt.GetFloat64Value()
|
||||||
|
if aFloat == nil || bFloat == nil {
|
||||||
|
value = NewScalar(nil)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
f := math.NaN()
|
||||||
|
if aFloat != nil && bFloat != nil {
|
||||||
|
f, err = binaryOp(node.OpStr, *aFloat, *bFloat)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = NewScalar(&f)
|
||||||
|
// Scalar op Scalar
|
||||||
|
case Number:
|
||||||
|
value, err = biScalarNumber(name, uni.Labels, node.OpStr, bt, aFloat, false)
|
||||||
|
// Scalar op Series
|
||||||
|
case Series:
|
||||||
|
value, err = biSeriesNumber(name, uni.Labels, node.OpStr, bt, aFloat, false)
|
||||||
|
default:
|
||||||
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
|
}
|
||||||
|
case Series:
|
||||||
|
switch bt := uni.B.(type) {
|
||||||
|
// Series Op Scalar
|
||||||
|
case Scalar:
|
||||||
|
bFloat := bt.GetFloat64Value()
|
||||||
|
value, err = biSeriesNumber(name, uni.Labels, node.OpStr, at, bFloat, true)
|
||||||
|
// case Series Op Number
|
||||||
|
case Number:
|
||||||
|
bFloat := bt.GetFloat64Value()
|
||||||
|
value, err = biSeriesNumber(name, uni.Labels, node.OpStr, at, bFloat, true)
|
||||||
|
// case Series op Series
|
||||||
|
case Series:
|
||||||
|
value, err = biSeriesSeries(name, uni.Labels, node.OpStr, at, bt)
|
||||||
|
default:
|
||||||
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
|
}
|
||||||
|
case Number:
|
||||||
|
aFloat := at.GetFloat64Value()
|
||||||
|
switch bt := uni.B.(type) {
|
||||||
|
case Scalar:
|
||||||
|
bFloat := bt.GetFloat64Value()
|
||||||
|
value, err = biScalarNumber(name, uni.Labels, node.OpStr, at, bFloat, true)
|
||||||
|
case Number:
|
||||||
|
bFloat := bt.GetFloat64Value()
|
||||||
|
value, err = biScalarNumber(name, uni.Labels, node.OpStr, at, bFloat, true)
|
||||||
|
case Series:
|
||||||
|
value, err = biSeriesNumber(name, uni.Labels, node.OpStr, bt, aFloat, false)
|
||||||
|
default:
|
||||||
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return res, fmt.Errorf("not implemented: binary %v on %T and %T", node.OpStr, uni.A, uni.B)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
res.Values = append(res.Values, value)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// binaryOp performs a binary operations (e.g. A+B or A>B) on two
|
||||||
|
// float values
|
||||||
|
// nolint:gocyclo
|
||||||
|
func binaryOp(op string, a, b float64) (r float64, err error) {
|
||||||
|
// Test short circuit before NaN.
|
||||||
|
switch op {
|
||||||
|
case "||":
|
||||||
|
if a != 0 {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
case "&&":
|
||||||
|
if a == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if math.IsNaN(a) || math.IsNaN(b) {
|
||||||
|
return math.NaN(), nil
|
||||||
|
}
|
||||||
|
switch op {
|
||||||
|
case "+":
|
||||||
|
r = a + b
|
||||||
|
case "*":
|
||||||
|
r = a * b
|
||||||
|
case "-":
|
||||||
|
r = a - b
|
||||||
|
case "/":
|
||||||
|
r = a / b
|
||||||
|
case "**":
|
||||||
|
r = math.Pow(a, b)
|
||||||
|
case "%":
|
||||||
|
r = math.Mod(a, b)
|
||||||
|
case "==":
|
||||||
|
if a == b {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case ">":
|
||||||
|
if a > b {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case "!=":
|
||||||
|
if a != b {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case "<":
|
||||||
|
if a < b {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case ">=":
|
||||||
|
if a >= b {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case "<=":
|
||||||
|
if a <= b {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case "||":
|
||||||
|
if a != 0 || b != 0 {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
case "&&":
|
||||||
|
if a != 0 && b != 0 {
|
||||||
|
r = 1
|
||||||
|
} else {
|
||||||
|
r = 0
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return r, fmt.Errorf("expr: unknown operator %s", op)
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func biScalarNumber(name string, labels data.Labels, op string, number Number, scalarVal *float64, numberFirst bool) (Number, error) {
|
||||||
|
newNumber := NewNumber(name, labels)
|
||||||
|
f := number.GetFloat64Value()
|
||||||
|
if f == nil || scalarVal == nil {
|
||||||
|
newNumber.SetValue(nil)
|
||||||
|
return newNumber, nil
|
||||||
|
}
|
||||||
|
nF := math.NaN()
|
||||||
|
var err error
|
||||||
|
if numberFirst {
|
||||||
|
nF, err = binaryOp(op, *f, *scalarVal)
|
||||||
|
} else {
|
||||||
|
nF, err = binaryOp(op, *scalarVal, *f)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return newNumber, err
|
||||||
|
}
|
||||||
|
newNumber.SetValue(&nF)
|
||||||
|
return newNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func biSeriesNumber(name string, labels data.Labels, op string, s Series, scalarVal *float64, seriesFirst bool) (Series, error) {
|
||||||
|
newSeries := NewSeries(name, labels, s.TimeIdx, s.TimeIsNullable, s.ValueIdx, s.ValueIsNullabe, s.Len())
|
||||||
|
var err error
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
nF := math.NaN()
|
||||||
|
t, f := s.GetPoint(i)
|
||||||
|
if f == nil || scalarVal == nil {
|
||||||
|
if err := newSeries.SetPoint(i, t, nil); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seriesFirst {
|
||||||
|
nF, err = binaryOp(op, *f, *scalarVal)
|
||||||
|
} else {
|
||||||
|
nF, err = binaryOp(op, *scalarVal, *f)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
if err := newSeries.SetPoint(i, t, &nF); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSeries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... if would you like some series with your series and then get some series, or is that enough series?
|
||||||
|
// biSeriesSeries performs a the binary operation for each value in the two series where the times
|
||||||
|
// are equal. If there are datapoints in A or B that do not share a time, they will be dropped.
|
||||||
|
func biSeriesSeries(name string, labels data.Labels, op string, aSeries, bSeries Series) (Series, error) {
|
||||||
|
bPoints := make(map[time.Time]*float64)
|
||||||
|
for i := 0; i < bSeries.Len(); i++ {
|
||||||
|
t, f := bSeries.GetPoint(i)
|
||||||
|
if t != nil {
|
||||||
|
bPoints[*t] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newSeries := NewSeries(name, labels, aSeries.TimeIdx, aSeries.TimeIsNullable || bSeries.TimeIsNullable, aSeries.ValueIdx, aSeries.ValueIsNullabe || bSeries.ValueIsNullabe, 0)
|
||||||
|
for aIdx := 0; aIdx < aSeries.Len(); aIdx++ {
|
||||||
|
aTime, aF := aSeries.GetPoint(aIdx)
|
||||||
|
bF, ok := bPoints[*aTime]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if aF == nil || bF == nil {
|
||||||
|
if err := newSeries.AppendPoint(aIdx, aTime, nil); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nF, err := binaryOp(op, *aF, *bF)
|
||||||
|
if err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
if err := newSeries.AppendPoint(aIdx, aTime, &nF); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSeries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *State) walkFunc(node *parse.FuncNode) (Results, error) {
|
||||||
|
var res Results
|
||||||
|
var err error
|
||||||
|
var in []reflect.Value
|
||||||
|
for _, a := range node.Args {
|
||||||
|
var v interface{}
|
||||||
|
switch t := a.(type) {
|
||||||
|
case *parse.StringNode:
|
||||||
|
v = t.Text
|
||||||
|
case *parse.VarNode:
|
||||||
|
v = e.Vars[t.Name]
|
||||||
|
case *parse.ScalarNode:
|
||||||
|
v = t.Float64
|
||||||
|
case *parse.FuncNode:
|
||||||
|
v, err = e.walkFunc(t)
|
||||||
|
case *parse.UnaryNode:
|
||||||
|
v, err = e.walkUnary(t)
|
||||||
|
case *parse.BinaryNode:
|
||||||
|
v, err = e.walkBinary(t)
|
||||||
|
default:
|
||||||
|
return res, fmt.Errorf("expr: unknown func arg type: %T", t)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
in = append(in, reflect.ValueOf(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
f := reflect.ValueOf(node.F.F)
|
||||||
|
|
||||||
|
fr := f.Call(append([]reflect.Value{reflect.ValueOf(e)}, in...))
|
||||||
|
|
||||||
|
res = fr[0].Interface().(Results)
|
||||||
|
if len(fr) > 1 && !fr[1].IsNil() {
|
||||||
|
err := fr[1].Interface().(error)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
423
pkg/expr/mathexp/exp_nan_null_val_test.go
Normal file
423
pkg/expr/mathexp/exp_nan_null_val_test.go
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNaN(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
results Results
|
||||||
|
willPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unary !: Op Number(NaN) is NaN",
|
||||||
|
expr: "! $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("", nil, NaN)}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, NaN)}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unary -: Op Number(NaN) is NaN",
|
||||||
|
expr: "! $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("", nil, NaN)}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, NaN)}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: Scalar Op(Non-AND/OR) Number(NaN) is NaN",
|
||||||
|
expr: "1 * $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("", nil, NaN)}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, NaN)}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: Scalar Op(AND/OR) Number(NaN) is 0/1",
|
||||||
|
expr: "1 || $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("", nil, NaN)}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, float64Pointer(1))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: Scalar Op(Non-AND/OR) Series(with NaN value) is NaN)",
|
||||||
|
expr: "1 - $A",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(-1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: Number Op(Non-AND/OR) Series(with NaN value) is Series with NaN",
|
||||||
|
expr: "$A == $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{[]Value{makeNumber("", nil, float64Pointer(0))}},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(0),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: Number(NaN) Op Series(with NaN value) is Series with NaN",
|
||||||
|
expr: "$A + $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{[]Value{makeNumber("", nil, NaN)}},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), NaN,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), NaN,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := cmp.Comparer(func(x, y float64) bool {
|
||||||
|
return (math.IsNaN(x) && math.IsNaN(y)) || x == y
|
||||||
|
})
|
||||||
|
options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testBlock := func() {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
if diff := cmp.Diff(res, tt.results, options...); diff != "" {
|
||||||
|
assert.FailNow(t, tt.name, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.willPanic {
|
||||||
|
assert.Panics(t, testBlock)
|
||||||
|
} else {
|
||||||
|
assert.NotPanics(t, testBlock)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullValues(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
results Results
|
||||||
|
willPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "scalar: unary ! null(): is null",
|
||||||
|
expr: "! null()",
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: NewScalarResults(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scalar: binary null() + null(): is null",
|
||||||
|
expr: "null() + null()",
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: NewScalarResults(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scalar: binary 1 + null(): is null",
|
||||||
|
expr: "1 + null()",
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: NewScalarResults(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series: unary with a null value in it has a null value in result",
|
||||||
|
expr: "- $A",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(-1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series: binary with a null value in it has a null value in result",
|
||||||
|
expr: "$A - $A",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(0),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series and scalar: binary with a null value in it has a nil value in result",
|
||||||
|
expr: "$A - 1",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(0),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number: unary ! null number: is null",
|
||||||
|
expr: "! $A",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number: binary null number and null number: is null",
|
||||||
|
expr: "$A + $A",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number: binary non-null number and null number: is null",
|
||||||
|
expr: "$A * $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(1)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number and series: binary non-null number and series with a null: is null",
|
||||||
|
expr: "$A * $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(1)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number and series: binary null number and series with non-null and null: is null and null",
|
||||||
|
expr: "$A * $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// go-cmp instead of testify assert is used to compare results here
|
||||||
|
// because it supports an option for NaN equality.
|
||||||
|
// https://github.com/stretchr/testify/pull/691#issuecomment-528457166
|
||||||
|
opt := cmp.Comparer(func(x, y float64) bool {
|
||||||
|
return (math.IsNaN(x) && math.IsNaN(y)) || x == y
|
||||||
|
})
|
||||||
|
options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testBlock := func() {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
if diff := cmp.Diff(tt.results, res, options...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.willPanic {
|
||||||
|
assert.Panics(t, testBlock)
|
||||||
|
} else {
|
||||||
|
testBlock()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
144
pkg/expr/mathexp/exp_scalar_no_test.go
Normal file
144
pkg/expr/mathexp/exp_scalar_no_test.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScalarExpr(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
resultIs assert.ComparisonAssertionFunc
|
||||||
|
Results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "a scalar",
|
||||||
|
expr: "1",
|
||||||
|
vars: Vars{},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
Results: Results{[]Value{NewScalar(float64Pointer(1.0))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unary: scalar",
|
||||||
|
expr: "! 1.2",
|
||||||
|
vars: Vars{},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
Results: Results{[]Value{NewScalar(float64Pointer(0.0))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: scalar Op scalar",
|
||||||
|
expr: "1 + 1",
|
||||||
|
vars: Vars{},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
Results: Results{[]Value{NewScalar(float64Pointer(2.0))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: scalar Op scalar - divide by zero",
|
||||||
|
expr: "1 / 0",
|
||||||
|
vars: Vars{},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
Results: Results{[]Value{NewScalar(float64Pointer(math.Inf(1)))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: scalar Op number",
|
||||||
|
expr: "1 + $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("temp", nil, float64Pointer(2.0))}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
Results: Results{[]Value{makeNumber("", nil, float64Pointer(3.0))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: number Op Scalar",
|
||||||
|
expr: "$A - 3",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("temp", nil, float64Pointer(2.0))}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
Results: Results{[]Value{makeNumber("", nil, float64Pointer(-1))}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
tt.resultIs(t, tt.Results, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumberExpr(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
resultIs assert.ComparisonAssertionFunc
|
||||||
|
results Results
|
||||||
|
willPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "binary: number Op Scalar",
|
||||||
|
expr: "$A / $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("temp", nil, float64Pointer(2.0))}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, float64Pointer(1))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unary: number",
|
||||||
|
expr: "- $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("temp", nil, float64Pointer(2.0))}}},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, float64Pointer(-2.0))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary: Scalar Op Number (Number will nil val) - currently Panics",
|
||||||
|
expr: "1 + $A",
|
||||||
|
vars: Vars{"A": Results{[]Value{makeNumber("", nil, nil)}}},
|
||||||
|
willPanic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testBlock := func() {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
tt.resultIs(t, tt.results, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.willPanic {
|
||||||
|
assert.Panics(t, testBlock)
|
||||||
|
} else {
|
||||||
|
assert.NotPanics(t, testBlock)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
383
pkg/expr/mathexp/exp_series_test.go
Normal file
383
pkg/expr/mathexp/exp_series_test.go
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSeriesExpr(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unary series",
|
||||||
|
expr: "! ! $A",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{ // Not sure about preservering names...
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary scalar Op series",
|
||||||
|
expr: "98 + $A",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{ // Not sure about preservering names...
|
||||||
|
unixTimePointer(5, 0), float64Pointer(100),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(99),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary series Op scalar",
|
||||||
|
expr: "$A + 98",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{ // Not sure about preservering names...
|
||||||
|
unixTimePointer(5, 0), float64Pointer(100),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(99),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series Op series",
|
||||||
|
expr: "$A + $A",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{ // Not sure about preservering names...
|
||||||
|
unixTimePointer(5, 0), float64Pointer(4),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series Op number",
|
||||||
|
expr: "$A + $B",
|
||||||
|
vars: aSeriesbNumber,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("id=1", data.Labels{"id": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(9),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(8),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number Op series",
|
||||||
|
expr: "$B + $A",
|
||||||
|
vars: aSeriesbNumber,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("id=1", data.Labels{"id": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(9),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(8),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series Op series with label union",
|
||||||
|
expr: "$A * $B",
|
||||||
|
vars: twoSeriesSets,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("sensor=a, turbine=1", data.Labels{"sensor": "a", "turbine": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(6 * .5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(8 * .2),
|
||||||
|
}),
|
||||||
|
makeSeriesNullableTime("sensor=b, turbine=1", data.Labels{"sensor": "b", "turbine": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(10 * .5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(16 * .2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Length of resulting series is A when A + B. However, only points where the time matches
|
||||||
|
// for A and B are added to the result
|
||||||
|
{
|
||||||
|
name: "series Op series with sparse time join",
|
||||||
|
expr: "$A + $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", data.Labels{}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("efficiency", data.Labels{}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(9, 0), float64Pointer(4),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{ // Not sure about preserving names...
|
||||||
|
unixTimePointer(5, 0), float64Pointer(4),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
if diff := cmp.Diff(tt.results, res, data.FrameTestCompareOptions()...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeriesAlternateFormsExpr(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unary series: non-nullable time",
|
||||||
|
expr: "! ! $A",
|
||||||
|
vars: aSeries,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("", nil, tp{ // Not sure about preservering names...
|
||||||
|
time.Unix(5, 0), float64Pointer(1),
|
||||||
|
}, tp{
|
||||||
|
time.Unix(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unary series: non-nullable time, time second",
|
||||||
|
expr: "! ! $A",
|
||||||
|
vars: aSeriesTimeSecond,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesTimeSecond("", nil, timeSecondTP{ // Not sure about preservering names...
|
||||||
|
float64Pointer(1), time.Unix(5, 0),
|
||||||
|
}, timeSecondTP{
|
||||||
|
float64Pointer(1), time.Unix(10, 0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unary series: non-nullable value",
|
||||||
|
expr: "! ! $A",
|
||||||
|
vars: aSeriesNoNull,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNoNullSeries("", nil, noNullTP{ // Not sure about preservering names...
|
||||||
|
time.Unix(5, 0), 1,
|
||||||
|
}, noNullTP{
|
||||||
|
time.Unix(10, 0), 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series Op series: nullable and non-nullable time",
|
||||||
|
expr: "$A + $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("temp", data.Labels{}, tp{
|
||||||
|
time.Unix(5, 0), float64Pointer(1),
|
||||||
|
}, tp{
|
||||||
|
time.Unix(10, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("efficiency", data.Labels{}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(4),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(4),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(6),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series Op series: nullable (time second) and non-nullable time (time first)",
|
||||||
|
expr: "$B + $A", // takes order from first operator
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesTimeSecond("temp", data.Labels{}, timeSecondTP{
|
||||||
|
float64Pointer(1), time.Unix(5, 0),
|
||||||
|
}, timeSecondTP{
|
||||||
|
float64Pointer(2), time.Unix(10, 0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("efficiency", data.Labels{}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(4),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(4),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(6),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "series Op series: nullable and non-nullable values",
|
||||||
|
expr: "$A + $B",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("temp", data.Labels{}, tp{
|
||||||
|
time.Unix(5, 0), float64Pointer(1),
|
||||||
|
}, tp{
|
||||||
|
time.Unix(10, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNoNullSeries("efficiency", data.Labels{}, noNullTP{
|
||||||
|
time.Unix(5, 0), 3,
|
||||||
|
}, noNullTP{
|
||||||
|
time.Unix(10, 0), 4,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("", nil, tp{
|
||||||
|
time.Unix(5, 0), float64Pointer(4),
|
||||||
|
}, tp{
|
||||||
|
time.Unix(10, 0), float64Pointer(6),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary scalar Op series: non-nullable time second",
|
||||||
|
expr: "98 + $A",
|
||||||
|
vars: aSeriesTimeSecond,
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesTimeSecond("", nil, timeSecondTP{ // Not sure about preservering names...
|
||||||
|
float64Pointer(100), time.Unix(5, 0),
|
||||||
|
}, timeSecondTP{
|
||||||
|
float64Pointer(99), time.Unix(10, 0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
if diff := cmp.Diff(tt.results, res, data.FrameTestCompareOptions()...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
181
pkg/expr/mathexp/exp_test.go
Normal file
181
pkg/expr/mathexp/exp_test.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common Test Constructor Utils and Types
|
||||||
|
type nullTimeTP struct {
|
||||||
|
t *time.Time
|
||||||
|
f *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type tp struct {
|
||||||
|
t time.Time
|
||||||
|
f *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type timeSecondTP struct {
|
||||||
|
f *float64
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type noNullTP struct {
|
||||||
|
t time.Time
|
||||||
|
f float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSeriesNullableTime(name string, labels data.Labels, points ...nullTimeTP) Series {
|
||||||
|
newSeries := NewSeries(name, labels, 0, true, 1, true, len(points))
|
||||||
|
for idx, p := range points {
|
||||||
|
_ = newSeries.SetPoint(idx, p.t, p.f)
|
||||||
|
}
|
||||||
|
return newSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSeries(name string, labels data.Labels, points ...tp) Series {
|
||||||
|
newSeries := NewSeries(name, labels, 0, false, 1, true, len(points))
|
||||||
|
for idx, p := range points {
|
||||||
|
err := newSeries.SetPoint(idx, &p.t, p.f)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNoNullSeries(name string, labels data.Labels, points ...noNullTP) Series {
|
||||||
|
newSeries := NewSeries(name, labels, 0, false, 1, false, len(points))
|
||||||
|
for idx, p := range points {
|
||||||
|
err := newSeries.SetPoint(idx, &p.t, &p.f)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSeriesTimeSecond(name string, labels data.Labels, points ...timeSecondTP) Series {
|
||||||
|
newSeries := NewSeries(name, labels, 1, false, 0, true, len(points))
|
||||||
|
for idx, p := range points {
|
||||||
|
err := newSeries.SetPoint(idx, &p.t, p.f)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNumber(name string, labels data.Labels, f *float64) Number {
|
||||||
|
newNumber := NewNumber(name, labels)
|
||||||
|
newNumber.SetValue(f)
|
||||||
|
return newNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
func unixTimePointer(sec, nsec int64) *time.Time {
|
||||||
|
t := time.Unix(sec, nsec)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64Pointer(f float64) *float64 {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
var aSeriesNullableTime = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var aSeries = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("temp", nil, tp{
|
||||||
|
time.Unix(5, 0), float64Pointer(2),
|
||||||
|
}, tp{
|
||||||
|
time.Unix(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var aSeriesTimeSecond = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesTimeSecond("temp", nil, timeSecondTP{
|
||||||
|
float64Pointer(2), time.Unix(5, 0),
|
||||||
|
}, timeSecondTP{
|
||||||
|
float64Pointer(1), time.Unix(10, 0),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var aSeriesNoNull = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNoNullSeries("temp", nil, noNullTP{
|
||||||
|
time.Unix(5, 0), 2,
|
||||||
|
}, noNullTP{
|
||||||
|
time.Unix(10, 0), 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var aSeriesbNumber = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("volt", data.Labels{"id": "1"}, float64Pointer(7)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoSeriesSets = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", data.Labels{"sensor": "a", "turbine": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(6),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(8),
|
||||||
|
}),
|
||||||
|
makeSeriesNullableTime("temp", data.Labels{"sensor": "b", "turbine": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(10),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(16),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"B": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("efficiency", data.Labels{"turbine": "1"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(.5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(.2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// NaN is just to make the calls a little cleaner, the one
|
||||||
|
// call is not for any sort of equality side effect in tests.
|
||||||
|
// note: cmp.Equal must be used to test Equality for NaNs.
|
||||||
|
var NaN = float64Pointer(math.NaN())
|
112
pkg/expr/mathexp/funcs.go
Normal file
112
pkg/expr/mathexp/funcs.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
var builtins = map[string]parse.Func{
|
||||||
|
"abs": {
|
||||||
|
Args: []parse.ReturnType{parse.TypeVariantSet},
|
||||||
|
VariantReturn: true,
|
||||||
|
F: abs,
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
Args: []parse.ReturnType{parse.TypeVariantSet},
|
||||||
|
VariantReturn: true,
|
||||||
|
F: log,
|
||||||
|
},
|
||||||
|
"nan": {
|
||||||
|
Return: parse.TypeScalar,
|
||||||
|
F: nan,
|
||||||
|
},
|
||||||
|
"inf": {
|
||||||
|
Return: parse.TypeScalar,
|
||||||
|
F: inf,
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
Return: parse.TypeScalar,
|
||||||
|
F: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// abs returns the absolute value for each result in NumberSet, SeriesSet, or Scalar
|
||||||
|
func abs(e *State, varSet Results) (Results, error) {
|
||||||
|
newRes := Results{}
|
||||||
|
for _, res := range varSet.Values {
|
||||||
|
newVal, err := perFloat(res, math.Abs)
|
||||||
|
if err != nil {
|
||||||
|
return newRes, err
|
||||||
|
}
|
||||||
|
newRes.Values = append(newRes.Values, newVal)
|
||||||
|
}
|
||||||
|
return newRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// log returns the natural logarithm value for each result in NumberSet, SeriesSet, or Scalar
|
||||||
|
func log(e *State, varSet Results) (Results, error) {
|
||||||
|
newRes := Results{}
|
||||||
|
for _, res := range varSet.Values {
|
||||||
|
newVal, err := perFloat(res, math.Log)
|
||||||
|
if err != nil {
|
||||||
|
return newRes, err
|
||||||
|
}
|
||||||
|
newRes.Values = append(newRes.Values, newVal)
|
||||||
|
}
|
||||||
|
return newRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nan returns a scalar nan value
|
||||||
|
func nan(e *State) Results {
|
||||||
|
aNaN := math.NaN()
|
||||||
|
return NewScalarResults(&aNaN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inf returns a scalar positive infinity value
|
||||||
|
func inf(e *State) Results {
|
||||||
|
aInf := math.Inf(0)
|
||||||
|
return NewScalarResults(&aInf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// null returns a null scalar value
|
||||||
|
func null(e *State) Results {
|
||||||
|
return NewScalarResults(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func perFloat(val Value, floatF func(x float64) float64) (Value, error) {
|
||||||
|
var newVal Value
|
||||||
|
switch val.Type() {
|
||||||
|
case parse.TypeNumberSet:
|
||||||
|
n := NewNumber(val.GetName(), val.GetLabels())
|
||||||
|
f := val.(Number).GetFloat64Value()
|
||||||
|
nF := math.NaN()
|
||||||
|
if f != nil {
|
||||||
|
nF = floatF(*f)
|
||||||
|
}
|
||||||
|
n.SetValue(&nF)
|
||||||
|
newVal = n
|
||||||
|
case parse.TypeScalar:
|
||||||
|
f := val.(Scalar).GetFloat64Value()
|
||||||
|
nF := math.NaN()
|
||||||
|
if f != nil {
|
||||||
|
nF = floatF(*f)
|
||||||
|
}
|
||||||
|
newVal = NewScalar(&nF)
|
||||||
|
case parse.TypeSeriesSet:
|
||||||
|
resSeries := val.(Series)
|
||||||
|
newSeries := NewSeries(resSeries.GetName(), resSeries.GetLabels(), resSeries.TimeIdx, resSeries.TimeIsNullable, resSeries.ValueIdx, resSeries.ValueIsNullabe, resSeries.Len())
|
||||||
|
for i := 0; i < resSeries.Len(); i++ {
|
||||||
|
t, f := resSeries.GetPoint(i)
|
||||||
|
nF := math.NaN()
|
||||||
|
if f != nil {
|
||||||
|
nF = floatF(*f)
|
||||||
|
}
|
||||||
|
if err := newSeries.SetPoint(i, t, &nF); err != nil {
|
||||||
|
return newSeries, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newVal = newSeries
|
||||||
|
}
|
||||||
|
return newVal, nil
|
||||||
|
}
|
88
pkg/expr/mathexp/funcs_test.go
Normal file
88
pkg/expr/mathexp/funcs_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFunc(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
vars Vars
|
||||||
|
newErrIs assert.ErrorAssertionFunc
|
||||||
|
execErrIs assert.ErrorAssertionFunc
|
||||||
|
resultIs assert.ComparisonAssertionFunc
|
||||||
|
results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "abs on number",
|
||||||
|
expr: "abs($A)",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(-7)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
results: Results{[]Value{makeNumber("", nil, float64Pointer(7))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "abs on scalar",
|
||||||
|
expr: "abs(-1)",
|
||||||
|
vars: Vars{},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
results: Results{[]Value{NewScalar(float64Pointer(1.0))}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "abs on series",
|
||||||
|
expr: "abs($A)",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(-2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(-1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "abs on string - should error",
|
||||||
|
expr: `abs("hi")`,
|
||||||
|
vars: Vars{},
|
||||||
|
newErrIs: assert.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e, err := New(tt.expr)
|
||||||
|
tt.newErrIs(t, err)
|
||||||
|
if e != nil {
|
||||||
|
res, err := e.Execute(tt.vars)
|
||||||
|
tt.execErrIs(t, err)
|
||||||
|
tt.resultIs(t, tt.results, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
337
pkg/expr/mathexp/parse/lex.go
Normal file
337
pkg/expr/mathexp/parse/lex.go
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// item represents a token or text string returned from the scanner.
|
||||||
|
type item struct {
|
||||||
|
typ itemType // The type of this item.
|
||||||
|
pos Pos // The starting position, in bytes, of this item in the input string.
|
||||||
|
val string // The value of this item.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i item) String() string {
|
||||||
|
switch {
|
||||||
|
case i.typ == itemEOF:
|
||||||
|
return "EOF"
|
||||||
|
case i.typ == itemError:
|
||||||
|
return i.val
|
||||||
|
case len(i.val) > 10:
|
||||||
|
return fmt.Sprintf("%.10q...", i.val)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%q", i.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// itemType identifies the type of lex items.
|
||||||
|
type itemType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
itemError itemType = iota // error occurred; value is text of error
|
||||||
|
itemEOF
|
||||||
|
itemNot // '!'
|
||||||
|
itemAnd // '&&'
|
||||||
|
itemOr // '||'
|
||||||
|
itemGreater // '>'
|
||||||
|
itemLess // '<'
|
||||||
|
itemGreaterEq // '>='
|
||||||
|
itemLessEq // '<='
|
||||||
|
itemEq // '=='
|
||||||
|
itemNotEq // '!='
|
||||||
|
itemPlus // '+'
|
||||||
|
itemMinus // '-'
|
||||||
|
itemMult // '*'
|
||||||
|
itemDiv // '/'
|
||||||
|
itemMod // '%'
|
||||||
|
itemNumber // simple number
|
||||||
|
itemComma
|
||||||
|
itemLeftParen
|
||||||
|
itemRightParen
|
||||||
|
itemString
|
||||||
|
itemFunc
|
||||||
|
itemVar // e.g. $A
|
||||||
|
itemPow // '**'
|
||||||
|
)
|
||||||
|
|
||||||
|
const eof = -1
|
||||||
|
|
||||||
|
// stateFn represents the state of the scanner as a function that returns the next state.
|
||||||
|
type stateFn func(*lexer) stateFn
|
||||||
|
|
||||||
|
// lexer holds the state of the scanner.
|
||||||
|
type lexer struct {
|
||||||
|
input string // the string being scanned
|
||||||
|
state stateFn // the next lexing function to enter
|
||||||
|
pos Pos // current position in the input
|
||||||
|
start Pos // start position of this item
|
||||||
|
width Pos // width of last rune read from input
|
||||||
|
lastPos Pos // position of most recent item returned by nextItem
|
||||||
|
items chan item // channel of scanned items
|
||||||
|
}
|
||||||
|
|
||||||
|
// next returns the next rune in the input.
|
||||||
|
func (l *lexer) next() rune {
|
||||||
|
if int(l.pos) >= len(l.input) {
|
||||||
|
l.width = 0
|
||||||
|
return eof
|
||||||
|
}
|
||||||
|
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||||
|
l.width = Pos(w)
|
||||||
|
l.pos += l.width
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// peek returns but does not consume the next rune in the input.
|
||||||
|
// nolint:unused
|
||||||
|
func (l *lexer) peek() rune {
|
||||||
|
r := l.next()
|
||||||
|
l.backup()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// backup steps back one rune. Can only be called once per call of next.
|
||||||
|
func (l *lexer) backup() {
|
||||||
|
l.pos -= l.width
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit passes an item back to the client.
|
||||||
|
func (l *lexer) emit(t itemType) {
|
||||||
|
l.items <- item{t, l.start, l.input[l.start:l.pos]}
|
||||||
|
l.start = l.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept consumes the next rune if it's from the valid set.
|
||||||
|
func (l *lexer) accept(valid string) bool {
|
||||||
|
if strings.ContainsRune(valid, l.next()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
l.backup()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// acceptRun consumes a run of runes from the valid set.
|
||||||
|
func (l *lexer) acceptRun(valid string) {
|
||||||
|
for strings.ContainsRune(valid, l.next()) {
|
||||||
|
}
|
||||||
|
l.backup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore skips over the pending input before this point.
|
||||||
|
func (l *lexer) ignore() {
|
||||||
|
l.start = l.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// lineNumber reports which line we're on, based on the position of
|
||||||
|
// the previous item returned by nextItem. Doing it this way
|
||||||
|
// means we don't have to worry about peek double counting.
|
||||||
|
// nolint:unused
|
||||||
|
func (l *lexer) lineNumber() int {
|
||||||
|
return 1 + strings.Count(l.input[:l.lastPos], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorf returns an error token and terminates the scan by passing
|
||||||
|
// back a nil pointer that will be the next state, terminating l.nextItem.
|
||||||
|
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||||
|
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextItem returns the next item from the input.
|
||||||
|
func (l *lexer) nextItem() item {
|
||||||
|
item := <-l.items
|
||||||
|
l.lastPos = item.pos
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// lex creates a new scanner for the input string.
|
||||||
|
func lex(input string) *lexer {
|
||||||
|
l := &lexer{
|
||||||
|
input: input,
|
||||||
|
items: make(chan item),
|
||||||
|
}
|
||||||
|
go l.run()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// run runs the state machine for the lexer.
|
||||||
|
func (l *lexer) run() {
|
||||||
|
for l.state = lexItem; l.state != nil; {
|
||||||
|
l.state = l.state(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// state functions
|
||||||
|
|
||||||
|
func lexItem(l *lexer) stateFn {
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
switch r := l.next(); {
|
||||||
|
case r == '$':
|
||||||
|
return lexVar
|
||||||
|
case isSymbol(r):
|
||||||
|
return lexSymbol
|
||||||
|
case isNumber(r):
|
||||||
|
l.backup()
|
||||||
|
return lexNumber
|
||||||
|
case unicode.IsLetter(r):
|
||||||
|
return lexFunc
|
||||||
|
case r == '(':
|
||||||
|
l.emit(itemLeftParen)
|
||||||
|
case r == ')':
|
||||||
|
l.emit(itemRightParen)
|
||||||
|
case r == '"':
|
||||||
|
return lexString
|
||||||
|
case r == ',':
|
||||||
|
l.emit(itemComma)
|
||||||
|
case isSpace(r):
|
||||||
|
l.ignore()
|
||||||
|
case r == eof:
|
||||||
|
l.emit(itemEOF)
|
||||||
|
break Loop
|
||||||
|
default:
|
||||||
|
return l.errorf("invalid character: %s", string(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
||||||
|
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
||||||
|
// and "089" - but when it's wrong the input is invalid and the parser (via
|
||||||
|
// strconv) will notice.
|
||||||
|
func lexNumber(l *lexer) stateFn {
|
||||||
|
if !l.scanNumber() {
|
||||||
|
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||||
|
}
|
||||||
|
l.emit(itemNumber)
|
||||||
|
return lexItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lexer) scanNumber() bool {
|
||||||
|
// Is it hex?
|
||||||
|
digits := "0123456789"
|
||||||
|
if l.accept("0") && l.accept("xX") {
|
||||||
|
digits = "0123456789abcdefABCDEF"
|
||||||
|
}
|
||||||
|
l.acceptRun(digits)
|
||||||
|
if l.accept(".") {
|
||||||
|
l.acceptRun(digits)
|
||||||
|
}
|
||||||
|
if l.accept("eE") {
|
||||||
|
l.accept("+-")
|
||||||
|
l.acceptRun("0123456789")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = "!<>=&|+-*/%"
|
||||||
|
|
||||||
|
func lexSymbol(l *lexer) stateFn {
|
||||||
|
l.acceptRun(symbols)
|
||||||
|
s := l.input[l.start:l.pos]
|
||||||
|
switch s {
|
||||||
|
case "!":
|
||||||
|
l.emit(itemNot)
|
||||||
|
case "&&":
|
||||||
|
l.emit(itemAnd)
|
||||||
|
case "||":
|
||||||
|
l.emit(itemOr)
|
||||||
|
case ">":
|
||||||
|
l.emit(itemGreater)
|
||||||
|
case "<":
|
||||||
|
l.emit(itemLess)
|
||||||
|
case ">=":
|
||||||
|
l.emit(itemGreaterEq)
|
||||||
|
case "<=":
|
||||||
|
l.emit(itemLessEq)
|
||||||
|
case "==":
|
||||||
|
l.emit(itemEq)
|
||||||
|
case "!=":
|
||||||
|
l.emit(itemNotEq)
|
||||||
|
case "+":
|
||||||
|
l.emit(itemPlus)
|
||||||
|
case "-":
|
||||||
|
l.emit(itemMinus)
|
||||||
|
case "*":
|
||||||
|
l.emit(itemMult)
|
||||||
|
case "**":
|
||||||
|
l.emit(itemPow)
|
||||||
|
case "/":
|
||||||
|
l.emit(itemDiv)
|
||||||
|
case "%":
|
||||||
|
l.emit(itemMod)
|
||||||
|
default:
|
||||||
|
l.emit(itemError)
|
||||||
|
}
|
||||||
|
return lexItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexFunc(l *lexer) stateFn {
|
||||||
|
for {
|
||||||
|
switch r := l.next(); {
|
||||||
|
case unicode.IsLetter(r):
|
||||||
|
// absorb
|
||||||
|
default:
|
||||||
|
l.backup()
|
||||||
|
l.emit(itemFunc)
|
||||||
|
return lexItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexVar(l *lexer) stateFn {
|
||||||
|
hasChar := false
|
||||||
|
for {
|
||||||
|
switch r := l.next(); {
|
||||||
|
case unicode.IsLetter(r):
|
||||||
|
hasChar = true
|
||||||
|
// absorb
|
||||||
|
default:
|
||||||
|
if !hasChar {
|
||||||
|
return l.errorf("incomplete variable")
|
||||||
|
}
|
||||||
|
l.backup()
|
||||||
|
l.emit(itemVar)
|
||||||
|
return lexItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexString(l *lexer) stateFn {
|
||||||
|
for {
|
||||||
|
switch l.next() {
|
||||||
|
case '"':
|
||||||
|
l.emit(itemString)
|
||||||
|
return lexItem
|
||||||
|
case eof:
|
||||||
|
return l.errorf("unterminated string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSpace reports whether r is a space character.
|
||||||
|
func isSpace(r rune) bool {
|
||||||
|
return unicode.IsSpace(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isVarchar should maybe be used in place of unicode is letter above,
|
||||||
|
// but do not want to modify it at this time, so adding lint exception.
|
||||||
|
// nolint:unused,deadcode
|
||||||
|
func isVarchar(r rune) bool {
|
||||||
|
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSymbol(r rune) bool {
|
||||||
|
return strings.ContainsRune(symbols, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumber(r rune) bool {
|
||||||
|
return unicode.IsDigit(r) || r == '.'
|
||||||
|
}
|
156
pkg/expr/mathexp/parse/lex_test.go
Normal file
156
pkg/expr/mathexp/parse/lex_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make the types prettyprint.
|
||||||
|
var itemName = map[itemType]string{
|
||||||
|
itemError: "error",
|
||||||
|
itemEOF: "EOF",
|
||||||
|
itemNot: "!",
|
||||||
|
itemAnd: "&&",
|
||||||
|
itemOr: "||",
|
||||||
|
itemGreater: ">",
|
||||||
|
itemLess: "<",
|
||||||
|
itemGreaterEq: ">=",
|
||||||
|
itemLessEq: "<=",
|
||||||
|
itemEq: "==",
|
||||||
|
itemNotEq: "!=",
|
||||||
|
itemPlus: "+",
|
||||||
|
itemMinus: "-",
|
||||||
|
itemMult: "*",
|
||||||
|
itemDiv: "/",
|
||||||
|
itemMod: "%",
|
||||||
|
itemNumber: "number",
|
||||||
|
itemComma: ",",
|
||||||
|
itemLeftParen: "(",
|
||||||
|
itemRightParen: ")",
|
||||||
|
itemString: "string",
|
||||||
|
itemFunc: "func",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i itemType) String() string {
|
||||||
|
s := itemName[i]
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Sprintf("item%d", int(i))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type lexTest struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
items []item
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tEOF = item{itemEOF, 0, ""}
|
||||||
|
tLt = item{itemLess, 0, "<"}
|
||||||
|
tGt = item{itemGreater, 0, ">"}
|
||||||
|
tOr = item{itemOr, 0, "||"}
|
||||||
|
tNot = item{itemNot, 0, "!"}
|
||||||
|
tAnd = item{itemAnd, 0, "&&"}
|
||||||
|
tLtEq = item{itemLessEq, 0, "<="}
|
||||||
|
tGtEq = item{itemGreaterEq, 0, ">="}
|
||||||
|
tNotEq = item{itemNotEq, 0, "!="}
|
||||||
|
tEq = item{itemEq, 0, "=="}
|
||||||
|
tPlus = item{itemPlus, 0, "+"}
|
||||||
|
tMinus = item{itemMinus, 0, "-"}
|
||||||
|
tMult = item{itemMult, 0, "*"}
|
||||||
|
tDiv = item{itemDiv, 0, "/"}
|
||||||
|
tMod = item{itemMod, 0, "%"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var lexTests = []lexTest{
|
||||||
|
{"empty", "", []item{tEOF}},
|
||||||
|
{"spaces", " \t\n", []item{tEOF}},
|
||||||
|
{"text", `"now is the time"`, []item{{itemString, 0, `"now is the time"`}, tEOF}},
|
||||||
|
{"operators", "! && || < > <= >= == != + - * / %", []item{
|
||||||
|
tNot,
|
||||||
|
tAnd,
|
||||||
|
tOr,
|
||||||
|
tLt,
|
||||||
|
tGt,
|
||||||
|
tLtEq,
|
||||||
|
tGtEq,
|
||||||
|
tEq,
|
||||||
|
tNotEq,
|
||||||
|
tPlus,
|
||||||
|
tMinus,
|
||||||
|
tMult,
|
||||||
|
tDiv,
|
||||||
|
tMod,
|
||||||
|
tEOF,
|
||||||
|
}},
|
||||||
|
{"numbers", "1 02 0x14 7.2 1e3 1.2e-4", []item{
|
||||||
|
{itemNumber, 0, "1"},
|
||||||
|
{itemNumber, 0, "02"},
|
||||||
|
{itemNumber, 0, "0x14"},
|
||||||
|
{itemNumber, 0, "7.2"},
|
||||||
|
{itemNumber, 0, "1e3"},
|
||||||
|
{itemNumber, 0, "1.2e-4"},
|
||||||
|
tEOF,
|
||||||
|
}},
|
||||||
|
{"number plus var", "1 + $A", []item{
|
||||||
|
{itemNumber, 0, "1"},
|
||||||
|
tPlus,
|
||||||
|
{itemVar, 0, "$A"},
|
||||||
|
tEOF,
|
||||||
|
}},
|
||||||
|
// errors
|
||||||
|
{"unclosed quote", "\"", []item{
|
||||||
|
{itemError, 0, "unterminated string"},
|
||||||
|
}},
|
||||||
|
{"single quote", "'single quote is invalid'", []item{
|
||||||
|
{itemError, 0, "invalid character: '"},
|
||||||
|
}},
|
||||||
|
{"invalid var", "$", []item{
|
||||||
|
{itemError, 0, "incomplete variable"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect gathers the emitted items into a slice.
|
||||||
|
func collect(t *lexTest) (items []item) {
|
||||||
|
l := lex(t.input)
|
||||||
|
for {
|
||||||
|
item := l.nextItem()
|
||||||
|
items = append(items, item)
|
||||||
|
if item.typ == itemEOF || item.typ == itemError {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func equal(i1, i2 []item, checkPos bool) bool {
|
||||||
|
if len(i1) != len(i2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k := range i1 {
|
||||||
|
if i1[k].typ != i2[k].typ {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if i1[k].val != i2[k].val {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if checkPos && i1[k].pos != i2[k].pos {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLex(t *testing.T) {
|
||||||
|
for i, test := range lexTests {
|
||||||
|
items := collect(&lexTests[i])
|
||||||
|
if !equal(items, test.items, false) {
|
||||||
|
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
422
pkg/expr/mathexp/parse/node.go
Normal file
422
pkg/expr/mathexp/parse/node.go
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Parse nodes.
|
||||||
|
|
||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Node is an element in the parse tree. The interface is trivial.
|
||||||
|
// The interface contains an unexported method so that only
|
||||||
|
// types local to this package can satisfy it.
|
||||||
|
type Node interface {
|
||||||
|
Type() NodeType
|
||||||
|
String() string
|
||||||
|
StringAST() string
|
||||||
|
Position() Pos // byte position of start of node in full original input string
|
||||||
|
Check(*Tree) error // performs type checking for itself and sub-nodes
|
||||||
|
Return() ReturnType
|
||||||
|
|
||||||
|
// Make sure only functions in this package can create Nodes.
|
||||||
|
unexported()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeType identifies the type of a parse tree node.
|
||||||
|
type NodeType int
|
||||||
|
|
||||||
|
// Pos represents a byte position in the original input text from which
|
||||||
|
// this template was parsed.
|
||||||
|
type Pos int
|
||||||
|
|
||||||
|
// Position returns the integer Position of p
|
||||||
|
func (p Pos) Position() Pos {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// unexported keeps Node implementations local to the package.
|
||||||
|
// All implementations embed Pos, so this takes care of it.
|
||||||
|
func (Pos) unexported() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns itself and provides an easy default implementation
|
||||||
|
// for embedding in a Node. Embedded in all non-trivial Nodes.
|
||||||
|
func (t NodeType) Type() NodeType {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NodeFunc is a function call.
|
||||||
|
NodeFunc NodeType = iota
|
||||||
|
// NodeBinary is a binary operator: math, logical, compare
|
||||||
|
NodeBinary
|
||||||
|
// NodeUnary is unary operator: !, -
|
||||||
|
NodeUnary
|
||||||
|
// NodeString is string constant.
|
||||||
|
NodeString
|
||||||
|
// NodeNumber is a numerical constant (Scalar).
|
||||||
|
NodeNumber
|
||||||
|
// NodeVar is variable: $A
|
||||||
|
NodeVar
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of the NodeType
|
||||||
|
func (t NodeType) String() string {
|
||||||
|
switch t {
|
||||||
|
case NodeFunc:
|
||||||
|
return "NodeFunc"
|
||||||
|
case NodeBinary:
|
||||||
|
return "NodeBinary"
|
||||||
|
case NodeUnary:
|
||||||
|
return "NodeUnary"
|
||||||
|
case NodeString:
|
||||||
|
return "NodeString"
|
||||||
|
case NodeNumber:
|
||||||
|
return "NodeNumber"
|
||||||
|
default:
|
||||||
|
return "NodeUnknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes.
|
||||||
|
|
||||||
|
// VarNode holds a variable reference.
|
||||||
|
type VarNode struct {
|
||||||
|
NodeType
|
||||||
|
Pos
|
||||||
|
Name string // Without the $
|
||||||
|
Text string // Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVar(pos Pos, name, text string) *VarNode {
|
||||||
|
return &VarNode{NodeType: NodeVar, Pos: pos, Name: name, Text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the Type of the VarNode so it fulfills the Node interface.
|
||||||
|
func (n *VarNode) Type() NodeType { return NodeVar }
|
||||||
|
|
||||||
|
// String returns the string representation of the VarNode so it fulfills the Node interface.
|
||||||
|
func (n *VarNode) String() string { return n.Text }
|
||||||
|
|
||||||
|
// StringAST returns the string representation of abstract syntax tree of the VarNode so it fulfills the Node interface.
|
||||||
|
func (n *VarNode) StringAST() string { return n.String() }
|
||||||
|
|
||||||
|
// Check performs parse time checking on the VarNode so it fulfills the Node interface.
|
||||||
|
func (n *VarNode) Check(*Tree) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return returns the result type of the VarNode so it fulfills the Node interface.
|
||||||
|
func (n *VarNode) Return() ReturnType {
|
||||||
|
return TypeSeriesSet // Vars are only time series for now I guess....
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuncNode holds a function invocation.
|
||||||
|
type FuncNode struct {
|
||||||
|
NodeType
|
||||||
|
Pos
|
||||||
|
Name string
|
||||||
|
F *Func
|
||||||
|
Args []Node
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFunc(pos Pos, name string, f Func) *FuncNode {
|
||||||
|
return &FuncNode{NodeType: NodeFunc, Pos: pos, Name: name, F: &f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FuncNode) append(arg Node) {
|
||||||
|
f.Args = append(f.Args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the FuncNode so it fulfills the Node interface.
|
||||||
|
func (f *FuncNode) String() string {
|
||||||
|
s := f.Name + "("
|
||||||
|
for i, arg := range f.Args {
|
||||||
|
if i > 0 {
|
||||||
|
s += ", "
|
||||||
|
}
|
||||||
|
s += arg.String()
|
||||||
|
}
|
||||||
|
s += ")"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringAST returns the string representation of abstract syntax tree of the FuncNode so it fulfills the Node interface.
|
||||||
|
func (f *FuncNode) StringAST() string {
|
||||||
|
s := f.Name + "("
|
||||||
|
for i, arg := range f.Args {
|
||||||
|
if i > 0 {
|
||||||
|
s += ", "
|
||||||
|
}
|
||||||
|
s += arg.StringAST()
|
||||||
|
}
|
||||||
|
s += ")"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performs parse time checking on the FuncNode so it fulfills the Node interface.
|
||||||
|
func (f *FuncNode) Check(t *Tree) error {
|
||||||
|
if len(f.Args) < len(f.F.Args) {
|
||||||
|
return fmt.Errorf("parse: not enough arguments for %s", f.Name)
|
||||||
|
} else if len(f.Args) > len(f.F.Args) {
|
||||||
|
return fmt.Errorf("parse: too many arguments for %s", f.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range f.Args {
|
||||||
|
funcType := f.F.Args[i]
|
||||||
|
argType := arg.Return()
|
||||||
|
// if funcType == TypeNumberSet && argType == TypeScalar {
|
||||||
|
// argType = TypeNumberSet
|
||||||
|
// }
|
||||||
|
if funcType == TypeVariantSet {
|
||||||
|
if !(argType == TypeNumberSet || argType == TypeSeriesSet || argType == TypeScalar) {
|
||||||
|
return fmt.Errorf("parse: expected %v or %v for argument %v, got %v", TypeNumberSet, TypeSeriesSet, i, argType)
|
||||||
|
}
|
||||||
|
} else if funcType != argType {
|
||||||
|
return fmt.Errorf("parse: expected %v, got %v for argument %v (%v)", funcType, argType, i, arg.String())
|
||||||
|
}
|
||||||
|
if err := arg.Check(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.F.Check != nil {
|
||||||
|
return f.F.Check(t, f)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return returns the result type of the FuncNode so it fulfills the Node interface.
|
||||||
|
func (f *FuncNode) Return() ReturnType {
|
||||||
|
return f.F.Return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarNode holds a number: signed or unsigned integer or float.
|
||||||
|
// The value is parsed and stored under all the types that can represent the value.
|
||||||
|
// This simulates in a small amount of code the behavior of Go's ideal constants.
|
||||||
|
type ScalarNode struct {
|
||||||
|
NodeType
|
||||||
|
Pos
|
||||||
|
IsUint bool // Number has an unsigned integral value.
|
||||||
|
IsFloat bool // Number has a floating-point value.
|
||||||
|
Uint64 uint64 // The unsigned integer value.
|
||||||
|
Float64 float64 // The floating-point value.
|
||||||
|
Text string // The original textual representation from the input.
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNumber(pos Pos, text string) (*ScalarNode, error) {
|
||||||
|
n := &ScalarNode{NodeType: NodeNumber, Pos: pos, Text: text}
|
||||||
|
// Do integer test first so we get 0x123 etc.
|
||||||
|
u, err := strconv.ParseUint(text, 0, 64) // will fail for -0.
|
||||||
|
if err == nil {
|
||||||
|
n.IsUint = true
|
||||||
|
n.Uint64 = u
|
||||||
|
}
|
||||||
|
// If an integer extraction succeeded, promote the float.
|
||||||
|
if n.IsUint {
|
||||||
|
n.IsFloat = true
|
||||||
|
n.Float64 = float64(n.Uint64)
|
||||||
|
} else {
|
||||||
|
f, err := strconv.ParseFloat(text, 64)
|
||||||
|
if err == nil {
|
||||||
|
n.IsFloat = true
|
||||||
|
n.Float64 = f
|
||||||
|
// If a floating-point extraction succeeded, extract the int if needed.
|
||||||
|
if !n.IsUint && float64(uint64(f)) == f {
|
||||||
|
n.IsUint = true
|
||||||
|
n.Uint64 = uint64(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !n.IsUint && !n.IsFloat {
|
||||||
|
return nil, fmt.Errorf("illegal number syntax: %q", text)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the ScalarNode so it fulfills the Node interface.
|
||||||
|
func (n *ScalarNode) String() string {
|
||||||
|
return n.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringAST returns the string representation of abstract syntax tree of the ScalarNode so it fulfills the Node interface.
|
||||||
|
func (n *ScalarNode) StringAST() string {
|
||||||
|
return n.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performs parse time checking on the ScalarNode so it fulfills the Node interface.
|
||||||
|
func (n *ScalarNode) Check(*Tree) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return returns the result type of the ScalarNode so it fulfills the Node interface.
|
||||||
|
func (n *ScalarNode) Return() ReturnType {
|
||||||
|
return TypeScalar
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringNode holds a string constant. The value has been "unquoted".
|
||||||
|
type StringNode struct {
|
||||||
|
NodeType
|
||||||
|
Pos
|
||||||
|
Quoted string // The original text of the string, with quotes.
|
||||||
|
Text string // The string, after quote processing.
|
||||||
|
}
|
||||||
|
|
||||||
|
func newString(pos Pos, orig, text string) *StringNode {
|
||||||
|
return &StringNode{NodeType: NodeString, Pos: pos, Quoted: orig, Text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the StringNode so it fulfills the Node interface.
|
||||||
|
func (s *StringNode) String() string {
|
||||||
|
return s.Quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringAST returns the string representation of abstract syntax tree of the StringNode so it fulfills the Node interface.
|
||||||
|
func (s *StringNode) StringAST() string {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performs parse time checking on the StringNode so it fulfills the Node interface.
|
||||||
|
func (s *StringNode) Check(*Tree) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return returns the result type of the TypeString so it fulfills the Node interface.
|
||||||
|
func (s *StringNode) Return() ReturnType {
|
||||||
|
return TypeString
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryNode holds two arguments and an operator.
|
||||||
|
type BinaryNode struct {
|
||||||
|
NodeType
|
||||||
|
Pos
|
||||||
|
Args [2]Node
|
||||||
|
Operator item
|
||||||
|
OpStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBinary(operator item, arg1, arg2 Node) *BinaryNode {
|
||||||
|
return &BinaryNode{NodeType: NodeBinary, Pos: operator.pos, Args: [2]Node{arg1, arg2}, Operator: operator, OpStr: operator.val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the BinaryNode so it fulfills the Node interface.
|
||||||
|
func (b *BinaryNode) String() string {
|
||||||
|
return fmt.Sprintf("%s %s %s", b.Args[0], b.Operator.val, b.Args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringAST returns the string representation of abstract syntax tree of the BinaryNode so it fulfills the Node interface.
|
||||||
|
func (b *BinaryNode) StringAST() string {
|
||||||
|
return fmt.Sprintf("%s(%s, %s)", b.Operator.val, b.Args[0], b.Args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performs parse time checking on the BinaryNode so it fulfills the Node interface.
|
||||||
|
func (b *BinaryNode) Check(t *Tree) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return returns the result type of the BinaryNode so it fulfills the Node interface.
|
||||||
|
func (b *BinaryNode) Return() ReturnType {
|
||||||
|
t0 := b.Args[0].Return()
|
||||||
|
t1 := b.Args[1].Return()
|
||||||
|
if t1 > t0 {
|
||||||
|
return t1
|
||||||
|
}
|
||||||
|
return t0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnaryNode holds one argument and an operator.
|
||||||
|
type UnaryNode struct {
|
||||||
|
NodeType
|
||||||
|
Pos
|
||||||
|
Arg Node
|
||||||
|
Operator item
|
||||||
|
OpStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnary(operator item, arg Node) *UnaryNode {
|
||||||
|
return &UnaryNode{NodeType: NodeUnary, Pos: operator.pos, Arg: arg, Operator: operator, OpStr: operator.val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the UnaryNode so it fulfills the Node interface.
|
||||||
|
func (u *UnaryNode) String() string {
|
||||||
|
return fmt.Sprintf("%s%s", u.Operator.val, u.Arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringAST returns the string representation of abstract syntax tree of the UnaryNode so it fulfills the Node interface.
|
||||||
|
func (u *UnaryNode) StringAST() string {
|
||||||
|
return fmt.Sprintf("%s(%s)", u.Operator.val, u.Arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performs parse time checking on the UnaryNode so it fulfills the Node interface.
|
||||||
|
func (u *UnaryNode) Check(t *Tree) error {
|
||||||
|
switch rt := u.Arg.Return(); rt {
|
||||||
|
case TypeNumberSet, TypeSeriesSet, TypeScalar:
|
||||||
|
return u.Arg.Check(t)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(`parse: type error in %s, expected "number", got %s`, u, rt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return returns the result type of the UnaryNode so it fulfills the Node interface.
|
||||||
|
func (u *UnaryNode) Return() ReturnType {
|
||||||
|
return u.Arg.Return()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk invokes f on n and sub-nodes of n.
|
||||||
|
func Walk(n Node, f func(Node)) {
|
||||||
|
f(n)
|
||||||
|
switch n := n.(type) {
|
||||||
|
case *BinaryNode:
|
||||||
|
Walk(n.Args[0], f)
|
||||||
|
Walk(n.Args[1], f)
|
||||||
|
case *FuncNode:
|
||||||
|
for _, a := range n.Args {
|
||||||
|
Walk(a, f)
|
||||||
|
}
|
||||||
|
case *ScalarNode, *StringNode:
|
||||||
|
// Ignore since these node types have no sub nodes.
|
||||||
|
case *UnaryNode:
|
||||||
|
Walk(n.Arg, f)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("other type: %T", n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnType represents the type that is returned from a node.
|
||||||
|
type ReturnType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TypeString is a single string.
|
||||||
|
TypeString ReturnType = iota
|
||||||
|
// TypeScalar is a unlabled number constant.
|
||||||
|
TypeScalar
|
||||||
|
// TypeNumberSet is a collection of labelled numbers.
|
||||||
|
TypeNumberSet
|
||||||
|
// TypeSeriesSet is a collection of labelled time series.
|
||||||
|
TypeSeriesSet
|
||||||
|
// TypeVariantSet is a collection of the same type Number, Series, or Scalar.
|
||||||
|
TypeVariantSet
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a string representation of the ReturnType.
|
||||||
|
func (f ReturnType) String() string {
|
||||||
|
switch f {
|
||||||
|
case TypeNumberSet:
|
||||||
|
return "numberSet"
|
||||||
|
case TypeString:
|
||||||
|
return "string"
|
||||||
|
case TypeSeriesSet:
|
||||||
|
return "seriesSet"
|
||||||
|
case TypeScalar:
|
||||||
|
return "scalar"
|
||||||
|
case TypeVariantSet:
|
||||||
|
return "variant"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
353
pkg/expr/mathexp/parse/parse.go
Normal file
353
pkg/expr/mathexp/parse/parse.go
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package parse builds parse trees for expressions as defined by expr. Clients
|
||||||
|
// should use that package to construct expressions rather than this one, which
|
||||||
|
// provides shared internal data structures not intended for general use.
|
||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tree is the representation of a single parsed expression.
|
||||||
|
type Tree struct {
|
||||||
|
Text string // text parsed to create the expression.
|
||||||
|
Root Node // top-level root of the tree, returns a number.
|
||||||
|
VarNames []string
|
||||||
|
|
||||||
|
funcs []map[string]Func
|
||||||
|
|
||||||
|
// Parsing only; cleared after parse.
|
||||||
|
lex *lexer
|
||||||
|
token [1]item // one-token lookahead for parser.
|
||||||
|
peekCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Func holds the structure of a parsed function call.
|
||||||
|
type Func struct {
|
||||||
|
Args []ReturnType
|
||||||
|
Return ReturnType
|
||||||
|
F interface{}
|
||||||
|
VariantReturn bool
|
||||||
|
Check func(*Tree, *FuncNode) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse returns a Tree, created by parsing the expression described in the
|
||||||
|
// argument string. If an error is encountered, parsing stops and an empty Tree
|
||||||
|
// is returned with the error.
|
||||||
|
func Parse(text string, funcs ...map[string]Func) (t *Tree, err error) {
|
||||||
|
t = New()
|
||||||
|
t.Text = text
|
||||||
|
err = t.Parse(text, funcs...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// next returns the next token.
|
||||||
|
func (t *Tree) next() item {
|
||||||
|
if t.peekCount > 0 {
|
||||||
|
t.peekCount--
|
||||||
|
} else {
|
||||||
|
t.token[0] = t.lex.nextItem()
|
||||||
|
}
|
||||||
|
return t.token[t.peekCount]
|
||||||
|
}
|
||||||
|
|
||||||
|
// backup backs the input stream up one token.
|
||||||
|
func (t *Tree) backup() {
|
||||||
|
t.peekCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// peek returns but does not consume the next token.
|
||||||
|
func (t *Tree) peek() item {
|
||||||
|
if t.peekCount > 0 {
|
||||||
|
return t.token[t.peekCount-1]
|
||||||
|
}
|
||||||
|
t.peekCount = 1
|
||||||
|
t.token[0] = t.lex.nextItem()
|
||||||
|
return t.token[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsing.
|
||||||
|
|
||||||
|
// New allocates a new parse tree with the given name.
|
||||||
|
func New(funcs ...map[string]Func) *Tree {
|
||||||
|
return &Tree{
|
||||||
|
funcs: funcs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorf formats the error and terminates processing.
|
||||||
|
func (t *Tree) errorf(format string, args ...interface{}) {
|
||||||
|
t.Root = nil
|
||||||
|
format = fmt.Sprintf("expr: %s", format)
|
||||||
|
panic(fmt.Errorf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// error terminates processing.
|
||||||
|
func (t *Tree) error(err error) {
|
||||||
|
t.errorf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect consumes the next token and guarantees it has the required type.
|
||||||
|
func (t *Tree) expect(expected itemType, context string) item {
|
||||||
|
token := t.next()
|
||||||
|
if token.typ != expected {
|
||||||
|
t.unexpected(token, context)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// expectOneOf consumes the next token and guarantees it has one of the required types.
|
||||||
|
// nolint:unused
|
||||||
|
func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item {
|
||||||
|
token := t.next()
|
||||||
|
if token.typ != expected1 && token.typ != expected2 {
|
||||||
|
t.unexpected(token, context)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// unexpected complains about the token and terminates processing.
|
||||||
|
func (t *Tree) unexpected(token item, context string) {
|
||||||
|
t.errorf("unexpected %s in %s", token, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recover is the handler that turns panics into returns from the top level of Parse.
|
||||||
|
func (t *Tree) recover(errp *error) {
|
||||||
|
e := recover()
|
||||||
|
if e != nil {
|
||||||
|
if _, ok := e.(runtime.Error); ok {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
if t != nil {
|
||||||
|
t.stopParse()
|
||||||
|
}
|
||||||
|
*errp = e.(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startParse initializes the parser, using the lexer.
|
||||||
|
func (t *Tree) startParse(funcs []map[string]Func, lex *lexer) {
|
||||||
|
t.Root = nil
|
||||||
|
t.lex = lex
|
||||||
|
t.funcs = funcs
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopParse terminates parsing.
|
||||||
|
func (t *Tree) stopParse() {
|
||||||
|
t.lex = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses the expression definition string to construct a representation
|
||||||
|
// of the expression for execution.
|
||||||
|
func (t *Tree) Parse(text string, funcs ...map[string]Func) (err error) {
|
||||||
|
defer t.recover(&err)
|
||||||
|
t.startParse(funcs, lex(text))
|
||||||
|
t.Text = text
|
||||||
|
t.parse()
|
||||||
|
t.stopParse()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse is the top-level parser for an expression.
|
||||||
|
// It runs to EOF.
|
||||||
|
func (t *Tree) parse() {
|
||||||
|
t.Root = t.O()
|
||||||
|
t.expect(itemEOF, "root input")
|
||||||
|
if err := t.Root.Check(t); err != nil {
|
||||||
|
t.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grammar:
|
||||||
|
O -> A {"||" A}
|
||||||
|
A -> C {"&&" C}
|
||||||
|
C -> P {( "==" | "!=" | ">" | ">=" | "<" | "<=") P}
|
||||||
|
P -> M {( "+" | "-" ) M}
|
||||||
|
M -> E {( "*" | "/" ) F}
|
||||||
|
E -> F {( "**" ) F}
|
||||||
|
F -> v | "(" O ")" | "!" O | "-" O
|
||||||
|
v -> number | func(..) | queryVar
|
||||||
|
Func -> name "(" param {"," param} ")"
|
||||||
|
param -> number | "string" | queryVar
|
||||||
|
*/
|
||||||
|
|
||||||
|
// expr:
|
||||||
|
|
||||||
|
// O is A {"||" A} in the grammar.
|
||||||
|
func (t *Tree) O() Node {
|
||||||
|
n := t.A()
|
||||||
|
for {
|
||||||
|
switch t.peek().typ {
|
||||||
|
case itemOr:
|
||||||
|
n = newBinary(t.next(), n, t.A())
|
||||||
|
default:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A is C {"&&" C} in the grammar.
|
||||||
|
func (t *Tree) A() Node {
|
||||||
|
n := t.C()
|
||||||
|
for {
|
||||||
|
switch t.peek().typ {
|
||||||
|
case itemAnd:
|
||||||
|
n = newBinary(t.next(), n, t.C())
|
||||||
|
default:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C is C -> P {( "==" | "!=" | ">" | ">=" | "<" | "<=") P} in the grammar.
|
||||||
|
func (t *Tree) C() Node {
|
||||||
|
n := t.P()
|
||||||
|
for {
|
||||||
|
switch t.peek().typ {
|
||||||
|
case itemEq, itemNotEq, itemGreater, itemGreaterEq, itemLess, itemLessEq:
|
||||||
|
n = newBinary(t.next(), n, t.P())
|
||||||
|
default:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// P is M {( "+" | "-" ) M} in the grammar.
|
||||||
|
func (t *Tree) P() Node {
|
||||||
|
n := t.M()
|
||||||
|
for {
|
||||||
|
switch t.peek().typ {
|
||||||
|
case itemPlus, itemMinus:
|
||||||
|
n = newBinary(t.next(), n, t.M())
|
||||||
|
default:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// M is E {( "*" | "/" ) F} in the grammar.
|
||||||
|
func (t *Tree) M() Node {
|
||||||
|
n := t.E()
|
||||||
|
for {
|
||||||
|
switch t.peek().typ {
|
||||||
|
case itemMult, itemDiv, itemMod:
|
||||||
|
n = newBinary(t.next(), n, t.E())
|
||||||
|
default:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E is F {( "**" ) F} in the grammar.
|
||||||
|
func (t *Tree) E() Node {
|
||||||
|
n := t.F()
|
||||||
|
for {
|
||||||
|
switch t.peek().typ {
|
||||||
|
case itemPow:
|
||||||
|
n = newBinary(t.next(), n, t.F())
|
||||||
|
default:
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// F is v | "(" O ")" | "!" O | "-" O in the grammar.
|
||||||
|
func (t *Tree) F() Node {
|
||||||
|
switch token := t.peek(); token.typ {
|
||||||
|
case itemNumber, itemFunc, itemVar:
|
||||||
|
return t.v()
|
||||||
|
case itemNot, itemMinus:
|
||||||
|
return newUnary(t.next(), t.F())
|
||||||
|
case itemLeftParen:
|
||||||
|
t.next()
|
||||||
|
n := t.O()
|
||||||
|
t.expect(itemRightParen, "input: F()")
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
t.unexpected(token, "input: F()")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// V is number | func(..) | queryVar in the grammar.
|
||||||
|
func (t *Tree) v() Node {
|
||||||
|
switch token := t.next(); token.typ {
|
||||||
|
case itemNumber:
|
||||||
|
n, err := newNumber(token.pos, token.val)
|
||||||
|
if err != nil {
|
||||||
|
t.error(err)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
case itemFunc:
|
||||||
|
t.backup()
|
||||||
|
return t.Func()
|
||||||
|
case itemVar:
|
||||||
|
t.backup()
|
||||||
|
return t.Var()
|
||||||
|
default:
|
||||||
|
t.unexpected(token, "input: v()")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Var is queryVar in the grammar.
|
||||||
|
func (t *Tree) Var() (v *VarNode) {
|
||||||
|
token := t.next()
|
||||||
|
varNoPrefix := strings.TrimPrefix(token.val, "$")
|
||||||
|
t.VarNames = append(t.VarNames, varNoPrefix)
|
||||||
|
return newVar(token.pos, varNoPrefix, token.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Func parses a FuncNode.
|
||||||
|
func (t *Tree) Func() (f *FuncNode) {
|
||||||
|
token := t.next()
|
||||||
|
funcv, ok := t.GetFunction(token.val)
|
||||||
|
if !ok {
|
||||||
|
t.errorf("non existent function %s", token.val)
|
||||||
|
}
|
||||||
|
f = newFunc(token.pos, token.val, funcv)
|
||||||
|
t.expect(itemLeftParen, "func")
|
||||||
|
for {
|
||||||
|
switch token = t.next(); token.typ {
|
||||||
|
default:
|
||||||
|
t.backup()
|
||||||
|
node := t.O()
|
||||||
|
f.append(node)
|
||||||
|
if len(f.Args) == 1 && f.F.VariantReturn {
|
||||||
|
f.F.Return = node.Return()
|
||||||
|
}
|
||||||
|
case itemString:
|
||||||
|
s, err := strconv.Unquote(token.val)
|
||||||
|
if err != nil {
|
||||||
|
t.errorf("Unquoting error: %s", err)
|
||||||
|
}
|
||||||
|
f.append(newString(token.pos, token.val, s))
|
||||||
|
case itemRightParen:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFunction gets a parsed Func from the functions available on the tree's func property.
|
||||||
|
func (t *Tree) GetFunction(name string) (v Func, ok bool) {
|
||||||
|
for _, funcMap := range t.funcs {
|
||||||
|
if funcMap == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v, ok = funcMap[name]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the parse tree.
|
||||||
|
func (t *Tree) String() string {
|
||||||
|
return t.Root.String()
|
||||||
|
}
|
101
pkg/expr/mathexp/reduce.go
Normal file
101
pkg/expr/mathexp/reduce.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Sum(v *data.Field) *float64 {
|
||||||
|
var sum float64
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
if f, ok := v.At(i).(*float64); ok {
|
||||||
|
if f == nil {
|
||||||
|
nan := math.NaN()
|
||||||
|
return &nan
|
||||||
|
}
|
||||||
|
sum += *f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func Avg(v *data.Field) *float64 {
|
||||||
|
sum := Sum(v)
|
||||||
|
f := *sum / float64(v.Len())
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func Min(fv *data.Field) *float64 {
|
||||||
|
var f float64
|
||||||
|
if fv.Len() == 0 {
|
||||||
|
nan := math.NaN()
|
||||||
|
return &nan
|
||||||
|
}
|
||||||
|
for i := 0; i < fv.Len(); i++ {
|
||||||
|
if v, ok := fv.At(i).(*float64); ok {
|
||||||
|
if v == nil {
|
||||||
|
nan := math.NaN()
|
||||||
|
return &nan
|
||||||
|
}
|
||||||
|
if i == 0 || *v < f {
|
||||||
|
f = *v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func Max(fv *data.Field) *float64 {
|
||||||
|
var f float64
|
||||||
|
if fv.Len() == 0 {
|
||||||
|
nan := math.NaN()
|
||||||
|
return &nan
|
||||||
|
}
|
||||||
|
for i := 0; i < fv.Len(); i++ {
|
||||||
|
if v, ok := fv.At(i).(*float64); ok {
|
||||||
|
if v == nil {
|
||||||
|
nan := math.NaN()
|
||||||
|
return &nan
|
||||||
|
}
|
||||||
|
if i == 0 || *v > f {
|
||||||
|
f = *v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func Count(fv *data.Field) *float64 {
|
||||||
|
f := float64(fv.Len())
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce turns the Series into a Number based on the given reduction function
|
||||||
|
func (s Series) Reduce(rFunc string) (Number, error) {
|
||||||
|
var l data.Labels
|
||||||
|
if s.GetLabels() != nil {
|
||||||
|
l = s.GetLabels().Copy()
|
||||||
|
}
|
||||||
|
number := NewNumber(fmt.Sprintf("%v_%v", rFunc, s.GetName()), l)
|
||||||
|
var f *float64
|
||||||
|
fVec := s.Frame.Fields[1]
|
||||||
|
switch rFunc {
|
||||||
|
case "sum":
|
||||||
|
f = Sum(fVec)
|
||||||
|
case "mean":
|
||||||
|
f = Avg(fVec)
|
||||||
|
case "min":
|
||||||
|
f = Min(fVec)
|
||||||
|
case "max":
|
||||||
|
f = Max(fVec)
|
||||||
|
case "count":
|
||||||
|
f = Count(fVec)
|
||||||
|
default:
|
||||||
|
return number, fmt.Errorf("reduction %v not implemented", rFunc)
|
||||||
|
}
|
||||||
|
number.SetValue(f)
|
||||||
|
|
||||||
|
return number, nil
|
||||||
|
}
|
240
pkg/expr/mathexp/reduce_test.go
Normal file
240
pkg/expr/mathexp/reduce_test.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var seriesWithNil = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("temp", nil, tp{
|
||||||
|
time.Unix(5, 0), float64Pointer(2),
|
||||||
|
}, tp{
|
||||||
|
time.Unix(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var seriesEmpty = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("temp", nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeriesReduce(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
red string
|
||||||
|
vars Vars
|
||||||
|
varToReduce string
|
||||||
|
errIs require.ErrorAssertionFunc
|
||||||
|
resultsIs require.ComparisonAssertionFunc
|
||||||
|
results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo reduction will error",
|
||||||
|
red: "foo",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
errIs: require.Error,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sum series",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("sum_", nil, float64Pointer(3)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sum series with a nil value",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("sum_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sum empty series",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("sum_", nil, float64Pointer(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mean series with a nil value",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("mean_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mean empty series",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("mean_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min series with a nil value",
|
||||||
|
red: "min",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("min_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min empty series",
|
||||||
|
red: "min",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("min_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max series with a nil value",
|
||||||
|
red: "max",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("max_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max empty series",
|
||||||
|
red: "max",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("max_", nil, NaN),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mean series",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: aSeriesNullableTime,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("mean_", nil, float64Pointer(1.5)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "count empty series",
|
||||||
|
red: "count",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("count_", nil, float64Pointer(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mean series with labels",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeriesNullableTime("temp", data.Labels{"host": "a"}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("mean_", data.Labels{"host": "a"}, float64Pointer(1.5)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
results := Results{}
|
||||||
|
seriesSet := tt.vars[tt.varToReduce]
|
||||||
|
for _, series := range seriesSet.Values {
|
||||||
|
ns, err := series.Value().(*Series).Reduce(tt.red)
|
||||||
|
tt.errIs(t, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results.Values = append(results.Values, ns)
|
||||||
|
}
|
||||||
|
opt := cmp.Comparer(func(x, y float64) bool {
|
||||||
|
return (math.IsNaN(x) && math.IsNaN(y)) || x == y
|
||||||
|
})
|
||||||
|
options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)
|
||||||
|
if diff := cmp.Diff(tt.results, results, options...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
83
pkg/expr/mathexp/resample.go
Normal file
83
pkg/expr/mathexp/resample.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resample turns the Series into a Number based on the given reduction function
|
||||||
|
func (s Series) Resample(interval time.Duration, downsampler string, upsampler string, tr backend.TimeRange) (Series, error) {
|
||||||
|
newSeriesLength := int(float64(tr.To.Sub(tr.From).Nanoseconds()) / float64(interval.Nanoseconds()))
|
||||||
|
if newSeriesLength <= 0 {
|
||||||
|
return s, fmt.Errorf("the series cannot be sampled further; the time range is shorter than the interval")
|
||||||
|
}
|
||||||
|
resampled := NewSeries(s.GetName(), s.GetLabels(), s.TimeIdx, s.TimeIsNullable, s.ValueIdx, s.ValueIsNullabe, newSeriesLength+1)
|
||||||
|
bookmark := 0
|
||||||
|
var lastSeen *float64
|
||||||
|
idx := 0
|
||||||
|
t := tr.From
|
||||||
|
for !t.After(tr.To) && idx <= newSeriesLength {
|
||||||
|
vals := make([]*float64, 0)
|
||||||
|
sIdx := bookmark
|
||||||
|
for {
|
||||||
|
if sIdx == s.Len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
st, v := s.GetPoint(sIdx)
|
||||||
|
if st.After(t) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bookmark++
|
||||||
|
sIdx++
|
||||||
|
lastSeen = v
|
||||||
|
vals = append(vals, v)
|
||||||
|
}
|
||||||
|
var value *float64
|
||||||
|
if len(vals) == 0 { // upsampling
|
||||||
|
switch upsampler {
|
||||||
|
case "pad":
|
||||||
|
if lastSeen != nil {
|
||||||
|
value = lastSeen
|
||||||
|
} else {
|
||||||
|
value = nil
|
||||||
|
}
|
||||||
|
case "backfilling":
|
||||||
|
if sIdx == s.Len() { // no vals left
|
||||||
|
value = nil
|
||||||
|
} else {
|
||||||
|
_, value = s.GetPoint(sIdx)
|
||||||
|
}
|
||||||
|
case "fillna":
|
||||||
|
value = nil
|
||||||
|
default:
|
||||||
|
return s, fmt.Errorf("upsampling %v not implemented", upsampler)
|
||||||
|
}
|
||||||
|
} else { // downsampling
|
||||||
|
fVec := data.NewField("", s.GetLabels(), vals)
|
||||||
|
var tmp *float64
|
||||||
|
switch downsampler {
|
||||||
|
case "sum":
|
||||||
|
tmp = Sum(fVec)
|
||||||
|
case "mean":
|
||||||
|
tmp = Avg(fVec)
|
||||||
|
case "min":
|
||||||
|
tmp = Min(fVec)
|
||||||
|
case "max":
|
||||||
|
tmp = Max(fVec)
|
||||||
|
default:
|
||||||
|
return s, fmt.Errorf("downsampling %v not implemented", downsampler)
|
||||||
|
}
|
||||||
|
value = tmp
|
||||||
|
}
|
||||||
|
tv := t // his is required otherwise all points keep the latest timestamp; anything better?
|
||||||
|
if err := resampled.SetPoint(idx, &tv, value); err != nil {
|
||||||
|
return resampled, err
|
||||||
|
}
|
||||||
|
t = t.Add(interval)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
return resampled, nil
|
||||||
|
}
|
260
pkg/expr/mathexp/resample_test.go
Normal file
260
pkg/expr/mathexp/resample_test.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResampleSeries(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
interval time.Duration
|
||||||
|
downsampler string
|
||||||
|
upsampler string
|
||||||
|
timeRange backend.TimeRange
|
||||||
|
seriesToResample Series
|
||||||
|
series Series
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "resample series: time range shorter than the rule interval",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "mean",
|
||||||
|
upsampler: "fillna",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(4, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: invalid time range",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "mean",
|
||||||
|
upsampler: "fillna",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(11, 0),
|
||||||
|
To: time.Unix(0, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: downsampling (mean / pad)",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "mean",
|
||||||
|
upsampler: "pad",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(16, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(9, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2.5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1.5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(15, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: downsampling (max / fillna)",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "max",
|
||||||
|
upsampler: "fillna",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(16, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(9, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(15, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: downsampling (min / fillna)",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "min",
|
||||||
|
upsampler: "fillna",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(16, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(9, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(15, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: downsampling (sum / fillna)",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "sum",
|
||||||
|
upsampler: "fillna",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(16, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(9, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(15, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: downsampling (mean / fillna)",
|
||||||
|
interval: time.Second * 5,
|
||||||
|
downsampler: "mean",
|
||||||
|
upsampler: "fillna",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(16, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(9, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(5, 0), float64Pointer(2.5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1.5),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(15, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: upsampling (mean / pad )",
|
||||||
|
interval: time.Second * 2,
|
||||||
|
downsampler: "mean",
|
||||||
|
upsampler: "pad",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(11, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), nil,
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(6, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(8, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resample series: upsampling (mean / backfilling )",
|
||||||
|
interval: time.Second * 2,
|
||||||
|
downsampler: "mean",
|
||||||
|
upsampler: "backfilling",
|
||||||
|
timeRange: backend.TimeRange{
|
||||||
|
From: time.Unix(0, 0),
|
||||||
|
To: time.Unix(11, 0),
|
||||||
|
},
|
||||||
|
seriesToResample: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(7, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(0, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(4, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(6, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(8, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(10, 0), nil,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
series, err := tt.seriesToResample.Resample(tt.interval, tt.downsampler, tt.upsampler, tt.timeRange)
|
||||||
|
if tt.series.Frame == nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.series, series)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
199
pkg/expr/mathexp/type_series.go
Normal file
199
pkg/expr/mathexp/type_series.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Series has time.Time and ...? *float64 fields.
|
||||||
|
type Series struct {
|
||||||
|
Frame *data.Frame
|
||||||
|
TimeIsNullable bool
|
||||||
|
TimeIdx int
|
||||||
|
ValueIsNullabe bool
|
||||||
|
ValueIdx int
|
||||||
|
// TODO:
|
||||||
|
// - Multiple Value Fields
|
||||||
|
// - Value can be different number types
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeriesFromFrame validates that the dataframe can be considered a Series type
|
||||||
|
// and populate meta information on Series about the frame.
|
||||||
|
func SeriesFromFrame(frame *data.Frame) (s Series, err error) {
|
||||||
|
if len(frame.Fields) != 2 {
|
||||||
|
return s, fmt.Errorf("frame must have exactly two fields to be a series, has %v", len(frame.Fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundTime := false
|
||||||
|
foundValue := false
|
||||||
|
for i, field := range frame.Fields {
|
||||||
|
switch field.Type() {
|
||||||
|
case data.FieldTypeTime:
|
||||||
|
s.TimeIdx = i
|
||||||
|
foundTime = true
|
||||||
|
case data.FieldTypeNullableTime:
|
||||||
|
s.TimeIsNullable = true
|
||||||
|
foundTime = true
|
||||||
|
s.TimeIdx = i
|
||||||
|
case data.FieldTypeFloat64:
|
||||||
|
foundValue = true
|
||||||
|
s.ValueIdx = i
|
||||||
|
case data.FieldTypeNullableFloat64:
|
||||||
|
s.ValueIsNullabe = true
|
||||||
|
foundValue = true
|
||||||
|
s.ValueIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundTime {
|
||||||
|
return s, fmt.Errorf("no time column found in frame %v", frame.Name)
|
||||||
|
}
|
||||||
|
if !foundValue {
|
||||||
|
return s, fmt.Errorf("no float64 value column found in frame %v", frame.Name)
|
||||||
|
}
|
||||||
|
s.Frame = frame
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSeries returns a dataframe of type Series.
|
||||||
|
func NewSeries(name string, labels data.Labels, timeIdx int, nullableTime bool, valueIdx int, nullableValue bool, size int) Series {
|
||||||
|
fields := make([]*data.Field, 2)
|
||||||
|
|
||||||
|
if nullableValue {
|
||||||
|
fields[valueIdx] = data.NewField(name, labels, make([]*float64, size))
|
||||||
|
} else {
|
||||||
|
fields[valueIdx] = data.NewField(name, labels, make([]float64, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if nullableTime {
|
||||||
|
fields[timeIdx] = data.NewField("Time", nil, make([]*time.Time, size))
|
||||||
|
} else {
|
||||||
|
fields[timeIdx] = data.NewField("Time", nil, make([]time.Time, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Series{
|
||||||
|
Frame: data.NewFrame("", fields...),
|
||||||
|
TimeIsNullable: nullableTime,
|
||||||
|
TimeIdx: timeIdx,
|
||||||
|
ValueIsNullabe: nullableValue,
|
||||||
|
ValueIdx: valueIdx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the Value type and allows it to fulfill the Value interface.
|
||||||
|
func (s Series) Type() parse.ReturnType { return parse.TypeSeriesSet }
|
||||||
|
|
||||||
|
// Value returns the actual value allows it to fulfill the Value interface.
|
||||||
|
func (s Series) Value() interface{} { return &s }
|
||||||
|
|
||||||
|
func (s Series) GetLabels() data.Labels { return s.Frame.Fields[s.ValueIdx].Labels }
|
||||||
|
|
||||||
|
func (s Series) SetLabels(ls data.Labels) { s.Frame.Fields[s.ValueIdx].Labels = ls }
|
||||||
|
|
||||||
|
func (s Series) GetName() string { return s.Frame.Name }
|
||||||
|
|
||||||
|
// AsDataFrame returns the underlying *data.Frame.
|
||||||
|
func (s Series) AsDataFrame() *data.Frame { return s.Frame }
|
||||||
|
|
||||||
|
// GetPoint returns the time and value at the specified index.
|
||||||
|
func (s Series) GetPoint(pointIdx int) (*time.Time, *float64) {
|
||||||
|
return s.GetTime(pointIdx), s.GetValue(pointIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPoint sets the time and value on the corresponding vectors at the specified index.
|
||||||
|
func (s Series) SetPoint(pointIdx int, t *time.Time, f *float64) (err error) {
|
||||||
|
if s.TimeIsNullable {
|
||||||
|
s.Frame.Fields[s.TimeIdx].Set(pointIdx, t)
|
||||||
|
} else {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("can not set null time value on non-nullable time field for series name %v", s.Frame.Name)
|
||||||
|
}
|
||||||
|
s.Frame.Fields[s.TimeIdx].Set(pointIdx, *t)
|
||||||
|
}
|
||||||
|
if s.ValueIsNullabe {
|
||||||
|
s.Frame.Fields[s.ValueIdx].Set(pointIdx, f)
|
||||||
|
} else {
|
||||||
|
if f == nil {
|
||||||
|
return fmt.Errorf("can not set null float value on non-nullable float field for series name %v", s.Frame.Name)
|
||||||
|
}
|
||||||
|
s.Frame.Fields[s.ValueIdx].Set(pointIdx, *f)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendPoint appends a point (time/value).
|
||||||
|
func (s Series) AppendPoint(pointIdx int, t *time.Time, f *float64) (err error) {
|
||||||
|
if s.TimeIsNullable {
|
||||||
|
s.Frame.Fields[s.TimeIdx].Append(t)
|
||||||
|
} else {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("can not append null time value on non-nullable time field for series name %v", s.Frame.Name)
|
||||||
|
}
|
||||||
|
s.Frame.Fields[s.TimeIdx].Append(*t)
|
||||||
|
}
|
||||||
|
if s.ValueIsNullabe {
|
||||||
|
s.Frame.Fields[s.ValueIdx].Append(f)
|
||||||
|
} else {
|
||||||
|
if f == nil {
|
||||||
|
return fmt.Errorf("can not append null float value on non-nullable float field for series name %v", s.Frame.Name)
|
||||||
|
}
|
||||||
|
s.Frame.Fields[s.ValueIdx].Append(*f)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the series.
|
||||||
|
func (s Series) Len() int {
|
||||||
|
return s.Frame.Fields[0].Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTime returns the time at the specified index.
|
||||||
|
func (s Series) GetTime(pointIdx int) *time.Time {
|
||||||
|
if s.TimeIsNullable {
|
||||||
|
return s.Frame.Fields[s.TimeIdx].At(pointIdx).(*time.Time)
|
||||||
|
}
|
||||||
|
t := s.Frame.Fields[s.TimeIdx].At(pointIdx).(time.Time)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the float value at the specified index.
|
||||||
|
func (s Series) GetValue(pointIdx int) *float64 {
|
||||||
|
if s.ValueIsNullabe {
|
||||||
|
return s.Frame.Fields[s.ValueIdx].At(pointIdx).(*float64)
|
||||||
|
}
|
||||||
|
f := s.Frame.Fields[s.ValueIdx].At(pointIdx).(float64)
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortByTime sorts the series by the time from oldest to newest.
|
||||||
|
// If desc is true, it will sort from newest to oldest.
|
||||||
|
// If any time values are nil, it will panic.
|
||||||
|
func (s Series) SortByTime(desc bool) {
|
||||||
|
if desc {
|
||||||
|
sort.Sort(sort.Reverse(SortSeriesByTime(s)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Sort(SortSeriesByTime(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortSeriesByTime allows a Series to be sorted by time
|
||||||
|
// the sort interface will panic if any timestamps are null
|
||||||
|
type SortSeriesByTime Series
|
||||||
|
|
||||||
|
func (ss SortSeriesByTime) Len() int { return Series(ss).Len() }
|
||||||
|
|
||||||
|
func (ss SortSeriesByTime) Swap(i, j int) {
|
||||||
|
iTimeVal, iFVal := Series(ss).GetPoint(i)
|
||||||
|
jTimeVal, jFVal := Series(ss).GetPoint(j)
|
||||||
|
_ = Series(ss).SetPoint(j, iTimeVal, iFVal)
|
||||||
|
_ = Series(ss).SetPoint(i, jTimeVal, jFVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss SortSeriesByTime) Less(i, j int) bool {
|
||||||
|
iTimeVal := Series(ss).GetTime(i)
|
||||||
|
jTimeVal := Series(ss).GetTime(j)
|
||||||
|
return iTimeVal.Before(*jTimeVal)
|
||||||
|
}
|
112
pkg/expr/mathexp/types.go
Normal file
112
pkg/expr/mathexp/types.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Results is a container for Value interfaces.
|
||||||
|
type Results struct {
|
||||||
|
Values Values
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values is a slice of Value interfaces
|
||||||
|
type Values []Value
|
||||||
|
|
||||||
|
// AsDataFrames returns each value as a slice of frames.
|
||||||
|
func (vals Values) AsDataFrames(refID string) []*data.Frame {
|
||||||
|
frames := make([]*data.Frame, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
frames[i] = v.AsDataFrame()
|
||||||
|
frames[i].RefID = refID
|
||||||
|
}
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value is the interface that holds different types such as a Scalar, Series, or Number.
|
||||||
|
// all Value implementations should be a *data.Frame
|
||||||
|
type Value interface {
|
||||||
|
Type() parse.ReturnType
|
||||||
|
Value() interface{}
|
||||||
|
GetLabels() data.Labels
|
||||||
|
SetLabels(data.Labels)
|
||||||
|
GetName() string
|
||||||
|
AsDataFrame() *data.Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar is the type that holds a single number constant.
|
||||||
|
// Before returning from an expression it will be wrapped in a
|
||||||
|
// data frame.
|
||||||
|
type Scalar struct{ Frame *data.Frame }
|
||||||
|
|
||||||
|
// Type returns the Value type and allows it to fulfill the Value interface.
|
||||||
|
func (s Scalar) Type() parse.ReturnType { return parse.TypeScalar }
|
||||||
|
|
||||||
|
// Value returns the actual value allows it to fulfill the Value interface.
|
||||||
|
func (s Scalar) Value() interface{} { return s }
|
||||||
|
|
||||||
|
func (s Scalar) GetLabels() data.Labels { return nil }
|
||||||
|
|
||||||
|
func (s Scalar) SetLabels(ls data.Labels) {}
|
||||||
|
|
||||||
|
func (s Scalar) GetName() string { return s.Frame.Name }
|
||||||
|
|
||||||
|
// AsDataFrame returns the underlying *data.Frame.
|
||||||
|
func (s Scalar) AsDataFrame() *data.Frame { return s.Frame }
|
||||||
|
|
||||||
|
// NewScalar creates a Scalar holding value f.
|
||||||
|
func NewScalar(f *float64) Scalar {
|
||||||
|
frame := data.NewFrame("",
|
||||||
|
data.NewField("Scalar", nil, []*float64{f}),
|
||||||
|
)
|
||||||
|
return Scalar{frame}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScalarResults creates a Results holding a single Scalar
|
||||||
|
func NewScalarResults(f *float64) Results {
|
||||||
|
return Results{
|
||||||
|
Values: []Value{NewScalar(f)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat64Value retrieves the single scalar value from the data
|
||||||
|
func (s Scalar) GetFloat64Value() *float64 {
|
||||||
|
return s.Frame.At(0, 0).(*float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number hold a labelled single number values.
|
||||||
|
type Number struct{ Frame *data.Frame }
|
||||||
|
|
||||||
|
// Type returns the Value type and allows it to fulfill the Value interface.
|
||||||
|
func (n Number) Type() parse.ReturnType { return parse.TypeNumberSet }
|
||||||
|
|
||||||
|
// Value returns the actual value allows it to fulfill the Value interface.
|
||||||
|
func (n Number) Value() interface{} { return &n }
|
||||||
|
|
||||||
|
func (n Number) GetLabels() data.Labels { return n.Frame.Fields[0].Labels }
|
||||||
|
|
||||||
|
func (n Number) SetLabels(ls data.Labels) { n.Frame.Fields[0].Labels = ls }
|
||||||
|
|
||||||
|
func (n Number) GetName() string { return n.Frame.Name }
|
||||||
|
|
||||||
|
// AsDataFrame returns the underlying *data.Frame.
|
||||||
|
func (n Number) AsDataFrame() *data.Frame { return n.Frame }
|
||||||
|
|
||||||
|
// SetValue sets the value of the Number to float64 pointer f
|
||||||
|
func (n Number) SetValue(f *float64) {
|
||||||
|
n.Frame.Set(0, 0, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat64Value retrieves the single scalar value from the data
|
||||||
|
func (n Number) GetFloat64Value() *float64 {
|
||||||
|
return n.Frame.At(0, 0).(*float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumber returns a data that holds a float64Vector
|
||||||
|
func NewNumber(name string, labels data.Labels) Number {
|
||||||
|
return Number{
|
||||||
|
data.NewFrame("",
|
||||||
|
data.NewField(name, labels, make([]*float64, 1)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
168
pkg/expr/mathexp/types_test.go
Normal file
168
pkg/expr/mathexp/types_test.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSeriesSort(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
descending bool
|
||||||
|
series Series
|
||||||
|
sortedSeriesIs assert.ComparisonAssertionFunc
|
||||||
|
sortedSeries Series
|
||||||
|
panics assert.PanicTestFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unordered series should sort by time ascending",
|
||||||
|
descending: false,
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(3, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(1, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
sortedSeriesIs: assert.Equal,
|
||||||
|
sortedSeries: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(1, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(3, 0), float64Pointer(3),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unordered series should sort by time descending",
|
||||||
|
descending: true,
|
||||||
|
series: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(3, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(1, 0), float64Pointer(1),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}),
|
||||||
|
sortedSeriesIs: assert.Equal,
|
||||||
|
sortedSeries: makeSeriesNullableTime("", nil, nullTimeTP{
|
||||||
|
unixTimePointer(3, 0), float64Pointer(3),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(2, 0), float64Pointer(2),
|
||||||
|
}, nullTimeTP{
|
||||||
|
unixTimePointer(1, 0), float64Pointer(1),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.series.SortByTime(tt.descending)
|
||||||
|
tt.sortedSeriesIs(t, tt.series, tt.sortedSeries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeriesFromFrame(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
frame *data.Frame
|
||||||
|
errIs assert.ErrorAssertionFunc
|
||||||
|
Is assert.ComparisonAssertionFunc
|
||||||
|
Series Series
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "[]time, []float frame should convert",
|
||||||
|
frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("time", nil, []time.Time{}),
|
||||||
|
data.NewField("value", nil, []float64{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errIs: assert.NoError,
|
||||||
|
Is: assert.Equal,
|
||||||
|
Series: Series{
|
||||||
|
Frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("time", nil, []time.Time{}),
|
||||||
|
data.NewField("value", nil, []float64{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TimeIdx: 0,
|
||||||
|
TimeIsNullable: false,
|
||||||
|
ValueIdx: 1,
|
||||||
|
ValueIsNullabe: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[]*float, []*time frame should convert",
|
||||||
|
frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("value", nil, []*float64{float64Pointer(5)}),
|
||||||
|
data.NewField("time", nil, []*time.Time{unixTimePointer(5, 0)}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errIs: assert.NoError,
|
||||||
|
Is: assert.Equal,
|
||||||
|
Series: Series{
|
||||||
|
Frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("value", nil, []*float64{float64Pointer(5)}),
|
||||||
|
data.NewField("time", nil, []*time.Time{unixTimePointer(5, 0)}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TimeIdx: 1,
|
||||||
|
TimeIsNullable: true,
|
||||||
|
ValueIdx: 0,
|
||||||
|
ValueIsNullabe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[]*time, []*time frame should error",
|
||||||
|
frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("time", nil, []*time.Time{}),
|
||||||
|
data.NewField("time", nil, []*time.Time{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errIs: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[]*float64, []float64 frame should error",
|
||||||
|
frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("value", nil, []*float64{}),
|
||||||
|
data.NewField("value", nil, []*float64{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errIs: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[]*float64 frame should error",
|
||||||
|
frame: &data.Frame{
|
||||||
|
Name: "test",
|
||||||
|
Fields: []*data.Field{
|
||||||
|
data.NewField("value", nil, []*float64{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errIs: assert.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := SeriesFromFrame(tt.frame)
|
||||||
|
tt.errIs(t, err)
|
||||||
|
if err == nil {
|
||||||
|
tt.Is(t, s, tt.Series)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
244
pkg/expr/mathexp/union_test.go
Normal file
244
pkg/expr/mathexp/union_test.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package mathexp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_union(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
aResults Results
|
||||||
|
bResults Results
|
||||||
|
unionsAre assert.ComparisonAssertionFunc
|
||||||
|
unions []*Union
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "equal tags single union",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1"},
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equal tags keys with no matching values will result in a union when len(A) == 1 && len(B) == 1",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "2"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "2"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equal tags keys with no matching values will result in no unions when len(A) != 1 && len(B) != 1",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
makeSeriesNullableTime("q", data.Labels{"id": "3"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "2"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty results will result in no unions",
|
||||||
|
aResults: Results{},
|
||||||
|
bResults: Results{},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "incompatible tags of different length with will result in no unions when len(A) != 1 && len(B) != 1",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"ID": "1"}),
|
||||||
|
makeSeriesNullableTime("q", data.Labels{"ID": "3"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A is subset of B results in single union with Labels of B",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"}, // Union gets the labels that is not the subset
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "B is subset of A results in single union with Labels of A",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"}, // Union gets the labels that is not the subset
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single valued A is subset of many valued B, results in many union with Labels of B",
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"},
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "red snapper"},
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A with different tags keys lengths to B makes 3 unions (with two unions have matching tags)",
|
||||||
|
// Is this the behavior we want? A result within the results will no longer
|
||||||
|
// be uniquely identifiable.
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
makeSeriesNullableTime("aa", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
makeSeriesNullableTime("bb", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"},
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "red snapper"},
|
||||||
|
A: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
B: makeSeriesNullableTime("bb", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"},
|
||||||
|
A: makeSeriesNullableTime("aa", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
B: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "B with different tags keys lengths to A makes 3 unions (with two unions have matching tags)",
|
||||||
|
// Is this the behavior we want? A result within the results will no longer
|
||||||
|
// be uniquely identifiable.
|
||||||
|
aResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
makeSeriesNullableTime("bb", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bResults: Results{
|
||||||
|
Values: Values{
|
||||||
|
makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
makeSeriesNullableTime("aa", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unionsAre: assert.EqualValues,
|
||||||
|
unions: []*Union{
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"},
|
||||||
|
A: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
B: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "herring"},
|
||||||
|
A: makeSeriesNullableTime("b", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
B: makeSeriesNullableTime("aa", data.Labels{"id": "1", "fish": "herring"}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: data.Labels{"id": "1", "fish": "red snapper"},
|
||||||
|
A: makeSeriesNullableTime("bb", data.Labels{"id": "1", "fish": "red snapper"}),
|
||||||
|
B: makeSeriesNullableTime("a", data.Labels{"id": "1"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
unions := union(tt.aResults, tt.bResults)
|
||||||
|
tt.unionsAre(t, tt.unions, unions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
358
pkg/expr/nodes.go
Normal file
358
pkg/expr/nodes.go
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||||
|
|
||||||
|
"gonum.org/v1/gonum/graph/simple"
|
||||||
|
)
|
||||||
|
|
||||||
|
// baseNode includes commmon properties used across DPNodes.
|
||||||
|
type baseNode struct {
|
||||||
|
id int64
|
||||||
|
refID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawNode struct {
|
||||||
|
RefID string `json:"refId"`
|
||||||
|
Query map[string]interface{}
|
||||||
|
QueryType string
|
||||||
|
TimeRange backend.TimeRange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rn *rawNode) GetDatasourceName() (string, error) {
|
||||||
|
rawDs, ok := rn.Query["datasource"]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("no datasource in query for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
dsName, ok := rawDs.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("expted datasource identifier to be a string, got %T", rawDs)
|
||||||
|
}
|
||||||
|
return dsName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rn *rawNode) GetCommandType() (c CommandType, err error) {
|
||||||
|
rawType, ok := rn.Query["type"]
|
||||||
|
if !ok {
|
||||||
|
return c, fmt.Errorf("no expression command type in query for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
typeString, ok := rawType.(string)
|
||||||
|
if !ok {
|
||||||
|
return c, fmt.Errorf("expected expression command type to be a string, got type %T", rawType)
|
||||||
|
}
|
||||||
|
return ParseCommandType(typeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the node. In particular for
|
||||||
|
// %v formating in error messages.
|
||||||
|
func (b *baseNode) String() string {
|
||||||
|
return b.refID
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMDNode is a DPNode that holds an expression command.
|
||||||
|
type CMDNode struct {
|
||||||
|
baseNode
|
||||||
|
CMDType CommandType
|
||||||
|
Command Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the id of the node so it can fulfill the gonum's graph Node interface.
|
||||||
|
func (b *baseNode) ID() int64 {
|
||||||
|
return b.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefID returns the refId of the node.
|
||||||
|
func (b *baseNode) RefID() string {
|
||||||
|
return b.refID
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeType returns the data pipeline node type.
|
||||||
|
func (gn *CMDNode) NodeType() NodeType {
|
||||||
|
return TypeCMDNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the node and adds the results to vars. If the node requires
|
||||||
|
// other nodes they must have already been executed and their results must
|
||||||
|
// already by in vars.
|
||||||
|
func (gn *CMDNode) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
|
||||||
|
return gn.Command.Execute(ctx, vars)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCMDNode(dp *simple.DirectedGraph, rn *rawNode) (*CMDNode, error) {
|
||||||
|
commandType, err := rn.GetCommandType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid expression command type in '%v'", rn.RefID)
|
||||||
|
}
|
||||||
|
|
||||||
|
node := &CMDNode{
|
||||||
|
baseNode: baseNode{
|
||||||
|
id: dp.NewNode().ID(),
|
||||||
|
refID: rn.RefID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch commandType {
|
||||||
|
case TypeMath:
|
||||||
|
node.Command, err = UnmarshalMathCommand(rn)
|
||||||
|
case TypeReduce:
|
||||||
|
node.Command, err = UnmarshalReduceCommand(rn)
|
||||||
|
case TypeResample:
|
||||||
|
node.Command, err = UnmarshalResampleCommand(rn)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("expression command type '%v' in '%v' not implemented", commandType, rn.RefID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultIntervalMS = int64(64)
|
||||||
|
defaultMaxDP = int64(5000)
|
||||||
|
)
|
||||||
|
|
||||||
|
// DSNode is a DPNode that holds a datasource request.
|
||||||
|
type DSNode struct {
|
||||||
|
baseNode
|
||||||
|
query json.RawMessage
|
||||||
|
datasourceID int64
|
||||||
|
orgID int64
|
||||||
|
queryType string
|
||||||
|
timeRange backend.TimeRange
|
||||||
|
intervalMS int64
|
||||||
|
maxDP int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeType returns the data pipeline node type.
|
||||||
|
func (dn *DSNode) NodeType() NodeType {
|
||||||
|
return TypeDatasourceNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDSNode(dp *simple.DirectedGraph, rn *rawNode) (*DSNode, error) {
|
||||||
|
encodedQuery, err := json.Marshal(rn.Query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dsNode := &DSNode{
|
||||||
|
baseNode: baseNode{
|
||||||
|
id: dp.NewNode().ID(),
|
||||||
|
refID: rn.RefID,
|
||||||
|
},
|
||||||
|
query: json.RawMessage(encodedQuery),
|
||||||
|
queryType: rn.QueryType,
|
||||||
|
intervalMS: defaultIntervalMS,
|
||||||
|
maxDP: defaultMaxDP,
|
||||||
|
timeRange: rn.TimeRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
rawDsID, ok := rn.Query["datasourceId"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no datasourceId in expression data source request for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
floatDsID, ok := rawDsID.(float64)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected datasourceId to be a float64, got type %T for refId %v", rawDsID, rn.RefID)
|
||||||
|
}
|
||||||
|
dsNode.datasourceID = int64(floatDsID)
|
||||||
|
|
||||||
|
rawOrgID, ok := rn.Query["orgId"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no orgId in expression data source request command for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
floatOrgID, ok := rawOrgID.(float64)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected orgId to be a float64, got type %T for refId %v", rawOrgID, rn.RefID)
|
||||||
|
}
|
||||||
|
dsNode.orgID = int64(floatOrgID)
|
||||||
|
|
||||||
|
var floatIntervalMS float64
|
||||||
|
if rawIntervalMS := rn.Query["intervalMs"]; ok {
|
||||||
|
if floatIntervalMS, ok = rawIntervalMS.(float64); !ok {
|
||||||
|
return nil, fmt.Errorf("expected intervalMs to be an float64, got type %T for refId %v", rawIntervalMS, rn.RefID)
|
||||||
|
}
|
||||||
|
dsNode.intervalMS = int64(floatIntervalMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
var floatMaxDP float64
|
||||||
|
if rawMaxDP := rn.Query["maxDataPoints"]; ok {
|
||||||
|
if floatMaxDP, ok = rawMaxDP.(float64); !ok {
|
||||||
|
return nil, fmt.Errorf("expected maxDataPoints to be an float64, got type %T for refId %v", rawMaxDP, rn.RefID)
|
||||||
|
}
|
||||||
|
dsNode.maxDP = int64(floatMaxDP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsNode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the node and adds the results to vars. If the node requires
|
||||||
|
// other nodes they must have already been executed and their results must
|
||||||
|
// already by in vars.
|
||||||
|
func (dn *DSNode) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
|
||||||
|
pc := backend.PluginContext{
|
||||||
|
OrgID: dn.orgID,
|
||||||
|
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||||
|
ID: dn.datasourceID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
q := []backend.DataQuery{
|
||||||
|
{
|
||||||
|
RefID: dn.refID,
|
||||||
|
MaxDataPoints: dn.maxDP,
|
||||||
|
Interval: time.Duration(int64(time.Millisecond) * dn.intervalMS),
|
||||||
|
JSON: dn.query,
|
||||||
|
TimeRange: dn.timeRange,
|
||||||
|
QueryType: dn.queryType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := QueryData(ctx, &backend.QueryDataRequest{
|
||||||
|
PluginContext: pc,
|
||||||
|
Queries: q,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return mathexp.Results{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := make([]mathexp.Value, 0)
|
||||||
|
for refID, qr := range resp.Responses {
|
||||||
|
if len(qr.Frames) == 1 {
|
||||||
|
frame := qr.Frames[0]
|
||||||
|
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeNot && isNumberTable(frame) {
|
||||||
|
backend.Logger.Debug("expression datasource query (numberSet)", "query", refID)
|
||||||
|
numberSet, err := extractNumberSet(frame)
|
||||||
|
if err != nil {
|
||||||
|
return mathexp.Results{}, err
|
||||||
|
}
|
||||||
|
for _, n := range numberSet {
|
||||||
|
vals = append(vals, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mathexp.Results{
|
||||||
|
Values: vals,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, frame := range qr.Frames {
|
||||||
|
backend.Logger.Debug("expression datasource query (seriesSet)", "query", refID)
|
||||||
|
series, err := WideToMany(frame)
|
||||||
|
if err != nil {
|
||||||
|
return mathexp.Results{}, err
|
||||||
|
}
|
||||||
|
for _, s := range series {
|
||||||
|
vals = append(vals, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mathexp.Results{
|
||||||
|
Values: vals,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumberTable(frame *data.Frame) bool {
|
||||||
|
if frame == nil || frame.Fields == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
numericCount := 0
|
||||||
|
stringCount := 0
|
||||||
|
otherCount := 0
|
||||||
|
for _, field := range frame.Fields {
|
||||||
|
fType := field.Type()
|
||||||
|
switch {
|
||||||
|
case fType.Numeric():
|
||||||
|
numericCount++
|
||||||
|
case fType == data.FieldTypeString || fType == data.FieldTypeNullableString:
|
||||||
|
stringCount++
|
||||||
|
default:
|
||||||
|
otherCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numericCount == 1 && otherCount == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNumberSet(frame *data.Frame) ([]mathexp.Number, error) {
|
||||||
|
numericField := 0
|
||||||
|
stringFieldIdxs := []int{}
|
||||||
|
stringFieldNames := []string{}
|
||||||
|
for i, field := range frame.Fields {
|
||||||
|
fType := field.Type()
|
||||||
|
switch {
|
||||||
|
case fType.Numeric():
|
||||||
|
numericField = i
|
||||||
|
case fType == data.FieldTypeString || fType == data.FieldTypeNullableString:
|
||||||
|
stringFieldIdxs = append(stringFieldIdxs, i)
|
||||||
|
stringFieldNames = append(stringFieldNames, field.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
numbers := make([]mathexp.Number, frame.Rows())
|
||||||
|
|
||||||
|
for rowIdx := 0; rowIdx < frame.Rows(); rowIdx++ {
|
||||||
|
val, _ := frame.FloatAt(numericField, rowIdx)
|
||||||
|
var labels data.Labels
|
||||||
|
for i := 0; i < len(stringFieldIdxs); i++ {
|
||||||
|
if i == 0 {
|
||||||
|
labels = make(data.Labels)
|
||||||
|
}
|
||||||
|
key := stringFieldNames[i] // TODO check for duplicate string column names
|
||||||
|
val, _ := frame.ConcreteAt(stringFieldIdxs[i], rowIdx)
|
||||||
|
labels[key] = val.(string) // TODO check assertion / return error
|
||||||
|
}
|
||||||
|
|
||||||
|
n := mathexp.NewNumber("", labels)
|
||||||
|
n.SetValue(&val)
|
||||||
|
numbers[rowIdx] = n
|
||||||
|
}
|
||||||
|
return numbers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WideToMany converts a data package wide type Frame to one or multiple Series. A series
|
||||||
|
// is created for each value type column of wide frame.
|
||||||
|
//
|
||||||
|
// This might not be a good idea long term, but works now as an adapter/shim.
|
||||||
|
func WideToMany(frame *data.Frame) ([]mathexp.Series, error) {
|
||||||
|
tsSchema := frame.TimeSeriesSchema()
|
||||||
|
if tsSchema.Type != data.TimeSeriesTypeWide {
|
||||||
|
return nil, fmt.Errorf("input data must be a wide series but got type %s (input refid)", tsSchema.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tsSchema.ValueIndices) == 1 {
|
||||||
|
s, err := mathexp.SeriesFromFrame(frame)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []mathexp.Series{s}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
series := []mathexp.Series{}
|
||||||
|
for _, valIdx := range tsSchema.ValueIndices {
|
||||||
|
l := frame.Rows()
|
||||||
|
f := data.NewFrameOfFieldTypes(frame.Name, l, frame.Fields[tsSchema.TimeIndex].Type(), frame.Fields[valIdx].Type())
|
||||||
|
f.Fields[0].Name = frame.Fields[tsSchema.TimeIndex].Name
|
||||||
|
f.Fields[1].Name = frame.Fields[valIdx].Name
|
||||||
|
if frame.Fields[valIdx].Labels != nil {
|
||||||
|
f.Fields[1].Labels = frame.Fields[valIdx].Labels.Copy()
|
||||||
|
}
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
f.SetRow(i, frame.Fields[tsSchema.TimeIndex].CopyAt(i), frame.Fields[valIdx].CopyAt(i))
|
||||||
|
}
|
||||||
|
s, err := mathexp.SeriesFromFrame(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
series = append(series, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return series, nil
|
||||||
|
}
|
39
pkg/expr/service.go
Normal file
39
pkg/expr/service.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatasourceName is the string constant used as the datasource name in requests
|
||||||
|
// to identify it as an expression command.
|
||||||
|
const DatasourceName = "__expr__"
|
||||||
|
|
||||||
|
// DatasourceID is the fake datasource id used in requests to identify it as an
|
||||||
|
// expression command.
|
||||||
|
const DatasourceID = -100
|
||||||
|
|
||||||
|
// Service is service representation for expression handling.
|
||||||
|
type Service struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPipeline builds a pipeline from a request.
|
||||||
|
func (s *Service) BuildPipeline(queries []backend.DataQuery) (DataPipeline, error) {
|
||||||
|
return buildPipeline(queries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePipeline executes an expression pipeline and returns all the results.
|
||||||
|
func (s *Service) ExecutePipeline(ctx context.Context, pipeline DataPipeline) (*backend.QueryDataResponse, error) {
|
||||||
|
res := backend.NewQueryDataResponse()
|
||||||
|
vars, err := pipeline.execute(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for refID, val := range vars {
|
||||||
|
res.Responses[refID] = backend.DataResponse{
|
||||||
|
Frames: val.Values.AsDataFrames(refID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
111
pkg/expr/service_test.go
Normal file
111
pkg/expr/service_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService(t *testing.T) {
|
||||||
|
dsDF := data.NewFrame("test",
|
||||||
|
data.NewField("time", nil, []*time.Time{utp(1)}),
|
||||||
|
data.NewField("value", nil, []*float64{fp(2)}))
|
||||||
|
|
||||||
|
registerEndPoint(dsDF)
|
||||||
|
|
||||||
|
s := Service{}
|
||||||
|
|
||||||
|
queries := []backend.DataQuery{
|
||||||
|
{
|
||||||
|
RefID: "A",
|
||||||
|
JSON: json.RawMessage(`{ "datasource": "test", "datasourceId": 1, "orgId": 1, "intervalMs": 1000, "maxDataPoints": 1000 }`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RefID: "B",
|
||||||
|
JSON: json.RawMessage(`{ "datasource": "__expr__", "datasourceId": -100, "type": "math", "expression": "$A * 2" }`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pl, err := s.BuildPipeline(queries)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := s.ExecutePipeline(context.Background(), pl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bDF := data.NewFrame("",
|
||||||
|
data.NewField("Time", nil, []*time.Time{utp(1)}),
|
||||||
|
data.NewField("", nil, []*float64{fp(4)}))
|
||||||
|
bDF.RefID = "B"
|
||||||
|
|
||||||
|
expect := &backend.QueryDataResponse{
|
||||||
|
Responses: backend.Responses{
|
||||||
|
"A": {
|
||||||
|
Frames: []*data.Frame{dsDF},
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
Frames: []*data.Frame{bDF},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service currently doesn't care about order of datas in the return.
|
||||||
|
trans := cmp.Transformer("Sort", func(in []*data.Frame) []*data.Frame {
|
||||||
|
out := append([]*data.Frame(nil), in...) // Copy input to avoid mutating it
|
||||||
|
sort.SliceStable(out, func(i, j int) bool {
|
||||||
|
return out[i].RefID > out[j].RefID
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
options := append([]cmp.Option{trans}, data.FrameTestCompareOptions()...)
|
||||||
|
if diff := cmp.Diff(expect, res, options...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func utp(sec int64) *time.Time {
|
||||||
|
t := time.Unix(sec, 0)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
func fp(f float64) *float64 {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockEndpoint struct {
|
||||||
|
Frames data.Frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *mockEndpoint) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||||
|
return &tsdb.Response{
|
||||||
|
Results: map[string]*tsdb.QueryResult{
|
||||||
|
"A": {
|
||||||
|
Dataframes: tsdb.NewDecodedDataFrames(me.Frames),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerEndPoint(df ...*data.Frame) {
|
||||||
|
me := &mockEndpoint{
|
||||||
|
Frames: df,
|
||||||
|
}
|
||||||
|
endpoint := func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||||
|
return me, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsdb.RegisterTsdbQueryEndpoint("test", endpoint)
|
||||||
|
bus.AddHandler("test", func(query *models.GetDataSourceByIdQuery) error {
|
||||||
|
query.Result = &models.DataSource{Id: 1, OrgId: 1, Type: "test"}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
211
pkg/expr/transform.go
Normal file
211
pkg/expr/transform.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||||
|
sdkReq := &backend.QueryDataRequest{
|
||||||
|
PluginContext: backend.PluginContext{
|
||||||
|
OrgID: query.User.OrgId,
|
||||||
|
},
|
||||||
|
Queries: []backend.DataQuery{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, q := range query.Queries {
|
||||||
|
modelJSON, err := q.Model.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sdkReq.Queries = append(sdkReq.Queries, backend.DataQuery{
|
||||||
|
JSON: modelJSON,
|
||||||
|
Interval: time.Duration(q.IntervalMs) * time.Millisecond,
|
||||||
|
RefID: q.RefId,
|
||||||
|
MaxDataPoints: q.MaxDataPoints,
|
||||||
|
QueryType: q.QueryType,
|
||||||
|
TimeRange: backend.TimeRange{
|
||||||
|
From: query.TimeRange.GetFromAsTimeUTC(),
|
||||||
|
To: query.TimeRange.GetToAsTimeUTC(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pbRes, err := TransformData(ctx, sdkReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tR := &tsdb.Response{
|
||||||
|
Results: make(map[string]*tsdb.QueryResult, len(pbRes.Responses)),
|
||||||
|
}
|
||||||
|
for refID, res := range pbRes.Responses {
|
||||||
|
tRes := &tsdb.QueryResult{
|
||||||
|
RefId: refID,
|
||||||
|
Dataframes: tsdb.NewDecodedDataFrames(res.Frames),
|
||||||
|
}
|
||||||
|
// if len(res.JsonMeta) != 0 {
|
||||||
|
// tRes.Meta = simplejson.NewFromAny(res.JsonMeta)
|
||||||
|
// }
|
||||||
|
if res.Error != nil {
|
||||||
|
tRes.Error = res.Error
|
||||||
|
tRes.ErrorString = res.Error.Error()
|
||||||
|
}
|
||||||
|
tR.Results[refID] = tRes
|
||||||
|
}
|
||||||
|
|
||||||
|
return tR, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformData takes Queries which are either expressions nodes
|
||||||
|
// or are datasource requests.
|
||||||
|
func TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
svc := Service{}
|
||||||
|
// Build the pipeline from the request, checking for ordering issues (e.g. loops)
|
||||||
|
// and parsing graph nodes from the queries.
|
||||||
|
pipeline, err := svc.BuildPipeline(req.Queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the pipeline
|
||||||
|
responses, err := svc.ExecutePipeline(ctx, pipeline)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.Unknown, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get which queries have the Hide property so they those queries' results
|
||||||
|
// can be excluded from the response.
|
||||||
|
hidden, err := hiddenRefIDs(req.Queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error((codes.Internal), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hidden) != 0 {
|
||||||
|
filteredRes := backend.NewQueryDataResponse()
|
||||||
|
for refID, res := range responses.Responses {
|
||||||
|
if _, ok := hidden[refID]; !ok {
|
||||||
|
filteredRes.Responses[refID] = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses = filteredRes
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hiddenRefIDs(queries []backend.DataQuery) (map[string]struct{}, error) {
|
||||||
|
hidden := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, query := range queries {
|
||||||
|
hide := struct {
|
||||||
|
Hide bool `json:"hide"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(query.JSON, &hide); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hide.Hide {
|
||||||
|
hidden[query.RefID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryData is called used to query datasources that are not expression commands, but are used
|
||||||
|
// alongside expressions and/or are the input of an expression command.
|
||||||
|
func QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
if len(req.Queries) == 0 {
|
||||||
|
return nil, fmt.Errorf("zero queries found in datasource request")
|
||||||
|
}
|
||||||
|
|
||||||
|
datasourceID := int64(0)
|
||||||
|
|
||||||
|
if req.PluginContext.DataSourceInstanceSettings != nil {
|
||||||
|
datasourceID = req.PluginContext.DataSourceInstanceSettings.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
getDsInfo := &models.GetDataSourceByIdQuery{
|
||||||
|
OrgId: req.PluginContext.OrgID,
|
||||||
|
Id: datasourceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(getDsInfo); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not find datasource: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert plugin-model (datasource) queries to tsdb queries
|
||||||
|
queries := make([]*tsdb.Query, len(req.Queries))
|
||||||
|
for i, query := range req.Queries {
|
||||||
|
sj, err := simplejson.NewJson(query.JSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
queries[i] = &tsdb.Query{
|
||||||
|
RefId: query.RefID,
|
||||||
|
IntervalMs: query.Interval.Microseconds(),
|
||||||
|
MaxDataPoints: query.MaxDataPoints,
|
||||||
|
QueryType: query.QueryType,
|
||||||
|
DataSource: getDsInfo.Result,
|
||||||
|
Model: sj,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now take Time Range from first query.
|
||||||
|
timeRange := tsdb.NewTimeRange(strconv.FormatInt(req.Queries[0].TimeRange.From.Unix()*1000, 10), strconv.FormatInt(req.Queries[0].TimeRange.To.Unix()*1000, 10))
|
||||||
|
|
||||||
|
tQ := &tsdb.TsdbQuery{
|
||||||
|
TimeRange: timeRange,
|
||||||
|
Queries: queries,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the converted queries
|
||||||
|
tsdbRes, err := tsdb.HandleRequest(ctx, getDsInfo.Result, tQ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Convert tsdb results (map) to plugin-model/datasource (slice) results.
|
||||||
|
// Only error, tsdb.Series, and encoded Dataframes responses are mapped.
|
||||||
|
responses := make(map[string]backend.DataResponse, len(tsdbRes.Results))
|
||||||
|
for refID, res := range tsdbRes.Results {
|
||||||
|
pRes := backend.DataResponse{}
|
||||||
|
if res.Error != nil {
|
||||||
|
pRes.Error = res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Dataframes != nil {
|
||||||
|
decoded, err := res.Dataframes.Decoded()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pRes.Frames = decoded
|
||||||
|
responses[refID] = pRes
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, series := range res.Series {
|
||||||
|
frame, err := tsdb.SeriesToFrame(series)
|
||||||
|
frame.RefID = refID
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pRes.Frames = append(pRes.Frames, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
responses[refID] = pRes
|
||||||
|
}
|
||||||
|
return &backend.QueryDataResponse{
|
||||||
|
Responses: responses,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -71,7 +71,6 @@ func getV2PluginSet() goplugin.PluginSet {
|
|||||||
"diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{},
|
"diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{},
|
||||||
"resource": &grpcplugin.ResourceGRPCPlugin{},
|
"resource": &grpcplugin.ResourceGRPCPlugin{},
|
||||||
"data": &grpcplugin.DataGRPCPlugin{},
|
"data": &grpcplugin.DataGRPCPlugin{},
|
||||||
"transform": &grpcplugin.TransformGRPCPlugin{},
|
|
||||||
"renderer": &pluginextensionv2.RendererGRPCPlugin{},
|
"renderer": &pluginextensionv2.RendererGRPCPlugin{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,6 +116,5 @@ type LegacyClient struct {
|
|||||||
// Client client for communicating with a plugin using the current (v2) plugin protocol.
|
// Client client for communicating with a plugin using the current (v2) plugin protocol.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
DataPlugin grpcplugin.DataClient
|
DataPlugin grpcplugin.DataClient
|
||||||
TransformPlugin grpcplugin.TransformClient
|
|
||||||
RendererPlugin pluginextensionv2.RendererPlugin
|
RendererPlugin pluginextensionv2.RendererPlugin
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ type clientV2 struct {
|
|||||||
grpcplugin.DiagnosticsClient
|
grpcplugin.DiagnosticsClient
|
||||||
grpcplugin.ResourceClient
|
grpcplugin.ResourceClient
|
||||||
grpcplugin.DataClient
|
grpcplugin.DataClient
|
||||||
grpcplugin.TransformClient
|
|
||||||
pluginextensionv2.RendererPlugin
|
pluginextensionv2.RendererPlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,11 +40,6 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rawTransform, err := rpcClient.Dispense("transform")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rawRenderer, err := rpcClient.Dispense("renderer")
|
rawRenderer, err := rpcClient.Dispense("renderer")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -70,12 +64,6 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rawTransform != nil {
|
|
||||||
if plugin, ok := rawTransform.(grpcplugin.TransformClient); ok {
|
|
||||||
c.TransformClient = instrumentTransformPlugin(plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rawRenderer != nil {
|
if rawRenderer != nil {
|
||||||
if plugin, ok := rawRenderer.(pluginextensionv2.RendererPlugin); ok {
|
if plugin, ok := rawRenderer.(pluginextensionv2.RendererPlugin); ok {
|
||||||
c.RendererPlugin = plugin
|
c.RendererPlugin = plugin
|
||||||
@ -85,7 +73,6 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
|
|||||||
if descriptor.startFns.OnStart != nil {
|
if descriptor.startFns.OnStart != nil {
|
||||||
client := &Client{
|
client := &Client{
|
||||||
DataPlugin: c.DataClient,
|
DataPlugin: c.DataClient,
|
||||||
TransformPlugin: c.TransformClient,
|
|
||||||
RendererPlugin: c.RendererPlugin,
|
RendererPlugin: c.RendererPlugin,
|
||||||
}
|
}
|
||||||
if err := descriptor.startFns.OnStart(descriptor.pluginID, client, logger); err != nil {
|
if err := descriptor.startFns.OnStart(descriptor.pluginID, client, logger); err != nil {
|
||||||
@ -189,24 +176,3 @@ func instrumentDataClient(plugin grpcplugin.DataClient) grpcplugin.DataClient {
|
|||||||
return resp, err
|
return resp, err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type transformPluginTransformDataFunc func(ctx context.Context, req *pluginv2.QueryDataRequest, callback grpcplugin.TransformDataCallBack) (*pluginv2.QueryDataResponse, error)
|
|
||||||
|
|
||||||
func (fn transformPluginTransformDataFunc) TransformData(ctx context.Context, req *pluginv2.QueryDataRequest, callback grpcplugin.TransformDataCallBack) (*pluginv2.QueryDataResponse, error) {
|
|
||||||
return fn(ctx, req, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
func instrumentTransformPlugin(plugin grpcplugin.TransformClient) grpcplugin.TransformClient {
|
|
||||||
if plugin == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformPluginTransformDataFunc(func(ctx context.Context, req *pluginv2.QueryDataRequest, callback grpcplugin.TransformDataCallBack) (*pluginv2.QueryDataResponse, error) {
|
|
||||||
var resp *pluginv2.QueryDataResponse
|
|
||||||
err := backendplugin.InstrumentTransformDataRequest(req.PluginContext.PluginId, func() (innerErr error) {
|
|
||||||
resp, innerErr = plugin.TransformData(ctx, req, callback)
|
|
||||||
return
|
|
||||||
})
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -65,11 +65,6 @@ func InstrumentQueryDataRequest(pluginID string, fn func() error) error {
|
|||||||
return instrumentPluginRequest(pluginID, "queryData", fn)
|
return instrumentPluginRequest(pluginID, "queryData", fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstrumentTransformDataRequest instruments success rate and latency of transform data request.
|
|
||||||
func InstrumentTransformDataRequest(pluginID string, fn func() error) error {
|
|
||||||
return instrumentPluginRequest(pluginID, "transformData", fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstrumentQueryDataHandler wraps a backend.QueryDataHandler with instrumentation of success rate and latency.
|
// InstrumentQueryDataHandler wraps a backend.QueryDataHandler with instrumentation of success rate and latency.
|
||||||
func InstrumentQueryDataHandler(handler backend.QueryDataHandler) backend.QueryDataHandler {
|
func InstrumentQueryDataHandler(handler backend.QueryDataHandler) backend.QueryDataHandler {
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
|
@ -31,7 +31,6 @@ var (
|
|||||||
Plugins map[string]*PluginBase
|
Plugins map[string]*PluginBase
|
||||||
PluginTypes map[string]interface{}
|
PluginTypes map[string]interface{}
|
||||||
Renderer *RendererPlugin
|
Renderer *RendererPlugin
|
||||||
Transform *TransformPlugin
|
|
||||||
|
|
||||||
GrafanaLatestVersion string
|
GrafanaLatestVersion string
|
||||||
GrafanaHasUpdate bool
|
GrafanaHasUpdate bool
|
||||||
@ -82,7 +81,6 @@ func (pm *PluginManager) Init() error {
|
|||||||
"datasource": DataSourcePlugin{},
|
"datasource": DataSourcePlugin{},
|
||||||
"app": AppPlugin{},
|
"app": AppPlugin{},
|
||||||
"renderer": RendererPlugin{},
|
"renderer": RendererPlugin{},
|
||||||
"transform": TransformPlugin{},
|
|
||||||
}
|
}
|
||||||
pluginScanningErrors = map[string]*PluginError{}
|
pluginScanningErrors = map[string]*PluginError{}
|
||||||
|
|
||||||
@ -357,16 +355,6 @@ func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
|
|||||||
return errors.New("did not find type or id properties in plugin.json")
|
return errors.New("did not find type or id properties in plugin.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The expressions feature toggle corresponds to transform plug-ins.
|
|
||||||
if pluginCommon.Type == "transform" {
|
|
||||||
isEnabled := s.cfg.IsExpressionsEnabled()
|
|
||||||
if !isEnabled {
|
|
||||||
s.log.Debug("Transform plugin is disabled since the expressions feature toggle is not enabled",
|
|
||||||
"pluginID", pluginCommon.Id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
|
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
|
||||||
pluginCommon.Signature = getPluginSignatureState(s.log, &pluginCommon)
|
pluginCommon.Signature = getPluginSignatureState(s.log, &pluginCommon)
|
||||||
|
|
||||||
@ -376,7 +364,7 @@ func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
|
func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
|
||||||
return pluginType == "renderer" || pluginType == "transform"
|
return pluginType == "renderer"
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateSignature validates a plugin's signature.
|
// validateSignature validates a plugin's signature.
|
||||||
|
@ -142,29 +142,6 @@ func TestPluginManager_Init(t *testing.T) {
|
|||||||
assert.Empty(t, fm.registeredPlugins)
|
assert.Empty(t, fm.registeredPlugins)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Transform plugins should be loaded when expressions feature is on", func(t *testing.T) {
|
|
||||||
origPluginsPath := setting.PluginsPath
|
|
||||||
t.Cleanup(func() {
|
|
||||||
setting.PluginsPath = origPluginsPath
|
|
||||||
})
|
|
||||||
setting.PluginsPath = "testdata/behind-feature-flag"
|
|
||||||
|
|
||||||
fm := &fakeBackendPluginManager{}
|
|
||||||
pm := &PluginManager{
|
|
||||||
Cfg: &setting.Cfg{
|
|
||||||
FeatureToggles: map[string]bool{
|
|
||||||
"expressions": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
BackendPluginManager: fm,
|
|
||||||
}
|
|
||||||
err := pm.Init()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Empty(t, pm.scanningErrors)
|
|
||||||
assert.Equal(t, []string{"gel"}, fm.registeredPlugins)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("With nested plugin duplicating parent", func(t *testing.T) {
|
t.Run("With nested plugin duplicating parent", func(t *testing.T) {
|
||||||
origPluginsPath := setting.PluginsPath
|
origPluginsPath := setting.PluginsPath
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
-----BEGIN PGP SIGNED MESSAGE-----
|
|
||||||
Hash: SHA512
|
|
||||||
|
|
||||||
{
|
|
||||||
"plugin": "gel",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"files": {
|
|
||||||
"plugin.json": "b9b3bb0dab3c4655a929a1e48a957466e3e2717992bdd29da27e5eed2fae090c"
|
|
||||||
},
|
|
||||||
"time": 1589274667427,
|
|
||||||
"keyId": "7e4d0c6a708866e7"
|
|
||||||
}
|
|
||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
Version: OpenPGP.js v4.10.1
|
|
||||||
Comment: https://openpgpjs.org
|
|
||||||
|
|
||||||
wqIEARMKAAYFAl66aCsACgkQfk0ManCIZufDMAIJAWoNVihI9ZSBpUpgXrzY
|
|
||||||
XXsI3OmHuVpzrv6M6bk5jYdzY4SyzZmdw4CB51TIDJW9SnUajlXxWLXGYY+w
|
|
||||||
B2rSYvuhAgkBlG9w5OV3jcyg/wfUrIcCO5XRHMydCg0hIOznClzuG0uWn3wm
|
|
||||||
d4RT/ap1ezislQ/91zvhsLgAIztZlm3EsNBv7sI=
|
|
||||||
=WPLw
|
|
||||||
-----END PGP SIGNATURE-----
|
|
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "transform",
|
|
||||||
"name": "GEL",
|
|
||||||
"id": "gel",
|
|
||||||
"backend": true,
|
|
||||||
"info": {
|
|
||||||
"description": "Test",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": {
|
|
||||||
"name": "Grafana Labs",
|
|
||||||
"url": "https://grafana.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,228 +0,0 @@
|
|||||||
package plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
sdkgrpcplugin "github.com/grafana/grafana-plugin-sdk-go/backend/grpcplugin"
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
|
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TransformPlugin struct {
|
|
||||||
PluginBase
|
|
||||||
|
|
||||||
Executable string `json:"executable,omitempty"`
|
|
||||||
|
|
||||||
*TransformWrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TransformPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) error {
|
|
||||||
if err := decoder.Decode(p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.registerPlugin(base); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ComposePluginStartCommand(p.Executable)
|
|
||||||
fullpath := filepath.Join(p.PluginDir, cmd)
|
|
||||||
factory := grpcplugin.NewBackendPlugin(p.Id, fullpath, grpcplugin.PluginStartFuncs{
|
|
||||||
OnStart: p.onPluginStart,
|
|
||||||
})
|
|
||||||
if err := backendPluginManager.Register(p.Id, factory); err != nil {
|
|
||||||
return errutil.Wrapf(err, "Failed to register backend plugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
Transform = p
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TransformPlugin) onPluginStart(pluginID string, client *grpcplugin.Client, logger log.Logger) error {
|
|
||||||
p.TransformWrapper = NewTransformWrapper(logger, client.TransformPlugin)
|
|
||||||
|
|
||||||
if client.DataPlugin != nil {
|
|
||||||
tsdb.RegisterTsdbQueryEndpoint(pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
|
||||||
return wrapper.NewDatasourcePluginWrapperV2(logger, p.Id, p.Type, client.DataPlugin), nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
// Wrapper Code
|
|
||||||
// ...
|
|
||||||
|
|
||||||
func NewTransformWrapper(log log.Logger, plugin sdkgrpcplugin.TransformClient) *TransformWrapper {
|
|
||||||
return &TransformWrapper{plugin, log, &transformCallback{log}}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransformWrapper struct {
|
|
||||||
sdkgrpcplugin.TransformClient
|
|
||||||
logger log.Logger
|
|
||||||
Callback *transformCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tw *TransformWrapper) Transform(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
|
||||||
pbQuery := &pluginv2.QueryDataRequest{
|
|
||||||
PluginContext: &pluginv2.PluginContext{
|
|
||||||
// TODO: Things probably
|
|
||||||
},
|
|
||||||
Queries: []*pluginv2.DataQuery{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, q := range query.Queries {
|
|
||||||
modelJSON, err := q.Model.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pbQuery.Queries = append(pbQuery.Queries, &pluginv2.DataQuery{
|
|
||||||
Json: modelJSON,
|
|
||||||
IntervalMS: q.IntervalMs,
|
|
||||||
RefId: q.RefId,
|
|
||||||
MaxDataPoints: q.MaxDataPoints,
|
|
||||||
QueryType: q.QueryType,
|
|
||||||
TimeRange: &pluginv2.TimeRange{
|
|
||||||
ToEpochMS: query.TimeRange.GetToAsMsEpoch(),
|
|
||||||
FromEpochMS: query.TimeRange.GetFromAsMsEpoch(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pbRes, err := tw.TransformClient.TransformData(ctx, pbQuery, tw.Callback)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tR := &tsdb.Response{
|
|
||||||
Results: make(map[string]*tsdb.QueryResult, len(pbRes.Responses)),
|
|
||||||
}
|
|
||||||
for refID, res := range pbRes.Responses {
|
|
||||||
tRes := &tsdb.QueryResult{
|
|
||||||
RefId: refID,
|
|
||||||
Dataframes: tsdb.NewEncodedDataFrames(res.Frames),
|
|
||||||
}
|
|
||||||
if len(res.JsonMeta) != 0 {
|
|
||||||
tRes.Meta = simplejson.NewFromAny(res.JsonMeta)
|
|
||||||
}
|
|
||||||
if res.Error != "" {
|
|
||||||
tRes.Error = fmt.Errorf(res.Error)
|
|
||||||
tRes.ErrorString = res.Error
|
|
||||||
}
|
|
||||||
tR.Results[refID] = tRes
|
|
||||||
}
|
|
||||||
|
|
||||||
return tR, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type transformCallback struct {
|
|
||||||
logger log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *transformCallback) QueryData(ctx context.Context, req *pluginv2.QueryDataRequest) (*pluginv2.QueryDataResponse, error) {
|
|
||||||
if len(req.Queries) == 0 {
|
|
||||||
return nil, fmt.Errorf("zero queries found in datasource request")
|
|
||||||
}
|
|
||||||
|
|
||||||
datasourceID := int64(0)
|
|
||||||
|
|
||||||
if req.PluginContext.DataSourceInstanceSettings != nil {
|
|
||||||
datasourceID = req.PluginContext.DataSourceInstanceSettings.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
getDsInfo := &models.GetDataSourceByIdQuery{
|
|
||||||
OrgId: req.PluginContext.OrgId,
|
|
||||||
Id: datasourceID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bus.Dispatch(getDsInfo); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not find datasource: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert plugin-model (datasource) queries to tsdb queries
|
|
||||||
queries := make([]*tsdb.Query, len(req.Queries))
|
|
||||||
for i, query := range req.Queries {
|
|
||||||
sj, err := simplejson.NewJson(query.Json)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
queries[i] = &tsdb.Query{
|
|
||||||
RefId: query.RefId,
|
|
||||||
IntervalMs: query.IntervalMS,
|
|
||||||
MaxDataPoints: query.MaxDataPoints,
|
|
||||||
QueryType: query.QueryType,
|
|
||||||
DataSource: getDsInfo.Result,
|
|
||||||
Model: sj,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now take Time Range from first query.
|
|
||||||
timeRange := tsdb.NewTimeRange(strconv.FormatInt(req.Queries[0].TimeRange.FromEpochMS, 10), strconv.FormatInt(req.Queries[0].TimeRange.ToEpochMS, 10))
|
|
||||||
|
|
||||||
tQ := &tsdb.TsdbQuery{
|
|
||||||
TimeRange: timeRange,
|
|
||||||
Queries: queries,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the converted queries
|
|
||||||
tsdbRes, err := tsdb.HandleRequest(ctx, getDsInfo.Result, tQ)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Convert tsdb results (map) to plugin-model/datasource (slice) results.
|
|
||||||
// Only error, tsdb.Series, and encoded Dataframes responses are mapped.
|
|
||||||
responses := make(map[string]*pluginv2.DataResponse, len(tsdbRes.Results))
|
|
||||||
for refID, res := range tsdbRes.Results {
|
|
||||||
pRes := &pluginv2.DataResponse{}
|
|
||||||
if res.Error != nil {
|
|
||||||
pRes.Error = res.Error.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Dataframes != nil {
|
|
||||||
encoded, err := res.Dataframes.Encoded()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pRes.Frames = encoded
|
|
||||||
responses[refID] = pRes
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, series := range res.Series {
|
|
||||||
frame, err := tsdb.SeriesToFrame(series)
|
|
||||||
frame.RefID = refID
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
encFrame, err := frame.MarshalArrow()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pRes.Frames = append(pRes.Frames, encFrame)
|
|
||||||
}
|
|
||||||
if res.Meta != nil {
|
|
||||||
b, err := res.Meta.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("failed to marshal json metadata", err)
|
|
||||||
}
|
|
||||||
pRes.JsonMeta = b
|
|
||||||
}
|
|
||||||
responses[refID] = pRes
|
|
||||||
}
|
|
||||||
return &pluginv2.QueryDataResponse{
|
|
||||||
Responses: responses,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -5,15 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultMaxDataPoints float64 = 100
|
const defaultMaxDataPoints float64 = 100
|
||||||
const defaultIntervalMS float64 = 1000
|
const defaultIntervalMS float64 = 1000
|
||||||
|
|
||||||
// DefaultExprDatasourceID is the datasource identifier for expressions.:w
|
|
||||||
const DefaultExprDatasourceID = -100
|
|
||||||
|
|
||||||
// Duration is a type used for marshalling durations.
|
// Duration is a type used for marshalling durations.
|
||||||
type Duration time.Duration
|
type Duration time.Duration
|
||||||
|
|
||||||
@ -51,10 +49,10 @@ func (rtr *RelativeTimeRange) isValid() bool {
|
|||||||
return rtr.From > rtr.To
|
return rtr.From > rtr.To
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rtr *RelativeTimeRange) toTimeRange(now time.Time) *pluginv2.TimeRange {
|
func (rtr *RelativeTimeRange) toTimeRange(now time.Time) backend.TimeRange {
|
||||||
return &pluginv2.TimeRange{
|
return backend.TimeRange{
|
||||||
FromEpochMS: now.Add(-time.Duration(rtr.From)).UnixNano() / 1e6,
|
From: now.Add(-time.Duration(rtr.From)),
|
||||||
ToEpochMS: now.Add(-time.Duration(rtr.To)).UnixNano() / 1e6,
|
To: now.Add(-time.Duration(rtr.To)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,9 +101,9 @@ func (aq *AlertQuery) setDatasource() error {
|
|||||||
return fmt.Errorf("failed to get datasource from query model")
|
return fmt.Errorf("failed to get datasource from query model")
|
||||||
}
|
}
|
||||||
|
|
||||||
if dsName == "__expr__" {
|
if dsName == expr.DatasourceName {
|
||||||
aq.DatasourceID = DefaultExprDatasourceID
|
aq.DatasourceID = expr.DatasourceID
|
||||||
aq.modelProps["datasourceId"] = DefaultExprDatasourceID
|
aq.modelProps["datasourceId"] = expr.DatasourceID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +125,7 @@ func (aq *AlertQuery) IsExpression() (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return aq.DatasourceID == DefaultExprDatasourceID, nil
|
return aq.DatasourceID == expr.DatasourceID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMaxDatapoints sets the model maxDataPoints if it's missing or invalid
|
// setMaxDatapoints sets the model maxDataPoints if it's missing or invalid
|
||||||
@ -194,6 +192,19 @@ func (aq *AlertQuery) getIntervalMS() (int64, error) {
|
|||||||
return int64(intervalMs), nil
|
return int64(intervalMs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aq *AlertQuery) getIntervalDuration() (time.Duration, error) {
|
||||||
|
err := aq.setIntervalMS()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalMs, ok := aq.modelProps["intervalMs"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("failed to cast intervalMs to float64: %v", aq.modelProps["intervalMs"])
|
||||||
|
}
|
||||||
|
return time.Duration(intervalMs) * time.Millisecond, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetDatasource returns the query datasource identifier.
|
// GetDatasource returns the query datasource identifier.
|
||||||
func (aq *AlertQuery) GetDatasource() (int64, error) {
|
func (aq *AlertQuery) GetDatasource() (int64, error) {
|
||||||
err := aq.setDatasource()
|
err := aq.setDatasource()
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -32,8 +33,8 @@ func TestAlertQuery(t *testing.T) {
|
|||||||
}`),
|
}`),
|
||||||
},
|
},
|
||||||
expectedIsExpression: true,
|
expectedIsExpression: true,
|
||||||
expectedDatasource: "__expr__",
|
expectedDatasource: expr.DatasourceName,
|
||||||
expectedDatasourceID: int64(DefaultExprDatasourceID),
|
expectedDatasourceID: int64(expr.DatasourceID),
|
||||||
expectedMaxPoints: int64(defaultMaxDataPoints),
|
expectedMaxPoints: int64(defaultMaxDataPoints),
|
||||||
expectedIntervalMS: int64(defaultIntervalMS),
|
expectedIntervalMS: int64(defaultIntervalMS),
|
||||||
},
|
},
|
||||||
|
@ -7,11 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results.
|
// invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results.
|
||||||
@ -99,11 +98,11 @@ func (c *Condition) Execute(ctx AlertExecCtx, fromStr, toStr string) (*Execution
|
|||||||
return nil, fmt.Errorf("invalid conditions")
|
return nil, fmt.Errorf("invalid conditions")
|
||||||
}
|
}
|
||||||
|
|
||||||
pbQuery := &pluginv2.QueryDataRequest{
|
queryDataReq := &backend.QueryDataRequest{
|
||||||
PluginContext: &pluginv2.PluginContext{
|
PluginContext: backend.PluginContext{
|
||||||
// TODO: Things probably
|
// TODO: Things probably
|
||||||
},
|
},
|
||||||
Queries: []*pluginv2.DataQuery{},
|
Queries: []backend.DataQuery{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range c.QueriesAndExpressions {
|
for i := range c.QueriesAndExpressions {
|
||||||
@ -112,7 +111,7 @@ func (c *Condition) Execute(ctx AlertExecCtx, fromStr, toStr string) (*Execution
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get query model: %w", err)
|
return nil, fmt.Errorf("failed to get query model: %w", err)
|
||||||
}
|
}
|
||||||
intervalMS, err := q.getIntervalMS()
|
interval, err := q.getIntervalDuration()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err)
|
return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err)
|
||||||
}
|
}
|
||||||
@ -122,18 +121,17 @@ func (c *Condition) Execute(ctx AlertExecCtx, fromStr, toStr string) (*Execution
|
|||||||
return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err)
|
return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pbQuery.Queries = append(pbQuery.Queries, &pluginv2.DataQuery{
|
queryDataReq.Queries = append(queryDataReq.Queries, backend.DataQuery{
|
||||||
Json: model,
|
JSON: model,
|
||||||
IntervalMS: intervalMS,
|
Interval: interval,
|
||||||
RefId: q.RefID,
|
RefID: q.RefID,
|
||||||
MaxDataPoints: maxDatapoints,
|
MaxDataPoints: maxDatapoints,
|
||||||
QueryType: q.QueryType,
|
QueryType: q.QueryType,
|
||||||
TimeRange: q.RelativeTimeRange.toTimeRange(time.Now()),
|
TimeRange: q.RelativeTimeRange.toTimeRange(time.Now()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tw := plugins.Transform
|
pbRes, err := expr.TransformData(ctx.Ctx, queryDataReq)
|
||||||
pbRes, err := tw.TransformClient.TransformData(ctx.Ctx, pbQuery, tw.Callback)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
@ -142,12 +140,7 @@ func (c *Condition) Execute(ctx AlertExecCtx, fromStr, toStr string) (*Execution
|
|||||||
if refID != c.RefID {
|
if refID != c.RefID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
df := tsdb.NewEncodedDataFrames(res.Frames)
|
result.Results = res.Frames
|
||||||
result.Results, err = df.Decoded()
|
|
||||||
if err != nil {
|
|
||||||
result.Error = err
|
|
||||||
return &result, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Results) == 0 {
|
if len(result.Results) == 0 {
|
||||||
|
@ -3,8 +3,8 @@ package ngalert
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// validateAlertDefinition validates that the alert definition contains at least one alert query
|
// validateAlertDefinition validates that the alert definition contains at least one alert query
|
||||||
@ -20,7 +20,7 @@ func (ng *AlertNG) validateAlertDefinition(alertDefinition *AlertDefinition, sig
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if datasourceID == eval.DefaultExprDatasourceID {
|
if datasourceID == expr.DatasourceID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,8 +55,8 @@ var engineCache = engineCacheType{
|
|||||||
|
|
||||||
var sqlIntervalCalculator = tsdb.NewIntervalCalculator(nil)
|
var sqlIntervalCalculator = tsdb.NewIntervalCalculator(nil)
|
||||||
|
|
||||||
//nolint:gocritic
|
|
||||||
// NewXormEngine is an xorm.Engine factory, that can be stubbed by tests.
|
// NewXormEngine is an xorm.Engine factory, that can be stubbed by tests.
|
||||||
|
//nolint:gocritic
|
||||||
var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) {
|
var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) {
|
||||||
return xorm.NewEngine(driverName, connectionString)
|
return xorm.NewEngine(driverName, connectionString)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": []
|
||||||
{
|
|
||||||
"name": "gel",
|
|
||||||
"version": "0.6.0",
|
|
||||||
"checksum": "eeab73565b0f167d3d576dc6da86d53419bff5bbb2ea927fae1cc2fe4b52f55f"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ export class ExpressionQueryEditor extends PureComponent<Props, State> {
|
|||||||
const { query, onChange } = this.props;
|
const { query, onChange } = this.props;
|
||||||
onChange({
|
onChange({
|
||||||
...query,
|
...query,
|
||||||
rule: item.value!,
|
window: item.value!,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,11 +109,11 @@ export class ExpressionQueryEditor extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onRuleChange = (evt: ChangeEvent<any>) => {
|
onWindowChange = (evt: ChangeEvent<any>) => {
|
||||||
const { query, onChange } = this.props;
|
const { query, onChange } = this.props;
|
||||||
onChange({
|
onChange({
|
||||||
...query,
|
...query,
|
||||||
rule: evt.target.value,
|
window: evt.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ export class ExpressionQueryEditor extends PureComponent<Props, State> {
|
|||||||
<Input onChange={this.onExpressionChange} value={query.expression} width={25} />
|
<Input onChange={this.onExpressionChange} value={query.expression} width={25} />
|
||||||
</InlineField>
|
</InlineField>
|
||||||
<InlineField label="Window">
|
<InlineField label="Window">
|
||||||
<Input onChange={this.onRuleChange} value={query.rule} width={25} />
|
<Input onChange={this.onWindowChange} value={query.window} width={25} />
|
||||||
</InlineField>
|
</InlineField>
|
||||||
<InlineField label="Downsample">
|
<InlineField label="Downsample">
|
||||||
<Select options={downsamplingTypes} value={downsampler} onChange={this.onSelectDownsampler} width={25} />
|
<Select options={downsamplingTypes} value={downsampler} onChange={this.onSelectDownsampler} width={25} />
|
||||||
|
@ -14,7 +14,7 @@ export interface ExpressionQuery extends DataQuery {
|
|||||||
type: GELQueryType;
|
type: GELQueryType;
|
||||||
reducer?: string;
|
reducer?: string;
|
||||||
expression?: string;
|
expression?: string;
|
||||||
rule?: string;
|
window?: string;
|
||||||
downsampler?: string;
|
downsampler?: string;
|
||||||
upsampler?: string;
|
upsampler?: string;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user