Go 語言之在 Gin 框架中使用 Zap 實現高效日誌管理

在現代 Web 開發中,日誌管理是確保應用程序性能、穩定性和可維護性的關鍵因素之一。Gin 作爲輕量級的 Go Web 框架,自帶了簡單的日誌功能。然而,對於追求高性能和靈活性的開發者來說,Zap 日誌庫是一個理想的選擇。本文將深入探討如何在 Gin 框架中集成 Zap 日誌庫,實現高效、分級和結構化的日誌記錄。

本文介紹瞭如何在 Go 的 Gin 框架中使用 Zap 日誌庫替換默認日誌功能,提供更高效、靈活的日誌管理方案。通過詳細的代碼示例,展示了 Zap 如何實現結構化日誌記錄、日誌級別控制以及性能優化,使開發者能夠構建更具可維護性和穩定性的應用程序。

Go 語言之在 gin 框架中使用 zap 日誌庫

gin 框架默認使用的是自帶的日誌

gin.Default()的源碼 Logger(), Recovery()

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

// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default, gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}

// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}

// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
iflen(recovery) > 0 {
return CustomRecoveryWithWriter(out, recovery[0])
 }
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}

// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
var logger *log.Logger
if out != nil {
  logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
 }
returnfunc(c *Context) {
deferfunc() {
   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 {
     var se *os.SyscallError
     if errors.As(ne, &se) {
      seStr := strings.ToLower(se.Error())
      if strings.Contains(seStr, "broken pipe") ||
       strings.Contains(seStr, "connection reset by peer") {
       brokenPipe = true
      }
     }
    }
    if logger != nil {
     stack := stack(3)
     httpRequest, _ := httputil.DumpRequest(c.Request, false)
     headers := strings.Split(string(httpRequest)"\r\n")
     for idx, header := range headers {
      current := strings.Split(header, ":")
      if current[0] == "Authorization" {
       headers[idx] = current[0]": *"
      }
     }
     headersToStr := strings.Join(headers, "\r\n")
     if brokenPipe {
      logger.Printf("%s\n%s%s", err, headersToStr, reset)
     } elseif IsDebugging() {
      logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
       timeFormat(time.Now()), headersToStr, err, stack, reset)
     } else {
      logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
       timeFormat(time.Now()), err, stack, reset)
     }
    }
    if brokenPipe {
     // If the connection is dead, we can't write a status to it.
     c.Error(err.(error)) //nolint: errcheck
     c.Abort()
    } else {
     handle(c, err)
    }
   }
  }()
  c.Next()
 }
}

自定義 Logger(), Recovery()

實操

package main

import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)

// 定義一個全局 logger 實例
// Logger提供快速、分級、結構化的日誌記錄。所有方法對於併發使用都是安全的。
// Logger是爲每一微秒和每一個分配都很重要的上下文設計的,
// 因此它的API有意傾向於性能和類型安全,而不是簡便性。
// 對於大多數應用程序,SugaredLogger在性能和人體工程學之間取得了更好的平衡。
var logger *zap.Logger

// SugaredLogger將基本的Logger功能封裝在一個較慢但不那麼冗長的API中。任何Logger都可以通過其Sugar方法轉換爲sugardlogger。
//與Logger不同,SugaredLogger並不堅持結構化日誌記錄。對於每個日誌級別,它公開了四個方法:
//   - methods named after the log level for log.Print-style logging
//   - methods ending in "w" for loosely-typed structured logging
//   - methods ending in "f" for log.Printf-style logging
//   - methods ending in "ln" for log.Println-style logging

// For example, the methods for InfoLevel are:
//
// Info(...any)           Print-style logging
// Infow(...any)          Structured logging (read as "info with")
// Infof(string, ...any)  Printf-style logging
// Infoln(...any)         Println-style logging
var sugarLogger *zap.SugaredLogger

//func main() {
// // 初始化
// InitLogger()
// // Sync調用底層Core的Sync方法,刷新所有緩衝的日誌條目。應用程序在退出之前應該注意調用Sync。
// // 在程序退出之前,把緩衝區裏的日誌刷到磁盤上
// defer logger.Sync()
// simpleHttpGet("www.baidu.com")
// simpleHttpGet("http://www.baidu.com")
//
// for i := 0; i < 10000; i++ {
//  logger.Info("test lumberjack for log rotate....")
// }
//}

func main() {
 InitLogger()
//r := gin.Default()

 r := gin.New()
 r.Use(GinLogger(logger), GinRecovery(logger, true))
 r.GET("/hello", func(c *gin.Context) {
  c.String(http.StatusOK, "hello xiaoqiao!")
 })
 r.Run()
}

// GinLogger
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
returnfunc(c *gin.Context) {
  start := time.Now()
  path := c.Request.URL.Path
  query := c.Request.URL.RawQuery
  c.Next() // 執行後續中間件

// Since returns the time elapsed since t.
// It is shorthand for time.Now().Sub(t).
  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
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
returnfunc(c *gin.Context) {
deferfunc() {
   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()
 }
}

func InitLogger() {
 writeSyncer := getLogWriter()
 encoder := getEncoder()
// NewCore創建一個向WriteSyncer寫入日誌的Core。

// A WriteSyncer is an io.Writer that can also flush any buffered data. Note
// that *os.File (and thus, os.Stderr and os.Stdout) implement WriteSyncer.

// LevelEnabler決定在記錄消息時是否啓用給定的日誌級別。
// Each concrete Level value implements a static LevelEnabler which returns
// true for itself and all higher logging levels. For example WarnLevel.Enabled()
// will return true for WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, and
// FatalLevel, but return false for InfoLevel and DebugLevel.
 core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

// New constructs a new Logger from the provided zapcore.Core and Options. If
// the passed zapcore.Core is nil, it falls back to using a no-op
// implementation.

// AddCaller configures the Logger to annotate each message with the filename,
// line number, and function name of zap's caller. See also WithCaller.
 logger = zap.New(core, zap.AddCaller())
// Sugar封裝了Logger,以提供更符合人體工程學的API,但速度略慢。糖化一個Logger的成本非常低,
// 因此一個應用程序同時使用Loggers和SugaredLoggers是合理的,在性能敏感代碼的邊界上在它們之間進行轉換。
 sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
// NewJSONEncoder創建了一個快速、低分配的JSON編碼器。編碼器適當地轉義所有字段鍵和值。
// NewProductionEncoderConfig returns an opinionated EncoderConfig for
// production environments.
//return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

// NewConsoleEncoder創建一個編碼器,其輸出是爲人類而不是機器設計的。
// 它以純文本格式序列化核心日誌條目數據(消息、級別、時間戳等),並將結構化上下文保留爲JSON。
 encoderConfig := zapcore.EncoderConfig{
  TimeKey:        "ts",
  LevelKey:       "level",
  NameKey:        "logger",
  CallerKey:      "caller",
  FunctionKey:    zapcore.OmitKey,
  MessageKey:     "msg",
  StacktraceKey:  "stacktrace",
  LineEnding:     zapcore.DefaultLineEnding,
  EncodeLevel:    zapcore.LowercaseLevelEncoder,
  EncodeTime:     zapcore.ISO8601TimeEncoder,
  EncodeDuration: zapcore.SecondsDurationEncoder,
  EncodeCaller:   zapcore.ShortCallerEncoder,
 }

return zapcore.NewConsoleEncoder(encoderConfig)
}

//func getLogWriter() zapcore.WriteSyncer {
// // Create創建或截斷指定文件。如果文件已經存在,它將被截斷。如果該文件不存在,則以模式0666(在umask之前)創建。
// // 如果成功,返回的File上的方法可以用於IO;關聯的文件描述符模式爲O_RDWR。如果有一個錯誤,它的類型將是PathError。
// //file, _ := os.Create("./test.log")
// file, err := os.OpenFile("./test.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
// if err != nil {
//  log.Fatalf("open log file failed with error: %v", err)
// }
// // AddSync converts an io.Writer to a WriteSyncer. It attempts to be
// // intelligent: if the concrete type of the io.Writer implements WriteSyncer,
// // we'll use the existing Sync method. If it doesn't, we'll add a no-op Sync.
// return zapcore.AddSync(file)
//}

func getLogWriter() zapcore.WriteSyncer {
// Logger is an io.WriteCloser that writes to the specified filename.
// 日誌記錄器在第一次寫入時打開或創建日誌文件。如果文件存在並且小於MaxSize兆字節,則lumberjack將打開並追加該文件。
// 如果該文件存在並且其大小爲>= MaxSize兆字節,
// 則通過將當前時間放在文件擴展名(或者如果沒有擴展名則放在文件名的末尾)的名稱中的時間戳中來重命名該文件。
// 然後使用原始文件名創建一個新的日誌文件。
// 每當寫操作導致當前日誌文件超過MaxSize兆字節時,將關閉當前文件,重新命名,並使用原始名稱創建新的日誌文件。
// 因此,您給Logger的文件名始終是“當前”日誌文件。
// 如果MaxBackups和MaxAge均爲0,則不會刪除舊的日誌文件。
 lumberJackLogger := &lumberjack.Logger{
// Filename是要寫入日誌的文件。備份日誌文件將保留在同一目錄下
  Filename: "./test.log",
// MaxSize是日誌文件旋轉之前的最大大小(以兆字節爲單位)。默認爲100兆字節。
  MaxSize: 1, // M
// MaxBackups是要保留的舊日誌文件的最大數量。默認是保留所有舊的日誌文件(儘管MaxAge仍然可能導致它們被刪除)。
  MaxBackups: 5, // 備份數量
// MaxAge是根據文件名中編碼的時間戳保留舊日誌文件的最大天數。
// 請注意,一天被定義爲24小時,由於夏令時、閏秒等原因,可能與日曆日不完全對應。默認情況下,不根據時間刪除舊的日誌文件。
  MaxAge: 30, // 備份天數
// Compress決定是否應該使用gzip壓縮旋轉的日誌文件。默認情況下不執行壓縮。
  Compress: false, // 是否壓縮
 }

return zapcore.AddSync(lumberJackLogger)
}

func simpleHttpGet(url string) {
// Get向指定的URL發出Get命令。如果響應是以下重定向代碼之一,則Get跟隨重定向,最多可重定向10個:
// 301 (Moved Permanently)
// 302 (Found)
// 303 (See Other)
// 307 (Temporary Redirect)
// 308 (Permanent Redirect)
// Get is a wrapper around DefaultClient.Get.
// 使用NewRequest和DefaultClient.Do來發出帶有自定義頭的請求。
 resp, err := http.Get(url)
if err != nil {
// Error在ErrorLevel記錄消息。該消息包括在日誌站點傳遞的任何字段,以及日誌記錄器上積累的任何字段。
//logger.Error(

// 錯誤使用fmt。以Sprint方式構造和記錄消息。
  sugarLogger.Error(
   "Error fetching url..",
   zap.String("url", url), // 字符串用給定的鍵和值構造一個字段。
   zap.Error(err))         // // Error is shorthand for the common idiom NamedError("error", err).
 } else {
// Info以infollevel記錄消息。該消息包括在日誌站點傳遞的任何字段,以及日誌記錄器上積累的任何字段。
//logger.Info("Success..",

// Info使用fmt。以Sprint方式構造和記錄消息。
  sugarLogger.Info("Success..",
   zap.String("statusCode", resp.Status),
   zap.String("url", url))
  resp.Body.Close()
 }
}

運行並訪問:http://localhost:8080/hello

Code/go/zap_demo via 🐹 v1.20.3 via 🅒 base 
➜ go run main.go 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

test.log

2023-06-17T14:17:08.553+0800 info zap_demo/main.go:42 test lumberjack for log rotate....
2023-06-17T16:48:25.600+0800 info zap_demo/main.go:76 /hello {"status": 200, "method": "GET", "path": "/hello", "query": "", "ip": "::1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "errors": "", "cost": 0.000057417}
2023-06-17T16:48:25.753+0800 info zap_demo/main.go:76 /favicon.ico {"status": 404, "method": "GET", "path": "/favicon.ico", "query": "", "ip": "::1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "errors": "", "cost": 0.000000541}

Zap 作爲一個高效的日誌管理工具,能夠提升開發效率和系統穩定性。無論是開發新的 Gin 項目還是優化現有項目的日誌管理,Zap 都是一個不可或缺的選擇。

參考

🚀 我的個人博客網站正式上線啦!

歡迎大家訪問 https://paxon.fun 🎉

博客名稱:Paxon Qiao’s Tech Blog --- 未來我會在這裏分享 Web3、區塊鏈、編程等技術文章,敬請期待!🙏

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LHKeRhzWLTwLc1G_hm1_GQ