Plugging logrus into go-retryablehttp

Yoan Blanc
2 min readFeb 5, 2022

How to leverage interfaces to combine libraries

So you’re already using logrus as a logging library but have trouble making it play nicely with Hashicorp’s go-retryablehttp. Here is a little solution inspired by the following issue: hashicorp/go-retryablehttp#101

The problem

The logger used by the Hashicorp library expects a logger with message, keys, and values while logrus offers an interface with formatting (à la fmt.Printf).

The goal will be create an adapter to our underlying logging library, logrus, that can be used by the go-retryablehttp library. Let’s take a peak at the interface it expects.

// LeveledLogger is an interface that can be implemented by any logger or a
// logger wrapper to provide leveled logging. The methods accept a message
// string and a variadic number of key-value pairs.
type LeveledLogger interface {
Error(msg string, keysAndValues ...interface{})
Info(msg string, keysAndValues ...interface{})
Debug(msg string, keysAndValues ...interface{})
Warn(msg string, keysAndValues ...interface{})
}

Turning logrus into a LeveledLogger

Create a new struct that embeds our logrus logger and implements the above interface.

type LeveledLogrus struct {
*logrus.Logger
}
func (l *LeveledLogrus) Error(msg string, keysAndValues ...interface{}) {
l.WithFields(keysAndValues).Error(msg)
}

func (l *LeveledLogrus) Info(msg string, keysAndValues ...interface{}) {
l.WithFields(keysAndValues).Info(msg)
}
func (l *LeveledLogrus) Debug(msg string, keysAndValues ...interface{}) {
l.WithFields(keysAndValues).Debug(msg)
}

func (l *LeveledLogrus) Warn(msg string, keysAndValues ...interface{}) {
l.WithFields(keysAndValues).Warn(msg)
}

The issue there is that WithFields expects a map[string]interface{} when we only have a slice of interface at hand. The function below transforms the slice of values into the expected map.


func fields(keysAndValues []interface{}) map[string]interface{} {
fields := make(map[string]interface{})

for i := 0; i < len(keysAndValues)-1; i += 2 {
fields[keysAndValues[i].(string)] = keysAndValues[i+1]
}

return fields
}

This function has many flaws such that it will truncate values or blow up if any key is not a string. Please don’t copy and paste code from Medium and run it in production.

Embedding a logrus logger and giving it to go-retryablehttp is as simple as follow.

log := logrus.New()
logger := retryablehttp.LeveledLogger(&LeveledLogrus{log})

logger.Info("hello", "name", "world", "age", 2020)

You can run this example on Go play, https://go.dev/play/p/zgxiykbXaol

Conclusion

In doubt, as a library author, consider the go-logr initiative such that writing those kind of adapters is not required.

--

--