Selectively query DB based on GraphQL query field collection

Fixes #5
This commit is contained in:
David Stotijn
2020-10-05 18:34:41 +02:00
parent 073bcea565
commit 5f4bff0155
12 changed files with 354 additions and 247 deletions

View File

@ -23,8 +23,8 @@ const HTTP_REQUEST_LOG = gql`
key
value
}
status
statusCode
statusReason
body
}
}

View File

@ -16,8 +16,8 @@ const HTTP_REQUEST_LOGS = gql`
url
timestamp
response {
status
statusCode
statusReason
}
}
}

View File

@ -128,7 +128,9 @@ function RequestListTable({
{response && (
<div>
<HttpStatusIcon status={response.statusCode} />{" "}
<code>{response.status}</code>
<code>
{response.statusCode} {response.statusReason}
</code>
</div>
)}
</TableCell>

View File

@ -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}
</Typography>
</Typography>{" "}
{response.status}
{response.statusCode} {response.statusReason}
</Typography>
</Box>

3
go.mod
View File

@ -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
)

20
go.sum
View File

@ -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=

View File

@ -63,8 +63,8 @@ type ComplexityRoot struct {
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
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++
}

View File

@ -28,8 +28,8 @@ type HTTPRequestLog struct {
type HTTPResponseLog struct {
RequestID int64 `json:"requestId"`
Proto string `json:"proto"`
Status string `json:"status"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
Body *string `json:"body"`
Headers []HTTPHeader `json:"headers"`
}

View File

@ -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

View File

@ -12,8 +12,8 @@ type HttpRequestLog {
type HttpResponseLog {
requestId: ID!
proto: String!
status: String!
statusCode: Int!
statusReason: String!
body: String
headers: [HttpHeader!]!
}

View File

@ -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
}

View File

@ -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
}