Add search expression support to admin interface

This commit is contained in:
David Stotijn
2020-12-21 12:50:09 +01:00
parent 8ab65fb55f
commit 194d727f4f
11 changed files with 469 additions and 19 deletions

108
pkg/db/sqlite/search.go Normal file
View File

@ -0,0 +1,108 @@
package sqlite
import (
"errors"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/dstotijn/hetty/pkg/search"
)
var stringLiteralMap = map[string]string{
// http_requests
"req.id": "req.id",
"req.proto": "req.proto",
"req.url": "req.url",
"req.method": "req.method",
"req.body": "req.body",
"req.timestamp": "req.timestamp",
// http_responses
"res.id": "res.id",
"res.proto": "res.proto",
"res.statusCode": "res.status_code",
"res.statusReason": "res.status_reason",
"res.body": "res.body",
"res.timestamp": "res.timestamp",
// TODO: http_headers
}
func parseSearchExpr(expr search.Expression) (sq.Sqlizer, error) {
switch e := expr.(type) {
case *search.PrefixExpression:
return parsePrefixExpr(e)
case *search.InfixExpression:
return parseInfixExpr(e)
default:
return nil, fmt.Errorf("expression type (%v) not supported", expr)
}
}
func parsePrefixExpr(expr *search.PrefixExpression) (sq.Sqlizer, error) {
switch expr.Operator {
case search.TokOpNot:
// TODO: Find a way to prefix an `sq.Sqlizer` with "NOT".
return nil, errors.New("not implemented")
default:
return nil, errors.New("operator is not supported")
}
}
func parseInfixExpr(expr *search.InfixExpression) (sq.Sqlizer, error) {
switch expr.Operator {
case search.TokOpAnd:
left, err := parseSearchExpr(expr.Left)
if err != nil {
return nil, err
}
right, err := parseSearchExpr(expr.Right)
if err != nil {
return nil, err
}
return sq.And{left, right}, nil
case search.TokOpOr:
left, err := parseSearchExpr(expr.Left)
if err != nil {
return nil, err
}
right, err := parseSearchExpr(expr.Right)
if err != nil {
return nil, err
}
return sq.Or{left, right}, nil
}
left, ok := expr.Left.(*search.StringLiteral)
if !ok {
return nil, errors.New("left operand must be a string literal")
}
right, ok := expr.Right.(*search.StringLiteral)
if !ok {
return nil, errors.New("right operand must be a string literal")
}
mappedLeft, ok := stringLiteralMap[left.Value]
if !ok {
return nil, fmt.Errorf("invalid string literal: %v", left)
}
switch expr.Operator {
case search.TokOpEq:
return sq.Eq{mappedLeft: right.Value}, nil
case search.TokOpNotEq:
return sq.NotEq{mappedLeft: right.Value}, nil
case search.TokOpGt:
return sq.Gt{mappedLeft: right.Value}, nil
case search.TokOpLt:
return sq.Lt{mappedLeft: right.Value}, nil
case search.TokOpGtEq:
return sq.GtOrEq{mappedLeft: right.Value}, nil
case search.TokOpLtEq:
return sq.LtOrEq{mappedLeft: right.Value}, nil
case search.TokOpRe:
return sq.Expr(fmt.Sprintf("regexp(?, %v)", mappedLeft), right.Value), nil
case search.TokOpNotRe:
return sq.Expr(fmt.Sprintf("NOT regexp(?, %v)", mappedLeft), right.Value), nil
default:
return nil, errors.New("unsupported operator")
}
}

View File

@ -0,0 +1,197 @@
package sqlite
import (
"reflect"
"testing"
sq "github.com/Masterminds/squirrel"
"github.com/dstotijn/hetty/pkg/search"
)
func TestParseSearchExpr(t *testing.T) {
tests := []struct {
name string
searchExpr search.Expression
expectedSqlizer sq.Sqlizer
expectedError error
}{
{
name: "req.body = bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.Eq{"req.body": "bar"},
expectedError: nil,
},
{
name: "req.body != bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpNotEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.NotEq{"req.body": "bar"},
expectedError: nil,
},
{
name: "req.body > bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpGt,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.Gt{"req.body": "bar"},
expectedError: nil,
},
{
name: "req.body < bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpLt,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.Lt{"req.body": "bar"},
expectedError: nil,
},
{
name: "req.body >= bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpGtEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.GtOrEq{"req.body": "bar"},
expectedError: nil,
},
{
name: "req.body <= bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpLtEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.LtOrEq{"req.body": "bar"},
expectedError: nil,
},
{
name: "req.body =~ bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpRe,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.Expr("regexp(?, req.body)", "bar"),
expectedError: nil,
},
{
name: "req.body !~ bar",
searchExpr: &search.InfixExpression{
Operator: search.TokOpNotRe,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
expectedSqlizer: sq.Expr("NOT regexp(?, req.body)", "bar"),
expectedError: nil,
},
{
name: "req.body = bar AND res.body = yolo",
searchExpr: &search.InfixExpression{
Operator: search.TokOpAnd,
Left: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
Right: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "res.body"},
Right: &search.StringLiteral{Value: "yolo"},
},
},
expectedSqlizer: sq.And{
sq.Eq{"req.body": "bar"},
sq.Eq{"res.body": "yolo"},
},
expectedError: nil,
},
{
name: "req.body = bar AND res.body = yolo AND req.method = POST",
searchExpr: &search.InfixExpression{
Operator: search.TokOpAnd,
Left: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
Right: &search.InfixExpression{
Operator: search.TokOpAnd,
Left: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "res.body"},
Right: &search.StringLiteral{Value: "yolo"},
},
Right: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "req.method"},
Right: &search.StringLiteral{Value: "POST"},
},
},
},
expectedSqlizer: sq.And{
sq.Eq{"req.body": "bar"},
sq.And{
sq.Eq{"res.body": "yolo"},
sq.Eq{"req.method": "POST"},
},
},
expectedError: nil,
},
{
name: "req.body = bar OR res.body = yolo",
searchExpr: &search.InfixExpression{
Operator: search.TokOpOr,
Left: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "req.body"},
Right: &search.StringLiteral{Value: "bar"},
},
Right: &search.InfixExpression{
Operator: search.TokOpEq,
Left: &search.StringLiteral{Value: "res.body"},
Right: &search.StringLiteral{Value: "yolo"},
},
},
expectedSqlizer: sq.Or{
sq.Eq{"req.body": "bar"},
sq.Eq{"res.body": "yolo"},
},
expectedError: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := parseSearchExpr(tt.searchExpr)
assertError(t, tt.expectedError, err)
if !reflect.DeepEqual(tt.expectedSqlizer, got) {
t.Errorf("expected: %#v, got: %#v", tt.expectedSqlizer, got)
}
})
}
}
func assertError(t *testing.T, exp, got error) {
switch {
case exp == nil && got != nil:
t.Fatalf("expected: nil, got: %v", got)
case exp != nil && got == nil:
t.Fatalf("expected: %v, got: nil", exp.Error())
case exp != nil && got != nil && exp.Error() != got.Error():
t.Fatalf("expected: %v, got: %v", exp.Error(), got.Error())
}
}

View File

@ -30,7 +30,7 @@ var regexpFn = func(pattern string, value interface{}) (bool, error) {
case string:
return regexp.MatchString(pattern, v)
case int64:
return regexp.MatchString(pattern, string(v))
return regexp.MatchString(pattern, fmt.Sprintf("%v", v))
case []byte:
return regexp.Match(pattern, v)
default:
@ -257,6 +257,14 @@ func (c *Client) FindRequestLogs(
}
}
if filter.SearchExpr != nil {
sqlizer, err := parseSearchExpr(filter.SearchExpr)
if err != nil {
return nil, fmt.Errorf("sqlite: could not parse search expression: %v", err)
}
reqQuery = reqQuery.Where(sqlizer)
}
sql, args, err := reqQuery.ToSql()
if err != nil {
return nil, fmt.Errorf("sqlite: could not parse query: %v", err)