JME
JME

Reputation: 949

Abstract implementation of log repo

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

Answers (6)

Arun Gopalpuri
Arun Gopalpuri

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

Zaq Wiedmann
Zaq Wiedmann

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-kits 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

user10753492
user10753492

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

Burak Serdar
Burak Serdar

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

Gagan Chouhan
Gagan Chouhan

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.

log-wrapper

Upvotes: 1

swapna p
swapna p

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

Related Questions