diff --git a/admin/src/components/reqlog/LogDetail.tsx b/admin/src/components/reqlog/LogDetail.tsx index 03d06dc..354d5d6 100644 --- a/admin/src/components/reqlog/LogDetail.tsx +++ b/admin/src/components/reqlog/LogDetail.tsx @@ -23,8 +23,8 @@ const HTTP_REQUEST_LOG = gql` key value } - status statusCode + statusReason body } } diff --git a/admin/src/components/reqlog/LogsOverview.tsx b/admin/src/components/reqlog/LogsOverview.tsx index 16a0992..3c5efcc 100644 --- a/admin/src/components/reqlog/LogsOverview.tsx +++ b/admin/src/components/reqlog/LogsOverview.tsx @@ -16,8 +16,8 @@ const HTTP_REQUEST_LOGS = gql` url timestamp response { - status statusCode + statusReason } } } diff --git a/admin/src/components/reqlog/RequestList.tsx b/admin/src/components/reqlog/RequestList.tsx index 50c6181..28f3636 100644 --- a/admin/src/components/reqlog/RequestList.tsx +++ b/admin/src/components/reqlog/RequestList.tsx @@ -128,7 +128,9 @@ function RequestListTable({ {response && (
{" "} - {response.status} + + {response.statusCode} {response.statusReason} +
)} diff --git a/admin/src/components/reqlog/ResponseDetail.tsx b/admin/src/components/reqlog/ResponseDetail.tsx index 3fdcd95..adcff7a 100644 --- a/admin/src/components/reqlog/ResponseDetail.tsx +++ b/admin/src/components/reqlog/ResponseDetail.tsx @@ -8,7 +8,7 @@ interface Props { response: { proto: string; statusCode: number; - status: string; + statusReason: string; headers: Array<{ key: string; value: string }>; body?: string; }; @@ -42,7 +42,7 @@ function ResponseDetail({ response }: Props): JSX.Element { {response.proto} {" "} - {response.status} + {response.statusCode} {response.statusReason} diff --git a/go.mod b/go.mod index b33a920..77c8b7b 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,14 @@ go 1.15 require ( github.com/99designs/gqlgen v0.11.3 github.com/GeertJohan/go.rice v1.0.0 + github.com/Masterminds/squirrel v1.4.0 github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.0 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/jmoiron/sqlx v1.2.0 github.com/mattn/go-sqlite3 v1.14.4 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/vektah/gqlparser/v2 v2.0.1 + google.golang.org/appengine v1.6.6 // indirect ) diff --git a/go.sum b/go.sum index eb58cb3..1f35729 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/ github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo/eN3w= +github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= @@ -25,7 +27,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM= github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= @@ -40,17 +45,26 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg= github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -78,6 +92,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= @@ -96,6 +111,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -106,12 +122,16 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtD golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw= golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/api/generated.go b/pkg/api/generated.go index f5b72b4..1f86b59 100644 --- a/pkg/api/generated.go +++ b/pkg/api/generated.go @@ -59,12 +59,12 @@ type ComplexityRoot struct { } HTTPResponseLog struct { - Body func(childComplexity int) int - Headers func(childComplexity int) int - Proto func(childComplexity int) int - RequestID func(childComplexity int) int - Status func(childComplexity int) int - StatusCode func(childComplexity int) int + Body func(childComplexity int) int + Headers func(childComplexity int) int + Proto func(childComplexity int) int + RequestID func(childComplexity int) int + StatusCode func(childComplexity int) int + StatusReason func(childComplexity int) int } Query struct { @@ -191,13 +191,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HTTPResponseLog.RequestID(childComplexity), true - case "HttpResponseLog.status": - if e.complexity.HTTPResponseLog.Status == nil { - break - } - - return e.complexity.HTTPResponseLog.Status(childComplexity), true - case "HttpResponseLog.statusCode": if e.complexity.HTTPResponseLog.StatusCode == nil { break @@ -205,6 +198,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HTTPResponseLog.StatusCode(childComplexity), true + case "HttpResponseLog.statusReason": + if e.complexity.HTTPResponseLog.StatusReason == nil { + break + } + + return e.complexity.HTTPResponseLog.StatusReason(childComplexity), true + case "Query.httpRequestLog": if e.complexity.Query.HTTPRequestLog == nil { break @@ -288,8 +288,8 @@ var sources = []*ast.Source{ type HttpResponseLog { requestId: ID! proto: String! - status: String! statusCode: Int! + statusReason: String! body: String headers: [HttpHeader!]! } @@ -791,40 +791,6 @@ func (ec *executionContext) _HttpResponseLog_proto(ctx context.Context, field gr return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _HttpResponseLog_status(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "HttpResponseLog", - Field: field, - Args: nil, - IsMethod: 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.Status, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - func (ec *executionContext) _HttpResponseLog_statusCode(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -859,6 +825,40 @@ func (ec *executionContext) _HttpResponseLog_statusCode(ctx context.Context, fie return ec.marshalNInt2int(ctx, field.Selections, res) } +func (ec *executionContext) _HttpResponseLog_statusReason(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpResponseLog", + Field: field, + Args: nil, + IsMethod: 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.StatusReason, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _HttpResponseLog_body(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2237,13 +2237,13 @@ func (ec *executionContext) _HttpResponseLog(ctx context.Context, sel ast.Select if out.Values[i] == graphql.Null { invalids++ } - case "status": - out.Values[i] = ec._HttpResponseLog_status(ctx, field, obj) + case "statusCode": + out.Values[i] = ec._HttpResponseLog_statusCode(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } - case "statusCode": - out.Values[i] = ec._HttpResponseLog_statusCode(ctx, field, obj) + case "statusReason": + out.Values[i] = ec._HttpResponseLog_statusReason(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } diff --git a/pkg/api/models_gen.go b/pkg/api/models_gen.go index e10959e..902b3a7 100644 --- a/pkg/api/models_gen.go +++ b/pkg/api/models_gen.go @@ -26,12 +26,12 @@ type HTTPRequestLog struct { } type HTTPResponseLog struct { - RequestID int64 `json:"requestId"` - Proto string `json:"proto"` - Status string `json:"status"` - StatusCode int `json:"statusCode"` - Body *string `json:"body"` - Headers []HTTPHeader `json:"headers"` + RequestID int64 `json:"requestId"` + Proto string `json:"proto"` + StatusCode int `json:"statusCode"` + StatusReason string `json:"statusReason"` + Body *string `json:"body"` + Headers []HTTPHeader `json:"headers"` } type HTTPMethod string diff --git a/pkg/api/resolvers.go b/pkg/api/resolvers.go index aa3855e..5dfd303 100644 --- a/pkg/api/resolvers.go +++ b/pkg/api/resolvers.go @@ -5,6 +5,7 @@ package api import ( "context" "fmt" + "strings" "github.com/dstotijn/hetty/pkg/reqlog" ) @@ -54,18 +55,21 @@ func (r *queryResolver) HTTPRequestLog(ctx context.Context, id int64) (*HTTPRequ func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) { method := HTTPMethod(req.Request.Method) - if !method.IsValid() { + if method != "" && !method.IsValid() { return HTTPRequestLog{}, fmt.Errorf("request has invalid method: %v", method) } log := HTTPRequestLog{ ID: req.ID, - URL: req.Request.URL.String(), Proto: req.Request.Proto, Method: method, Timestamp: req.Timestamp, } + if req.Request.URL != nil { + log.URL = req.Request.URL.String() + } + if len(req.Body) > 0 { reqBody := string(req.Body) log.Body = &reqBody @@ -85,11 +89,14 @@ func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) { if req.Response != nil { log.Response = &HTTPResponseLog{ - RequestID: req.ID, + RequestID: req.Response.RequestID, Proto: req.Response.Response.Proto, - Status: req.Response.Response.Status, StatusCode: req.Response.Response.StatusCode, } + statusReasonSubs := strings.SplitN(req.Response.Response.Status, " ", 2) + if len(statusReasonSubs) == 2 { + log.Response.StatusReason = statusReasonSubs[1] + } if len(req.Response.Body) > 0 { resBody := string(req.Response.Body) log.Response.Body = &resBody diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index d43cffd..07ccff1 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -12,8 +12,8 @@ type HttpRequestLog { type HttpResponseLog { requestId: ID! proto: String! - status: String! statusCode: Int! + statusReason: String! body: String headers: [HttpHeader!]! } diff --git a/pkg/db/sqlite/dto.go b/pkg/db/sqlite/dto.go index 8518df4..f3acac5 100644 --- a/pkg/db/sqlite/dto.go +++ b/pkg/db/sqlite/dto.go @@ -1,11 +1,82 @@ package sqlite -import "time" +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/dstotijn/hetty/pkg/reqlog" +) + +type reqURL url.URL + +type httpRequest struct { + ID int64 `db:"req_id"` + Proto string `db:"req_proto"` + URL reqURL `db:"url"` + Method string `db:"method"` + Body []byte `db:"req_body"` + Timestamp time.Time `db:"req_timestamp"` + httpResponse +} type httpResponse struct { - ID *int64 - Proto *string - StatusCode *int - Body *[]byte - Timestamp *time.Time + ID sql.NullInt64 `db:"res_id"` + RequestID sql.NullInt64 `db:"res_req_id"` + Proto sql.NullString `db:"res_proto"` + StatusCode sql.NullInt64 `db:"status_code"` + StatusReason sql.NullString `db:"status_reason"` + Body []byte `db:"res_body"` + Timestamp sql.NullTime `db:"res_timestamp"` +} + +// Value implements driver.Valuer. +func (u *reqURL) Scan(value interface{}) error { + rawURL, ok := value.(string) + if !ok { + return errors.New("sqlite: cannot scan non-string value") + } + + parsed, err := url.Parse(rawURL) + if err != nil { + return fmt.Errorf("sqlite: could not parse URL: %v", err) + } + + *u = reqURL(*parsed) + + return nil +} + +func (dto httpRequest) toRequestLog() reqlog.Request { + u := url.URL(dto.URL) + reqLog := reqlog.Request{ + ID: dto.ID, + Request: http.Request{ + Proto: dto.Proto, + Method: dto.Method, + URL: &u, + }, + Body: dto.Body, + Timestamp: dto.Timestamp, + } + + if dto.httpResponse.ID.Valid { + reqLog.Response = &reqlog.Response{ + ID: dto.httpResponse.ID.Int64, + RequestID: dto.httpResponse.RequestID.Int64, + Response: http.Response{ + Status: strconv.FormatInt(dto.StatusCode.Int64, 10) + " " + dto.StatusReason.String, + StatusCode: int(dto.StatusCode.Int64), + Proto: dto.httpResponse.Proto.String, + }, + Body: dto.httpResponse.Body, + Timestamp: dto.httpResponse.Timestamp.Time, + } + } + + return reqLog } diff --git a/pkg/db/sqlite/sqlite.go b/pkg/db/sqlite/sqlite.go index 9468b64..dbe97fe 100644 --- a/pkg/db/sqlite/sqlite.go +++ b/pkg/db/sqlite/sqlite.go @@ -8,19 +8,29 @@ import ( "net/url" "os" "path/filepath" - "strconv" "time" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/scope" + "github.com/99designs/gqlgen/graphql" + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + // Register sqlite3 for use via database/sql. _ "github.com/mattn/go-sqlite3" ) // Client implements reqlog.Repository. type Client struct { - db *sql.DB + db *sqlx.DB +} + +type httpRequestLogsQuery struct { + requestCols []string + requestHeaderCols []string + responseHeaderCols []string + joinResponse bool } // New returns a new Client. @@ -36,7 +46,7 @@ func New(filename string) (*Client, error) { opts.Set("_foreign_keys", "1") dsn := fmt.Sprintf("file:%v?%v", filename, opts.Encode()) - db, err := sql.Open("sqlite3", dsn) + db, err := sqlx.Open("sqlite3", dsn) if err != nil { return nil, err } @@ -99,213 +109,107 @@ func (c *Client) Close() error { return c.db.Close() } +var reqFieldToColumnMap = map[string]string{ + "proto": "proto AS req_proto", + "url": "url", + "method": "method", + "body": "body AS req_body", + "timestamp": "timestamp AS req_timestamp", +} + +var resFieldToColumnMap = map[string]string{ + "requestId": "req_id AS res_req_id", + "proto": "proto AS res_proto", + "statusCode": "status_code", + "statusReason": "status_reason", + "body": "body AS res_body", + "timestamp": "timestamp AS res_timestamp", +} + +var headerFieldToColumnMap = map[string]string{ + "key": "key", + "value": "value", +} + func (c *Client) FindRequestLogs( ctx context.Context, opts reqlog.FindRequestsOptions, scope *scope.Scope, ) (reqLogs []reqlog.Request, err error) { - // TODO: Pass GraphQL field collections upstream, so we can query only - // requested fields. - // TODO: Use opts and scope to filter. - reqQuery := `SELECT - req.id, - req.proto, - req.url, - req.method, - req.body, - req.timestamp, - res.id, - res.proto, - res.status_code, - res.status_reason, - res.body, - res.timestamp - FROM http_requests req - LEFT JOIN http_responses res ON req.id = res.req_id - ORDER BY req.id DESC` + httpReqLogsQuery := parseHTTPRequestLogsQuery(ctx) - rows, err := c.db.QueryContext(ctx, reqQuery) + reqQuery := sq. + Select(httpReqLogsQuery.requestCols...). + From("http_requests req"). + OrderBy("req.id DESC") + if httpReqLogsQuery.joinResponse { + reqQuery = reqQuery.LeftJoin("http_responses res ON req.id = res.req_id") + } + + sql, _, err := reqQuery.ToSql() + if err != nil { + return nil, fmt.Errorf("sqlite: could not parse query: %v", err) + } + + rows, err := c.db.QueryxContext(ctx, sql, nil) if err != nil { return nil, fmt.Errorf("sqlite: could not execute query: %v", err) } defer rows.Close() for rows.Next() { - var reqLog reqlog.Request - var resDTO httpResponse - var statusReason *string - var rawURL string - - err := rows.Scan( - &reqLog.ID, - &reqLog.Request.Proto, - &rawURL, - &reqLog.Request.Method, - &reqLog.Body, - &reqLog.Timestamp, - &resDTO.ID, - &resDTO.Proto, - &resDTO.StatusCode, - &statusReason, - &resDTO.Body, - &resDTO.Timestamp, - ) + var dto httpRequest + err = rows.StructScan(&dto) if err != nil { return nil, fmt.Errorf("sqlite: could not scan row: %v", err) } - - u, err := url.Parse(rawURL) - if err != nil { - return nil, fmt.Errorf("sqlite: could not parse URL: %v", err) - } - reqLog.Request.URL = u - - if resDTO.ID != nil { - status := strconv.Itoa(*resDTO.StatusCode) + " " + *statusReason - reqLog.Response = &reqlog.Response{ - ID: *resDTO.ID, - RequestID: reqLog.ID, - Response: http.Response{ - Status: status, - StatusCode: *resDTO.StatusCode, - Proto: *resDTO.Proto, - }, - Body: *resDTO.Body, - Timestamp: *resDTO.Timestamp, - } - } - - reqLogs = append(reqLogs, reqLog) + reqLogs = append(reqLogs, dto.toRequestLog()) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("sqlite: could not iterate over rows: %v", err) } rows.Close() - reqHeadersStmt, err := c.db.PrepareContext(ctx, `SELECT key, value FROM http_headers WHERE req_id = ?`) - if err != nil { - return nil, fmt.Errorf("sqlite: could not prepare statement: %v", err) - } - defer reqHeadersStmt.Close() - resHeadersStmt, err := c.db.PrepareContext(ctx, `SELECT key, value FROM http_headers WHERE res_id = ?`) - if err != nil { - return nil, fmt.Errorf("sqlite: could not prepare statement: %v", err) - } - defer resHeadersStmt.Close() - - for _, reqLog := range reqLogs { - headers, err := findHeaders(ctx, reqHeadersStmt, reqLog.ID) - if err != nil { - return nil, fmt.Errorf("sqlite: could not query request headers: %v", err) - } - reqLog.Request.Header = headers - - if reqLog.Response != nil { - headers, err := findHeaders(ctx, resHeadersStmt, reqLog.Response.ID) - if err != nil { - return nil, fmt.Errorf("sqlite: could not query response headers: %v", err) - } - reqLog.Response.Response.Header = headers - } + if err := c.queryHeaders(ctx, httpReqLogsQuery, reqLogs); err != nil { + return nil, fmt.Errorf("sqlite: could not query headers: %v", err) } return reqLogs, nil } func (c *Client) FindRequestLogByID(ctx context.Context, id int64) (reqlog.Request, error) { - // TODO: Pass GraphQL field collections upstream, so we can query only - // requested fields. - reqQuery := `SELECT - req.id, - req.proto, - req.url, - req.method, - req.body, - req.timestamp, - res.id, - res.proto, - res.status_code, - res.status_reason, - res.body, - res.timestamp - FROM http_requests req - LEFT JOIN http_responses res ON req.id = res.req_id - WHERE req_id = ? - ORDER BY req.id DESC` + httpReqLogsQuery := parseHTTPRequestLogsQuery(ctx) - var reqLog reqlog.Request - var resDTO httpResponse - var statusReason *string - var rawURL string + reqQuery := sq. + Select(httpReqLogsQuery.requestCols...). + From("http_requests req"). + Where("req.id = ?") + if httpReqLogsQuery.joinResponse { + reqQuery = reqQuery.LeftJoin("http_responses res ON req.id = res.req_id") + } - err := c.db.QueryRowContext(ctx, reqQuery, id).Scan( - &reqLog.ID, - &reqLog.Request.Proto, - &rawURL, - &reqLog.Request.Method, - &reqLog.Body, - &reqLog.Timestamp, - &resDTO.ID, - &resDTO.Proto, - &resDTO.StatusCode, - &statusReason, - &resDTO.Body, - &resDTO.Timestamp, - ) + reqSQL, _, err := reqQuery.ToSql() + if err != nil { + return reqlog.Request{}, fmt.Errorf("sqlite: could not parse query: %v", err) + } + + row := c.db.QueryRowxContext(ctx, reqSQL, id) + var dto httpRequest + err = row.StructScan(&dto) if err == sql.ErrNoRows { return reqlog.Request{}, reqlog.ErrRequestNotFound } if err != nil { return reqlog.Request{}, fmt.Errorf("sqlite: could not scan row: %v", err) } + reqLog := dto.toRequestLog() - u, err := url.Parse(rawURL) - if err != nil { - return reqlog.Request{}, fmt.Errorf("sqlite: could not parse URL: %v", err) - } - reqLog.Request.URL = u - - if resDTO.ID != nil { - status := strconv.Itoa(*resDTO.StatusCode) + " " + *statusReason - reqLog.Response = &reqlog.Response{ - ID: *resDTO.ID, - RequestID: reqLog.ID, - Response: http.Response{ - Status: status, - StatusCode: *resDTO.StatusCode, - Proto: *resDTO.Proto, - }, - Body: *resDTO.Body, - Timestamp: *resDTO.Timestamp, - } + reqLogs := []reqlog.Request{reqLog} + if err := c.queryHeaders(ctx, httpReqLogsQuery, reqLogs); err != nil { + return reqlog.Request{}, fmt.Errorf("sqlite: could not query headers: %v", err) } - reqHeadersStmt, err := c.db.PrepareContext(ctx, `SELECT key, value FROM http_headers WHERE req_id = ?`) - if err != nil { - return reqlog.Request{}, fmt.Errorf("sqlite: could not prepare statement: %v", err) - } - defer reqHeadersStmt.Close() - resHeadersStmt, err := c.db.PrepareContext(ctx, `SELECT key, value FROM http_headers WHERE res_id = ?`) - if err != nil { - return reqlog.Request{}, fmt.Errorf("sqlite: could not prepare statement: %v", err) - } - defer resHeadersStmt.Close() - - headers, err := findHeaders(ctx, reqHeadersStmt, reqLog.ID) - if err != nil { - return reqlog.Request{}, fmt.Errorf("sqlite: could not query request headers: %v", err) - } - reqLog.Request.Header = headers - - if reqLog.Response != nil { - headers, err := findHeaders(ctx, resHeadersStmt, reqLog.Response.ID) - if err != nil { - return reqlog.Request{}, fmt.Errorf("sqlite: could not query response headers: %v", err) - } - reqLog.Response.Response.Header = headers - } - - return reqLog, nil + return reqLogs[0], nil } func (c *Client) AddRequestLog( @@ -321,7 +225,7 @@ func (c *Client) AddRequestLog( Timestamp: timestamp, } - tx, err := c.db.BeginTx(ctx, nil) + tx, err := c.db.BeginTxx(ctx, nil) if err != nil { return nil, fmt.Errorf("sqlite: could not start transaction: %v", err) } @@ -491,3 +395,103 @@ func findHeaders(ctx context.Context, stmt *sql.Stmt, id int64) (http.Header, er return headers, nil } + +func parseHTTPRequestLogsQuery(ctx context.Context) httpRequestLogsQuery { + var joinResponse bool + var reqHeaderCols, resHeaderCols []string + + opCtx := graphql.GetOperationContext(ctx) + reqFields := graphql.CollectFieldsCtx(ctx, nil) + reqCols := []string{"req.id AS req_id", "res.id AS res_id"} + + for _, reqField := range reqFields { + if col, ok := reqFieldToColumnMap[reqField.Name]; ok { + reqCols = append(reqCols, "req."+col) + } + if reqField.Name == "headers" { + headerFields := graphql.CollectFields(opCtx, reqField.Selections, nil) + for _, headerField := range headerFields { + if col, ok := headerFieldToColumnMap[headerField.Name]; ok { + reqHeaderCols = append(reqHeaderCols, col) + } + } + } + if reqField.Name == "response" { + joinResponse = true + resFields := graphql.CollectFields(opCtx, reqField.Selections, nil) + for _, resField := range resFields { + if resField.Name == "headers" { + reqCols = append(reqCols, "res.id AS res_id") + headerFields := graphql.CollectFields(opCtx, resField.Selections, nil) + for _, headerField := range headerFields { + if col, ok := headerFieldToColumnMap[headerField.Name]; ok { + resHeaderCols = append(resHeaderCols, col) + } + } + } + if col, ok := resFieldToColumnMap[resField.Name]; ok { + reqCols = append(reqCols, "res."+col) + } + } + } + } + + return httpRequestLogsQuery{ + requestCols: reqCols, + requestHeaderCols: reqHeaderCols, + responseHeaderCols: resHeaderCols, + joinResponse: joinResponse, + } +} + +func (c *Client) queryHeaders( + ctx context.Context, + query httpRequestLogsQuery, + reqLogs []reqlog.Request, +) error { + if len(query.requestHeaderCols) > 0 { + reqHeadersQuery, _, err := sq. + Select(query.requestHeaderCols...). + From("http_headers").Where("req_id = ?"). + ToSql() + if err != nil { + return fmt.Errorf("could not parse request headers query: %v", err) + } + reqHeadersStmt, err := c.db.PrepareContext(ctx, reqHeadersQuery) + if err != nil { + return fmt.Errorf("could not prepare statement: %v", err) + } + defer reqHeadersStmt.Close() + for i := range reqLogs { + headers, err := findHeaders(ctx, reqHeadersStmt, reqLogs[i].ID) + if err != nil { + return fmt.Errorf("could not query request headers: %v", err) + } + reqLogs[i].Request.Header = headers + } + } + + if len(query.responseHeaderCols) > 0 { + resHeadersQuery, _, err := sq. + Select(query.responseHeaderCols...). + From("http_headers").Where("res_id = ?"). + ToSql() + if err != nil { + return fmt.Errorf("could not parse response headers query: %v", err) + } + resHeadersStmt, err := c.db.PrepareContext(ctx, resHeadersQuery) + if err != nil { + return fmt.Errorf("could not prepare statement: %v", err) + } + defer resHeadersStmt.Close() + for i := range reqLogs { + headers, err := findHeaders(ctx, resHeadersStmt, reqLogs[i].Response.ID) + if err != nil { + return fmt.Errorf("could not query response headers: %v", err) + } + reqLogs[i].Response.Response.Header = headers + } + } + + return nil +}