本文将介绍在基于gin
框架开发的项目中如何配置并使用zap
来接收gin
框架默认的日志以及如何配置日志切割。
我们在基于gin
框架开发项目时通常都会选择使用专业的日志库来记录项目中的日志,go
语言常用的日志库有zap、logrus
等。我们该项目中使用zap
。
我们该如何在日志中记录gin
框架本身输出的那些日志呢?
一、gin默认的中间件
首先我们来看一个最简单的gin
项目:
func main() {
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
c.String("hello q1mi!")
})
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
框架默认输出的日志。
从上面gin.Default
的源码可以看出,创建gin
的引擎实际就是先用New
创建了引擎,然后使用Use
配置Logger
和Recovery
中间件,也就是说我们可以不用gin.Default
创建引擎,而是可以直接用New
创建引擎,然后用Use
使用我们自己的中间件。
此外,也可以看下gin
中Logger()
中间件的部分源码
这里以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
代码如下:
注:下面代码中用到了后面要介绍的viper读取配置文件等,所以可能会有一些前置代码,不过不影响整体阅读的。
package logger
import (
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"scheduler/config"
"strings"
"time"
)
var Logger *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)
Logger = zap.New(core, zap.AddCaller())
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(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,并使用zap记录相关日志
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()
}
}
然后定义日志相关配置:
type LogConfig struct {
Level string `json:"level"` // 日志等级
Filename string `json:"filename"` // 基准日志文件名
MaxSize int `json:"maxsize"` // 单个日志文件最大内容,单位:MB
MaxAge int `json:"max_age"` // 日志文件保存时间,单位:天
MaxBackups int `json:"max_backups"` // 最多保存几个日志文件
}
在项目中先初始化配置信息,再调用logger.InitLogger(cfg.LogConfig)
即可完成日志的初始化。
func main() {
// load config from conf/conf.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
}
r := routes.SetupRouter()
addr := fmt.Sprintf(":%v", config.Conf.ServerConfig.Port)
r.Run(addr)
}
注册中间件的操作在routes.SetupRouter()
中:
func SetupRouter() *gin.Engine {
//gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(logger.GinLogger(logger.Logger), logger.GinRecovery(logger.Logger, true))
mainGroup := r.Group("/api")
{
...
}
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
return r
}