Go 日志组件封装

uuid · 2021-10-28 12:53:32 · 2115 次点击 · 预计阅读时间 10 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2021-10-28 12:53:32 的文章,其中的信息可能已经有所发展或是发生改变。

背景

Go目前流行多个日志组件:logrus、zap、zerolog、seelog,他们有各自的优点,但是使用方式存在差异, 如何屏蔽这些差异,对外提供统一的接口,减少对具体的日志组件依赖,从而方便后续组件替换?

对此,我们可以增加一层防腐层,定义一套统一的日志接口,业务代码不直接依赖具体的日志实现,而是依赖抽象接口,这样就可以将具体的实现进行隔离,业务代码就不会包含具体日志组件相关的代码,当日志组件需要替换时,只要做一层适配,实现约定好的日志接口方法,就可以无缝进行切换日志组件。

方案

定义日志接口

我们需要预先定义好一套完整的日志接口:

log/log.go

package log

type Fields map[string]interface{} 

type Logger interface {
    WithField(key string, value interface{}) Logger
    WithFields(fields Fields) Logger
    Trace(args ...interface{})
    Tracef(format string, args ...interface{})
    Debug(args ...interface{})
    Debugf(format string, args ...interface{})
    Info(args ...interface{})
    Infof(format string, args ...interface{})
    Warn(args ...interface{})
    Warnf(format string, args ...interface{})
    Error(args ...interface{})
    Errorf(format string, args ...interface{})
    Panic(args ...interface{})
    Panicf(format string, args ...interface{})
    Fatal(args ...interface{})
    Fatalf(format string, args ...interface{})
}

接口包含日常使用的所有方法,接口没有任何的外部依赖。

接口定义放在独立的log包下,不需要担心包名和原来的日志冲突,因为到时会替换所有的日志组件

定义全局日志

日志组件一般会有一个全局默认的实例,使用的时候直接调用包下对应的函数,例如:

log.Info("hello world")
log.Error("hello world")

所以我们同样定义一套包级别的导出方法:

log/exported.go

package log

var std Logger // 标准输出

func WithField(key string, value interface{}) Logger {
    return std.WithField(key, value)
}
func WithFields(fields Fields) Logger {
    return std.WithFields(fields)
}
func Trace(args ...interface{}) {
    std.Trace(args...)
}
func Tracef(format string, args ...interface{}) {
    std.Tracef(format, args...)
}
func Debug(args ...interface{}) {
    std.Debug(args...)
}
func Debugf(format string, args ...interface{}) {
    std.Debugf(format, args...)
}
func Info(args ...interface{}) {
    std.Info(args...)
}
func Infof(format string, args ...interface{}) {
    std.Infof(format, args...)
}
func Warn(args ...interface{}) {
    std.Warn(args...)
}
func Warnf(format string, args ...interface{}) {
    std.Warnf(format, args...)
}
func Error(args ...interface{}) {
    std.Error(args...)
}
func Errorf(format string, args ...interface{}) {
    std.Errorf(format, args...)
}
func Panic(args ...interface{}) {
    std.Panic(args...)
}
func Panicf(format string, args ...interface{}) {
    std.Panicf(format, args...)
}
func Fatal(args ...interface{}) {
    std.Fatal(args...)
}
func Fatalf(format string, args ...interface{}) {
    std.Fatalf(format, args...)
}
// 初始化全局日志
func InitGlobal(s Logger)  {
    std = s
}

注意:

  • 全局日志使用前需要调用初始化方法:InitGlobal(s Logger)
  • exported.go同样也是不依赖具体实现,只依赖接口Logger

适配Logrus组件

如果项目要使用Logrus时,我们可以对Logrus做一层适配,实现我们的定义好的接口:

log/logrus.go

/**
 * @Title Logrus适配
 * @Description
 * @Author hyman
 * @Date 2021-10-26
 **/
package log

import (
    "github.com/sirupsen/logrus"
)

type logrusAdapt struct {
    l *logrus.Logger
}

func (s logrusAdapt) WithField(key string, value interface{}) Logger {
    return newFieldAdapt(s.l.WithField(key, value))
}

func (s logrusAdapt) Tracef(format string, args ...interface{}) {
    s.l.Tracef(format, args...)
}

func (s logrusAdapt) Debugf(format string, args ...interface{}) {
    s.l.Debugf(format, args...)
}

func (s logrusAdapt) Infof(format string, args ...interface{}) {
    s.l.Infof(format, args...)
}

func (s logrusAdapt) Warnf(format string, args ...interface{}) {
    s.l.Warnf(format, args...)
}

func (s logrusAdapt) Errorf(format string, args ...interface{}) {
    s.l.Errorf(format, args...)
}

func (s logrusAdapt) Panicf(format string, args ...interface{}) {
    s.l.Panicf(format, args...)
}

func (s logrusAdapt) Fatalf(format string, args ...interface{}) {
    s.l.Fatalf(format, args...)
}

func (s logrusAdapt) WithFields(fields Fields) Logger {
    return newFieldAdapt(s.l.WithFields(logrus.Fields(fields)))
}

func (s logrusAdapt) Trace(args ...interface{}) {
    s.l.Trace(args...)
}

func (s logrusAdapt) Debug(args ...interface{}) {
    s.l.Debug(args...)
}

func (s logrusAdapt) Print(args ...interface{}) {
    s.l.Print(args...)
}

func (s logrusAdapt) Info(args ...interface{}) {
    s.l.Info(args...)
}

func (s logrusAdapt) Warn(args ...interface{}) {
    s.l.Warn(args...)
}

func (s logrusAdapt) Error(args ...interface{}) {
    s.l.Error(args...)
}

func (s logrusAdapt) Panic(args ...interface{}) {
    s.l.Panic(args...)
}

func (s logrusAdapt) Fatal(args ...interface{}) {
    s.l.Fatal(args...)
}
// 封装logrus.Entry
type fieldAdapt struct {
    e *logrus.Entry
}

func (f fieldAdapt) WithField(key string, value interface{}) Logger {
    return newFieldAdapt(f.e.WithField(key, value))
}

func (f fieldAdapt) WithFields(fields Fields) Logger {
    return newFieldAdapt(f.e.WithFields(logrus.Fields(fields)))
}

func (f fieldAdapt) Tracef(format string, args ...interface{}) {
    panic("implement me")
}

func (f fieldAdapt) WithError(err error) Logger {
    return newFieldAdapt(f.e.WithError(err))
}

func (f fieldAdapt) Debugf(format string, args ...interface{}) {
    f.e.Debugf(format, args...)
}

func (f fieldAdapt) Infof(format string, args ...interface{}) {
    f.e.Infof(format, args...)
}

func (f fieldAdapt) Printf(format string, args ...interface{}) {
    f.e.Printf(format, args...)
}

func (f fieldAdapt) Warnf(format string, args ...interface{}) {
    f.e.Warnf(format, args...)
}

func (f fieldAdapt) Warningf(format string, args ...interface{}) {
    f.e.Warningf(format, args...)
}

func (f fieldAdapt) Errorf(format string, args ...interface{}) {
    f.e.Errorf(format, args...)
}

func (f fieldAdapt) Fatalf(format string, args ...interface{}) {
    f.e.Fatalf(format, args...)
}

func (f fieldAdapt) Panicf(format string, args ...interface{}) {
    f.e.Panicf(format, args...)
}

func (f fieldAdapt) Debug(args ...interface{}) {
    f.e.Debug(args...)
}

func (f fieldAdapt) Info(args ...interface{}) {
    f.e.Info(args...)
}

func (f fieldAdapt) Print(args ...interface{}) {
    f.e.Print(args...)
}

func (f fieldAdapt) Warn(args ...interface{}) {
    f.e.Warn(args...)
}

func (f fieldAdapt) Warning(args ...interface{}) {
    f.e.Warning(args...)
}

func (f fieldAdapt) Error(args ...interface{}) {
    f.e.Error(args...)
}

func (f fieldAdapt) Fatal(args ...interface{}) {
    f.e.Fatal(args...)
}

func (f fieldAdapt) Panic(args ...interface{}) {
    f.e.Panic(args...)
}

func (f fieldAdapt) Debugln(args ...interface{}) {
    f.e.Debugln(args...)
}

func (f fieldAdapt) Infoln(args ...interface{}) {
    f.e.Infoln(args...)
}

func (f fieldAdapt) Println(args ...interface{}) {
    f.e.Println(args...)
}

func (f fieldAdapt) Warnln(args ...interface{}) {
    f.e.Warnln(args...)
}

func (f fieldAdapt) Warningln(args ...interface{}) {
    f.e.Warningln(args...)
}

func (f fieldAdapt) Errorln(args ...interface{}) {
    f.e.Errorln(args...)
}

func (f fieldAdapt) Fatalln(args ...interface{}) {
    f.e.Fatalln(args...)
}

func (f fieldAdapt) Panicln(args ...interface{}) {
    f.e.Panicln(args...)
}

func (f fieldAdapt) Trace(args ...interface{}) {
    f.e.Trace(args...)
}

func newFieldAdapt(e *logrus.Entry) Logger {
    return fieldAdapt{e}
}

func NewLogrusAdapt(l *logrus.Logger) Logger {
    return &logrusAdapt{
        l: l,
    }
}

适配器只是对logrus实例进行轻量级封装,采用组合方式,不会破坏原有逻辑。

logrus的WithField功能比较特别,返回的是logrus.Entry对象,所以还需要对logrus.Entry进行适配:fieldAdapt

项目使用

封装好后,要如何使用?

首先一般我们是在main.go中实例化我们的日志对象

import (
    "github.com/sirupsen/logrus"
    "log/log"
)
// 全局日志实例
log.InitGlobal(log.NewLogrusAdapt(logrus.StandardLogger())) // 初始化
// 自定义日志实例
var customLog = log.NewLogrusAdapt(logrus.New())

我们只是对组件进行适配,组件的配置还是和原来保持一样,例如:日志输出,级别等

logrusLog := logrus.New()
logrusLog.SetLevel(logrus.InfoLevel)
src, err := os.OpenFile(os.DevNull, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
    panic(err)
}
writer := bufio.NewWriter(src)
logrusLog.SetOutput(writer)
customLog:=log.NewLogrusAdapt(logrusLog)

注:一般情况下项目只有main.go才会和具体日志组件有耦合,其他地方只会和logger接口进行关联,后续需要替换日志组件,只需要修改main.go。

全局日志调用

import (
    "log/log"
)
func Hello() {
  log.Info("hellow world") // 调用
}

自定义日志调用

一般通过注入方式,把具体日志实例注入到模块中,然后在模块中使用:

main.go

customLog:=log.NewLogrusAdapt(logrusLog)
business:= NewBusiness(customLog)

business.go

import (
    "log/log"
)
type Business struct {
  log log.Logger
}
func (r *Business) SayHello() {
  r.log.Info("say hello")
}
func NewBusiness(log log.Logger) *Business {
  return &Business{log: log}
}

组件替换

后续如果需要替换日志组件只需要增加对应的适配器,实现指定的接口,然后修改main.go中的实例方法就可以达到日志替换的目的。

原文地址:Go 日志组件封装 | 艺术码农的小栈


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2115 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传