背景
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 { config := zapcore.EncoderConfig{ MessageKey: "msg", LevelKey: "level", TimeKey: "ts", CallerKey: "file", StacktraceKey: "stacktrace", EncodeTime: zapcore.ISO8601TimeEncoder, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } jsonEncoder := zapcore.NewJSONEncoder(config) 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", MessageKey: "logLev", CallerKey: "file", EncodeTime: zapcore.ISO8601TimeEncoder, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }
|
创建Encoder
自定义Encoder类型
1 2 3 4 5 6 7 8 9
| type keyValueEncoder struct { *zapcore.EncoderConfig buf *buffer.Buffer
reflectBuf *buffer.Buffer reflectEnc zapcore.ReflectedEncoder }
|
作为定制Encoder,需要实现zapcore.Encoder 和zapcore.ObjectEncoder 两个接口的方法,主要针对如下函数做改造
zapcore.ObjectEncoder, 主要是对各种数据类型做编码的方法
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') }
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
| 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{}) { 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 {
encoderConfig := &zapcore.EncoderConfig{ TimeKey: "time", MessageKey: "logLev", CallerKey: "file", EncodeTime: zapcore.ISO8601TimeEncoder, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }
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 且制表符为字段分割的日志模式