Repository: ably/ably-go
Pull request #321 (2021-05-20 11:31:15)

Simplify and uniformize logging
Cleans up logging-related code as a prerequisite for #160. Commit messages contain rationale.


Patch:
diff --git a/ably/auth.go b/ably/auth.go
index 5689b32..b3ffa33 100644
--- a/ably/auth.go
+++ b/ably/auth.go
@@ -178,13 +178,12 @@ func (a *Auth) RequestToken(ctx context.Context, params *TokenParams, opts ...Au
 }
 
 func (a *Auth) requestToken(ctx context.Context, params *TokenParams, opts *authOptions) (tok *TokenDetails, tokReqClientID string, err error) {
-	log := a.logger().sugar()
 	switch {
 	case opts != nil && opts.Token != "":
-		log.Verbose("Auth: found token in []AuthOption")
+		a.log().Verbose("Auth: found token in []AuthOption")
 		return newTokenDetails(opts.Token), "", nil
 	case opts != nil && opts.TokenDetails != nil:
-		log.Verbose("Auth: found TokenDetails in []AuthOption")
+		a.log().Verbose("Auth: found TokenDetails in []AuthOption")
 		return opts.TokenDetails, "", nil
 	}
 	if params == nil {
@@ -194,10 +193,10 @@ func (a *Auth) requestToken(ctx context.Context, params *TokenParams, opts *auth
 	var tokReq *TokenRequest
 	switch {
 	case opts.AuthCallback != nil:
-		log.Verbose("Auth: found AuthCallback in []AuthOption")
+		a.log().Verbose("Auth: found AuthCallback in []AuthOption")
 		v, err := opts.AuthCallback(context.TODO(), *params)
 		if err != nil {
-			log.Error("Auth: failed calling opts.AuthCallback ", err)
+			a.log().Error("Auth: failed calling opts.AuthCallback ", err)
 			return nil, "", newError(ErrErrorFromClientTokenCallback, err)
 		}
 
@@ -221,10 +220,10 @@ func (a *Auth) requestToken(ctx context.Context, params *TokenParams, opts *auth
 			panic(fmt.Errorf("unhandled TokenLike: %T", v))
 		}
 	case opts.AuthURL != "":
-		log.Verbose("Auth: found AuthURL in []AuthOption")
+		a.log().Verbose("Auth: found AuthURL in []AuthOption")
 		res, err := a.requestAuthURL(ctx, params, opts)
 		if err != nil {
-			log.Error("Auth: failed calling requesting token with AuthURL ", err)
+			a.log().Error("Auth: failed calling requesting token with AuthURL ", err)
 			return nil, "", err
 		}
 		switch res := res.(type) {
@@ -235,7 +234,7 @@ func (a *Auth) requestToken(ctx context.Context, params *TokenParams, opts *auth
 			tokReqClientID = tokReq.ClientID
 		}
 	default:
-		log.Verbose("Auth: using default token request")
+		a.log().Verbose("Auth: using default token request")
 
 		req, err := a.createTokenRequest(params, opts)
 		if err != nil {
@@ -272,7 +271,6 @@ func (a *Auth) Authorize(ctx context.Context, params *TokenParams, setOpts ...Au
 }
 
 func (a *Auth) authorize(ctx context.Context, params *TokenParams, opts *authOptions, force bool) (*TokenDetails, error) {
-	log := a.logger().sugar()
 	switch tok := a.token(); {
 	case tok != nil && !force && (tok.Expires == 0 || !tok.expired(a.opts().Now())):
 		return tok, nil
@@ -281,22 +279,22 @@ func (a *Auth) authorize(ctx context.Context, params *TokenParams, opts *authOpt
 	case params == nil && a.clientID != "":
 		params = &TokenParams{ClientID: a.clientID}
 	}
-	log.Info("Auth: sending  token request")
+	a.log().Info("Auth: sending  token request")
 	tok, tokReqClientID, err := a.requestToken(ctx, params, opts)
 	if err != nil {
-		log.Error("Auth: failed to get token", err)
+		a.log().Error("Auth: failed to get token", err)
 		return nil, err
 	}
 	// Fail if the non-empty ClientID, that was set explicitely via clientOptions, does
 	// not match the non-wildcard ClientID returned with the token.
 	if areClientIDsSet(a.clientID, tok.ClientID) && a.clientID != tok.ClientID {
-		log.Error("Auth: ", errClientIDMismatch)
+		a.log().Error("Auth: ", errClientIDMismatch)
 		return nil, newError(ErrInvalidClientID, errClientIDMismatch)
 	}
 	// Fail if non-empty ClientID requested by a TokenRequest
 	// does not match the non-wildcard ClientID that arrived with the token.
 	if areClientIDsSet(tokReqClientID, tok.ClientID) && tokReqClientID != tok.ClientID {
-		log.Error("Auth: ", errClientIDMismatch)
+		a.log().Error("Auth: ", errClientIDMismatch)
 		return nil, newError(ErrInvalidClientID, errClientIDMismatch)
 	}
 	a.method = authToken
@@ -309,7 +307,7 @@ func (a *Auth) authorize(ctx context.Context, params *TokenParams, opts *authOpt
 func (a *Auth) reauthorize(ctx context.Context) (*TokenDetails, error) {
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
-	a.logger().sugar().Info("Auth: reauthorize")
+	a.log().Info("Auth: reauthorize")
 	return a.authorize(ctx, a.params, nil, true)
 }
 
@@ -476,8 +474,8 @@ func (a *Auth) token() *TokenDetails {
 	return a.opts().TokenDetails
 }
 
-func (a *Auth) logger() *LoggerOptions {
-	return a.client.logger()
+func (a *Auth) log() logger {
+	return a.client.log
 }
 
 func detectAuthMethod(opts *clientOptions) (int, error) {
diff --git a/ably/event_emitter.go b/ably/event_emitter.go
index 70b5a94..259ed8f 100644
--- a/ably/event_emitter.go
+++ b/ably/event_emitter.go
@@ -8,7 +8,7 @@ import (
 type eventEmitter struct {
 	sync.Mutex
 	listeners listenersForEvent
-	logger    Logger
+	log       logger
 }
 
 type emitterEvent interface {
@@ -30,7 +30,7 @@ type eventListener struct {
 	queue    []emitterData
 }
 
-func (l *eventListener) handle(e emitterData, log Logger) {
+func (l *eventListener) handle(e emitterData, log logger) {
 	// The goroutine that finds the queue empty launches a goroutine for emptying it,
 	// ie. process its emitterData and afterwards any emitterData that concurrent
 	// goroutines may have left in the queue in the meantime.
@@ -69,24 +69,24 @@ func (l *eventListener) handle(e emitterData, log Logger) {
 	}()
 }
 
-func safeHandle(e emitterData, handle func(emitterData), log Logger) {
+func safeHandle(e emitterData, handle func(emitterData), log logger) {
 	defer func() {
 		r := recover()
 		if r == nil {
 			return
 		}
-		log.Printf(LogError, "EventEmitter: panic in event handler: %v\n%s", r, debug.Stack())
+		log.Errorf("EventEmitter: panic in event handler: %v\n%s", r, debug.Stack())
 	}()
 
 	handle(e)
 }
 
-func newEventEmitter(logger Logger) *eventEmitter {
+func newEventEmitter(log logger) *eventEmitter {
 	return &eventEmitter{
 		listeners: listenersForEvent{
 			nil: listenerSet{},
 		},
-		logger: logger,
+		log: log,
 	}
 }
 
@@ -173,11 +173,11 @@ func (em *eventEmitter) Emit(event emitterEvent, data emitterData) {
 	// which would otherwise deadlock.
 
 	for _, handle := range em.handlersForEvent(event) {
-		handle(data, em.logger)
+		handle(data, em.log)
 	}
 }
 
-func (em *eventEmitter) handlersForEvent(event emitterEvent) (handlers []func(emitterData, Logger)) {
+func (em *eventEmitter) handlersForEvent(event emitterEvent) (handlers []func(emitterData, logger)) {
 	em.Lock()
 	defer em.Unlock()
 
diff --git a/ably/logger.go b/ably/logger.go
index cbe7ce6..203c6e5 100644
--- a/ably/logger.go
+++ b/ably/logger.go
@@ -3,7 +3,6 @@ package ably
 import (
 	"fmt"
 	"log"
-	"os"
 )
 
 type LogLevel uint
@@ -17,116 +16,93 @@ const (
 	LogDebug
 )
 
-var logLevels = map[LogLevel]string{
-	LogError:   "[ERROR] ",
-	LogWarning: "[WARN] ",
-	LogInfo:    "[INFO] ",
-	LogVerbose: "[VERBOSE] ",
-	LogDebug:   "[DEBUG] ",
+var logLevelNames = map[LogLevel]string{
+	LogError:   "ERROR",
+	LogWarning: "WARN",
+	LogInfo:    "INFO",
+	LogVerbose: "VERBOSE",
+	LogDebug:   "DEBUG",
 }
 
-var defaultLog = LoggerOptions{
-	Logger: &StdLogger{Logger: log.New(os.Stderr, "", log.LstdFlags)},
-	Level:  LogNone,
+func (l LogLevel) String() string {
+	return logLevelNames[l]
 }
 
-// LoggerOptions defines options for ably logging.
-type LoggerOptions struct {
+type filteredLogger struct {
 	Logger Logger
 	Level  LogLevel
 }
 
-func (l LoggerOptions) Is(level LogLevel) bool {
+func (l filteredLogger) Is(level LogLevel) bool {
 	return l.Level != LogNone && l.Level >= level
 }
 
-func (l LoggerOptions) Print(level LogLevel, v ...interface{}) {
+func (l filteredLogger) Printf(level LogLevel, format string, v ...interface{}) {
 	if l.Is(level) {
-		l.GetLogger().Print(level, v...)
+		l.Logger.Printf(level, format, v...)
 	}
 }
 
-func (l LoggerOptions) Printf(level LogLevel, format string, v ...interface{}) {
-	if l.Is(level) {
-		l.GetLogger().Printf(level, format, v...)
-	}
-}
-
-// GetLogger returns the custom logger if any. This will return the default
-// logger if custom logger was not specified.
-func (l LoggerOptions) GetLogger() Logger {
-	if l.Logger != nil {
-		return l.Logger
-	}
-	return defaultLog.Logger
-}
-
-func (l LoggerOptions) sugar() *sugaredLogger {
-	return &sugaredLogger{LoggerOptions: l}
-}
-
 // Logger is an interface for ably loggers.
 type Logger interface {
-	Print(level LogLevel, v ...interface{})
 	Printf(level LogLevel, format string, v ...interface{})
 }
 
-// StdLogger wraps log.Logger to satisfy the Logger interface.
-type StdLogger struct {
+// stdLogger wraps log.Logger to satisfy the Logger interface.
+type stdLogger struct {
 	*log.Logger
 }
 
-func (s *StdLogger) Printf(level LogLevel, format string, v ...interface{}) {
-	s.Logger.Printf(logLevels[level]+format, v...)
+func (s *stdLogger) Printf(level LogLevel, format string, v ...interface{}) {
+	s.Logger.Printf(fmt.Sprintf("[%s] %s", level, format), v...)
 }
 
-func (s *StdLogger) Print(level LogLevel, v ...interface{}) {
-	if len(v) != 0 {
-		v[0] = fmt.Sprintf(logLevels[level]+"%v", v[0])
-		s.Logger.Print(v...)
-	}
+// logger is the internal logger type, with helper methods that wrap the raw
+// Logger interface.
+type logger struct {
+	l Logger
 }
 
-type sugaredLogger struct {
-	LoggerOptions
+func (l logger) Error(v ...interface{}) {
+	l.print(LogError, v...)
 }
 
-func (s sugaredLogger) Error(v ...interface{}) {
-	s.Print(LogError, v...)
+func (l logger) Errorf(fmt string, v ...interface{}) {
+	l.l.Printf(LogError, fmt, v...)
 }
 
-func (s sugaredLogger) Errorf(fmt string, v ...interface{}) {
-	s.Printf(LogError, fmt, v...)
+func (l logger) Warn(v ...interface{}) {
+	l.print(LogWarning, v...)
 }
 
-func (s sugaredLogger) Warn(v ...interface{}) {
-	s.Print(LogWarning, v...)
+func (l logger) Warnf(fmt string, v ...interface{}) {
+	l.l.Printf(LogWarning, fmt, v...)
 }
 
-func (s sugaredLogger) Warnf(fmt string, v ...interface{}) {
-	s.Printf(LogWarning, fmt, v...)
+func (l logger) Info(v ...interface{}) {
+	l.print(LogInfo, v...)
 }
 
-func (s sugaredLogger) Info(v ...interface{}) {
-	s.Print(LogInfo, v...)
+func (l logger) Infof(fmt string, v ...interface{}) {
+	l.l.Printf(LogInfo, fmt, v...)
 }
 
-func (s sugaredLogger) Infof(fmt string, v ...interface{}) {
-	s.Printf(LogInfo, fmt, v...)
+func (l logger) Verbose(v ...interface{}) {
+	l.print(LogVerbose, v...)
 }
 
-func (s sugaredLogger) Verbose(v ...interface{}) {
-	s.Print(LogVerbose, v...)
+func (l logger) Verbosef(fmt string, v ...interface{}) {
+	l.l.Printf(LogVerbose, fmt, v...)
 }
 
-func (s sugaredLogger) Verbosef(fmt string, v ...interface{}) {
-	s.Printf(LogVerbose, fmt, v...)
+func (l logger) Debugf(fmt string, v ...interface{}) {
+	l.l.Printf(LogDebug, fmt, v...)
 }
 
-func (s sugaredLogger) Debugf(fmt string, v ...interface{}) {
-	s.Printf(LogDebug, fmt, v...)
+func (l logger) Debug(v ...interface{}) {
+	l.print(LogDebug, v...)
 }
 
-func (s sugaredLogger) Debug(v ...interface{}) {
-	s.Print(LogDebug, v...)
+func (l logger) print(level LogLevel, v ...interface{}) {
+	l.l.Printf(level, fmt.Sprint(v...))
 }
diff --git a/ably/options.go b/ably/options.go
index 80e1a44..59b7fb2 100644
--- a/ably/options.go
+++ b/ably/options.go
@@ -3,10 +3,12 @@ package ably
 import (
 	"context"
 	"fmt"
+	"log"
 	"net"
 	"net/http"
 	"net/http/httptrace"
 	"net/url"
+	"os"
 	"strconv"
 	"strings"
 	"time"
@@ -41,6 +43,7 @@ var defaultOptions = clientOptions{
 	TLSPort:                  443,
 	Now:                      time.Now,
 	After:                    ablyutil.After,
+	LogLevel:                 LogWarning, // RSC2
 }
 
 func defaultFallbackHosts() []string {
@@ -167,13 +170,12 @@ type clientOptions struct {
 	RESTHost string // optional; overwrite endpoint hostname for REST client
 
 	FallbackHosts   []string
-	RealtimeHost    string        // optional; overwrite endpoint hostname for Realtime client
-	Environment     string        // optional; prefixes both hostname with the environment string
-	Port            int           // optional: port to use for non-TLS connections and requests
-	TLSPort         int           // optional: port to use for TLS connections and requests
-	ClientID        string        // optional; required for managing realtime presence of the current client
-	Recover         string        // optional; used to recover client state
-	Logger          LoggerOptions // optional; overwrite logging defaults
+	RealtimeHost    string // optional; overwrite endpoint hostname for Realtime client
+	Environment     string // optional; prefixes both hostname with the environment string
+	Port            int    // optional: port to use for non-TLS connections and requests
+	TLSPort         int    // optional: port to use for TLS connections and requests
+	ClientID        string // optional; required for managing realtime presence of the current client
+	Recover         string // optional; used to recover client state
 	TransportParams url.Values
 
 	// max number of fallback hosts to use as a fallback.
@@ -235,6 +237,9 @@ type clientOptions struct {
 	// Now returns the time the library should take as current.
 	Now   func() time.Time
 	After func(context.Context, time.Duration) <-chan time.Time
+
+	LogLevel   LogLevel
+	LogHandler Logger
 }
 
 func (opts *clientOptions) timeoutConnect() time.Duration {
@@ -581,13 +586,13 @@ func WithEnvironment(env string) ClientOption {
 
 func WithLogHandler(handler Logger) ClientOption {
 	return func(os *clientOptions) {
-		os.Logger.Logger = handler
+		os.LogHandler = handler
 	}
 }
 
 func WithLogLevel(level LogLevel) ClientOption {
 	return func(os *clientOptions) {
-		os.Logger.Level = level
+		os.LogLevel = level
 	}
 }
 
@@ -711,10 +716,10 @@ func WithDial(dial func(protocol string, u *url.URL, timeout time.Duration) (pro
 	}
 }
 
-func applyOptionsWithDefaults(os ...ClientOption) *clientOptions {
+func applyOptionsWithDefaults(opts ...ClientOption) *clientOptions {
 	to := defaultOptions
 
-	for _, set := range os {
+	for _, set := range opts {
 		set(&to)
 	}
 
@@ -724,6 +729,11 @@ func applyOptionsWithDefaults(os ...ClientOption) *clientOptions {
 		}
 	}
 
+	if to.LogHandler == nil {
+		to.LogHandler = &stdLogger{Logger: log.New(os.Stderr, "", log.LstdFlags)}
+	}
+	to.LogHandler = filteredLogger{Logger: to.LogHandler, Level: to.LogLevel}
+
 	return &to
 }
 
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index c3ba606..9b591b4 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -160,14 +160,14 @@ type RealtimeChannel struct {
 
 func newRealtimeChannel(name string, client *Realtime) *RealtimeChannel {
 	c := &RealtimeChannel{
-		ChannelEventEmitter: ChannelEventEmitter{newEventEmitter(client.logger())},
+		ChannelEventEmitter: ChannelEventEmitter{newEventEmitter(client.log())},
 		Name:                name,
 
 		state:           ChannelStateInitialized,
-		internalEmitter: ChannelEventEmitter{newEventEmitter(client.logger())},
+		internalEmitter: ChannelEventEmitter{newEventEmitter(client.log())},
 
 		client:         client,
-		messageEmitter: newEventEmitter(client.logger()),
+		messageEmitter: newEventEmitter(client.log()),
 	}
 	c.Presence = newRealtimePresence(c)
 	c.queue = newMsgQueue(client.Connection)
@@ -658,8 +658,8 @@ func (c *RealtimeChannel) opts() *clientOptions {
 	return c.client.opts()
 }
 
-func (c *RealtimeChannel) logger() *LoggerOptions {
-	return c.client.logger()
+func (c *RealtimeChannel) log() logger {
+	return c.client.log()
 }
 
 func (c *RealtimeChannel) setState(state ChannelState, err error, resumed bool) error {
diff --git a/ably/realtime_client.go b/ably/realtime_client.go
index 16d99c6..66773b0 100644
--- a/ably/realtime_client.go
+++ b/ably/realtime_client.go
@@ -104,6 +104,6 @@ func (c *Realtime) opts() *clientOptions {
 	return c.rest.opts
 }
 
-func (c *Realtime) logger() *LoggerOptions {
-	return c.rest.logger()
+func (c *Realtime) log() logger {
+	return c.rest.log
 }
diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go
index f41b418..82323ff 100644
--- a/ably/realtime_conn.go
+++ b/ably/realtime_conn.go
@@ -84,12 +84,12 @@ type connCallbacks struct {
 func newConn(opts *clientOptions, auth *Auth, callbacks connCallbacks) *Connection {
 	c := &Connection{
 
-		ConnectionEventEmitter: ConnectionEventEmitter{newEventEmitter(auth.logger())},
+		ConnectionEventEmitter: ConnectionEventEmitter{newEventEmitter(auth.log())},
 		state:                  ConnectionStateInitialized,
-		internalEmitter:        ConnectionEventEmitter{newEventEmitter(auth.logger())},
+		internalEmitter:        ConnectionEventEmitter{newEventEmitter(auth.log())},
 
 		opts:      opts,
-		pending:   newPendingEmitter(auth.logger()),
+		pending:   newPendingEmitter(auth.log()),
 		auth:      auth,
 		callbacks: callbacks,
 	}
@@ -97,10 +97,9 @@ func newConn(opts *clientOptions, auth *Auth, callbacks connCallbacks) *Connecti
 	if !opts.NoConnect {
 		c.setState(ConnectionStateConnecting, nil, 0)
 		go func() {
-			lg := opts.Logger.sugar()
-			lg.Info("Trying to establish a connection asynchronously")
+			c.log().Info("Trying to establish a connection asynchronously")
 			if _, err := c.connect(connArgs{}); err != nil {
-				lg.Errorf("Failed to open connection with err:%v", err)
+				c.log().Errorf("Failed to open connection with err:%v", err)
 			}
 		}()
 	}
@@ -108,9 +107,8 @@ func newConn(opts *clientOptions, auth *Auth, callbacks connCallbacks) *Connecti
 }
 
 func (c *Connection) dial(proto string, u *url.URL) (conn proto.Conn, err error) {
-	lg := c.logger().sugar()
 	start := time.Now()
-	lg.Debugf("Dial protocol=%q url %q ", proto, u.String())
+	c.log().Debugf("Dial protocol=%q url %q ", proto, u.String())
 	// (RTN23b)
 	query := u.Query()
 	query.Add("heartbeats", "true")
@@ -122,10 +120,10 @@ func (c *Connection) dial(proto string, u *url.URL) (conn proto.Conn, err error)
 		conn, err = ablyutil.DialWebsocket(proto, u, timeout)
 	}
 	if err != nil {
-		lg.Debugf("Dial Failed in %v with %v", time.Since(start), err)
+		c.log().Debugf("Dial Failed in %v with %v", time.Since(start), err)
 		return nil, err
 	}
-	lg.Debugf("Dial success in %v", time.Since(start))
+	c.log().Debugf("Dial success in %v", time.Since(start))
 	return conn, err
 }
 
@@ -273,7 +271,6 @@ const connectionStateTTLErrFmt = "Exceeded connectionStateTtl=%v while in DISCON
 var errClosedWhileReconnecting = errors.New("connection explicitly closed while trying to reconnect")
 
 func (c *Connection) connectWithRetryLoop(arg connArgs) (result, error) {
-	lg := c.logger().sugar()
 	res, err := c.connectWith(arg)
 	if err == nil {
 		return res, nil
@@ -282,7 +279,7 @@ func (c *Connection) connectWithRetryLoop(arg connArgs) (result, error) {
 		return nil, c.setState(ConnectionStateFailed, err, 0)
 	}
 
-	lg.Errorf("Received recoverable error %v", err)
+	c.log().Errorf("Received recoverable error %v", err)
 	retryIn := c.opts.disconnectedRetryTimeout()
 	c.setState(ConnectionStateDisconnected, err, retryIn)
 	idleState := ConnectionStateDisconnected
@@ -320,14 +317,14 @@ func (c *Connection) connectWithRetryLoop(arg connArgs) (result, error) {
 				c.setState(ConnectionStateSuspended, err, c.opts.suspendedRetryTimeout())
 				idleState = ConnectionStateSuspended
 				// (RTN14f)
-				lg.Debug("Reached SUSPENDED state while opening connection")
+				c.log().Debug("Reached SUSPENDED state while opening connection")
 				retryIn = c.opts.suspendedRetryTimeout()
 				continue // wait for re-connection with new retry timeout for suspended
 			default:
 			}
 		}
 
-		lg.Debug("Attemting to open connection")
+		c.log().Debug("Attemting to open connection")
 		res, err := c.connectWith(arg)
 		if err == nil {
 			return res, nil
@@ -335,7 +332,7 @@ func (c *Connection) connectWithRetryLoop(arg connArgs) (result, error) {
 		if recoverable(err) {
 			// Go back to previous state and wait again until the next
 			// connection attempt.
-			lg.Errorf("Received recoverable error %v", err)
+			c.log().Errorf("Received recoverable error %v", err)
 			c.setState(idleState, err, retryIn)
 			continue
 		}
@@ -382,11 +379,7 @@ func (c *Connection) connectWith(arg connArgs) (result, error) {
 	c.mtx.Lock()
 	defer c.mtx.Unlock()
 
-	if c.logger().Is(LogVerbose) {
-		c.setConn(verboseConn{conn: conn, logger: c.logger()})
-	} else {
-		c.setConn(conn)
-	}
+	c.setConn(verboseConn{conn: conn, logger: c.log()})
 	// Start eventloop
 	go c.eventloop()
 
@@ -657,8 +650,8 @@ func (c *Connection) setConn(conn proto.Conn) {
 	c.connMtx.Unlock()
 }
 
-func (c *Connection) logger() *LoggerOptions {
-	return c.auth.logger()
+func (c *Connection) log() logger {
+	return c.auth.log()
 }
 
 func (c *Connection) setSerial(serial int64) {
@@ -666,13 +659,12 @@ func (c *Connection) setSerial(serial int64) {
 }
 
 func (c *Connection) resendPending() {
-	lg := c.logger().sugar()
 	c.mtx.Lock()
 	cx := make([]msgCh, len(c.pending.queue))
 	copy(cx, c.pending.queue)
 	c.pending.queue = []msgCh{}
 	c.mtx.Unlock()
-	lg.Debugf("resending %d messages waiting for ACK/NACK", len(cx))
+	c.log().Debugf("resending %d messages waiting for ACK/NACK", len(cx))
 	for _, v := range cx {
 		c.send(v.msg, v.ch)
 	}
@@ -887,11 +879,11 @@ func (c *Connection) lockedReauthorizationFailed(err error) {
 
 type verboseConn struct {
 	conn   proto.Conn
-	logger *LoggerOptions
+	logger logger
 }
 
 func (vc verboseConn) Send(msg *proto.ProtocolMessage) error {
-	vc.logger.Printf(LogVerbose, "Realtime Connection: sending %s", msg)
+	vc.logger.Verbosef("Realtime Connection: sending %s", msg)
 	return vc.conn.Send(msg)
 }
 
@@ -900,12 +892,12 @@ func (vc verboseConn) Receive(deadline time.Time) (*proto.ProtocolMessage, error
 	if err != nil {
 		return nil, err
 	}
-	vc.logger.Printf(LogVerbose, "Realtime Connection: received %s", msg)
+	vc.logger.Verbosef("Realtime Connection: received %s", msg)
 	return msg, nil
 }
 
 func (vc verboseConn) Close() error {
-	vc.logger.Printf(LogVerbose, "Realtime Connection: closed")
+	vc.logger.Verbosef("Realtime Connection: closed")
 	return vc.conn.Close()
 }
 
diff --git a/ably/realtime_presence.go b/ably/realtime_presence.go
index ed8ee43..94d99f0 100644
--- a/ably/realtime_presence.go
+++ b/ably/realtime_presence.go
@@ -35,7 +35,7 @@ type RealtimePresence struct {
 
 func newRealtimePresence(channel *RealtimeChannel) *RealtimePresence {
 	pres := &RealtimePresence{
-		messageEmitter: newEventEmitter(channel.logger()),
+		messageEmitter: newEventEmitter(channel.log()),
 		channel:        channel,
 		members:        make(map[string]*proto.PresenceMessage),
 		syncState:      syncInitial,
@@ -472,6 +472,6 @@ func (pres *RealtimePresence) auth() *Auth {
 	return pres.channel.client.Auth
 }
 
-func (pres *RealtimePresence) logger() *LoggerOptions {
-	return pres.channel.logger()
+func (pres *RealtimePresence) log() logger {
+	return pres.channel.log()
 }
diff --git a/ably/rest_channel.go b/ably/rest_channel.go
index 852771a..e37d0b6 100644
--- a/ably/rest_channel.go
+++ b/ably/rest_channel.go
@@ -287,7 +287,7 @@ func (t *fullMessagesDecoder) decodeMessagesData() {
 		*m, err = m.WithDecodedData(cipher)
 		if err != nil {
 			// RSL6b
-			t.c.logger().sugar().Errorf("Couldn't fully decode message data from channel %q: %w", t.c.Name, err)
+			t.c.log().Errorf("Couldn't fully decode message data from channel %q: %w", t.c.Name, err)
 		}
 	}
 }
@@ -318,6 +318,6 @@ func (p *MessagesPaginatedItems) Item() *Message {
 	return p.item
 }
 
-func (c *RESTChannel) logger() *LoggerOptions {
-	return c.client.logger()
+func (c *RESTChannel) log() logger {
+	return c.client.log
 }
diff --git a/ably/rest_client.go b/ably/rest_client.go
index 3b6dc3e..656adc2 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -13,7 +13,6 @@ import (
 	"mime"
 	"net/http"
 	"net/http/httptrace"
-	"net/http/httputil"
 	"net/url"
 	"reflect"
 	"strconv"
@@ -118,6 +117,7 @@ type REST struct {
 	Channels            *RESTChannels
 	opts                *clientOptions
 	successFallbackHost *fallbackCache
+	log                 logger
 }
 
 // NewREST constructs a new REST.
@@ -125,6 +125,7 @@ func NewREST(options ...ClientOption) (*REST, error) {
 	c := &REST{
 		opts: applyOptionsWithDefaults(options...),
 	}
+	c.log = logger{l: c.opts.LogHandler}
 	auth, err := newAuth(c)
 	if err != nil {
 		return nil, err
@@ -568,45 +569,26 @@ func (f *fallbackCache) put(host string) {
 }
 
 func (c *REST) doWithHandle(ctx context.Context, r *request, handle func(*http.Response, interface{}) (*http.Response, error)) (*http.Response, error) {
-	log := c.opts.Logger.sugar()
 	req, err := c.newHTTPRequest(ctx, r)
 	if err != nil {
 		return nil, err
 	}
 	if h := c.successFallbackHost.get(); h != "" {
 		req.URL.Host = h // RSC15f
-		log.Verbosef("RestClient: setting URL.Host=%q", h)
+		c.log.Verbosef("RestClient: setting URL.Host=%q", h)
 	}
 	if c.opts.Trace != nil {
 		req = req.WithContext(httptrace.WithClientTrace(req.Context(), c.opts.Trace))
-		log.Verbose("RestClient: enabling httptrace")
-	}
-	if log.Is(LogVerbose) {
-		b, err := httputil.DumpRequest(req, false)
-		if err != nil {
-			log.Error("RestClient: error trying to dump request: ", err)
-		} else {
-			log.Verbose("RestClient: ", string(b))
-		}
+		c.log.Verbose("RestClient: enabling httptrace")
 	}
 	resp, err := c.opts.httpclient().Do(req)
 	if err != nil {
-		log.Error("RestClient: failed sending a request ", err)
+		c.log.Error("RestClient: failed sending a request ", err)
 		return nil, newError(ErrInternalError, err)
 	}
-	if log.Is(LogVerbose) {
-		typ, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
-		// dumping msgpack body isn't that helpbul when debugging
-		b, err := httputil.DumpResponse(resp, typ != "application/x-msgpack")
-		if err != nil {
-			log.Error("RestClient: error trying to dump response: ", err)
-		} else {
-			log.Verbose("RestClient: ", string(b))
-		}
-	}
 	resp, err = handle(resp, r.Out)
 	if err != nil {
-		log.Error("RestClient: error handling response: ", err)
+		c.log.Error("RestClient: error handling response: ", err)
 		if e, ok := err.(*ErrorInfo); ok {
 			if canFallBack(e.StatusCode) &&
 				(strings.HasPrefix(req.URL.Host, defaultOptions.RESTHost) ||
@@ -615,7 +597,7 @@ func (c *REST) doWithHandle(ctx context.Context, r *request, handle func(*http.R
 				if c.opts.FallbackHosts != nil {
 					fallback = c.opts.FallbackHosts
 				}
-				log.Info("RestClient: trying to fallback with hosts=%v", fallback)
+				c.log.Infof("RestClient: trying to fallback with hosts=%v", fallback)
 				if len(fallback) > 0 {
 					left := fallback
 					iteration := 0
@@ -623,11 +605,11 @@ func (c *REST) doWithHandle(ctx context.Context, r *request, handle func(*http.R
 					if maxLimit == 0 {
 						maxLimit = defaultOptions.HTTPMaxRetryCount
 					}
-					log.Infof("RestClient: maximum fallback retry limit=%d", maxLimit)
+					c.log.Infof("RestClient: maximum fallback retry limit=%d", maxLimit)
 
 					for {
 						if len(left) == 0 {
-							log.Errorf("RestClient: exhausted fallback hosts", err)
+							c.log.Errorf("RestClient: exhausted fallback hosts", err)
 							return nil, err
 						}
 						var h string
@@ -647,26 +629,18 @@ func (c *REST) doWithHandle(ctx context.Context, r *request, handle func(*http.R
 						if err != nil {
 							return nil, err
 						}
-						log.Infof("RestClient:  chose fallback host=%q ", h)
+						c.log.Infof("RestClient:  chose fallback host=%q ", h)
 						req.URL.Host = h
 						req.Host = ""
 						req.Header.Set(proto.HostHeader, h)
-						if log.Is(LogVerbose) {
-							b, err := httputil.DumpRequest(req, true)
-							if err != nil {
-								log.Error("RestClient: error trying to dump retry request with fallback host: ", err)
-							} else {
-								log.Verbose("RestClient: ", string(b))
-							}
-						}
 						resp, err := c.opts.httpclient().Do(req)
 						if err != nil {
-							log.Error("RestClient: failed sending a request to a fallback host", err)
+							c.log.Error("RestClient: failed sending a request to a fallback host", err)
 							return nil, newError(ErrInternalError, err)
 						}
 						resp, err = handle(resp, r.Out)
 						if err != nil {
-							log.Error("RestClient: error handling response: ", err)
+							c.log.Error("RestClient: error handling response: ", err)
 							if iteration == maxLimit-1 {
 								return nil, err
 							}
@@ -678,16 +652,6 @@ func (c *REST) doWithHandle(ctx context.Context, r *request, handle func(*http.R
 							}
 							return nil, err
 						}
-						if log.Is(LogVerbose) {
-							typ, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
-							// dumping msgpack body isn't that helpbul when debugging
-							b, err := httputil.DumpResponse(resp, typ != "application/x-msgpack")
-							if err != nil {
-								log.Error("RestClient: error trying to dump retry response: ", err)
-							} else {
-								log.Verbose("RestClient:: ", string(b))
-							}
-						}
 						c.successFallbackHost.put(h)
 						return resp, nil
 					}
@@ -756,27 +720,22 @@ func (c *REST) newHTTPRequest(ctx context.Context, r *request) (*http.Request, e
 }
 
 func (c *REST) handleResponse(resp *http.Response, out interface{}) (*http.Response, error) {
-	log := c.opts.Logger.sugar()
-	log.Info("RestClient:checking valid http response")
+	c.log.Info("RestClient:checking valid http response")
 	if err := checkValidHTTPResponse(resp); err != nil {
-		log.Error("RestClient: failed to check valid http response ", err)
+		c.log.Error("RestClient: failed to check valid http response ", err)
 		return nil, err
 	}
 	if out == nil {
 		return resp, nil
 	}
-	log.Info("RestClient: decoding response")
+	c.log.Info("RestClient: decoding response")
 	if err := decodeResp(resp, out); err != nil {
-		log.Error("RestClient: failed to decode response ", err)
+		c.log.Error("RestClient: failed to decode response ", err)
 		return nil, err
 	}
 	return resp, nil
 }
 
-func (c *REST) logger() *LoggerOptions {
-	return &c.opts.Logger
-}
-
 func encode(typ string, in interface{}) ([]byte, error) {
 	switch typ {
 	case "application/json":
diff --git a/ably/rest_presence.go b/ably/rest_presence.go
index 11b804a..c7b606b 100644
--- a/ably/rest_presence.go
+++ b/ably/rest_presence.go
@@ -56,8 +56,8 @@ func (o *getPresenceOptions) apply(opts ...GetPresenceOption) url.Values {
 	return o.params
 }
 
-func (p *RESTPresence) logger() *LoggerOptions {
-	return p.client.logger()
+func (p *RESTPresence) log() logger {
+	return p.client.log
 }
 
 // History gives the channel's presence history.
@@ -204,7 +204,7 @@ func (t *fullPresenceDecoder) decodeMessagesData() {
 		m.Message, err = m.Message.WithDecodedData(cipher)
 		if err != nil {
 			// RSL6b
-			t.c.logger().sugar().Errorf("Couldn't fully decode presence message data from channel %q: %w", t.c.Name, err)
+			t.c.log().Errorf("Couldn't fully decode presence message data from channel %q: %w", t.c.Name, err)
 		}
 	}
 }
diff --git a/ably/state.go b/ably/state.go
index 8fd0459..756d0de 100644
--- a/ably/state.go
+++ b/ably/state.go
@@ -107,13 +107,13 @@ func channelStateError(state ChannelState, err error) *ErrorInfo {
 
 // queuedEmitter emits confirmation events triggered by ACK or NACK messages.
 type pendingEmitter struct {
-	queue  []msgCh
-	logger *LoggerOptions
+	queue []msgCh
+	log   logger
 }
 
-func newPendingEmitter(log *LoggerOptions) pendingEmitter {
+func newPendingEmitter(log logger) pendingEmitter {
 	return pendingEmitter{
-		logger: log,
+		log: log,
 	}
 }
 
@@ -143,7 +143,7 @@ func (q *pendingEmitter) Enqueue(msg *proto.ProtocolMessage, ch chan<- error) {
 	case i == q.Len():
 		q.queue = append(q.queue, msgCh{msg, ch})
 	case q.queue[i].msg.MsgSerial == msg.MsgSerial:
-		q.logger.Printf(LogWarning, "duplicated message serial: %d", msg.MsgSerial)
+		q.log.Warnf("duplicated message serial: %d", msg.MsgSerial)
 	default:
 		q.queue = append(q.queue, msgCh{})
 		copy(q.queue[i+1:], q.queue[i:])
@@ -172,11 +172,11 @@ func (q *pendingEmitter) Ack(msg *proto.ProtocolMessage, errInfo *ErrorInfo) {
 		err = errSerialSkipped
 	}
 	for _, sch := range q.queue[:nack] {
-		q.logger.Printf(LogVerbose, "received NACK for message serial %d", sch.msg.MsgSerial)
+		q.log.Verbosef("received NACK for message serial %d", sch.msg.MsgSerial)
 		sch.ch <- err
 	}
 	for _, sch := range q.queue[nack:ack] {
-		q.logger.Printf(LogVerbose, "received ACK for message serial %d", sch.msg.MsgSerial)
+		q.log.Verbosef("received ACK for message serial %d", sch.msg.MsgSerial)
 		sch.ch <- nil
 	}
 	q.queue = q.queue[ack:]
@@ -197,7 +197,7 @@ func (q *pendingEmitter) Nack(msg *proto.ProtocolMessage, errInfo *ErrorInfo) {
 	}
 	err := errInfo.unwrapNil()
 	for _, sch := range q.queue[:nack] {
-		q.logger.Printf(LogVerbose, "received NACK for message serial %d", sch.msg.MsgSerial)
+		q.log.Verbosef("received NACK for message serial %d", sch.msg.MsgSerial)
 		sch.ch <- err
 	}
 	q.queue = q.queue[nack:]
@@ -239,15 +239,15 @@ func (q *msgQueue) Flush() {
 func (q *msgQueue) Fail(err error) {
 	q.mtx.Lock()
 	for _, msgch := range q.queue {
-		q.logger().Printf(LogError, "failure sending message (serial=%d): %v", msgch.msg.MsgSerial, err)
+		q.log().Errorf("failure sending message (serial=%d): %v", msgch.msg.MsgSerial, err)
 		msgch.ch <- newError(90000, err)
 	}
 	q.queue = nil
 	q.mtx.Unlock()
 }
 
-func (q *msgQueue) logger() *LoggerOptions {
-	return q.conn.logger()
+func (q *msgQueue) log() logger {
+	return q.conn.log()
 }
 
 var nopResult *errResult


Test patch:
diff --git a/ably/ablytest/ablytest.go b/ably/ablytest/ablytest.go
index 0610a30..c0cfe08 100644
--- a/ably/ablytest/ablytest.go
+++ b/ably/ablytest/ablytest.go
@@ -17,7 +17,7 @@ import (
 
 var Timeout = 30 * time.Second
 var NoBinaryProtocol bool
-var DefaultLogger = ably.LoggerOptions{Level: ably.LogNone}
+var DefaultLogLevel = ably.LogNone
 var Environment = "sandbox"
 
 func nonil(err ...error) error {
@@ -39,7 +39,7 @@ func init() {
 		NoBinaryProtocol = true
 	}
 	if n, err := strconv.Atoi(os.Getenv("ABLY_LOGLEVEL")); err == nil {
-		DefaultLogger.Level = ably.LogLevel(n)
+		DefaultLogLevel = ably.LogLevel(n)
 	}
 	if s := os.Getenv("ABLY_ENV"); s != "" {
 		Environment = s
diff --git a/ably/ablytest/logger.go b/ably/ablytest/logger.go
index ccaa0a8..81549a2 100644
--- a/ably/ablytest/logger.go
+++ b/ably/ablytest/logger.go
@@ -19,10 +19,6 @@ type testLogger struct {
 	messages chan<- LogMessage
 }
 
-func (l testLogger) Print(level ably.LogLevel, v ...interface{}) {
-	l.Printf(level, "%s", v...)
-}
-
 func (l testLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {
 	l.messages <- LogMessage{
 		Level:   level,
@@ -34,6 +30,4 @@ var DiscardLogger ably.Logger = discardLogger{}
 
 type discardLogger struct{}
 
-func (discardLogger) Print(level ably.LogLevel, v ...interface{}) {}
-
 func (discardLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {}
diff --git a/ably/ablytest/sandbox.go b/ably/ablytest/sandbox.go
index 36782f2..e88cfd6 100644
--- a/ably/ablytest/sandbox.go
+++ b/ably/ablytest/sandbox.go
@@ -210,9 +210,8 @@ func (app *Sandbox) Options(opts ...ably.ClientOption) []ably.ClientOption {
 		ably.WithKey(app.Key()),
 		ably.WithEnvironment(app.Environment),
 		ably.WithUseBinaryProtocol(!NoBinaryProtocol),
-		ably.WithLogHandler(DefaultLogger.GetLogger()),
-		ably.WithLogLevel(DefaultLogger.Level),
 		ably.WithHTTPClient(appHTTPClient),
+		ably.WithLogLevel(DefaultLogLevel),
 	}
 
 	// If opts want to record round trips inject the recording transport
diff --git a/ably/auth_test.go b/ably/auth_test.go
index 0e7e7ca..097890a 100644
--- a/ably/auth_test.go
+++ b/ably/auth_test.go
@@ -1,7 +1,6 @@
 package ably_test
 
 import (
-	"bytes"
 	"context"
 	"encoding/base64"
 	"errors"
@@ -151,16 +150,6 @@ func TestAuth_TokenAuth(t *testing.T) {
 	}
 }
 
-type bufferLogger struct {
-	buf bytes.Buffer
-}
-
-func (b *bufferLogger) Print(level ably.LogLevel, v ...interface{}) {
-	v = append([]interface{}{level}, v...)
-	fmt.Fprint(&b.buf, v...)
-}
-func (b *bufferLogger) Printf(level ably.LogLevel, str string, v ...interface{}) {}
-
 func TestAuth_TimestampRSA10k(t *testing.T) {
 	t.Parallel()
 	now, err := time.Parse(time.RFC822, time.RFC822)
diff --git a/ably/event_emitter_spec_test.go b/ably/event_emitter_spec_test.go
index c2bde8b..bbc5669 100644
--- a/ably/event_emitter_spec_test.go
+++ b/ably/event_emitter_spec_test.go
@@ -28,7 +28,7 @@ func Test_RTE3_EventEmitter_On(t *testing.T) {
 		t.Run(fmt.Sprintf("event: %s, data: %v", tc.event, tc.data), func(t *testing.T) {
 			t.Parallel()
 
-			em := ably.NewEventEmitter(ablytest.DiscardLogger)
+			em := ably.NewEventEmitter(ably.NewInternalLogger(ablytest.DiscardLogger))
 
 			received := map[ably.EmitterString]chan ably.EmitterData{
 				"*":   make(chan ably.EmitterData, 999),
@@ -88,7 +88,7 @@ func Test_RTE4_EventEmitter_Once(t *testing.T) {
 		t.Run(fmt.Sprintf("event: %s, data: %v", tc.event, tc.data), func(t *testing.T) {
 			t.Parallel()
 
-			em := ably.NewEventEmitter(ablytest.DiscardLogger)
+			em := ably.NewEventEmitter(ably.NewInternalLogger(ablytest.DiscardLogger))
 
 			received := map[ably.EmitterString]chan ably.EmitterData{
 				"*":   make(chan ably.EmitterData, 999),
@@ -154,7 +154,7 @@ func Test_RTE5_EventEmitter_Off(t *testing.T) {
 			"bar": make(chan ably.EmitterData, 999),
 		}
 
-		em = ably.NewEventEmitter(ablytest.DiscardLogger)
+		em = ably.NewEventEmitter(ably.NewInternalLogger(ablytest.DiscardLogger))
 
 		allOff = em.OnAll(func(v ably.EmitterData) {
 			received["*"] <- v
@@ -249,7 +249,7 @@ func Test_RTE6_EventEmitter_EmitPanic(t *testing.T) {
 	t.Parallel()
 
 	logs := make(chan ablytest.LogMessage, 999)
-	em := ably.NewEventEmitter(ablytest.NewLogger(logs))
+	em := ably.NewEventEmitter(ably.NewInternalLogger(ablytest.NewLogger(logs)))
 
 	shouldPanic := true
 	called := make(chan struct{}, 999)
@@ -281,7 +281,7 @@ func Test_RTE6a_EventEmitter_EmitToFixedListenersCollection(t *testing.T) {
 	// Give it a few tries; the order in which listeners are triggered isn't
 	// deterministic.
 	for i := 0; i < 10; i++ {
-		em := ably.NewEventEmitter(ablytest.DiscardLogger)
+		em := ably.NewEventEmitter(ably.NewInternalLogger(ablytest.DiscardLogger))
 
 		addedCalled := make(chan struct{}, 1)
 		removedCalled := make(chan struct{}, 1)
diff --git a/ably/event_emitter_test.go b/ably/event_emitter_test.go
index 945b8a7..97a0543 100644
--- a/ably/event_emitter_test.go
+++ b/ably/event_emitter_test.go
@@ -8,7 +8,7 @@ import (
 )
 
 func TestEventEmitterConcurrency(t *testing.T) {
-	em := ably.NewEventEmitter(ablytest.DiscardLogger)
+	em := ably.NewEventEmitter(ably.NewInternalLogger(ablytest.DiscardLogger))
 
 	type called struct {
 		i    int
diff --git a/ably/export_test.go b/ably/export_test.go
index c9823e8..3eee6ba 100644
--- a/ably/export_test.go
+++ b/ably/export_test.go
@@ -139,3 +139,9 @@ func (c *RESTChannels) Len() int {
 	defer c.mu.RUnlock()
 	return len(c.cache)
 }
+
+func NewInternalLogger(l Logger) logger {
+	return logger{l: l}
+}
+
+type FilteredLogger = filteredLogger
diff --git a/ably/logger_test.go b/ably/logger_test.go
index e09aa13..f2af6a0 100644
--- a/ably/logger_test.go
+++ b/ably/logger_test.go
@@ -7,71 +7,44 @@ import (
 )
 
 type dummyLogger struct {
-	print  int
 	printf int
 }
 
-func (d *dummyLogger) Print(level ably.LogLevel, v ...interface{}) {
-	d.print++
-}
-
 func (d *dummyLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {
 	d.printf++
 }
 
-func TestLoggerOptions(t *testing.T) {
-	t.Run("must use default logger when no custom logger", func(t *testing.T) {
-		lg := &ably.LoggerOptions{}
-		l := lg.GetLogger()
-		if l == nil {
-			t.Fatal("expected to get a valid logger")
-		}
-		_, ok := l.(*ably.StdLogger)
-		if !ok {
-			t.Error("expected default logger")
-		}
-	})
-
-	t.Run("must use custom logger", func(t *testing.T) {
-		lg := &ably.LoggerOptions{
-			Logger: &dummyLogger{},
-		}
-		l := lg.GetLogger()
-		if l == nil {
-			t.Fatal("expected to get a valid logger")
-		}
-		_, ok := l.(*dummyLogger)
-		if !ok {
-			t.Error("expected custom logger")
-		}
-	})
-
+func TestFilteredLogger(t *testing.T) {
 	t.Run("must log smaller or same level", func(t *testing.T) {
-		lg := &ably.LoggerOptions{
+		t.Parallel()
+
+		l := &dummyLogger{}
+		lg := &ably.FilteredLogger{
 			Level:  ably.LogDebug,
-			Logger: &dummyLogger{},
+			Logger: l,
 		}
 		say := "log this"
-		lg.Print(ably.LogVerbose, say)
-		lg.Print(ably.LogInfo, say)
-		lg.Print(ably.LogWarning, say)
-		lg.Print(ably.LogError, say)
-		l := lg.GetLogger().(*dummyLogger)
-		if l.print != 4 {
-			t.Errorf("expected 4 log messages got %d", l.print)
+		lg.Printf(ably.LogVerbose, say)
+		lg.Printf(ably.LogInfo, say)
+		lg.Printf(ably.LogWarning, say)
+		lg.Printf(ably.LogError, say)
+		if l.printf != 4 {
+			t.Errorf("expected 4 log messages got %d", l.printf)
 		}
 	})
 	t.Run("must log nothing  for LogNone", func(t *testing.T) {
-		lg := &ably.LoggerOptions{
-			Logger: &dummyLogger{},
+		t.Parallel()
+
+		l := &dummyLogger{}
+		lg := &ably.FilteredLogger{
+			Logger: l,
 		}
 		say := "log this"
-		lg.Print(ably.LogVerbose, say)
-		lg.Print(ably.LogInfo, say)
-		lg.Print(ably.LogWarning, say)
-		lg.Print(ably.LogError, say)
-		l := lg.GetLogger().(*dummyLogger)
-		if l.print > 0 {
+		lg.Printf(ably.LogVerbose, say)
+		lg.Printf(ably.LogInfo, say)
+		lg.Printf(ably.LogWarning, say)
+		lg.Printf(ably.LogError, say)
+		if l.printf > 0 {
 			t.Error("expected nothing to be logged")
 		}
 	})
diff --git a/ably/realtime_conn_spec_test.go b/ably/realtime_conn_spec_test.go
index dcef3db..ded3517 100644
--- a/ably/realtime_conn_spec_test.go
+++ b/ably/realtime_conn_spec_test.go
@@ -2237,10 +2237,6 @@ type writerLogger struct {
 	w io.Writer
 }
 
-func (w *writerLogger) Print(level ably.LogLevel, v ...interface{}) {
-	fmt.Fprint(w.w, v...)
-}
-
 func (w *writerLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {
 	fmt.Fprintf(w.w, format, v...)
 }
diff --git a/ably/state_test.go b/ably/state_test.go
index 47d3f11..2d75f25 100644
--- a/ably/state_test.go
+++ b/ably/state_test.go
@@ -37,13 +37,17 @@ func emit(msg *proto.ProtocolMessage, fn func(*pendingEmitter, *proto.ProtocolMe
 	}
 }
 
+type discardLogger struct{}
+
+func (discardLogger) Printf(level LogLevel, format string, v ...interface{}) {}
+
 func testQueuedEmitter(t *testing.T, serials, ack, nack []int64, emit func(*pendingEmitter)) {
 	ch := chans(len(serials))
 	index := make(map[int64]int)
 	for i, serial := range serials {
 		index[serial] = i
 	}
-	q := &pendingEmitter{logger: &LoggerOptions{}}
+	q := &pendingEmitter{log: NewInternalLogger(discardLogger{})}
 	for serial, i := range index {
 		q.Enqueue(&proto.ProtocolMessage{MsgSerial: serial}, ch[i])
 	}