Go 日志组件封装

2021/10/28 实践总结Go

# 背景

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{})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

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

定义全局日志

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

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

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

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

注意:

  • 全局日志使用前需要调用初始化方法: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,
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213

适配器只是对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())
1
2
3
4
5
6
7
8

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

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)
1
2
3
4
5
6
7
8
9

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

全局日志调用

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

1
2
3
4
5
6
7

自定义日志调用

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

main.go

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

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}
}
1
2
3
4
5
6
7
8
9
10
11
12

# 组件替换

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