theme: channing-cyan

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情

日志

前言

本文主要介绍Go语言日志库如何简易定制化,以及如何在开发中使用。

为什么需要日志?

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

日志的好处

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

日志都有什么

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

infodebugwarnerrorfatal

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
 }
NewWriteros.OpenFile()os.Stdoutos.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)
 }
FatalFatalos.Exit(1)

goLogger的不足

FatalPrintFatalPainc
gologger

Zap日志库

引入日志库依赖

go get -u go.uber.org/zap

zapUberzapzap

如何使用zap

格式化配置

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

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

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)

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

image.png

zapcore.TimeEncoderLayout

日志分割

image.png

zaplumberjack

image.png

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

image.png

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框架进行使用

ginloggergin

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), "\r\n")
                 split := strings.Split(string(debug.Stack()), "\n\t")
                 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()
     }
 }
 
Panicrecoverfalse

小结

zap日志可以灵活的定制时间、编码输出格式、颜色等信息。

zap日志级别丰富,不利用反射,效率高,但需要手动对类型进行定义。