mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add search expression support to admin interface
This commit is contained in:
108
pkg/db/sqlite/search.go
Normal file
108
pkg/db/sqlite/search.go
Normal 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")
|
||||
}
|
||||
}
|
197
pkg/db/sqlite/search_test.go
Normal file
197
pkg/db/sqlite/search_test.go
Normal 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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Reference in New Issue
Block a user