actually fix expr-debugger to work with the new version (#2124)

This commit is contained in:
blotus 2023-03-16 15:20:48 +01:00 committed by GitHub
parent 94c7efdb5b
commit c77fe16943
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 33 deletions

View file

@ -6,6 +6,7 @@ import (
"strings"
"github.com/antonmedv/expr/parser"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
@ -19,27 +20,61 @@ Visitor is used to reconstruct variables with its property called in an expr fil
Thus, we can debug expr filter by displaying all variables contents present in the filter
*/
type visitor struct {
newVar bool
currentID string
properties []string
vars []string
newVar bool
currentId string
vars map[string][]string
logger *log.Entry
}
func (v *visitor) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.IdentifierNode); ok {
if !v.newVar {
v.newVar = true
v.currentID = n.Value
} else {
fullVar := fmt.Sprintf("%s.%s", v.currentID, strings.Join(v.properties, "."))
v.vars = append(v.vars, fullVar)
v.properties = []string{}
v.currentID = n.Value
}
} else if n2, ok := (*node).(*ast.MemberNode); ok {
if ns, ok := (n2.Property).(*ast.StringNode); ok {
v.properties = append(v.properties, ns.Value)
switch n := (*node).(type) {
case *ast.IdentifierNode:
v.newVar = true
uid, _ := uuid.NewUUID()
v.currentId = uid.String()
v.vars[v.currentId] = []string{n.Value}
case *ast.MemberNode:
if n2, ok := n.Property.(*ast.StringNode); ok {
v.vars[v.currentId] = append(v.vars[v.currentId], n2.Value)
}
case *ast.StringNode: //Don't reset here, as any attribute of a member node is a string node (in evt.X, evt is member node, X is string node)
default:
v.newVar = false
v.currentId = ""
/*case *ast.IntegerNode:
v.logger.Infof("integer node found: %+v", n)
case *ast.FloatNode:
v.logger.Infof("float node found: %+v", n)
case *ast.BoolNode:
v.logger.Infof("boolean node found: %+v", n)
case *ast.ArrayNode:
v.logger.Infof("array node found: %+v", n)
case *ast.ConstantNode:
v.logger.Infof("constant node found: %+v", n)
case *ast.UnaryNode:
v.logger.Infof("unary node found: %+v", n)
case *ast.BinaryNode:
v.logger.Infof("binary node found: %+v", n)
case *ast.CallNode:
v.logger.Infof("call node found: %+v", n)
case *ast.BuiltinNode:
v.logger.Infof("builtin node found: %+v", n)
case *ast.ConditionalNode:
v.logger.Infof("conditional node found: %+v", n)
case *ast.ChainNode:
v.logger.Infof("chain node found: %+v", n)
case *ast.PairNode:
v.logger.Infof("pair node found: %+v", n)
case *ast.MapNode:
v.logger.Infof("map node found: %+v", n)
case *ast.SliceNode:
v.logger.Infof("slice node found: %+v", n)
case *ast.ClosureNode:
v.logger.Infof("closure node found: %+v", n)
case *ast.PointerNode:
v.logger.Infof("pointer node found: %+v", n)
default:
v.logger.Infof("unknown node found: %+v | type: %T", n, n)*/
}
}
@ -52,34 +87,30 @@ func (v *visitor) Build(filter string, exprEnv expr.Option) (*ExprDebugger, erro
filter: filter,
}
if filter == "" {
log.Debugf("unable to create expr debugger with empty filter")
v.logger.Debugf("unable to create expr debugger with empty filter")
return &ExprDebugger{}, nil
}
v.newVar = false
v.vars = make(map[string][]string)
tree, err := parser.Parse(filter)
if err != nil {
return nil, err
}
ast.Walk(&tree.Node, v)
if v.currentID != "" && len(v.properties) > 0 { // if its a variable with property (eg. evt.Line.Labels)
fullVar := fmt.Sprintf("%s.%s", v.currentID, strings.Join(v.properties, "."))
v.vars = append(v.vars, fullVar)
} else if v.currentID != "" && len(v.properties) == 0 { // if it's a variable without property
fullVar := v.currentID
v.vars = append(v.vars, fullVar)
} else {
log.Debugf("no variable in filter : '%s'", filter)
}
v.properties = []string{}
v.currentID = ""
log.Debugf("vars: %+v", v.vars)
for _, variable := range v.vars {
debugFilter, err := expr.Compile(variable, exprEnv)
if variable[0] != "evt" {
continue
}
toBuild := strings.Join(variable, ".")
v.logger.Debugf("compiling expression '%s'", toBuild)
debugFilter, err := expr.Compile(toBuild, exprEnv)
if err != nil {
return ret, fmt.Errorf("compilation of variable '%s' failed: %v", variable, err)
return ret, fmt.Errorf("compilation of variable '%s' failed: %v", toBuild, err)
}
tmpExpression := &expression{
variable,
toBuild,
debugFilter,
}
expressions = append(expressions, tmpExpression)
@ -123,7 +154,8 @@ func (e *ExprDebugger) Run(logger *logrus.Entry, filterResult bool, exprEnv map[
// NewDebugger is the exported function that build the debuggers expressions
func NewDebugger(filter string, exprEnv expr.Option) (*ExprDebugger, error) {
visitor := &visitor{}
logger := log.WithField("component", "expr-debugger")
visitor := &visitor{logger: logger}
exprDebugger, err := visitor.Build(filter, exprEnv)
return exprDebugger, err
}

View file

@ -0,0 +1,100 @@
package exprhelpers
import (
"sort"
"testing"
"github.com/antonmedv/expr"
log "github.com/sirupsen/logrus"
)
func TestVisitorBuild(t *testing.T) {
tests := []struct {
name string
expr string
want []string
env map[string]interface{}
}{
{
name: "simple",
expr: "evt.X",
want: []string{"evt.X"},
env: map[string]interface{}{
"evt": map[string]interface{}{
"X": 1,
},
},
},
{
name: "two vars",
expr: "evt.X && evt.Y",
want: []string{"evt.X", "evt.Y"},
env: map[string]interface{}{
"evt": map[string]interface{}{
"X": 1,
"Y": 2,
},
},
},
{
name: "in",
expr: "evt.X in [1,2,3]",
want: []string{"evt.X"},
env: map[string]interface{}{
"evt": map[string]interface{}{
"X": 1,
},
},
},
{
name: "in complex",
expr: "evt.X in [1,2,3] && evt.Y in [1,2,3] || evt.Z in [1,2,3]",
want: []string{"evt.X", "evt.Y", "evt.Z"},
env: map[string]interface{}{
"evt": map[string]interface{}{
"X": 1,
"Y": 2,
"Z": 3,
},
},
},
{
name: "function call",
expr: "Foo(evt.X, 'ads')",
want: []string{"evt.X"},
env: map[string]interface{}{
"evt": map[string]interface{}{
"X": 1,
},
"Foo": func(x int, y string) int {
return x
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := &visitor{logger: log.NewEntry(log.New())}
ret, err := v.Build(tt.expr, expr.Env(tt.env))
if err != nil {
t.Errorf("visitor.Build() error = %v", err)
return
}
if len(ret.expression) != len(tt.want) {
t.Errorf("visitor.Build() = %v, want %v", ret.expression, tt.want)
}
//Sort both slices as the order is not guaranteed ??
sort.Slice(tt.want, func(i, j int) bool {
return tt.want[i] < tt.want[j]
})
sort.Slice(ret.expression, func(i, j int) bool {
return ret.expression[i].Str < ret.expression[j].Str
})
for idx, v := range ret.expression {
if v.Str != tt.want[idx] {
t.Errorf("visitor.Build() = %v, want %v", v.Str, tt.want[idx])
}
}
})
}
}