关于golang:zap-接收-gin-框架默认的日志

后面两篇曾经为大家介绍了golang中的日志如何应用,并在诸多日志框架库中抉择了zap作为咱们的日志框架,本篇将会解说:

  1. 如何联合当下支流的Web框架gin进行申请日志的打印
  2. 对zap进行二次封装,注入trace信息,一遍咱们能够在业务中查问一次申请的所有残缺日志

这里是前两篇的链接:

  1. https://www.yuque.com/jinsesi…
  2. https://www.yuque.com/jinsesi…

1、gin 默认的中间件

首先咱们来看一个最简略的 gin 我的项目:

func main() {
    r := gin.Default()
    r.GET("/hello", func(c *gin.Context) {
        c.String("hello jianfan.com!")
    })
    r.Run(
}

接下来咱们看一下gin.Default()的源码:

func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

也就是咱们在应用gin.Default()的同时是用到了 gin 框架内的两个默认中间件Logger()Recovery()

其中Logger()是把 gin 框架自身的日志输入到规范输入(咱们本地开发调试时在终端输入的那些日志就是它的功绩),而Recovery()是在程序呈现 panic 的时候复原现场并写入 500 响应的。

2、基于 zap 的中间件

gin框架反对用户自定义的middleware,咱们能够模拟Logger()Recovery()的实现,应用咱们的日志库来接管 gin 框架默认输入的日志。
这里以 zap 为例,咱们实现两个中间件如下:

// GinLogger 接管gin框架默认的日志
func GinLogger(logger *zap.Logger) 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)
        logger.Info(path,
            zap.Int("status", c.Writer.Status()),
            zap.String("method", c.Request.Method),
            zap.String("path", path),
            zap.String("query", query),
            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),
        )
    }
}

// GinRecovery recover掉我的项目可能呈现的panic
func GinRecovery(logger *zap.Logger, 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 {
                    logger.Error(c.Request.URL.Path,
                        zap.Any("error", err),
                        zap.String("request", string(httpRequest)),
                    )
                    // If the connection is dead, we can't write a status to it.
                    c.Error(err.(error)) // nolint: errcheck
                    c.Abort()
                    return
                }

                if stack {
                    logger.Error("[Recovery from panic]",
                        zap.Any("error", err),
                        zap.String("request", string(httpRequest)),
                        zap.String("stack", string(debug.Stack())),
                    )
                } else {
                    logger.Error("[Recovery from panic]",
                        zap.Any("error", err),
                        zap.String("request", string(httpRequest)),
                    )
                }
                c.AbortWithStatus(http.StatusInternalServerError)
            }
        }()
        c.Next()
    }
}

如果不想本人实现,能够应用 github 上有他人封装好的 https://github.com/gin-contrib/zap

这样咱们就能够在 gin 框架中应用咱们下面定义好的两个中间件来代替 gin 框架默认的Logger()Recovery()了。

r := gin.New()
r.Use(GinLogger(), GinRecovery())

3、减少Trace信息

3.1 定义trace信息

// 定义trace构造体
type Trace struct {
    TraceId   string  `json:"trace_id"`
    SpanId    string  `json:"span_id"`
    Caller    string  `json:"caller"`
    SrcMethod *string `json:"srcMethod,omitempty"`
    UserId    int   `json:"user_id"`
}

// 依据gin的context获取context,使log trace更加通用
func GetTraceCtx(c *gin.Context) context.Context {
    return c.MustGet(consts.TraceCtx).(context.Context)
}

3.2 包装基于zap的日志工具

  • 包装办法将context传入
  • 解析trace信息,打印到logger中

    package log
    
    import (
      "best-practics/common"
      "best-practics/common/consts"
      "context"
      "go.uber.org/zap"
    )
    
    type LogWrapper struct {
      logger *zap.Logger
    }
    
    var Log LogWrapper
    
    func Debug(tag string, fields ...zap.Field) {
      Log.logger.Debug(tag, fields...)
    }
    
    func DebugF(ctx context.Context, tag string, fields ...zap.Field) {
      trace := ctx.Value(consts.TraceKey).(*common.Trace)
      Log.logger.Debug(tag,
          append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
      )
    }
    
    func Info(tag string, fields ...zap.Field) {
      Log.logger.Info(tag, fields...)
    }
    
    func InfoF(ctx context.Context, tag string, fields ...zap.Field) {
      trace := ctx.Value(consts.TraceKey).(*common.Trace)
      Log.logger.Info(tag,
          append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
      )
    }
    
    func Warn(tag string, fields ...zap.Field) {
      Log.logger.Warn(tag, fields...)
    }
    
    func WarnF(ctx context.Context, tag string, fields ...zap.Field) {
      trace := ctx.Value(consts.TraceKey).(*common.Trace)
      Log.logger.Warn(tag,
          append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
      )
    }
    
    func Error(tag string, fields ...zap.Field) {
      Log.logger.Error(tag, fields...)
    }
    
    func ErrorF(ctx context.Context, tag string, fields ...zap.Field) {
      trace := ctx.Value(consts.TraceKey).(*common.Trace)
      Log.logger.Error(tag,
          append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
      )
    }
    
    func Fatal(tag string, fields ...zap.Field) {
      Log.logger.Fatal(tag, fields...)
    }
    
    func FatalF(ctx context.Context, tag string, fields ...zap.Field) {
      trace := ctx.Value(consts.TraceKey).(*common.Trace)
      Log.logger.Fatal(tag,
          append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
      )
    }
    

    3.3 解决Caller问题

    此形式存在个问题,就是打印的caller信息全都是对应日志工具类的代码行,而不是调用处。看了下zap的源码,zap打印时将整个调用的stack链路会寄存到内存中,默认打印调用处的caller信息。所以为了解决解决这个问题,须要再初始化zap时额定减少AddCallerSkip跳过指定层级的caller,外围代码如下:

    logger = logger.WithOptions(zap.AddCaller(),zap.AddCallerSkip(1))

    残缺的zap工具类如下:

    /*
     * Copyright (C) 2021 Baidu, Inc. All Rights Reserved.
     */
    package log
    
    import (
      "best-practics/common"
      "best-practics/utils"
      "fmt"
      "os"
      "time"
    
      rotatelogs "github.com/lestrrat-go/file-rotatelogs"
      "go.uber.org/zap"
      "go.uber.org/zap/zapcore"
    )
    
    func InitZap() {
      if ok, _ := utils.PathExists(common.GlobalConfig.Zap.Director); !ok { // 判断是否有Director文件夹
          fmt.Printf("create %v directory\n", common.GlobalConfig.Zap.Director)
          _ = os.Mkdir(common.GlobalConfig.Zap.Director, os.ModePerm)
      }
      // 调试级别
      debugPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
          return lev == zap.DebugLevel
      })
      // 日志级别
      infoPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
          return lev == zap.InfoLevel
      })
      // 正告级别
      warnPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
          return lev == zap.WarnLevel
      })
      // 谬误级别
      errorPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
          return lev >= zap.ErrorLevel
      })
    
      cores := [...]zapcore.Core{
          getEncoderCore(fmt.Sprintf("./%s/server_debug.log", common.GlobalConfig.Zap.Director), debugPriority),
          getEncoderCore(fmt.Sprintf("./%s/server_info.log", common.GlobalConfig.Zap.Director), infoPriority),
          getEncoderCore(fmt.Sprintf("./%s/server_warn.log", common.GlobalConfig.Zap.Director), warnPriority),
          getEncoderCore(fmt.Sprintf("./%s/server_error.log", common.GlobalConfig.Zap.Director), errorPriority),
      }
      logger := zap.New(zapcore.NewTee(cores[:]...), zap.AddCaller(), zap.AddCallerSkip(1))
    
      common.Logger = logger
    }
    
    // getEncoderConfig 获取zapcore.EncoderConfig
    func getEncoderConfig() (config zapcore.EncoderConfig) {
      config = zapcore.EncoderConfig{
          MessageKey:     "message",
          LevelKey:       "level",
          TimeKey:        "time",
          NameKey:        "logger",
          CallerKey:      "caller",
          StacktraceKey:  common.GlobalConfig.Zap.StacktraceKey,
          LineEnding:     zapcore.DefaultLineEnding,
          EncodeLevel:    zapcore.LowercaseLevelEncoder,
          EncodeTime:     CustomTimeEncoder,
          EncodeDuration: zapcore.SecondsDurationEncoder,
          EncodeCaller:   zapcore.FullCallerEncoder,
      }
      switch {
      case common.GlobalConfig.Zap.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
          config.EncodeLevel = zapcore.LowercaseLevelEncoder
      case common.GlobalConfig.Zap.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带色彩
          config.EncodeLevel = zapcore.LowercaseColorLevelEncoder
      case common.GlobalConfig.Zap.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
          config.EncodeLevel = zapcore.CapitalLevelEncoder
      case common.GlobalConfig.Zap.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带色彩
          config.EncodeLevel = zapcore.CapitalColorLevelEncoder
      default:
          config.EncodeLevel = zapcore.LowercaseLevelEncoder
      }
      return config
    }
    
    // getEncoder 获取zapcore.Encoder
    func getEncoder() zapcore.Encoder {
      if common.GlobalConfig.Zap.Format == "json" {
          return zapcore.NewJSONEncoder(getEncoderConfig())
      }
      return zapcore.NewConsoleEncoder(getEncoderConfig())
    }
    
    // getEncoderCore 获取Encoder的zapcore.Core
    func getEncoderCore(fileName string, level zapcore.LevelEnabler) (core zapcore.Core) {
      writer := GetWriteSyncer(fileName) // 应用file-rotatelogs进行日志宰割
      return zapcore.NewCore(getEncoder(), writer, level)
    }
    
    // 自定义日志输入工夫格局
    func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
      enc.AppendString(t.Format("2006/01/02 15:04:05"))
    }
    
    //@function: GetWriteSyncer
    //@description: zap logger中退出file-rotatelogs
    //@return: zapcore.WriteSyncer, error
    func GetWriteSyncer(file string) zapcore.WriteSyncer {
      // 每小时一个文件
      logf, _ := rotatelogs.New(file +".%Y%m%d%H",
          rotatelogs.WithLinkName(file),
          rotatelogs.WithMaxAge(7*24*time.Hour),
          rotatelogs.WithRotationTime(time.Minute),
      )
    
      if common.GlobalConfig.Zap.LogInConsole {
          return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(logf))
      }
      return zapcore.AddSync(logf)
    }

    3.4 Gin的middleware中减少trace信息

    func SetLoggerMiddleware() gin.HandlerFunc {
      return func(c *gin.Context) {
          start := time.Now()
          uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "")
          path := c.Request.URL.Path
          userId := c.GetInt("user_id")
          ctx := context.WithValue(context.Background(), consts.TraceKey, &common.Trace{TraceId: uuidStr, Caller: path, UserId: userId})
          c.Set(consts.TraceCtx,ctx)
    
          c.Next()
          cost := time.Since(start)
          common.Logger.Info("_com_request_info",
              zap.Int("Status", c.Writer.Status()),
              zap.String("Method", c.Request.Method),
              zap.String("IP",c.ClientIP()),
              zap.String("Path",path),
              zap.String("TraceId", uuidStr),
              zap.Int("UserId", userId),
              zap.String("query", c.Request.URL.RawQuery),
              zap.String("UserAgent",c.Request.UserAgent()),
              zap.Duration("Cost",cost),
          )
      }
    }

    3.5 Controller层生成trace_context

    func main() {
      r := gin.Default()
      r.GET("/hello", func(c *gin.Context) {
          // 3.1中封装的转换方法
          ctx := common.GetTraceCtx(c)
          log.WarnF(ctx,"BookNotFoundError",zap.String("test","666"))
          c.String("hello jianfan.com!")
      })
      r.Run(
    }

至此咱们就实现了在一个gin我的项目中接入zap作为日志框架,并可能打印残缺的链路日志信息。联合前两节咱们曾经满足了对常见日志框架的需要:

  1. 良好日志写入性能
  2. 反对不同的日志级别。并且可拆散成多个日志文件
  3. 多输入 - 同时反对规范输入,文件等
  4. 可能打印根本信息,如调用文件 / 函数名和行号,日志工夫等
  5. 可读性与结构化,Json格局或有分隔符,不便后续的日志采集、监控等
  6. 文件切割,可按小时、天进行日志拆分,或者按文件大小
  7. 日志书写敌对,反对通过context主动log trace等
  8. 文件定时删除
  9. 开源性,与其余开源框架反对较好

参考资料:

  • https://liwenzhou.com/posts/Go/use_zap_in_gin/

咱们下期见,Peace😘

我是简凡,一个励志用最简略的语言,形容最简单问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么纳闷,欢送在我的微信公众号中留言,我还能够为你提供以下帮忙:

  • 帮忙建设本人的常识体系
  • 互联网实在高并发场景实战解说
  • 不定期分享Golang、Java相干业内的经典场景实际

我的博客:https://besthpt.github.io/
我的微信:bestbear666
微信公众号:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理