0%

zap日志库实战

背景

Golang项目中日志打印是很重要的功能。方便记录和定位程序执行过程及发生的错误。Golang提供了基础的log功能,但是功能不够强大,无法支持如日志格式或者日志归档功能。此次介绍的为zap日志库

zap案例

json类型日志打印在控制台

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
func createJsonLogger() *zap.Logger {
// 1、 创建logger配置
config := zapcore.EncoderConfig{
MessageKey: "msg", // json中msg信息的key名字,如果为空串,则json不会打印该信息
LevelKey: "level", // 日志级别的key名字
TimeKey: "ts", // 时间的key名字
CallerKey: "file", // 日志打印的文件及行号的key名字
StacktraceKey: "stacktrace", // 堆栈调用信息的key名字
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeLevel: zapcore.CapitalLevelEncoder, // 日志级别大写
EncodeCaller: zapcore.ShortCallerEncoder, // 短路径,文件名+行号
}
// 2、创建日志编码器
jsonEncoder := zapcore.NewJSONEncoder(config)
// 2、 创建日志 core
core := zapcore.NewCore(jsonEncoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel)

logger := zap.New(core, zap.AddCaller())
return logger

}


logger := createJsonLogger()
logger.Info("haha")

输出为
{"level":"INFO","ts":"2024-12-17T14:24:10.092+0800","file":"logs/zlog_test.go:12","msg":"haha"}

自定义日志输出实战

3.1 日志框架流程

1、创建zapcore.EncoderConfig 定制日志打印时候的相关参数

2、创建zapcore.Encoder 获取日志编码器,如内置的json,console等

3、创建zapcore.AddSync 获取日志输出的方式,可以选择标准输出,也可以和其他框架组合使用写入文件

4、创建zapcore.NewCore 获取日志核心

5、使用zap.New() 创建出对应logger,即可打印日志

3.2 自定义实战

有的时候zap原生的日志格式不满足需要,可以通过修改配置和自定义Encoder来满足需求,定制自己的日志格式,如下面的日志,记录时间,调用的文件行数,日志级别,运行阶段,和日志信息

1
time=2024-12-17T11:04:14.888+0800	file=logs/log.go:33	logLev=[INFO]		stage=test	info=test msg

项目类图

创建config

1
2
3
4
5
6
7
8
encoderConfig := &zapcore.EncoderConfig{
TimeKey: "time", // 时间字段的key名字
MessageKey: "logLev", // msg字段的key名字,已经做了改造,看后续
CallerKey: "file", // 调用文件的key名字
EncodeTime: zapcore.ISO8601TimeEncoder, // 时间编码器
EncodeLevel: zapcore.CapitalLevelEncoder, // 日志级别编码,会将日志级别转成大写
EncodeCaller: zapcore.ShortCallerEncoder, // 打印调用文件名时候 打印文件名称和行号,简称模式
}

创建Encoder

自定义Encoder类型

1
2
3
4
5
6
7
8
9
// 自定义 Key=Value 的日志格式
type keyValueEncoder struct {
*zapcore.EncoderConfig
buf *buffer.Buffer

// for encoding generic values by reflection
reflectBuf *buffer.Buffer
reflectEnc zapcore.ReflectedEncoder
}

作为定制Encoder,需要实现zapcore.Encoder 和zapcore.ObjectEncoder 两个接口的方法,主要针对如下函数做改造

zapcore.ObjectEncoder, 主要是对各种数据类型做编码的方法

  • addKey 定制日志的单个kv字段的分隔符

    1
    2
    3
    4
    5
    6
    // key 和value之间的分隔符
    func (enc *keyValueEncoder) addKey(key string) {
    enc.addElementSeparator()
    enc.buf.AppendString(key)
    enc.buf.AppendByte('=')
    }
  • addElementSeparator 定制日志的多个字段间的分割符

    1
    2
    3
    4
    // 添加列与列之间的分隔符
    func (enc *keyValueEncoder) addElementSeparator() {
    enc.buf.AppendByte('\t')
    }
  • Addxxx 对于各类数据类型数据的拼接

zapcore.Encoder 主要是 对 日志内容进行拼接组装和 输出的方法

  • Clone 在编码日志时候,缓冲区的构建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func (enc *keyValueEncoder) Clone() zapcore.Encoder {
    clone := enc.clone()
    // 将输入写入clone的切片中
    clone.buf.Write(enc.buf.Bytes())
    return clone
    }

    func (enc *keyValueEncoder) clone() *keyValueEncoder {
    // 从对象池中获取一个编码器
    clone := getKeyValueEncoder()
    clone.EncoderConfig = enc.EncoderConfig
    // 获取字节切片
    clone.buf = _bufferPool.Get()
    return clone
    }
  • EncodeEntry 在编码日志过程

    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
    // 从对象池中获取对象
    func getKeyValueEncoder() *keyValueEncoder {
    return _keyValueEncoderPool.Get().(*keyValueEncoder)
    }

    // 将对象还给对象池
    func putJSONEncoder(enc *keyValueEncoder) {
    if enc.reflectBuf != nil {
    enc.reflectBuf.Free()
    }
    enc.EncoderConfig = nil
    enc.buf = nil
    enc.reflectBuf = nil
    enc.reflectEnc = nil
    _keyValueEncoderPool.Put(enc)
    }

    func (enc *keyValueEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
    clone := enc.clone()

    // 时间
    if clone.TimeKey != "" {
    clone.buf.AppendString(enc.TimeKey)
    clone.buf.AppendByte('=')
    clone.EncodeTime(ent.Time, clone)
    clone.buf.AppendByte('\t')
    }


    // foo.go:123
    if clone.CallerKey != "" && ent.Caller.Defined {
    clone.buf.AppendString(enc.CallerKey)
    clone.buf.AppendByte('=')
    clone.EncodeCaller(ent.Caller, clone)
    clone.buf.AppendByte('\t')
    }

    if clone.MessageKey != "" {
    clone.buf.AppendString(fmt.Sprintf("%v=%v", clone.MessageKey, ent.Message))
    }

    clone.buf.AppendByte('\t')

    // 遍历字段
    for i := range fields {
    fields[i].AddTo(clone)
    }

    clone.buf.AppendString("\n")

    ret := clone.buf
    putJSONEncoder(clone)
    return ret, nil
    }

创建Encoder函数

1
2
3
4
5
6
func newKeyValueEncoder(config *zapcore.EncoderConfig) zapcore.Encoder {
return &keyValueEncoder{
EncoderConfig: config,
buf: _bufferPool.Get(),
}
}

定制Loger

基于开放封闭原则, 日志库设计需要定制接口,当不同日志打印需求到来时候,不影响原有日志功能,所以需要设计通用一些

创建接口

1
2
3
4
5
6
7
8
9
10
11
12
	// log level
LL_DEBUG = "[DEBUG]"
LL_INFO = "[INFO]"
LL_WARN = "[WARN]"
LL_ERROR = "[ERROR]"
LL_FATAL = "[FATAL]"

type zloger interface {
log(level, stage, info string) // 打印日志信息
logErr(level, stage, info string, err interface{}) //打印错误
sync() // 日志文件同步
}

当level不同时候,可以打印不同日志级别的日志

kvLoger日志实现

定义kvLoger日志打印功能,实现zloger接口,定制出自己的打印风格

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
type kvLogger struct {
*zap.Logger
logFuncMap map[string]_TYPE_ZAP_LOG_fUNC
}

type _TYPE_ZAP_LOG_fUNC func(msg string, fields ...zap.Field)

type kvLogger struct {
*zap.Logger
logFuncMap map[string]_TYPE_ZAP_LOG_fUNC
}

func (k *kvLogger) log(level, stage, info string) {
k.getLogFunc(level)(level,
zap.String("stage", stage),
zap.String("info", info),
)
}

func (k *kvLogger) logErr(level, stage, info string, err interface{}) {
//panic("implement me")
k.getLogFunc(level)(level,
zap.String("stage", stage),
zap.String("info", info),
zap.Any("err", err),
)
}

func (k *kvLogger) sync() {
k.Logger.Sync()
}

func (k kvLogger) getLogFunc(level string) _TYPE_ZAP_LOG_fUNC {
unc, ok := k.logFuncMap[level]
if !ok {
k.Logger.Error("ERROR", zap.String("stage", "zlog"),
zap.String("errInfo", "get level logFunc err ["+level+"], use info"))
return k.Logger.Info
}
return unc
}

创建kvLoger

创建kvLoger函数,和lumberjack库结合,将文件和文档归集整理功能集成。

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
func createKVLogger(conf *LogConf) zloger {

// 1、创建config
encoderConfig := &zapcore.EncoderConfig{
TimeKey: "time",
MessageKey: "logLev", // 标识阶段
CallerKey: "file",
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}

// 2、使用自定义的 encoder 创建core
core := zapcore.NewCore(
newKeyValueEncoder(encoderConfig),
zapcore.AddSync(getLogWriter(conf)),
zapcore.DebugLevel,
)
logger := zap.New(core, zap.AddCaller())
kvlogger := &kvLogger{
Logger: logger,
}
kvlogger.logFuncMap = make(map[string]_TYPE_ZAP_LOG_fUNC, 5)
kvlogger.logFuncMap[LL_DEBUG] = logger.Debug
kvlogger.logFuncMap[LL_INFO] = logger.Info
kvlogger.logFuncMap[LL_WARN] = logger.Warn
kvlogger.logFuncMap[LL_ERROR] = logger.Error
kvlogger.logFuncMap[LL_FATAL] = logger.Error

return kvlogger
}

func getLogWriter(conf *LogConf) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: conf.LogDir + "/" + conf.FileName,
MaxSize: conf.MaxMB * 1024 * 1024,
MaxBackups: conf.MaxBackups,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}

Loger功能暴露

暴露主功能函数,初始化log,和通过打印日志的公用函数。底层调用创建kvLoger的函数。

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
var gZLoger zloger

type LogConf struct {
LogDir string
MaxMB int
MaxBackups int
FileName string
}

func (c *LogConf) reset(logConf *LogConf) {
c.LogDir = "./log"
if logConf.LogDir != "" {
c.LogDir = logConf.LogDir
}
c.MaxMB = 100
if logConf.MaxMB != 0 {
c.MaxMB = logConf.MaxMB
}
c.MaxBackups = 5
if logConf.MaxBackups != 0 {
c.MaxBackups = logConf.MaxBackups
}
c.FileName = filepath.Base(os.Args[0]) + ".log"
if logConf.FileName != "" {
c.FileName = logConf.FileName
}

}

func InitLogger(logConf *LogConf) {
conf := new(LogConf)
conf.reset(logConf)
logger := createKVLogger(conf)
gZLoger = logger
}

func Log(level, stage, info string) {
gZLoger.log(level, stage, info)
}
func LogErr(level, stage, info string, err interface{}) {
gZLoger.logErr(level, stage, info, err)
}
func Sync() {
gZLoger.sync()
}

通过上述逻辑,可以构建出k=v 且制表符为字段分割的日志模式