Plugging logrus into go-retryablehttp
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.