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:
@ -31,6 +31,7 @@ const FILTER = gql`
|
|||||||
query HttpRequestLogFilter {
|
query HttpRequestLogFilter {
|
||||||
httpRequestLogFilter {
|
httpRequestLogFilter {
|
||||||
onlyInScope
|
onlyInScope
|
||||||
|
searchExpression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -39,6 +40,7 @@ const SET_FILTER = gql`
|
|||||||
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||||
setHttpRequestLogFilter(filter: $filter) {
|
setHttpRequestLogFilter(filter: $filter) {
|
||||||
onlyInScope
|
onlyInScope
|
||||||
|
searchExpression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -75,14 +77,21 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
|
|
||||||
export interface SearchFilter {
|
export interface SearchFilter {
|
||||||
onlyInScope: boolean;
|
onlyInScope: boolean;
|
||||||
|
searchExpression: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Search(): JSX.Element {
|
function Search(): JSX.Element {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [searchExpr, setSearchExpr] = useState("");
|
||||||
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
|
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
|
||||||
FILTER
|
FILTER,
|
||||||
|
{
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSearchExpr(data.httpRequestLogFilter.searchExpression || "");
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@ -111,6 +120,15 @@ function Search(): JSX.Element {
|
|||||||
const [filterOpen, setFilterOpen] = useState(false);
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||||
|
setFilterMutate({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
...withoutTypename(filter?.httpRequestLogFilter),
|
||||||
|
searchExpression: searchExpr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setFilterOpen(false);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -142,8 +160,7 @@ function Search(): JSX.Element {
|
|||||||
className={classes.iconButton}
|
className={classes.iconButton}
|
||||||
onClick={() => setFilterOpen(!filterOpen)}
|
onClick={() => setFilterOpen(!filterOpen)}
|
||||||
style={{
|
style={{
|
||||||
color:
|
color: filter?.httpRequestLogFilter?.onlyInScope
|
||||||
filter?.httpRequestLogFilter !== null
|
|
||||||
? theme.palette.secondary.main
|
? theme.palette.secondary.main
|
||||||
: "inherit",
|
: "inherit",
|
||||||
}}
|
}}
|
||||||
@ -161,6 +178,8 @@ function Search(): JSX.Element {
|
|||||||
<InputBase
|
<InputBase
|
||||||
className={classes.input}
|
className={classes.input}
|
||||||
placeholder="Search proxy logs…"
|
placeholder="Search proxy logs…"
|
||||||
|
value={searchExpr}
|
||||||
|
onChange={(e) => setSearchExpr(e.target.value)}
|
||||||
onFocus={() => setFilterOpen(true)}
|
onFocus={() => setFilterOpen(true)}
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Search">
|
<Tooltip title="Search">
|
||||||
|
@ -73,6 +73,7 @@ type ComplexityRoot struct {
|
|||||||
|
|
||||||
HTTPRequestLogFilter struct {
|
HTTPRequestLogFilter struct {
|
||||||
OnlyInScope func(childComplexity int) int
|
OnlyInScope func(childComplexity int) int
|
||||||
|
SearchExpression func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPResponseLog struct {
|
HTTPResponseLog struct {
|
||||||
@ -249,6 +250,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.HTTPRequestLogFilter.OnlyInScope(childComplexity), true
|
return e.complexity.HTTPRequestLogFilter.OnlyInScope(childComplexity), true
|
||||||
|
|
||||||
|
case "HttpRequestLogFilter.searchExpression":
|
||||||
|
if e.complexity.HTTPRequestLogFilter.SearchExpression == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.HTTPRequestLogFilter.SearchExpression(childComplexity), true
|
||||||
|
|
||||||
case "HttpResponseLog.body":
|
case "HttpResponseLog.body":
|
||||||
if e.complexity.HTTPResponseLog.Body == nil {
|
if e.complexity.HTTPResponseLog.Body == nil {
|
||||||
break
|
break
|
||||||
@ -579,10 +587,12 @@ type ClearHTTPRequestLogResult {
|
|||||||
|
|
||||||
input HttpRequestLogFilterInput {
|
input HttpRequestLogFilterInput {
|
||||||
onlyInScope: Boolean
|
onlyInScope: Boolean
|
||||||
|
searchExpression: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpRequestLogFilter {
|
type HttpRequestLogFilter {
|
||||||
onlyInScope: Boolean!
|
onlyInScope: Boolean!
|
||||||
|
searchExpression: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
@ -1239,6 +1249,38 @@ func (ec *executionContext) _HttpRequestLogFilter_onlyInScope(ctx context.Contex
|
|||||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _HttpRequestLogFilter_searchExpression(ctx context.Context, field graphql.CollectedField, obj *HTTPRequestLogFilter) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "HttpRequestLogFilter",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.SearchExpression, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*string)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _HttpResponseLog_requestId(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) {
|
func (ec *executionContext) _HttpResponseLog_requestId(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -3288,6 +3330,14 @@ func (ec *executionContext) unmarshalInputHttpRequestLogFilterInput(ctx context.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
|
case "searchExpression":
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("searchExpression"))
|
||||||
|
it.SearchExpression, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3551,6 +3601,8 @@ func (ec *executionContext) _HttpRequestLogFilter(ctx context.Context, sel ast.S
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
|
case "searchExpression":
|
||||||
|
out.Values[i] = ec._HttpRequestLogFilter_searchExpression(ctx, field, obj)
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,12 @@ type HTTPRequestLog struct {
|
|||||||
|
|
||||||
type HTTPRequestLogFilter struct {
|
type HTTPRequestLogFilter struct {
|
||||||
OnlyInScope bool `json:"onlyInScope"`
|
OnlyInScope bool `json:"onlyInScope"`
|
||||||
|
SearchExpression *string `json:"searchExpression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPRequestLogFilterInput struct {
|
type HTTPRequestLogFilterInput struct {
|
||||||
OnlyInScope *bool `json:"onlyInScope"`
|
OnlyInScope *bool `json:"onlyInScope"`
|
||||||
|
SearchExpression *string `json:"searchExpression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPResponseLog struct {
|
type HTTPResponseLog struct {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/dstotijn/hetty/pkg/proj"
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -263,15 +264,14 @@ func (r *mutationResolver) SetHTTPRequestLogFilter(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
input *HTTPRequestLogFilterInput,
|
input *HTTPRequestLogFilterInput,
|
||||||
) (*HTTPRequestLogFilter, error) {
|
) (*HTTPRequestLogFilter, error) {
|
||||||
filter := findRequestsFilterFromInput(input)
|
filter, err := findRequestsFilterFromInput(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse request log filter: %v", err)
|
||||||
|
}
|
||||||
if err := r.RequestLogService.SetRequestLogFilter(ctx, filter); err != nil {
|
if err := r.RequestLogService.SetRequestLogFilter(ctx, filter); err != nil {
|
||||||
return nil, fmt.Errorf("could not set request log filter: %v", err)
|
return nil, fmt.Errorf("could not set request log filter: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
empty := reqlog.FindRequestsFilter{}
|
|
||||||
if filter == empty {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return findReqFilterToHTTPReqLogFilter(filter), nil
|
return findReqFilterToHTTPReqLogFilter(filter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,13 +297,21 @@ func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
|
|||||||
return scopeRules
|
return scopeRules
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter) {
|
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) {
|
||||||
if input == nil {
|
if input == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.OnlyInScope != nil {
|
if input.OnlyInScope != nil {
|
||||||
filter.OnlyInScope = *input.OnlyInScope
|
filter.OnlyInScope = *input.OnlyInScope
|
||||||
}
|
}
|
||||||
|
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
||||||
|
expr, err := search.ParseQuery(*input.SearchExpression)
|
||||||
|
if err != nil {
|
||||||
|
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %v", err)
|
||||||
|
}
|
||||||
|
filter.RawSearchExpr = *input.SearchExpression
|
||||||
|
filter.SearchExpr = expr
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -317,5 +325,9 @@ func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *H
|
|||||||
OnlyInScope: findReqFilter.OnlyInScope,
|
OnlyInScope: findReqFilter.OnlyInScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if findReqFilter.RawSearchExpr != "" {
|
||||||
|
httpReqLogFilter.SearchExpression = &findReqFilter.RawSearchExpr
|
||||||
|
}
|
||||||
|
|
||||||
return httpReqLogFilter
|
return httpReqLogFilter
|
||||||
}
|
}
|
||||||
|
@ -64,10 +64,12 @@ type ClearHTTPRequestLogResult {
|
|||||||
|
|
||||||
input HttpRequestLogFilterInput {
|
input HttpRequestLogFilterInput {
|
||||||
onlyInScope: Boolean
|
onlyInScope: Boolean
|
||||||
|
searchExpression: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpRequestLogFilter {
|
type HttpRequestLogFilter {
|
||||||
onlyInScope: Boolean!
|
onlyInScope: Boolean!
|
||||||
|
searchExpression: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
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:
|
case string:
|
||||||
return regexp.MatchString(pattern, v)
|
return regexp.MatchString(pattern, v)
|
||||||
case int64:
|
case int64:
|
||||||
return regexp.MatchString(pattern, string(v))
|
return regexp.MatchString(pattern, fmt.Sprintf("%v", v))
|
||||||
case []byte:
|
case []byte:
|
||||||
return regexp.Match(pattern, v)
|
return regexp.Match(pattern, v)
|
||||||
default:
|
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()
|
sql, args, err := reqQuery.ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("sqlite: could not parse query: %v", err)
|
return nil, fmt.Errorf("sqlite: could not parse query: %v", err)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/dstotijn/hetty/pkg/proj"
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
@ -52,6 +54,8 @@ type Service struct {
|
|||||||
|
|
||||||
type FindRequestsFilter struct {
|
type FindRequestsFilter struct {
|
||||||
OnlyInScope bool
|
OnlyInScope bool
|
||||||
|
SearchExpr search.Expression `json:"-"`
|
||||||
|
RawSearchExpr string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -210,6 +214,34 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (f *FindRequestsFilter) UnmarshalJSON(b []byte) error {
|
||||||
|
var dto struct {
|
||||||
|
OnlyInScope bool
|
||||||
|
RawSearchExpr string
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &dto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := FindRequestsFilter{
|
||||||
|
OnlyInScope: dto.OnlyInScope,
|
||||||
|
RawSearchExpr: dto.RawSearchExpr,
|
||||||
|
}
|
||||||
|
|
||||||
|
if dto.RawSearchExpr != "" {
|
||||||
|
expr, err := search.ParseQuery(dto.RawSearchExpr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filter.SearchExpr = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
*f = filter
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (svc *Service) loadSettings() error {
|
func (svc *Service) loadSettings() error {
|
||||||
return svc.repo.FindSettingsByModule(context.Background(), moduleName, svc)
|
return svc.repo.FindSettingsByModule(context.Background(), moduleName, svc)
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ type precedence int
|
|||||||
const (
|
const (
|
||||||
_ precedence = iota
|
_ precedence = iota
|
||||||
precLowest
|
precLowest
|
||||||
precEq
|
|
||||||
precAnd
|
precAnd
|
||||||
precOr
|
precOr
|
||||||
precNot
|
precNot
|
||||||
|
precEq
|
||||||
precLessGreater
|
precLessGreater
|
||||||
precPrefix
|
precPrefix
|
||||||
precGroup
|
precGroup
|
||||||
@ -86,7 +86,7 @@ func ParseQuery(input string) (expr Expression, err error) {
|
|||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
if p.curTokenIs(TokEOF) {
|
if p.curTokenIs(TokEOF) {
|
||||||
return nil, fmt.Errorf("unexpected EOF")
|
return nil, fmt.Errorf("search: unexpected EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
for !p.curTokenIs(TokEOF) {
|
for !p.curTokenIs(TokEOF) {
|
||||||
|
@ -17,7 +17,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
name: "empty query",
|
name: "empty query",
|
||||||
input: "",
|
input: "",
|
||||||
expectedExpression: nil,
|
expectedExpression: nil,
|
||||||
expectedError: errors.New("unexpected EOF"),
|
expectedError: errors.New("search: unexpected EOF"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "string literal expression",
|
name: "string literal expression",
|
||||||
@ -199,6 +199,24 @@ func TestParseQuery(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedError: nil,
|
expectedError: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "eq operator takes precedence over boolean ops",
|
||||||
|
input: "foo=bar OR baz=yolo",
|
||||||
|
expectedExpression: &InfixExpression{
|
||||||
|
Operator: TokOpOr,
|
||||||
|
Left: &InfixExpression{
|
||||||
|
Operator: TokOpEq,
|
||||||
|
Left: &StringLiteral{Value: "foo"},
|
||||||
|
Right: &StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
Right: &InfixExpression{
|
||||||
|
Operator: TokOpEq,
|
||||||
|
Left: &StringLiteral{Value: "baz"},
|
||||||
|
Right: &StringLiteral{Value: "yolo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
Reference in New Issue
Block a user