Golang定制化zap日志库如何使用

寻技术 Go编程 2023年08月23日 74

本篇内容主要讲解“Golang定制化zap日志库如何使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Golang定制化zap日志库如何使用”吧!

为什么需要日志

一个产品的诞生一定是因为有需求!新技术大部分都是为了更加便利和实用而诞生的,日志也不例外。日志顾名思义就是对整个项目的事件进行记录。日志可以帮助我们查看某一天中某一时刻项目的运转情况等等。

日志的好处

在日常开发过程中难免会遇到BUG出现的情况,日志可以记录这些BUG出现的地点从而方便进行快速定位和排查。可以根据需求对日志进行自定义的输出,比如输出到控制台、文件等。日志也可以帮助我们在开发过程中检测到程序潜在的问题和程序运行的流程,能够有效的提高我们的开发效率。

日志都有什么

要让程序记录有效的,便利的日志。** Logger (日志记录器) 应该具备以下特点**:

  • 可以将日志信息输出到控制台、文件等地方,输出到文件便于项目长久运行,输出到控制台有助于开发过程中检错的效率。

  • 一个日志应该具有多个基本的级别,比如

    info
    debug
    warn
    error
    fatal
    等,他们可以对日志进行分类。
  • 可以对日志进行切割,按照日志大小、日期、时间间隔等因素分割。

  • 可以手动或自动记录一些开发信息。如前端传入的数据,异常错误信息,程序运行结果,错误行数,日志打印位置等等信息进行打印。

Go中默认的日志

Go语言中默认集成了一个

log
日志库
func New(out io.Writer, prefix string, flag int) *Logger {
   l := &Logger{out: out, prefix: prefix, flag: flag}
   if out == io.Discard {
      l.isDiscard = 1
   }
   return l
}

使用

New
可以获取到该日志对象。第一个参数为实现了
Writer
接口的对象。可以使用
os.OpenFile()
选择一个文件,然后将该文件对象作为输出,也可以使用
os.Stdout
os.Stderr
输出到控制台。第二个参数需要传入一个日志信息每一行的前缀(如果输出到控制台该处可以填空字符串)。第三个参数是设置打印默认信息的能力,比如打印时间等。

测试日志

var l *log.Logger
func main() {
	l.Printf("main method exec fail, err: %v", errors.New("nil Pointer error"))
	l.Println("test go log status")
	l.Fatal("wait five seconds")
	time.Sleep(time.Second * 5)
	l.Println("five seconds after!")
}
func init() {
	l = log.New(os.Stdout, "[我是一个前缀]", log.LstdFlags)
}

打印信息

[我是一个前缀]2023/02/10 21:15:22 main method exec fail, err: nil Pointer error

[我是一个前缀]2023/02/10 21:15:22 test go log status

[我是一个前缀]2023/02/10 21:15:22 wait five seconds

// Fatal is equivalent to l.Print() followed by a call to os.Exit(1).
func (l *Logger) Fatal(v ...any) {
   l.Output(2, fmt.Sprint(v...))
   os.Exit(1)
}

Fatal
之后的程序均不会执行,因为
Fatal
执行后会在内部调用
os.Exit(1)
,从而在打印结束后退出进程。

goLogger的不足

  • 日志级别只支持

    Fatal
    ,只有一个
    Print
    函数,没有其他级别
  • 日志自定义参数过少,无法打印栈信息,无法确定请求位置等

  • Fatal
    Painc
    都是执行后退出,无法容忍错误情况的出现就会退出程序
  • 无法指定输出格式,只能以文本形式进行输出,没有根据日志大小、时间间隔、日期进行分割的能力

虽然

gologger
支持并发,但也只限于简单用着还行,实际开发用起来并不舒服的情况。

Zap日志库

引入日志库依赖

go get -u go.uber.org/zap

zap
日志库是
Uber
开源的。性能很好,因为不用反射实现,但需要自己去手动指明打印信息的类型(下面会有示例)。个人觉得自己指定打印还是挺舒服的。
zap
的使用率非常高,不仅支持日志库的基本功能,而且很灵活的支持你去进一步的封装或者定制化。
zap
支持异步打印。

如何使用zap

格式化配置

func NewDevelopmentEncoderConfig() zapcore.EncoderConfig
func NewProductionEncoderConfig() zapcore.EncoderConfig
func NewProductionConfig() Config
func NewDevelopmentConfig() Config

这里可以根据实际生产和测试环境需求进行选择,也可以直接使用其他初始化方式。

// NewProductionEncoderConfig returns an opinionated EncoderConfig for
// production environments.
func NewProductionEncoderConfig() zapcore.EncoderConfig {
   return zapcore.EncoderConfig{
       // 设置log内容里的一些属性的key
      TimeKey:        "ts",//时间对应的key名
      LevelKey:       "level",//日志级别对应的key名
      NameKey:        "logger",//logger名对应的key名
      CallerKey:      "caller",//调用者对应的key名
      FunctionKey:    zapcore.OmitKey,
      MessageKey:     "msg",//日志内容对应的key名,此参数必须不为空,否则日志主体不处理
      StacktraceKey:  "stacktrace",//栈追踪的key名
       // const DefaultLineEnding = "
" 行末输出格式
      LineEnding:     zapcore.DefaultLineEnding,
       // 日志编码级别
      EncodeLevel:    zapcore.LowercaseLevelEncoder,
       // 日志时间解析
      EncodeTime:     zapcore.EpochTimeEncoder,
       // 日志日期解析
      EncodeDuration: zapcore.SecondsDurationEncoder,
       // 日志调用路径
      EncodeCaller:   zapcore.ShortCallerEncoder,
   }
}

使用

NewProductionEncoderConfig()
创建的
Logger
在记录日志时会自动记录调用函数的信息、打日志的时间,日志级别等信息。

EncodeLevel

// A LevelEncoder serializes a Level to a primitive type.
type LevelEncoder func(Level, PrimitiveArrayEncoder)
// 将日志级别进行大写并带上颜色
func CapitalColorLevelEncoder(l Level, enc PrimitiveArrayEncoder)
// 将日志级别大写不带颜色
func CapitalLevelEncoder(l Level, enc PrimitiveArrayEncoder)
// 将日志级别小写带上颜色
func LowercaseColorLevelEncoder(l Level, enc PrimitiveArrayEncoder)
// 将日志级别小写不带颜色
func LowercaseLevelEncoder(l Level, enc PrimitiveArrayEncoder)

需要实现

LevelEncoder
接口。可以调整日志编码级别,并且选择带上或者不带输出颜色。

EncodeTime

// A TimeEncoder serializes a time.Time to a primitive type.
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)
// 根据不同时间进行格式化
func EpochTimeEncoder(t time.Time, enc PrimitiveArrayEncoder)
func EpochMillisTimeEncoder(t time.Time, enc PrimitiveArrayEncoder)
func EpochNanosTimeEncoder(t time.Time, enc PrimitiveArrayEncoder)

定制化时间格式解析,需要实现

TimeEncoder
接口。

EncodeDruation

// A DurationEncoder serializes a time.Duration to a primitive type.
type DurationEncoder func(time.Duration, PrimitiveArrayEncoder)
// 将日期根据不同时间进行格式化
func SecondsDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder)
func NanosDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder)
func MillisDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder)
func StringDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder)

定制日期格式解析。需要实现

DruationEncoder
接口

定制化zap

编码格式

encoderConfig := zap.NewProductionEncoderConfig()
// 打印级别为大写 & 彩色
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
// 时间编码进行指定格式解析 layout -> "[2006-01-02 15:04:05]"
encoderConfig.EncodeTime = parseTime(settings.Conf.Layout)

修改日志打印级别和时间编码格式

// parseTime 进行时间格式处理
func parseTime(layout string) zapcore.TimeEncoder {
   return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
      type appendTimeEncoder interface {
         AppendTimeLayout(time.Time, string)
      }
      if enc, ok := enc.(appendTimeEncoder); ok {
         enc.AppendTimeLayout(t, layout)
         return
      }
      enc.AppendString(t.Format(layout))
   }
}

实现

zapcore.TimeEncoder
接口,将指定的
Layout
参数进行传入实现闭包即可。

日志分割

// 日志输出配置, 借助另外一个库 lumberjack 协助完成日志切割。
lumberjackLogger := &lumberjack.Logger{
   Filename:   settings.Conf.Filename,   // -- 日志文件名
   MaxSize:    settings.Conf.MaxSize,    // -- 最大日志数 M为单位!!!
   MaxAge:     settings.Conf.MaxAge,     // -- 最大存在天数
   MaxBackups: settings.Conf.MaxBackups, // -- 最大备份数量
   Compress:   false,                    // --是否压缩
}
syncer := zapcore.AddSync(lumberjackLogger)

zap
日志本身不支持日志切割,借助另外一个库
lumberjack
协助完成日志切割。
// -- 用于开发者模式和生产模式之间的切换
var core zapcore.Core
if settings.Conf.AppConfig.Mode == "debug" {
   encoder := zapcore.NewConsoleEncoder(encoderConfig) // 输出控制台编码格式
   core = zapcore.NewTee(
      zapcore.NewCore(encoder, syncer, zapcore.DebugLevel), // debug级别打印到日志文件
      zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), // debug级别打印到控制台
   )
} else {
   encoder := zapcore.NewJSONEncoder(encoderConfig)// 输出Json格式,便于日志检索
   core = zapcore.NewCore(encoder, syncer, zapcore.InfoLevel)// info级别打印到日志文件
}
lg := zap.New(core, zap.AddCaller()) // --添加函数调用信息

根据配置信息去选择具体打印需求。

zap.ReplaceGlobals(lg)               // 替换该日志为全局日志
var (
	_globalMu sync.RWMutex
	_globalL  = NewNop()
)
// L returns the global Logger, which can be reconfigured with ReplaceGlobals.
// It's safe for concurrent use.
func L() *Logger {
	_globalMu.RLock()
	l := _globalL
	_globalMu.RUnlock()
	return l
}

设置该日志为全局日志,将原日志进行替换,即可在任意位置使用

zap.L()
调用该日志。

完整代码

// init 初始化日志库
func init() {
   encoderConfig := zap.NewProductionEncoderConfig()
   // 打印级别为大写 & 彩色
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // 时间编码进行指定格式解析
   encoderConfig.EncodeTime = parseTime(settings.Conf.Layout)
   // 日志输出配置, 借助另外一个库 lumberjack 协助完成日志切割。
   lumberjackLogger := &lumberjack.Logger{
      Filename:   settings.Conf.Filename,   // -- 日志文件名
      MaxSize:    settings.Conf.MaxSize,    // -- 最大日志数 M为单位!!!
      MaxAge:     settings.Conf.MaxAge,     // -- 最大存在天数
      MaxBackups: settings.Conf.MaxBackups, // -- 最大备份数量
      Compress:   false,                    // --是否压缩
   }
   syncer := zapcore.AddSync(lumberjackLogger)
   // -- 用于开发者模式和生产模式之间的切换
   var core zapcore.Core
   if settings.Conf.AppConfig.Mode == "debug" {
      encoder := zapcore.NewConsoleEncoder(encoderConfig)
      core = zapcore.NewTee(
         zapcore.NewCore(encoder, syncer, zapcore.DebugLevel),
         zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),
      )
   } else {
      encoder := zapcore.NewJSONEncoder(encoderConfig)
      core = zapcore.NewCore(encoder, syncer, zapcore.InfoLevel)
   }
   lg := zap.New(core, zap.AddCaller()) // --添加函数调用信息
   zap.ReplaceGlobals(lg)               // 替换该日志为全局日志
}
// parseTime 进行时间格式处理
func parseTime(layout string) zapcore.TimeEncoder {
   return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
      type appendTimeEncoder interface {
         AppendTimeLayout(time.Time, string)
      }
      if enc, ok := enc.(appendTimeEncoder); ok {
         enc.AppendTimeLayout(t, layout)
         return
      }
      enc.AppendString(t.Format(layout))
   }
}

测试日志打印情况

zap.L().Info("test info", zap.String("test String", "ok"), zap.Int("test cnt", 1))
zap.L().Debug("test debug", zap.String("test String", "ok"), zap.Int("test cnt", 2))
zap.L().Error("test error", zap.String("test String", "ok"), zap.Int("test cnt", 3))

[2023-02-10 22:22:17] INFO xxxxx/main.go:22 test info {“test String”: “ok”, “test cnt”: 1}

[2023-02-10 22:22:17] DEBUG xxxxx/main.go:23 test debug {“test String”: “ok”, “test cnt”: 2}

[2023-02-10 22:22:17] ERROR xxxxx/main.go:24 test error {“test String”: “ok”, “test cnt”: 3}

这里就是上述所说的自指定类型进行输出的情况。

结合gin框架进行使用

虽然

gin
框架有自带的
logger
中间件,但我们可以根据
gin
框架实现的原生日志和异常恢复中间件进行改造并进行替换。

Loger

// GinLogger 替换gin中默认的logger
func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()
	cost := time.Since(start)
	if c.Writer.Status() != http.StatusOK {
		// 记录异常信息
		zap.L().Error(query,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

如果有错误请求,只要不是状态码为200的全部进行打印->状态码、请求方法(get、post…)、路径、ip、用户授权方、错误信息、请求花费时间。

// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
							strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					zap.L().Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("httpRequest", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}
                // 这里可以选择全部打印出来不必要分割然后循环输出
				request := strings.Split(string(httpRequest), "
")
				split := strings.Split(string(debug.Stack()), "
	")
				if stack {
					zap.L().Error("[Recovery from panic]",
						zap.Any("error", err))
					for _, str := range request {
						zap.L().Error("[Recovery from request panic]", zap.String("request", str))
					}
					for _, str := range split {
						zap.L().Error("[Recovery from Stack panic]", zap.String("stack", str))
					}
				} else {
					zap.L().Error("[Recovery from panic]",
						zap.Any("error", err))
					for _, str := range request {
						zap.L().Error("[Recovery from request panic]", zap.String("request", str))
					}
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

这里在

Panic
的时候我采用了分割循环打印的方法,也可以全部输出,但是一堆异常情况,不容易看清楚。也可以选择不打印栈轨迹输出,只需要在使用
recover
中间件时传入
false
参数即可。
关闭

用微信“扫一扫”