From 9e160cc2f7bbfa646c92219e6e01d723099db26e Mon Sep 17 00:00:00 2001 From: Rohan Raj Date: Tue, 8 Mar 2022 07:36:34 +0530 Subject: [PATCH] feat: add req and resp logging --- config.go | 57 +++++++++++++++++++++ examples/main.go | 62 +++++++++++++++------- go.mod | 12 +++-- go.sum | 19 +++++-- httpclient.go | 131 ++++++++++++++++++++++++++++++----------------- options.go | 26 ---------- 6 files changed, 208 insertions(+), 99 deletions(-) create mode 100644 config.go delete mode 100644 options.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..aece653 --- /dev/null +++ b/config.go @@ -0,0 +1,57 @@ +package httpclient + +import ( + "time" + + "github.com/rohanraj7316/logger" +) + +var ( + REQUEST_TIMEOUT = "2s" +) + +type Config struct { + // Timeout gives you timeout for request + // Default: 30s + Timeout time.Duration + + // bool flag which help us in configuring proxy + // Default: false + UseProxy bool + + // url need to do the proxy + // Default: nil + ProxyURL string + + // LogReqResEnable helps in logging request & responses. + // Default true + LogReqResEnable bool + + // LogReqResBodyEnable helps in logging request and responses body + // Default true + LogReqResBodyEnable bool +} + +var ConfigDefault = Config{ + UseProxy: false, + LogReqResEnable: true, + LogReqResBodyEnable: true, +} + +func configDefault(config ...Config) Config { + timeout, err := time.ParseDuration(REQUEST_TIMEOUT) + if err != nil { + logger.Error(err.Error()) + ConfigDefault.Timeout = 2 * time.Second + } else { + ConfigDefault.Timeout = timeout + } + + if len(config) < 1 { + return ConfigDefault + } + + cfg := config[0] + + return cfg +} diff --git a/examples/main.go b/examples/main.go index 92a8c6b..3ef19df 100644 --- a/examples/main.go +++ b/examples/main.go @@ -1,45 +1,69 @@ package main import ( + "bytes" "context" - "libs/httpclient" + "encoding/json" "log" "net/http" - "time" - "github.com/rohanraj7316/logger" + "github.com/rohanraj7316/httpclient" ) -func NewLogConfig(o *logger.Options) (*logger.Options, error) { - o.JSONEncoding = true - o.IncludeCallerSourceLocation = true - o.LogGrpc = true - return o, nil +type Handler struct { + client *httpclient.HttpClient } -func main() { - lOptions, _ := NewLogConfig(logger.NewOptions()) - hOptions := httpclient.Options{ - Timeout: 20 * time.Second, - LoggerOptions: lOptions, - LogReqResEnable: true, - LogReqResBodyEnable: true, +func NewHandler() *Handler { + client, err := httpclient.New() + if err != nil { + log.Println(err) } - client, err := httpclient.NewHTTPClient(hOptions) + return &Handler{ + client: client, + } +} + +func (h *Handler) Get(ctx context.Context) { + url := "https://httpbin.org/anything" + header := map[string]string{ + "content-type": "application/json", + } + _, err := h.client.Request(ctx, http.MethodGet, url, header, nil) if err != nil { log.Println(err) } +} - // GET - ctx := context.Background() +func (h *Handler) Post(ctx context.Context) { url := "https://httpbin.org/anything" header := map[string]string{ "content-type": "application/json", } - _, err = client.Request(ctx, http.MethodGet, url, header, nil) + body := map[string]string{ + "name": "Rohan Raj", + } + bBody, err := json.Marshal(&body) + if err != nil { + log.Fatal(err) + } + + _, err = h.client.Request(ctx, http.MethodPost, url, header, bytes.NewBuffer(bBody)) if err != nil { log.Println(err) } } + +func main() { + nH := NewHandler() + c := context.Background() + ctx := context.WithValue(c, "requestId", "123123123123123") + + // GET + nH.Get(ctx) + + // Post + nH.Post(ctx) +} diff --git a/go.mod b/go.mod index 55734a9..84e419d 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,15 @@ -module libs/httpclient +module github.com/rohanraj7316/httpclient go 1.17 require ( - github.com/rohanraj7316/logger v0.0.0-20220201091549-a76bf7c5c14c // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + github.com/google/uuid v1.1.2 + github.com/rohanraj7316/logger v0.0.2 +) + +require ( + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.21.0 // indirect google.golang.org/grpc v1.44.0 // indirect ) diff --git a/go.sum b/go.sum index 84f6bd3..70e50b3 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -13,6 +14,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -39,28 +41,35 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rohanraj7316/logger v0.0.0-20220201091549-a76bf7c5c14c h1:Vjem+rRjxG4HIP92K8nUlEcWSIUlx4URcnEoND2yovU= -github.com/rohanraj7316/logger v0.0.0-20220201091549-a76bf7c5c14c/go.mod h1:f6bE8r3B+e0evPIw8BWwGKyA7OCTdBZXxJ3RGnApYlw= +github.com/rohanraj7316/logger v0.0.2 h1:0JUm5qUVD9cZuyHSkXj/Zut0xK9olzIWyR6bajKsC9g= +github.com/rohanraj7316/logger v0.0.2/go.mod h1:pr8vvYXBOjDuQ7xk9nSkFNgrfVYwO95UEW/bXvlROvg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -136,8 +145,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/httpclient.go b/httpclient.go index bcd7427..4ab4fb7 100644 --- a/httpclient.go +++ b/httpclient.go @@ -3,13 +3,14 @@ package httpclient import ( "context" "crypto/tls" + "encoding/json" "fmt" "io" "net/http" "net/url" + "time" "github.com/rohanraj7316/logger" - "go.uber.org/zap/zapcore" ) type HttpClient struct { @@ -18,7 +19,9 @@ type HttpClient struct { reqResBodyLogging bool } -func NewHTTPClient(o Options) (*HttpClient, error) { +func New(config ...Config) (*HttpClient, error) { + cfg := configDefault(config...) + transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, @@ -26,8 +29,8 @@ func NewHTTPClient(o Options) (*HttpClient, error) { } // setting up proxy - if o.UseProxy { - pProxyURL, err := url.Parse(o.ProxyURL) + if cfg.UseProxy { + pProxyURL, err := url.Parse(cfg.ProxyURL) if err != nil { return nil, err } @@ -35,81 +38,117 @@ func NewHTTPClient(o Options) (*HttpClient, error) { transport.Proxy = http.ProxyURL(pProxyURL) } - err := logger.Configure(o.LoggerOptions) + err := logger.Configure() if err != nil { return nil, err } return &HttpClient{ client: &http.Client{ - Timeout: o.Timeout, + Timeout: cfg.Timeout, Transport: transport, }, - reqResLogging: o.LogReqResEnable, - reqResBodyLogging: o.LogReqResBodyEnable, + reqResLogging: cfg.LogReqResEnable, + reqResBodyLogging: cfg.LogReqResBodyEnable, }, nil } -func (h *HttpClient) successLogging(method, url, status string, statusCode int, headers, request, response interface{}) { +func (h *HttpClient) successLogging(method, url, status string, statusCode int, response io.ReadCloser, + start time.Time, fields ...logger.Field) { + l := time.Since(start).Round(time.Millisecond).String() if h.reqResLogging { - lStr := fmt.Sprintf("HttpClient | %s | %s | %d | %s", method, url, statusCode, status) + lStr := fmt.Sprintf("HttpClient | %s | %s | %d | %s | %s", method, url, statusCode, status, l) if h.reqResBodyLogging { - reqResLogger := []zapcore.Field{ + var responseBody map[string]interface{} + if response != nil { + err := json.NewDecoder(response).Decode(&responseBody) + if err != nil { + logger.Error(err.Error()) + } + } + + fields = append(fields, []logger.Field{ { - Key: "request", - Type: zapcore.ReflectType, - Interface: request, + Key: "statusCode", + Value: statusCode, }, { - Key: "response", - Type: zapcore.ReflectType, - Interface: response, + Key: "response", + Value: responseBody, }, { - Key: "headers", - Type: zapcore.ReflectType, - Interface: headers, + Key: "latency", + Value: l, }, - } - logger.Info(lStr, reqResLogger...) + }...) + logger.Info(lStr, fields...) } else { logger.Info(lStr) } } } -func (h *HttpClient) errorLogging(method, url string, request, headers interface{}, err error) { - lStr := fmt.Sprintf("HttpClient | %s | %s | %d | %s", method, url, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - reqResLogger := []zapcore.Field{ +func (h *HttpClient) errorLogging(method, url string, start time.Time, err error, fields ...logger.Field) { + l := time.Since(start).Round(time.Millisecond).String() + lStr := fmt.Sprintf("HttpClient | %s | %s | %d | %s | %s", method, + url, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), l) + fields = append(fields, []logger.Field{ { - Key: "error", - Type: zapcore.StringType, - String: err.Error(), + Key: "error", + Value: err.Error(), }, - } - if h.reqResBodyLogging { - reqResLogger = append(reqResLogger, zapcore.Field{ - Key: "request", - Type: zapcore.ReflectType, - Interface: request, - }, zapcore.Field{ - Key: "headers", - Type: zapcore.ReflectType, - Interface: headers, - }) - logger.Error(lStr, reqResLogger...) - } else { - logger.Error(lStr) - } + { + Key: "latency", + Value: l, + }, + }...) + logger.Error(lStr, fields...) } // Request responsible for sending http request // by using the Option set at the time of initialization. func (h *HttpClient) Request(ctx context.Context, method, url string, headers map[string]string, request io.Reader) (*http.Response, error) { + start := time.Now() + lBody := []logger.Field{ + { + Key: "requestId", + Value: ctx.Value("requestId"), + }, + { + Key: "url", + Value: url, + }, + { + Key: "method", + Value: method, + }, + } + + if h.reqResBodyLogging { + if request != nil { + bRequest, err := io.ReadAll(request) + if err != nil { + logger.Error(err.Error()) + } else { + lBody = append(lBody, logger.Field{ + Key: "request", + Value: bRequest, + }) + } + } + + if headers != nil { + lBody = append(lBody, logger.Field{ + Key: "headers", + Value: headers, + }) + } + } + rBody, err := http.NewRequestWithContext(ctx, method, url, request) if err != nil { - h.errorLogging(method, url, request, headers, err) + h.errorLogging(method, url, start, err, lBody...) return nil, err } @@ -119,11 +158,11 @@ func (h *HttpClient) Request(ctx context.Context, method, url string, headers ma response, err := h.client.Do(rBody) if err != nil { - h.errorLogging(method, url, request, headers, err) + h.errorLogging(method, url, start, err, lBody...) return nil, err } - h.successLogging(method, url, response.Status, response.StatusCode, headers, request, response.Body) + h.successLogging(method, url, response.Status, response.StatusCode, response.Body, start, lBody...) return response, nil } diff --git a/options.go b/options.go deleted file mode 100644 index 1f7e50c..0000000 --- a/options.go +++ /dev/null @@ -1,26 +0,0 @@ -package httpclient - -import ( - "time" - - "github.com/rohanraj7316/logger" -) - -type Options struct { - // gives you timeout for request - Timeout time.Duration - - // bool flag which help us in configuring proxy - UseProxy bool - - // url need to do the proxy - ProxyURL string - - // default false. true when you need request+response logging - LogReqResEnable bool - - // default false. true when you need request+response body logging - LogReqResBodyEnable bool - - LoggerOptions *logger.Options -}