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