使用zap接收gin框架默认的日志并配置日志归档

发表于:2021-3-05 09:27

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:小学弟-    来源:博客园

  本文介绍了在基于gin框架开发的项目中如何配置并使用zap来接收并记录gin框架默认的日志和如何配置日志归档。
  我们在基于gin框架开发项目时通常都会选择使用专业的日志库来记录项目中的日志,go语言常用的日志库有zap、logrus等。网上也有很多类似的教程,但是我们该如何在日志中记录gin框架本身输出的那些日志呢?
  gin默认的中间件
  首先我们来看一个最简单的gin项目:
  func main() {
  r := gin.Default()
  r.GET("/hello", func(c *gin.Context) {
  c.String("hello liwenzhou.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响应的。
  基于zap的中间件
  我们可以模仿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())
  在gin项目中使用zap
  最后我们再加入我们项目中常用的日志切割,完整版的logger.go代码如下:
  package logger
  import (
  "gin_zap_demo/config"
  "net"
  "net/http"
  "net/http/httputil"
  "os"
  "runtime/debug"
  "strings"
  "time"
  "github.com/gin-gonic/gin"
  "github.com/natefinch/lumberjack"
  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
  )
  var lg *zap.Logger
  // InitLogger 初始化Logger
  func InitLogger(cfg *config.LogConfig) (err error) {
  writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
  encoder := getEncoder()
  var l = new(zapcore.Level)
  err = l.UnmarshalText([]byte(cfg.Level))
  if err != nil {
  return
  }
  core := zapcore.NewCore(encoder, writeSyncer, l)
  lg = zap.New(core, zap.AddCaller())
  zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
  return
  }
  func getEncoder() zapcore.Encoder {
  encoderConfig := zap.NewProductionEncoderConfig()
  encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
  encoderConfig.TimeKey = "time"
  encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
  encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
  encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
  return zapcore.NewJSONEncoder(encoderConfig)
  }
  func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
  lumberJackLogger := &lumberjack.Logger{
  Filename:   filename,
  MaxSize:    maxSize,
  MaxBackups: maxBackup,
  MaxAge:     maxAge,
  }
  return zapcore.AddSync(lumberJackLogger)
  }
  // GinLogger 接收gin框架默认的日志
  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)
  lg.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,并使用zap记录相关日志
  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 {
  lg.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 {
  lg.Error("[Recovery from panic]",
  zap.Any("error", err),
  zap.String("request", string(httpRequest)),
  zap.String("stack", string(debug.Stack())),
  )
  } else {
  lg.Error("[Recovery from panic]",
  zap.Any("error", err),
  zap.String("request", string(httpRequest)),
  )
  }
  c.AbortWithStatus(http.StatusInternalServerError)
  }
  }()
  c.Next()
  }
  }
  然后定义日志相关配置:
  type LogConfig struct {
  Level string `json:"level"`
  Filename string `json:"filename"`
  MaxSize int `json:"maxsize"`
  MaxAge int `json:"max_age"`
  MaxBackups int `json:"max_backups"`
  }
  在项目中先从配置文件加载配置信息,再调用logger.InitLogger(config.Conf.LogConfig)即可完成logger实例的初识化。其中,通过r.Use(logger.GinLogger(), logger.GinRecovery(true))注册我们的中间件来使用zap接收gin框架自身的日志,在项目中需要的地方通过使用zap.L().Xxx()方法来记录自定义日志信息。
  package main
  import (
  "fmt"
  "gin_zap_demo/config"
  "gin_zap_demo/logger"
  "net/http"
  "os"
  "go.uber.org/zap"
  "github.com/gin-gonic/gin"
  )
  func main() {
  // load config from config.json
  if len(os.Args) < 1 {
  return
  }
  if err := config.Init(os.Args[1]); err != nil {
  panic(err)
  }
  // init logger
  if err := logger.InitLogger(config.Conf.LogConfig); err != nil {
  fmt.Printf("init logger failed, err:%v\n", err)
  return
  }
  gin.SetMode(config.Conf.Mode)
  r := gin.Default()
  // 注册zap相关中间件
  r.Use(logger.GinLogger(), logger.GinRecovery(true))
  r.GET("/hello", func(c *gin.Context) {
  // 假设你有一些数据需要记录到日志中
  var (
  name = "q1mi"
  age  = 18
  )
  // 记录日志并使用zap.Xxx(key, val)记录相关字段
  zap.L().Debug("this is hello func", zap.String("user", name), zap.Int("age", age))
  c.String(http.StatusOK, "hello liwenzhou.com!")
  })
  addr := fmt.Sprintf(":%v", config.Conf.Port)
  r.Run(addr)
  }

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号