From 2b80bae2a493e28f09f544e11679bb148f2161bf Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Fri, 3 Mar 2023 19:35:04 -0500 Subject: [PATCH 1/2] Add "WaitForReady" method This couples with the non-blocking start so that subsequent calls can wait for the dial to finish. --- client.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/client.go b/client.go index 4578aa4..f0f9bd2 100644 --- a/client.go +++ b/client.go @@ -121,6 +121,27 @@ func (cc *ClientConn) WaitForStateChange(ctx context.Context, sourceState connec } } +// WaitForReady waits until the state becomes Ready +// It returns true when that happens +// It returns false if the context is cancelled, or the conn is shut down +func (cc *ClientConn) WaitForReady(ctx context.Context) bool { + ch := cc.csMgr.getNotifyChan() + switch cc.csMgr.getState() { + case connectivity.Ready: + return true + case connectivity.Shutdown: + return false + case connectivity.Idle, connectivity.Connecting, connectivity.TransientFailure: + break + } + select { + case <-ctx.Done(): + return false + case <-ch: + return cc.WaitForReady(ctx) + } +} + // GetState gets the current connectivity state. func (cc *ClientConn) GetState() connectivity.State { return cc.csMgr.getState() From 3f5d73d7563bb12fe43296a96dcf0341a0a65d61 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Mon, 6 Mar 2023 15:08:55 -0500 Subject: [PATCH 2/2] Pass through logger --- client.go | 18 ++- dialoptions.go | 17 ++- go.mod | 3 +- go.sum | 61 ++++++++- internal/transport/transport.go | 5 +- internal/transport/websocket_client.go | 11 +- logger/logger.go | 176 ++++++++++++++++++++++++ logger/logger_test.go | 181 +++++++++++++++++++++++++ 8 files changed, 448 insertions(+), 24 deletions(-) create mode 100644 logger/logger.go create mode 100644 logger/logger_test.go diff --git a/client.go b/client.go index f0f9bd2..b9dd06e 100644 --- a/client.go +++ b/client.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "sync" "time" @@ -16,6 +15,7 @@ import ( "github.com/smartcontractkit/wsrpc/internal/message" "github.com/smartcontractkit/wsrpc/internal/transport" "github.com/smartcontractkit/wsrpc/internal/wsrpcsync" + "github.com/smartcontractkit/wsrpc/logger" ) var ( @@ -55,6 +55,8 @@ type ClientConn struct { // The RPC service definition service *serviceInfo + + logger logger.Logger } func Dial(target string, opts ...DialOption) (*ClientConn, error) { @@ -134,6 +136,7 @@ func (cc *ClientConn) WaitForReady(ctx context.Context) bool { case connectivity.Idle, connectivity.Connecting, connectivity.TransientFailure: break } + cc.logger.Debugf("Waiting for connection to be ready, current state: %s", cc.csMgr.getState()) select { case <-ctx.Done(): return false @@ -222,7 +225,7 @@ func (cc *ClientConn) handleRead(done <-chan struct{}) { case *message.Message_Response: go cc.handleMessageResponse(ex.Response) default: - log.Println("Invalid message type") + cc.logger.Errorf("Invalid message type: %T", ex) } case <-done: return @@ -254,7 +257,7 @@ func (cc *ClientConn) handleMessageRequest(r *message.Request) { } if err := cc.conn.transport.Write(replyMsg); err != nil { - log.Printf("error writing to transport: %s", err) + cc.logger.Errorf("error writing to transport: %s", err) } } } @@ -464,6 +467,7 @@ func (ac *addrConn) resetTransport() { newTr, reconnect, err := ac.createTransport(addr, copts) if err != nil { + ac.dopts.logger.Errorf("failed to connect to server at %s, got: %v", addr, err) // After connection failure, the addrConn enters TRANSIENT_FAILURE. ac.mu.Lock() if ac.state == connectivity.Shutdown { @@ -475,7 +479,7 @@ func (ac *addrConn) resetTransport() { ac.mu.Unlock() // Reconnection backoff time - log.Println("[wsrpc] attempting reconnection in", backoffFor) + ac.dopts.logger.Infof("attempting reconnection in %s", backoffFor) timer := time.NewTimer(backoffFor) select { @@ -505,13 +509,13 @@ func (ac *addrConn) resetTransport() { ac.mu.Unlock() - log.Println("[wsrpc] Connected to", ac.addr) + ac.dopts.logger.Debugf("Connected to %s", ac.addr) // Block until the created transport is down. When this happens, we // attempt to reconnect by starting again from the top <-reconnect.Done() - log.Println("[wsrpc] Reconnecting to server...") + ac.dopts.logger.Info("Reconnecting to server...") } } @@ -535,7 +539,7 @@ func (ac *addrConn) createTransport(addr string, copts transport.ConnectOptions) reconnect.Fire() } - tr, err := transport.NewClientTransport(ac.cc.ctx, addr, copts, onClose) + tr, err := transport.NewClientTransport(ac.cc.ctx, ac.dopts.logger, addr, copts, onClose) return tr, reconnect, err } diff --git a/dialoptions.go b/dialoptions.go index f686c9a..4278b60 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -8,14 +8,16 @@ import ( "github.com/smartcontractkit/wsrpc/credentials" "github.com/smartcontractkit/wsrpc/internal/backoff" "github.com/smartcontractkit/wsrpc/internal/transport" + "github.com/smartcontractkit/wsrpc/logger" ) // dialOptions configure a Dial call. dialOptions are set by the DialOption // values passed to Dial. type dialOptions struct { - copts transport.ConnectOptions - bs backoff.Strategy - block bool + copts transport.ConnectOptions + bs backoff.Strategy + block bool + logger logger.Logger } // DialOption configures how we set up the connection. @@ -74,8 +76,15 @@ func WithWriteTimeout(d time.Duration) DialOption { }) } +func WithLogger(lggr logger.Logger) DialOption { + return newFuncDialOption(func(o *dialOptions) { + o.logger = lggr + }) +} + func defaultDialOptions() dialOptions { return dialOptions{ - copts: transport.ConnectOptions{}, + copts: transport.ConnectOptions{}, + logger: logger.DefaultLogger, } } diff --git a/go.mod b/go.mod index ae9d20d..fb19f1d 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/uuid v1.2.0 github.com/gorilla/websocket v1.4.2 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.0 + go.uber.org/zap v1.24.0 google.golang.org/protobuf v1.26.0 ) diff --git a/go.sum b/go.sum index 827ff38..fde2f1f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -9,20 +12,66 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.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/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 9d4030c..df78e81 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/websocket" "github.com/smartcontractkit/wsrpc/credentials" + "github.com/smartcontractkit/wsrpc/logger" ) const ( @@ -44,8 +45,8 @@ type ClientTransport interface { // NewClientTransport establishes the transport with the required ConnectOptions // and returns it to the caller. -func NewClientTransport(ctx context.Context, addr string, opts ConnectOptions, onClose func()) (ClientTransport, error) { - return newWebsocketClient(ctx, addr, opts, onClose) +func NewClientTransport(ctx context.Context, lggr logger.Logger, addr string, opts ConnectOptions, onClose func()) (ClientTransport, error) { + return newWebsocketClient(ctx, lggr, addr, opts, onClose) } // state of transport. diff --git a/internal/transport/websocket_client.go b/internal/transport/websocket_client.go index c739511..428fc46 100644 --- a/internal/transport/websocket_client.go +++ b/internal/transport/websocket_client.go @@ -3,11 +3,11 @@ package transport import ( "context" "fmt" - "log" "net/http" "time" "github.com/gorilla/websocket" + "github.com/smartcontractkit/wsrpc/logger" ) // WebsocketClient implements the ClientTransport interface with websockets. @@ -31,11 +31,13 @@ type WebsocketClient struct { done chan struct{} // A signal channel called when the transport is closed interrupt chan struct{} + + log logger.Logger } // newWebsocketClient establishes the transport with the required ConnectOptions // and returns it to the caller. -func newWebsocketClient(ctx context.Context, addr string, opts ConnectOptions, onClose func()) (_ *WebsocketClient, err error) { +func newWebsocketClient(ctx context.Context, log logger.Logger, addr string, opts ConnectOptions, onClose func()) (_ *WebsocketClient, err error) { writeTimeout := defaultWriteTimeout if opts.WriteTimeout != 0 { writeTimeout = opts.WriteTimeout @@ -61,6 +63,7 @@ func newWebsocketClient(ctx context.Context, addr string, opts ConnectOptions, o read: make(chan []byte), // Should this be buffered? done: make(chan struct{}), interrupt: make(chan struct{}), + log: log, } // Start go routines to establish the read/write channels @@ -115,7 +118,7 @@ func (c *WebsocketClient) readPump() { for { _, msg, err := c.conn.ReadMessage() if err != nil { - log.Println("[wsrpc] Read error: ", err) + c.log.Errorw("[wsrpc] Read error", "err", err) return } @@ -146,7 +149,7 @@ func (c *WebsocketClient) writePump() { c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) err := c.conn.WriteMessage(websocket.BinaryMessage, msg) if err != nil { - log.Printf("[wsrpc] write error: %v\n", err) + c.log.Errorf("Write error: %v", err) c.conn.Close() diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..a52f1b5 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,176 @@ +package logger + +import ( + "reflect" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + "go.uber.org/zap/zaptest/observer" +) + +// Logger is a minimal subset of smartcontractkit/chainlink/core/logger.Logger implemented by go.uber.org/zap.SugaredLogger +type Logger interface { + Name() string + + Debug(args ...interface{}) + Info(args ...interface{}) + Warn(args ...interface{}) + Error(args ...interface{}) + Panic(args ...interface{}) + Fatal(args ...interface{}) + + Debugf(format string, values ...interface{}) + Infof(format string, values ...interface{}) + Warnf(format string, values ...interface{}) + Errorf(format string, values ...interface{}) + Panicf(format string, values ...interface{}) + Fatalf(format string, values ...interface{}) + + Debugw(msg string, keysAndValues ...interface{}) + Infow(msg string, keysAndValues ...interface{}) + Warnw(msg string, keysAndValues ...interface{}) + Errorw(msg string, keysAndValues ...interface{}) + Panicw(msg string, keysAndValues ...interface{}) + Fatalw(msg string, keysAndValues ...interface{}) + + Sync() error +} + +type Config struct { + Level zapcore.Level +} + +var defaultConfig Config + +var DefaultLogger Logger + +func init() { + var err error + DefaultLogger, err = New() + if err != nil { + panic(err) + } +} + +// New returns a new Logger with the default configuration. +func New() (Logger, error) { return defaultConfig.New() } + +// New returns a new Logger for Config. +func (c *Config) New() (Logger, error) { + cfg := zap.NewProductionConfig() + cfg.Level.SetLevel(c.Level) + core, err := cfg.Build() + if err != nil { + return nil, err + } + return &logger{core.Sugar(), ""}, nil +} + +// Test returns a new test Logger for tb. +func Test(tb testing.TB) Logger { + return &logger{zaptest.NewLogger(tb).Sugar(), ""} +} + +// TestObserved returns a new test Logger for tb and ObservedLogs at the given Level. +func TestObserved(tb testing.TB, lvl zapcore.Level) (Logger, *observer.ObservedLogs) { + oCore, logs := observer.New(lvl) + observe := zap.WrapCore(func(c zapcore.Core) zapcore.Core { + return zapcore.NewTee(c, oCore) + }) + return &logger{zaptest.NewLogger(tb, zaptest.WrapOptions(observe)).Sugar(), ""}, logs +} + +// Nop returns a no-op Logger. +func Nop() Logger { + return &logger{zap.New(zapcore.NewNopCore()).Sugar(), ""} +} + +type logger struct { + *zap.SugaredLogger + name string +} + +func (l *logger) with(args ...interface{}) Logger { + return &logger{l.SugaredLogger.With(args...), ""} +} + +var ( + loggerVar Logger + typeOfLogger = reflect.ValueOf(&loggerVar).Elem().Type() +) + +func joinName(old, new string) string { + if old == "" { + return new + } + return old + "." + new +} + +func (l *logger) named(name string) Logger { + newLogger := *l + newLogger.name = joinName(l.name, name) + newLogger.SugaredLogger = l.SugaredLogger.Named(name) + return &newLogger +} + +func (l *logger) Name() string { + return l.name +} + +// With returns a Logger with keyvals, if l has a method `With(...interface{}) L`, where L implements Logger, otherwise it returns l. +func With(l Logger, keyvals ...interface{}) Logger { + switch t := l.(type) { + case *logger: + return t.with(keyvals...) + } + v := reflect.ValueOf(l) + m := v.MethodByName("With") + if m == (reflect.Value{}) { + // not available + return l + } + + r := m.CallSlice([]reflect.Value{reflect.ValueOf(keyvals)}) + if len(r) != 1 { + // unclear how to handle + return l + } + t := r[0].Type() + if !t.Implements(typeOfLogger) { + // unable to assign + return l + } + + var w Logger + reflect.ValueOf(&w).Elem().Set(r[0]) + return w +} + +// Named return a logger with name `n“, if l has a method `Named(name string) L`, where L implements Logger, otherwise it returns l. +func Named(l Logger, n string) Logger { + switch t := l.(type) { + case *logger: + return t.named(n) + } + v := reflect.ValueOf(l) + m := v.MethodByName("Named") + if m == (reflect.Value{}) { + // not available + return l + } + + r := m.Call([]reflect.Value{reflect.ValueOf(n)}) + if len(r) != 1 { + // unclear how to handle + return l + } + ret, ok := r[0].Interface().(Logger) + + // return is not a Logger + if !ok { + return l + } + return ret +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..8b06f8c --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,181 @@ +package logger + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +func TestWith(t *testing.T) { + prod, err := New() + if err != nil { + t.Fatal(err) + } + for _, tt := range []struct { + name string + logger Logger + expSame bool + }{ + { + name: "test", + logger: Test(t), + }, + { + name: "nop", + logger: Nop(), + }, + { + name: "prod", + logger: prod, + }, + { + name: "other", + logger: &other{zaptest.NewLogger(t).Sugar(), ""}, + }, + { + name: "different", + logger: &different{zaptest.NewLogger(t).Sugar(), ""}, + }, + { + name: "missing", + logger: &mismatch{zaptest.NewLogger(t).Sugar(), ""}, + expSame: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got := With(tt.logger, "foo", "bar") + same := got == tt.logger + if same && !tt.expSame { + t.Error("expected a new logger with foo==bar, but got same") + } else if tt.expSame && !same { + t.Errorf("expected the same logger %v, w/o foo=bar, but got %v", tt.logger, got) + } + }) + } + +} + +func TestNamed(t *testing.T) { + prod, err := New() + if err != nil { + t.Fatal(err) + } + for _, tt := range []struct { + logger Logger + expectedName string + }{ + { + expectedName: "test.test1", + logger: Named(Named(Test(t), "test"), "test1"), + }, + { + expectedName: "nop.nested", + logger: Named(Named(Nop(), "nop"), "nested"), + }, + { + expectedName: "prod", + logger: Named(prod, "prod"), + }, + { + expectedName: "initialized", + logger: &other{zaptest.NewLogger(t).Sugar(), "initialized"}, + }, + { + expectedName: "different.should_still_work", + logger: Named(&different{zaptest.NewLogger(t).Sugar(), "different"}, "should_still_work"), + }, + { + expectedName: "mismatch", + logger: Named(&mismatch{zaptest.NewLogger(t).Sugar(), "mismatch"}, "should_not_work"), + }, + } { + t.Run(fmt.Sprintf("test_logger_name_expect_%s", tt.expectedName), func(t *testing.T) { + require.Equal(t, tt.expectedName, tt.logger.Name()) + }) + } + +} + +type other struct { + *zap.SugaredLogger + name string +} + +func (o *other) With(args ...interface{}) Logger { + return &other{o.SugaredLogger.With(args...), ""} +} + +func (o *other) Name() string { + return o.name +} + +func (o *other) Named(name string) Logger { + newLogger := *o + newLogger.name = joinName(o.name, name) + newLogger.SugaredLogger = o.SugaredLogger.Named(name) + return &newLogger +} + +type different struct { + *zap.SugaredLogger + name string +} + +func (d *different) With(args ...interface{}) differentLogger { + return &different{d.SugaredLogger.With(args...), ""} +} + +func (d *different) Name() string { + return d.name +} + +func (d *different) Named(name string) Logger { + newLogger := *d + newLogger.name = joinName(d.name, name) + newLogger.SugaredLogger = d.SugaredLogger.Named(name) + return &newLogger +} + +type mismatch struct { + *zap.SugaredLogger + name string +} + +func (m *mismatch) With(args ...interface{}) interface{} { + return &mismatch{m.SugaredLogger.With(args...), ""} +} + +func (m *mismatch) Name() string { + return m.name +} + +type differentLogger interface { + Name() string + Named(string) Logger + + Debug(args ...interface{}) + Info(args ...interface{}) + Warn(args ...interface{}) + Error(args ...interface{}) + Panic(args ...interface{}) + Fatal(args ...interface{}) + + Debugf(format string, values ...interface{}) + Infof(format string, values ...interface{}) + Warnf(format string, values ...interface{}) + Errorf(format string, values ...interface{}) + Panicf(format string, values ...interface{}) + Fatalf(format string, values ...interface{}) + + Debugw(msg string, keysAndValues ...interface{}) + Infow(msg string, keysAndValues ...interface{}) + Warnw(msg string, keysAndValues ...interface{}) + Errorw(msg string, keysAndValues ...interface{}) + Panicw(msg string, keysAndValues ...interface{}) + Fatalw(msg string, keysAndValues ...interface{}) + + Sync() error +}