go语言之日志库zap
zap是Uber开发的非常快的、结构化的,分日志级别的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、请求接口后,可以看到日志实时输出,如图:



