From fe636f68adf4354d123106991f27881608f95974 Mon Sep 17 00:00:00 2001 From: Kevin Franklin Kim Date: Tue, 25 Jan 2022 14:26:33 +0100 Subject: [PATCH] feat: add roundtripperwares --- example/go.mod | 1 + example/go.sum | 5 + example/roundtripwares/logger/client.go | 25 +++ example/roundtripwares/logger/main.go | 13 ++ example/roundtripwares/logger/server.go | 33 ++++ go.mod | 1 + go.sum | 5 + log/with.go | 44 ++++- net/http/client.go | 247 ++++++++++++++++++++++++ net/http/roundtripware/dump.go | 39 ++++ net/http/roundtripware/helper.go | 45 +++++ net/http/roundtripware/logger.go | 27 +++ net/http/roundtripware/recover.go | 63 ++++++ net/http/roundtripware/roundtripware.go | 33 ++++ 14 files changed, 575 insertions(+), 6 deletions(-) create mode 100644 example/roundtripwares/logger/client.go create mode 100644 example/roundtripwares/logger/main.go create mode 100644 example/roundtripwares/logger/server.go create mode 100644 net/http/client.go create mode 100644 net/http/roundtripware/dump.go create mode 100644 net/http/roundtripware/helper.go create mode 100644 net/http/roundtripware/logger.go create mode 100644 net/http/roundtripware/recover.go create mode 100644 net/http/roundtripware/roundtripware.go diff --git a/example/go.mod b/example/go.mod index 7f3432b..9919477 100644 --- a/example/go.mod +++ b/example/go.mod @@ -10,6 +10,7 @@ require ( github.com/jackc/pgx/v4 v4.14.1 github.com/nats-io/nats.go v1.13.0 github.com/pkg/errors v0.9.1 + github.com/tinylib/msgp v1.1.6 // indirect go.mongodb.org/mongo-driver v1.8.1 go.opentelemetry.io/otel v0.20.0 go.opentelemetry.io/otel/metric v0.20.0 diff --git a/example/go.sum b/example/go.sum index ac22fa4..dd2eb9e 100644 --- a/example/go.sum +++ b/example/go.sum @@ -514,6 +514,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -619,6 +621,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= +github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -992,6 +996,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/example/roundtripwares/logger/client.go b/example/roundtripwares/logger/client.go new file mode 100644 index 0000000..8ef1b37 --- /dev/null +++ b/example/roundtripwares/logger/client.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/foomo/keel/log" + "github.com/foomo/keel/net/http" + "github.com/foomo/keel/net/http/roundtripware" +) + +func client() { + l := log.Logger() + + client := http.NewHTTPClient( + http.HTTPClientWithRoundTripware(l, + roundtripware.Logger(), + ), + ) + + var err error + + _, err = client.Get("http://localhost:8080") // nolint:noctx + log.Must(l, err, "failed to retrieve response") + + _, err = client.Get("http://localhost:8080/404") // nolint:noctx + log.Must(l, err, "failed to retrieve response") +} diff --git a/example/roundtripwares/logger/main.go b/example/roundtripwares/logger/main.go new file mode 100644 index 0000000..2dd5174 --- /dev/null +++ b/example/roundtripwares/logger/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "time" +) + +func main() { + go server() + + go client() + + time.Sleep(time.Second * 5) +} diff --git a/example/roundtripwares/logger/server.go b/example/roundtripwares/logger/server.go new file mode 100644 index 0000000..f9b2b62 --- /dev/null +++ b/example/roundtripwares/logger/server.go @@ -0,0 +1,33 @@ +package main + +import ( + "net/http" + + "github.com/foomo/keel" +) + +func server() { + svr := keel.NewServer() + + // get logger + l := svr.Logger() + + // create demo service + svs := http.NewServeMux() + + svs.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + svs.HandleFunc("/404", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + }) + + svr.AddService( + keel.NewServiceHTTP(l, "demo", ":8080", svs), + ) + + svr.Run() +} diff --git a/go.mod b/go.mod index c97846f..2c645e2 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/prometheus/client_golang v1.11.0 github.com/spf13/viper v1.10.1 github.com/stretchr/testify v1.7.0 + github.com/tinylib/msgp v1.1.6 go.mongodb.org/mongo-driver v1.8.1 go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.20.0 go.opentelemetry.io/contrib/instrumentation/host v0.20.0 diff --git a/go.sum b/go.sum index ac22fa4..dd2eb9e 100644 --- a/go.sum +++ b/go.sum @@ -514,6 +514,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -619,6 +621,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= +github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -992,6 +996,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/log/with.go b/log/with.go index 6c9b5e3..d77fd1d 100644 --- a/log/with.go +++ b/log/with.go @@ -9,8 +9,6 @@ import ( "go.opentelemetry.io/otel/trace" "go.uber.org/zap" - - httputils "github.com/foomo/keel/net/http" ) func With(l *zap.Logger, fields ...zap.Field) *zap.Logger { @@ -50,10 +48,10 @@ func WithHTTPRequest(l *zap.Logger, r *http.Request) *zap.Logger { if r.Host != "" { fields = append(fields, FHTTPHost(r.Host)) } - if id := r.Header.Get(httputils.HeaderXRequestID); id != "" { + if id := r.Header.Get("X-Request-ID"); id != "" { fields = append(fields, FHTTPRequestID(id)) } - if id := r.Header.Get(httputils.HeaderXSessionID); id != "" { + if id := r.Header.Get("X-Session-ID"); id != "" { fields = append(fields, FHTTPSessionID(id)) } if r.TLS != nil { @@ -68,13 +66,13 @@ func WithHTTPRequest(l *zap.Logger, r *http.Request) *zap.Logger { } var clientIP string - if value := r.Header.Get(httputils.HeaderXForwardedFor); value != "" { + if value := r.Header.Get("X-Forwarded-For"); value != "" { if i := strings.IndexAny(value, ", "); i > 0 { clientIP = value[:i] } else { clientIP = value } - } else if value := r.Header.Get(httputils.HeaderXRealIP); value != "" { + } else if value := r.Header.Get("X-Real-IP"); value != "" { clientIP = value } else if value, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { clientIP = value @@ -91,3 +89,37 @@ func WithHTTPRequest(l *zap.Logger, r *http.Request) *zap.Logger { return With(l, fields...) } + +func WithHTTPRequestOut(l *zap.Logger, r *http.Request) *zap.Logger { + fields := []zap.Field{ + FHTTPWroteBytes(r.ContentLength), + FHTTPMethod(r.Method), + FHTTPTarget(r.URL.Path), + } + + if r.URL.Host != "" { + fields = append(fields, FHTTPHost(r.URL.Host)) + } + if id := r.Header.Get("X-Request-ID"); id != "" { + fields = append(fields, FHTTPRequestID(id)) + } + if id := r.Header.Get("X-Session-ID"); id != "" { + fields = append(fields, FHTTPSessionID(id)) + } + if r.TLS != nil { + fields = append(fields, FHTTPScheme("https")) + } else { + fields = append(fields, FHTTPScheme("http")) + } + if r.ProtoMajor == 1 { + fields = append(fields, FHTTPFlavor(fmt.Sprintf("1.%d", r.ProtoMinor))) + } else if r.ProtoMajor == 2 { + fields = append(fields, FHTTPFlavor("2")) + } + + if spanCtx := trace.SpanContextFromContext(r.Context()); spanCtx.IsValid() { + fields = append(fields, FTraceID(spanCtx.TraceID().String())) + } + + return With(l, fields...) +} diff --git a/net/http/client.go b/net/http/client.go new file mode 100644 index 0000000..4b57cf9 --- /dev/null +++ b/net/http/client.go @@ -0,0 +1,247 @@ +package http + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "net/url" + "time" + + "go.uber.org/zap" + + "github.com/foomo/keel/net/http/roundtripware" +) + +type HTTPClientOption func(*http.Client) + +func HTTPClientWithTimeout(o time.Duration) HTTPClientOption { + return func(v *http.Client) { + v.Timeout = o + } +} + +func HTTPClientWithJar(o http.CookieJar) HTTPClientOption { + return func(v *http.Client) { + v.Jar = o + } +} + +func HTTPClientWithTransport(o http.RoundTripper) HTTPClientOption { + return func(v *http.Client) { + v.Transport = o + } +} + +func HTTPClientWithCheckRedirect(o func(req *http.Request, via []*http.Request) error) HTTPClientOption { + return func(v *http.Client) { + v.CheckRedirect = o + } +} + +func HTTPClientWithProxy(o func(request *http.Request) (*url.URL, error)) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.Proxy = o + v.Transport = t + } + } +} + +func HTTPClientWithDialContext(o func(ctx context.Context, network, addr string) (net.Conn, error)) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.DialContext = o + v.Transport = t + } + } +} + +func HTTPClientWithDialTLSContext(o func(ctx context.Context, network, addr string) (net.Conn, error)) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.DialTLSContext = o + v.Transport = t + } + } +} + +func HTTPClientWithTLSClientConfig(o *tls.Config) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.TLSClientConfig = o + v.Transport = t + } + } +} + +func HTTPClientWithTLSHandshakeTimeout(o time.Duration) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.TLSHandshakeTimeout = o + v.Transport = t + } + } +} + +func HTTPClientWithDisableKeepAlives(o bool) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.DisableKeepAlives = o + v.Transport = t + } + } +} + +func HTTPClientWithDisableCompression(o bool) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.DisableCompression = o + v.Transport = t + } + } +} + +func HTTPClientWithMaxIdleConns(o int) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.MaxIdleConns = o + v.Transport = t + } + } +} + +func HTTPClientWithMaxIdleConnsPerHost(o int) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.MaxIdleConnsPerHost = o + v.Transport = t + } + } +} + +func HTTPClientWithMaxConnsPerHost(o int) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.MaxConnsPerHost = o + v.Transport = t + } + } +} + +func HTTPClientWithIdleConnTimeout(o time.Duration) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.IdleConnTimeout = o + v.Transport = t + } + } +} + +func HTTPClientWithResponseHeaderTimeout(o time.Duration) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.ResponseHeaderTimeout = o + v.Transport = t + } + } +} + +func HTTPClientWithExpectContinueTimeout(o time.Duration) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.ExpectContinueTimeout = o + v.Transport = t + } + } +} + +func HTTPClientWithTLSNextProto(o map[string]func(authority string, c *tls.Conn) http.RoundTripper) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.TLSNextProto = o + v.Transport = t + } + } +} + +func HTTPClientWithProxyConnectHeader(o http.Header) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.ProxyConnectHeader = o + v.Transport = t + } + } +} + +func HTTPClientWithGetProxyConnectHeader(o func(ctx context.Context, proxyURL *url.URL, target string) (http.Header, error)) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.GetProxyConnectHeader = o + v.Transport = t + } + } +} + +func HTTPClientWithMaxResponseHeaderBytes(o int64) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.MaxResponseHeaderBytes = o + v.Transport = t + } + } +} + +func HTTPClientWithWriteBufferSize(o int) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.WriteBufferSize = o + v.Transport = t + } + } +} + +func HTTPClientWithReadBufferSize(o int) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.ReadBufferSize = o + v.Transport = t + } + } +} + +func HTTPClientWithForceAttemptHTTP2(o bool) HTTPClientOption { + return func(v *http.Client) { + if t, ok := v.Transport.(*http.Transport); ok { + t.ForceAttemptHTTP2 = o + v.Transport = t + } + } +} + +func HTTPClientWithRoundTripware(l *zap.Logger, roundTripware ...roundtripware.RoundTripware) HTTPClientOption { + return func(v *http.Client) { + v.Transport = roundtripware.NewRoundTripper(l, v.Transport, roundTripware...) + } +} + +func NewHTTPClient(opts ...HTTPClientOption) *http.Client { + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 45 * time.Second, + KeepAlive: 45 * time.Second, + }).DialContext, + DisableKeepAlives: true, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 5 * time.Second, + } + inst := &http.Client{ + Transport: transport, + Timeout: 2 * time.Minute, + } + for _, opt := range opts { + opt(inst) + } + return inst +} diff --git a/net/http/roundtripware/dump.go b/net/http/roundtripware/dump.go new file mode 100644 index 0000000..71c9183 --- /dev/null +++ b/net/http/roundtripware/dump.go @@ -0,0 +1,39 @@ +package roundtripware + +import ( + "fmt" + "net/http" + + "go.uber.org/zap" +) + +// DumpRequest returns a RoundTripper which prints out the request object +func DumpRequest() RoundTripware { + return func(l *zap.Logger, next Handler) Handler { + return func(req *http.Request) (*http.Response, error) { + if req.Header != nil && req.Header.Get("Content-Type") != "" { + var body string + if req.Body, body = readBodyPretty(req.Header.Get("Content-Type"), req.Body); body != "" { + fmt.Printf("Request %s:\n%s\n", req.URL, body) + } + } + return next(req) + } + } +} + +// DumpResponse returns a RoundTripper which prints out the response object +func DumpResponse() RoundTripware { + return func(l *zap.Logger, next Handler) Handler { + return func(req *http.Request) (*http.Response, error) { + resp, err := next(req) + if resp.Header != nil && resp.Header.Get("Content-Type") != "" { + var body string + if resp.Body, body = readBodyPretty(resp.Header.Get("Content-Type"), resp.Body); body != "" { + fmt.Printf("Response %s:\n%s\n", req.URL, body) + } + } + return resp, err + } + } +} diff --git a/net/http/roundtripware/helper.go b/net/http/roundtripware/helper.go new file mode 100644 index 0000000..b1dbdec --- /dev/null +++ b/net/http/roundtripware/helper.go @@ -0,0 +1,45 @@ +package roundtripware + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "strings" + + "github.com/tinylib/msgp/msgp" +) + +func readBodyPretty(contentType string, original io.ReadCloser) (io.ReadCloser, string) { + var bs bytes.Buffer + var body string + defer func() { + _ = original.Close() + }() + + // read in body + if _, err := io.Copy(&bs, original); err != nil { + return original, "" + } else { + body = bs.String() + } + + switch { + case strings.HasPrefix(contentType, "application/json"): + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, bs.Bytes(), "", " "); err == nil { + body = prettyJSON.String() + } + case strings.HasPrefix(contentType, "application/msgpack"): + var prettyJSON bytes.Buffer + var out bytes.Buffer + if _, err := msgp.UnmarshalAsJSON(&out, bs.Bytes()); err == nil { + if err := json.Indent(&prettyJSON, out.Bytes(), "", " "); err == nil { + body = prettyJSON.String() + } + } + } + + // return copy of the original + return ioutil.NopCloser(strings.NewReader(bs.String())), body +} diff --git a/net/http/roundtripware/logger.go b/net/http/roundtripware/logger.go new file mode 100644 index 0000000..84a45bb --- /dev/null +++ b/net/http/roundtripware/logger.go @@ -0,0 +1,27 @@ +package roundtripware + +import ( + "net/http" + + "go.uber.org/zap" + + "github.com/foomo/keel/log" + keeltime "github.com/foomo/keel/time" +) + +// Logger returns a RoundTripware which logs all requests +func Logger() RoundTripware { + msg := "sent request" + return func(l *zap.Logger, next Handler) Handler { + return func(req *http.Request) (*http.Response, error) { + start := keeltime.Now() + resp, err := next(req) + log.WithHTTPRequestOut(l, req).Info(msg, + log.FDuration(keeltime.Now().Sub(start)), + log.FHTTPStatusCode(resp.StatusCode), + log.FHTTPRequestContentLength(resp.ContentLength), + ) + return resp, err + } + } +} diff --git a/net/http/roundtripware/recover.go b/net/http/roundtripware/recover.go new file mode 100644 index 0000000..0bc94f6 --- /dev/null +++ b/net/http/roundtripware/recover.go @@ -0,0 +1,63 @@ +package roundtripware + +import ( + "fmt" + "net/http" + + "go.uber.org/zap" + + "github.com/foomo/keel/log" +) + +type ( + RecoverOptions struct { + DisablePrintStack bool + } + RecoverOption func(options *RecoverOptions) +) + +// GetDefaultRecoverOptions returns the default options +func GetDefaultRecoverOptions() RecoverOptions { + return RecoverOptions{ + DisablePrintStack: false, + } +} + +// RecoverWithDisablePrintStack roundTripware option +func RecoverWithDisablePrintStack(v bool) RecoverOption { + return func(o *RecoverOptions) { + o.DisablePrintStack = v + } +} + +// Recover returns a RoundTripper which catches any panics +func Recover(opts ...RecoverOption) RoundTripware { + options := GetDefaultRecoverOptions() + for _, opt := range opts { + if opt != nil { + opt(&options) + } + } + return RecoverWithOptions(options) +} + +// RecoverWithOptions returns a RoundTripper which catches any panics +func RecoverWithOptions(opts RecoverOptions) RoundTripware { + return func(l *zap.Logger, next Handler) Handler { + return func(req *http.Request) (*http.Response, error) { + defer func() { + if e := recover(); e != nil { + err, ok := e.(error) + if !ok { + err = fmt.Errorf("%v", e) + } + if !opts.DisablePrintStack { + l = l.With(log.FStackSkip(3)) + } + log.WithError(l, err).Error("recovering from panic") + } + }() + return next(req) + } + } +} diff --git a/net/http/roundtripware/roundtripware.go b/net/http/roundtripware/roundtripware.go new file mode 100644 index 0000000..fbba955 --- /dev/null +++ b/net/http/roundtripware/roundtripware.go @@ -0,0 +1,33 @@ +package roundtripware + +import ( + "net/http" + + "go.uber.org/zap" +) + +type ( + Handler func(r *http.Request) (*http.Response, error) + RoundTripware func(l *zap.Logger, next Handler) Handler +) + +type RoundTripper struct { + http.RoundTripper + handler Handler +} + +func NewRoundTripper(l *zap.Logger, parent http.RoundTripper, roundTripwares ...RoundTripware) *RoundTripper { + next := parent.RoundTrip + for _, roundTripware := range roundTripwares { + next = roundTripware(l, next) + } + + return &RoundTripper{ + RoundTripper: parent, + handler: next, + } +} + +func (rt *RoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { + return rt.handler(req) +}