背景

日志是框架中至关重要的一部分。在 Golang 只也提供了基本的日志组件,但其没有提供给比较多的功能。当然现在也有很多第三方已经封装好的库使用起来也很方便。作为成熟的开发者也应该学会封装自己的类库,以便后续在自己的个人项目中使用它。

Logger 封装的理解

Logger 库主要是一系列方法或接口,提供给应用程序代码调用然后将相关的数据记录到日志记录库中。就是在代码和底层日志库中间的一层。需要实现的功能是能够记录日志,且支持不同日志记录库的切换或添加新的记录库。

封装包,首先需要先定义一些能够描述日志方法的接口,基本的都是你的应用程序所需要的方法。比如,基本的日志接口基本的方法有 Info、Warn、 Error。然后就可以创建此接口的实现,将数据记录到记录库中。定义了接口和实现,就可以创建一个 logger 封装,并将相关的接口方法公开。

简单包封装

以下定义了五个方法:Info, Warn, Error 和 Fatal. 这些方法接口接收 2个参数,一个字符串和一个字段映射map.

type Logger interface {
	Debug(msg string, field map[string]interface{})
	Info(msg string, field map[string]interface{})
	Warn(msg string, field map[string]interface{})
	Error(msg string, field map[string]interface{})
	Fatal(msg string, field map[string]interface{})
}

接下来就是实现啦 Logger 方法,底层的实现主要是依赖使用 Logrus。首先需要创建一个 LLogger 结构体,该结构体包含 Logrus 实例。当然还需要一个 NewLLogger 方法来实例化一个 LLogger。

type LLogger struct {
	logger *logrus.Logger
	ctx    context.Context
}
func NewLLogger(ctx context.Context) *LLogger {
	logger := logrus.New()
	logger.Out = os.Stdout
	return &LLogger{logger: logger, ctx: ctx}
}
func (l *LLogger) Info(msg string, fields map[string]interface{}) {
	l.logger.WithFields(fields).Info(msg)
}
func (l *LLogger) Warn(msg string, fields map[string]interface{}) {
	l.logger.WithFields(fields).Warn(msg)
}
func (l *LLogger) Error(msg string, fields map[string]interface{}) {
	l.logger.WithFields(fields).Error(msg)
}
func (l *LLogger) Fatal(msg string, fields map[string]interface{}) {
	l.logger.WithFields(fields).Fatal(msg)
}

这样一个简单日志封装就完成了,使用时就可以直接使用。

func main() {
	ctx := ... // context 信息
	lg := NewLLogger(ctx)
	fields := map[string]interface{}{
		"userId":    "xiaoxiong",
		"ipAddress": "127.0.0.1",
	}
	lg.Info("这是测试信息", fields)
}

多日志库切换

当底层需要使用的是不同的日志库进行记录日志时,那就需要封装另外的日志,但是在实例化方法时可以增加一种日志类型参数,然后根据不同的类型实例化不同的日志。

比如使用了zap 包,那童养媳与奥实现接口的四个方法 :Info, Warn, Error and Fatal.

type ZapLog struct {
	logger *zap.Logger
	ctx    context.Context
}
func NewZapLog(ctx context.Context) *ZapLog {
	logger, _ := zap.NewProduction()
	return &ZapLog{logger: logger, ctx: ctx}
}
func (l *ZapLog) Info(msg string, fields map[string]interface{}) {
	l.logger.Info(msg, zap.Any("args", fields))
}
func (l *ZapLog) Warn(msg string, fields map[string]interface{}) {
	l.logger.Warn(msg, zap.Any("args", fields))
}
func (l *ZapLog) Error(msg string, fields map[string]interface{}) {
	l.logger.Error(msg, zap.Any("args", fields))
}
func (l *ZapLog) Fatal(msg string, fields map[string]interface{}) {
	l.logger.Fatal(msg, zap.Any("args", fields))
}

定义其他日志库的方法后就只需对这个两个日志库进行封装,根据不同的类型初始化出不同的日志,然后使用日志时候就使用相应的包进行处理。

type LoggerWrapper struct {
	logger Logger
}
func NewLoggerWrapper(loggerType string, ctx context.Context) *LoggerWrapper {
	var logger Logger
	switch loggerType {
	case "logrus":
		logger = NewLLogger(ctx)
	case "zap":
		logger = NewZapLog(ctx)
	default:
		logger = NewLLogger(ctx)
	}
	return &LoggerWrapper{logger: logger}
}

使用中间件记录公共信息

封装中增加了 Context 上下文的内容,主要是为了记录一些公共属性。就像经常在链路追踪时可需要请求ID ,请求IP 等相关的信息。但是日志实例都是在程序启动时进行初始化的,在每次的请求时可能请求ID或者IP 都是不一样,所以要记录这部分信息,可以使用中间件将上下文的信息添加到日志消息中。