Reputation: 949
I want to wrap / abstract
the log API (hide the implementation) and use some library
The reason that we want to hide the implementation is that we want to provide our log API and hide the logger lib which is used under the hod , now its [logrus][1]
and it can be also zap
klog
and who use the log api do not need to change the his log code usage when I switch to different logger implementation (we are just changing the engine... )
What I did is create struct and the init logger
return my struct and in addition create functions (see below) that wrap the functionality ,
package logger
import (
"fmt"
"os"
"github.com/sirupsen/logrus"
)
const (
AppLogLevel = "APP_LOG"
defLevel = "error"
)
type Logger struct {
label string
version string
loggerImpl *logrus.Logger
}
// init logger
func NewLogger(level string,version string) *Logger {
lvl := logLevel(level)
logger := &logrus.Logger{
Out: os.Stdout,
Level: lvl,
Formatter: &logrus.TextFormatter{},
}
return &Logger{
version: version,
loggerImpl: logger,
}
}
// GetLogLevel - Get level from env
func getLogLevel() string {
lvl, _ := os.LookupEnv(AppLogLevel)
if lvl != "" {
return lvl
}
return defLevel
}
func logLevel(lvl string) logrus.Level {
switch lvl {
case "debug":
return logrus.DebugLevel
case "info":
return logrus.InfoLevel
case "error":
return logrus.ErrorLevel
case "warn":
return logrus.WarnLevel
case "fatal":
return logrus.FatalLevel
case "panic":
return logrus.PanicLevel
default:
panic(fmt.Sprintf("the specified %s log level is not supported", lvl))
}
}
func (logger *Logger) SetLevel(level string) {
lvl := logLevel(level)
logger.loggerImpl.SetLevel(lvl)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
logger.loggerImpl.Debugf(format, args...)
}
func (logger *Logger) Infof(format string, args ...interface{}) {
logger.loggerImpl.Infof(format, args...)
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
logger.loggerImpl.Errorf(format, args...)
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
logger.loggerImpl.Fatalf(format, args...)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
logger.loggerImpl.Panicf(format, args...)
}
func (logger *Logger) Debug(args ...interface{}) {
logger.loggerImpl.Debug(args...)
}
func (logger *Logger) Info(args ...interface{}) {
logger.loggerImpl.Info(args...)
}
func (logger *Logger) Warn(args ...interface{}) {
logger.loggerImpl.Warn(args...)
}
func (logger *Logger) Error(args ...interface{}) {
logger.loggerImpl.Error(args...)
}
func (logger *Logger) Fatal(args ...interface{}) {
logger.loggerImpl.Fatal(args...)
}
func (logger *Logger) Panic(args ...interface{}) {
logger.loggerImpl.Panic(args...)
}
...
Do I miss something ? As when I try to change it to zap (change the structure to the following:
type Logger struct {
label string
version string
loggerImpl *zap.Logger
}
This code is not working (all the functions code which works for logrus)
logger.loggerImpl.SetLevel(lvl)
and also
logger.loggerImpl.Tracef(format, args...)
etc as zap
lib doenst have them ,any idea how to abstract it which can support both or more in the future?
update
I try with the following (adapter pattern) : (but it looks that inside the method I have now recursive calls ) any idea how to avoid it ?
package logger
import (
log "github.com/sirupsen/logrus"
)
type Logger struct {
adapter Adapter
}
func (l *Logger) SetLogger(a Adapter) {
l.adapter = a
}
func (l *Logger) Debugf(fmt string, args ...interface{}) {
l.adapter.Debugf(fmt, args...)
}
type Adapter interface {
SetLevel(level string)
Tracef(format string, args ...interface{})
Debugf(string, ...interface{})
Infof(format string, args ...interface{})
Warnf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
Trace(args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Warn(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
}
type StdLoggerAdapter struct {
}
func (l StdLoggerAdapter) SetLevel(level string) {
lvl := logLevel(level)
l.SetLevel(string(lvl))
}
func (l StdLoggerAdapter) Tracef(format string, args ...interface{}) {
l.Tracef(format, args...)
}
func (l StdLoggerAdapter) Infof(format string, args ...interface{}) {
l.Infof(format,args)
}
func (l StdLoggerAdapter) Warnf(format string, args ...interface{}) {
l.Warnf(format,args)
}
...
func (l StdLoggerAdapter) Debugf(fmt string, args ...interface{}) {
log.Printf(fmt, args...)
}
func NewLogger(a Adapter) Logger {
return Logger{adapter: a}
}
func main() {
logger := NewLogger(StdLoggerAdapter{})
logger.Debugf("stdlib logger debug msg")
}
func logLevel(lvl string) log.Level {
var level log.Level
switch lvl {
//case "trace":
// level = log.TraceLevel
case "debug":
level = log.DebugLevel
case "info":
level = log.InfoLevel
case "warn":
level = log.WarnLevel
case "error":
level = log.ErrorLevel
case "fatal":
level = log.FatalLevel
case "panic":
level = log.PanicLevel
default:
level = log.ErrorLevel
}
return level
}
Upvotes: 2
Views: 3055
Reputation: 2473
I had the same issue, I had to create a library that would be used by multiple microservices and needed some HTTP logging. Instead of making all microservices use one log library, I had to allow either logrus or zap.
I created a logger interface with two implementations (logrus and zap).
Upvotes: 0
Reputation: 333
Suggest thinking about the problem differently and avoiding the type/interface shenanigans entirely. Create a stateless logger with an API you like and that feels reasonable to implement across different loggers. If you want to update the underlying logger, update it, users only need to update their dependencies to see it. If you want to maintain separation, you can isolate logger backends by import paths.
https://gitlab.com/tight5/kit/blob/master/logger/logger.go follows this and might inspire your own purposes. It's heavily based around go-kit's logger, which we liked to begin with, and wanted to be able to drop it in as needed for instrumentation. It also redirects the stdlib log package, which we valued so users did not have to update all of they're existing log statements to our library.
package main
import (
"gitlab.com/tight5/kit/logger"
"log"
)
func main() {
logger.Info().Log("msg", "this is a logging example")
log.Println("also logs through kit logger")
}
https://play.golang.org/p/6sBfI85Yx6g
go-kit
s log interface is powerful and I'd evaluate that before any home grow: https://github.com/go-kit/kit/blob/master/log/log.go is just func Log(keyvals ...interface{})
, and supports zap
and logrus
backends right now.
https://github.com/go-kit/kit/tree/master/log/logrus
https://github.com/go-kit/kit/tree/master/log/zap
In the linked package-level logger for example, changing the backend for users is as easy as changing the default in: https://gitlab.com/tight5/kit/blob/master/logger/logger.go#L42-53 (we called it format in our abstraction, but you're really just picking the logger implementation)
Here is an excerpt from the above to show an example implementation for logrus (zap would be very similar).
... other imports ...
import kitlogrus "github.com/go-kit/kit/log/logrus"
import "github.com/sirupsen/logrus"
func Init(format LogFormat, logLevel LogLevel) {
var l log.Logger
switch format {
case FormatJson:
l = log.NewJSONLogger(os.Stdout)
case FormatLogfmt:
l = log.NewLogfmtLogger(os.Stdout)
case FormatNop:
l = log.NewNopLogger()
case FormatLogrus:
l = kitlogrus.NewLogrusLogger(logrus.New())
case FormatZap:
default:
panic(fmt.Errorf("invalid log format: %v", format))
}
...
}
Hope this sparks an idea for your own implementation!
Upvotes: 4
Reputation: 768
I'm a bit confused to be honest about what you're trying to do.
In general, to answer the meat of your question:
To make it possible to switch between different logging libraries in your code, you must define a specific interface and then implement it for each library. Since you can't implement methods on structs in another package, you will have to wrap the other libraries and define the methods on your wrappers.
Your example code has 'level' as a property of your logger; I guess you want your logger to decide what level of logging you want, and just use the library logger as a pipe for pumping messages.
So, let's assume a simplified version:
type LogLevel int
const (
LevelInfo LogLevel = iota
LevelDebug
LevelWarn
LevelError
LevelFatal
)
type interface ILogger {
Log(args ...interface{})
}
type struct Logger {
Level LogLevel
internal ILogger
}
This will be the basis of everything else.
There's a question worth pausing over:
Do the different loggers provide compatible interfaces? If you switch between 'zap' and 'logrus' is it because you maybe actually want to use their specific interfaces? Maybe they provide some more specialized functionality that you actually want.
If you hide them behind a common interface like ILogger
here, you will lose any benefit these loggers provide in terms of actually logging.
Anyway, we will proceed, ignoring this concern, let's see how you can use these primitives:
func NewLogger(internal ILogger) *Logger {
return &Logger{
Level: LeveLInfo,
internal: internal,
}
}
func (logger *Logger) Log(level LogLevel, args ...interface{}) {
if level >= logger.Level {
logger.internal.Log(args...)
}
}
func (logger *Logger) Logf(level LogLevel, fmt string, args ...interface{}) {
if level >= logger.Level {
msg := fmt.Sprintf(fmt, args...)
logger.internal.Log(msg)
}
}
Now you can implement Info
and Infof
and Debug
and Debugf
.. etc as simple convenience methods.
Here's an example. The rest will be left as an exercise to the reader.
func (logger *Logger) Infof(format string, args ...interface{}) {
logger.Logf(LevelInfo, format, args...)
}
func (logger *Logger) Info(args ...interface{}) {
logger.Log(LevelInfo, args...)
}
Now the hard part: forcing all third party libraries to conform to your interface.
I'm not familiar with all the different logging libraries, so this may prove to be not so simple. You might have to change the design of the ILogger
interface to make it more feasible.
In any case, this is how you would generally do it:
type ZapLogger *zap.Logger
func (z ZapLogger) Log(args ...interface{}) {
zz := *zap.Logger(z)
// TODO: do something to pump `args` into `zz`
}
type LogrusLogger *logrus.Logger
func (g LogrusLogger) Log(args ...interface{}) {
gg := *logrus.Logger(g)
// TODO: do something to pump `args` into `gg`
}
Now, you have these types like LogrusLogger
and ZapLogger
which implement ILogger
and can be easily cast back and forth with the underlying logging library at no cost.
So you can instantiate your own logger to wrap the underlying 3rd party logger
var underlying *logrus.Logger = MakeMyLogrusLogger(...)
var myLogger = NewLogger(LogrusLogger(underlying))
Now everyone can call myLogger.Infof(....)
to log stuff.
If you ever decide to switch to zap
or whatever, you would change the above line (and also you would have to define the implementation of the ILogger interface for zap)
var underlying *zap.Logger = MakeMyZapLogger(...)
var myLogger = NewLogger(ZapLogger(underlying))
With all that said, I think this whole endeavor is fruitless and not really worth it. Since these libraries don't seem to provide compatible interfaces, and the whole point of using one library over the other is because you like the different interface that this other library provides for you.
Upvotes: 1
Reputation: 51467
You can use an adapter:
package main
import (
"log"
"github.com/sirupsen/logrus"
)
type Logger struct {
adapter Adapter
}
func (l *Logger) SetLogger(a Adapter) {
l.adapter=a
}
func (l *Logger) Debugf(fmt string,args...interface{}) {
l.adapter.Debugf(fmt,args...)
}
type Adapter interface {
Debugf(string,...interface{})
}
type StdLoggerAdapter struct {}
func (l StdLoggerAdapter) Debugf(fmt string,args...interface{}) {
log.Printf(fmt,args...)
}
type LogrusAdapter struct {}
func (l LogrusAdapter) Debugf(fmt string,args...interface{}) {
logrus.Debugf(fmt,args...)
}
func NewLogger(a Adapter) Logger {
return Logger{adapter:a}
}
func main() {
logger:=NewLogger(StdLoggerAdapter{})
logger.Debugf("stdlib logger debug msg")
logger.SetLogger(LogrusAdapter{})
logger.Debugf("logrus debug msg")
}
Upvotes: 1
Reputation: 342
I have created this repository for personal use I think it can be improved to serves your purpose.
You can have a look.
PS: While adding new logger like (zerolog) you can change value of the variable logger
and change the methods (Info(args ...)
, Debug(args ...)
etc.) according to your needs.
Upvotes: 1
Reputation: 83
Abstract out the methods required or common methods by creating a interface and implement the interface like:
type Logger interface {
SetLevel(level string)
Errorf(format string, args ...interface{})
}
type LogrusLogger struct {
label string
version string
loggerImpl *logrus.Logger
}
type zapLogger struct {
label string
version string
loggerImpl *logrus.Logger
}
Initialize the log based on requirement:
Logger log := new LogrusLogger{}
or
Logger log := new ZapLogger{}
use it as:
log.Errorf("message")
Upvotes: 0