go语言之日志库zap

zapUber开发的非常快的、结构化的,分日志级别的Go日志库。根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好,也比标准库更快

可实现功能:

  • 不同级别的日志输出到不同文件中
  • 日志文件按照文件大小或日期进行切割存储,以避免单一日志文件过大
  • 日志使用简单方便,一次定义全局使用

github地址:https://github.com/uber-go/zap

本例子项目名称为oamp,所用配置文件库为gopkg.in/ini.v1,官方地址:https://ini.unknwon.io/,

1、首先在配置文件oamp/conf/app.ini中编写需要的配置,如下:

[log]
#定义日志级别,分为debug和info
Level = info
#定义日志格式化类型,分为json类型和普通类型console
Format = json
#定义日志所在的文件夹,此文件夹为相对于项目根目录的文件夹
Directory = log
#是否显示行号
ShowLine = true
#编码级别,包含四种:LowercaseLevelEncoder、LowercaseColorLevelEncoder、CapitalLevelEncoder、CapitalColorLevelEncoder
EncodeLevel = LowercaseLevelEncoder
#是否输出到控制台
LogToConsole = false
#进行切割之前日志文件最大大小(MB)
MaxSize = 10
#保留旧文件的最大个数
MaxBackups = 5
#保留旧文件的最大天数
MaxAge = 10
#是否压缩
Compress = true
#栈名
StacktraceKey = stacktrace

2、将配置文件映射到结构体中

(1)、编写文件oamp/pkg/setting/setting.go,先定义结构体,内容如下:

注意:结构体中字段要与配置文件中字段一样
type log struct {
    Level         string
    Format        string
    Directory     string
    ShowLine      bool
    StacktraceKey string
    LogToConsole  bool
    EncodeLevel   string
    MaxSize       int
    MaxBackups    int
    MaxAge        int
    Compress      bool
}
var LogSetting = &log{}

(2)、将配置文件内容映射到结构体中,如下:

func Setup() {
	//读取配置文件
	cfg, err := ini.Load("conf/app.ini")
	if err != nil {
		global.Log.Error("err")
	}
	//通过MapTo将配置文件log映射到结构体LogSetting中
	if err := cfg.Section("log").MapTo(LogSetting); err != nil {
		global.Log.Error(err.Error())
	}
}

注:映射后,其余文件通过setting.LogSetting.Level就可以读取响应配置信息

3、定义全局变量Log,编辑文件oamp/global/global.go,内容如下:

package global

import (
	"go.uber.org/zap"
)

//外部访问,因为Log需要大写
var (
	Log *zap.Logger
)

4、编写zap日志库文件,编辑oamp/pkg/logging/zaplog.go,内容如下:

package logging

import (
	"fmt"
	"oamp/global"
	"oamp/pkg/setting"
	"os"
	"time"

	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

//初始化日志,传递日志级别参数
func InitLog(level string) {
	var (
		logLevel = zap.InfoLevel //定义出初始的logLevel为info
	)
	//通过switch,根据传入的level不同,给logLevel赋予不同的值
	switch level {
	case "debug":
		logLevel = zap.DebugLevel //debug
	case "info":
		logLevel = zap.InfoLevel //info
	case "warn":
		logLevel = zap.WarnLevel //warn
	case "error":
		logLevel = zap.ErrorLevel //error
	case "panic":
		logLevel = zap.PanicLevel //panic
	case "fatal":
		logLevel = zap.FatalLevel //fatal
	default:
		logLevel = zap.InfoLevel //默认就是info
	}
	//日志级别,zap.LevelEnablerFunc(func(lev zapcore.Level) bool 用来划分不同级别的输出,zapcore.Level类型为int8
	debugPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //debug级别
		return lev == zap.DebugLevel
	})
	infoPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //info级别
		return lev == zap.InfoLevel
	})
	errorPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //error级别
		return lev == zap.ErrorLevel
	})
	//定义接口类型切片,zapcore.Core为一个接口
	var cores []zapcore.Core
	//如果日志级别为debug,那么向切片中添加数据,打印debug日志,否则打印info和error
	if logLevel == zap.DebugLevel {
		cores = append(cores, getEncoderCore(fmt.Sprintf("./%s/server_debug.log", setting.LogSetting.Directory), debugPriority))
	} else if logLevel == zap.InfoLevel {
		cores = append(cores, getEncoderCore(fmt.Sprintf("./%s/server_info.log", setting.LogSetting.Directory), infoPriority))
		cores = append(cores, getEncoderCore(fmt.Sprintf("./%s/server_error.log", setting.LogSetting.Directory), errorPriority))
	}
	//通过zap.New创建一个logger,NewTee创建一个Core,将日志条目复制到两个或更多的底层Core中,...表示解压缩切片
	logger := zap.New(zapcore.NewTee(cores[:]...))
	//判断是否显示行号
	if setting.LogSetting.ShowLine {
		logger = logger.WithOptions(zap.AddCaller())
	}
	//将创建后的logger赋值给全局变量global.Log
	global.Log = logger
}

//函数参数为上面传递过来的文件名和级别,返回为接口类型
func getEncoderCore(fileName string, level zapcore.LevelEnabler) (core zapcore.Core) {
	writer := GetWriteSyncer(fileName) //调用函数对日志进行切割,并返回写入位置
        /*
             zapcore.NewCore方法中三个参数:
             getEncoder(): 以什么格式写入日志
             writer: 日志写到哪里
             level:   什么级别的日志可以被写入
         */
	return zapcore.NewCore(getEncoder(), writer, level)
}

//定义日志输出格式为json还是普通格式
func getEncoder() zapcore.Encoder {
	//获取配置文件的输出格式,json or console
	if setting.LogSetting.Format == "json" {
		return zapcore.NewJSONEncoder(getEncoderConfig())
	}
	return zapcore.NewConsoleEncoder(getEncoderConfig())
}

//自定义日志格式,zapcore.EncoderConfig为结构体
func getEncoderConfig() (config zapcore.EncoderConfig) {
	config = zapcore.EncoderConfig{
		MessageKey:     "message",
		LevelKey:       "level",
		TimeKey:        "time",
		NameKey:        "logger",
		CallerKey:      "caller",                         //日志文件位置
		StacktraceKey:  setting.LogSetting.StacktraceKey, //栈名
		LineEnding:     zapcore.DefaultLineEnding,        //默认的结尾\n
		EncodeLevel:    zapcore.LowercaseLevelEncoder,    //小写字母输出
		EncodeTime:     CustomTimeEncoder,                //自定义时间格式
		EncodeDuration: zapcore.SecondsDurationEncoder,   //编码间隔
		EncodeCaller:   zapcore.FullCallerEncoder,        //控制打印的文件位置是绝对路径,ShortCallerEncoder 是相对路径
	}
	//根据配置文件重新配置编码颜色和字体
	switch {
	case setting.LogSetting.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
		config.EncodeLevel = zapcore.LowercaseLevelEncoder
	case setting.LogSetting.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
		config.EncodeLevel = zapcore.LowercaseColorLevelEncoder
	case setting.LogSetting.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
		config.EncodeLevel = zapcore.CapitalLevelEncoder
	case setting.LogSetting.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
		config.EncodeLevel = zapcore.CapitalColorLevelEncoder
	default:
		config.EncodeLevel = zapcore.LowercaseLevelEncoder
	}
	return config
}

// 自定义日志输出时间格式
func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
}

//日志切割,返回写入位置
func GetWriteSyncer(file string) zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   file,                          // 日志文件的位置
		MaxSize:    setting.LogSetting.MaxSize,    // 在进行切割之前,日志文件的最大大小(以MB为单位)
		MaxBackups: setting.LogSetting.MaxBackups, // 保留旧文件的最大个数
		MaxAge:     setting.LogSetting.MaxAge,     // 保留旧文件的最大天数
		Compress:   setting.LogSetting.Compress,   // 是否压缩/归档旧文件
	}
	//如果setting.LogSetting.LogToConsole为true,那么日志既输入到控制台又输出到文件
	if setting.LogSetting.LogToConsole {
		return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger))
	}
	return zapcore.AddSync(lumberJackLogger)
}

注:上述设置如果配置文件中Level为debug的时候,只打印debug日志,否则打印Info和error

5、在main.go文件中,初始化setting.go文件和zao日志库文件,如下:

初始化日志库的时候,要传递日志级别,在打印日志的时候,在任意位置通过global.Log.Info()即可打印

启动后打印的日志内容如下:

[root@VM-16-6-centos oamp]# tail -n 100 log/server_info.log 
{"level":"info","time":"2022-09-28 10:33:11.469","caller":"/data/oamp/main.go:22","message":"Info........"}
{"level":"info","time":"2022-09-28 10:44:54.032","caller":"/data/oamp/main.go:22","message":"Info........"}
{"level":"info","time":"2022-09-28 11:19:33.957","caller":"/data/oamp/main.go:22","message":"Info........"}
[root@VM-16-6-centos oamp]# tail -n 100 log/server_error.log
{"level":"error","time":"2022-09-28 10:33:11.469","caller":"/data/oamp/main.go:23","message":"Error........"}
{"level":"error","time":"2022-09-28 10:44:54.032","caller":"/data/oamp/main.go:23","message":"Error........"}
{"level":"error","time":"2022-09-28 11:19:33.957","caller":"/data/oamp/main.go:23","message":"Error........"}
  • level:当前日志级别
  • time:当前时间
  • caller:日志具体位置
  • message:具体的描述信息

Gin中使用zap日志库

在初始化gin引擎的时候,有两种方式gin.Default()和gin.New(),两者的区别是:

  • gin.Default()用到了gin框架内自带的两个默认中间价Logger()和Recovery(),Logger()是把gin框架本身的日志输出到标准输出,而Recovery()是在程序出现panic的时候恢复现场并写入500响应的
  • gin.New()没有用自带的中间件,因此如果通过gin.New()实例引擎,控制台看不到请求接口的任何输出

现在我们可以将初始化引擎改为gin.New(),然后自定义两个中间件GinLogger()和GinRecovery(),实现将gin框架中的日志输出到日志库zap中

1、修改oamp/router/router.go文件,初始化引擎改为gin.New(),如图:

2、编辑中间件文件, oamp/middleware/zaplog/zaplog.go,内容如下:

package zaplog

import (
	"net"
	"net/http"
	"net/http/httputil"
	"oamp/global"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

//正常的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)
		global.Log.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),
		)
	}
}

//项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				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 {
					global.Log.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					c.Error(err.(error))
					c.Abort()
					return
				}

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

3、在路由组中注册中间件,如图:

4、请求接口后,可以看到日志实时输出,如图:

标签