在项目中,日志非常重要,方便我们快速定位错误,之前使用fmt输出的代码,都需要改成用日志输出,输出位置可能是控制台,或者是文件,gin框架中,默认提供了日志记录中间件
func main() {
// 直接配置
gin.DisableConsoleColor() // 进制控制台日志颜色
// 控制日志输出到文件
f, _ := os.OpenFile("./app.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
// 默认输出位置, 日志输出到文件和控制台两个位置
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200,"成功")
})
r.Run(":8000")
}
func main() {
r := gin.New()
r.Use(gin.Recovery())
f,_ := os.OpenFile("./app01.log",os.O_CREATE|os.O_APPEND|os.O_RDWR,0644)
// 配置中间件
r.Use(gin.LoggerWithWriter(io.MultiWriter(f,os.Stdout)))
r.GET("/", func(c *gin.Context) {
c.String(200,"成功")
})
r.Run(":8000")
}
func main() {
r := gin.New()
r.Use(gin.Recovery())
f,_ := os.OpenFile("./app01.log",os.O_CREATE|os.O_APPEND|os.O_RDWR,0644)
// 配置中间件
r.Use(gin.LoggerWithWriter(io.MultiWriter(f,os.Stdout)))
// 返回什么格式,日志格式就是什么样子
var formatter = func(param gin.LogFormatterParams) string{
return fmt.Sprintf("客户端IP:%s,请求时间:[%s],请求方式:%s,请求地址:%s,http协议版本:%s,请求状态码:%d,响应时间:%s,客户端:%s,错误信息:%s\n",
param.ClientIP,
param.TimeStamp.Format("2006年01月02日 15:03:04"),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}
r.Use(gin.LoggerWithFormatter(formatter))
r.GET("/", func(c *gin.Context) {
c.String(200,"成功")
})
r.Run(":8000")
}
虽然上方代码,同样也能实现定制日志格式并修改输出位置的设置,但不是推荐写法,还有一种更加简介的写法
func main() {
r := gin.New()
r.Use(gin.Recovery())
f,_ := os.OpenFile("./app01.log",os.O_CREATE|os.O_APPEND|os.O_RDWR,0644)
// 配置中间件
//r.Use(gin.LoggerWithWriter(io.MultiWriter(f,os.Stdout)))
// 返回什么格式,日志格式就是什么样子
var conf = gin.LoggerConfig{
Formatter: func(param gin.LogFormatterParams) string{
return fmt.Sprintf("客户端IP:%s,请求时间:[%s],请求方式:%s,请求地址:%s,http协议版本:%s,请求状态码:%d,响应时间:%s,客户端:%s,错误信息:%s\n",
param.ClientIP,
param.TimeStamp.Format("2006年01月02日 15:03:04"),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
},
Output: io.MultiWriter(os.Stdout,f),
}
r.Use(gin.LoggerWithConfig(conf))
r.GET("/", func(c *gin.Context) {
c.String(200,"成功")
})
r.Run(":8000")
}
由于gin默认日志有缺陷,不能轮转,在视图函数中不能直接使用日志记录(go标准库的logger),不能序列化等等,说白了就是功能不够强大,因此,go又有很多开源的日志包,如下
logrus
目前Github上star数量最多的日志库,也是最兼容标准库的日志库
项目地址: https://github.com/sirupsen/logrus
zap
是Uber推出的一个快速、结构化的分级日志库, 无反射, 零分配的JSON编码器(本文介绍),是最快的一个日志库。原因:不是基于反射做的
项目地址:https://github.com/uber-go/zap
官方文档:https://pkg.go.dev/go.uber.org/zap
zerolog
它的 API 设计非常注重开发体验和性能。zerolog
只专注于记录 JSON 格式的日志,号称 0 内存分配
项目地址:https://github.com/rs/zerolog
zap 提供了两种日志记录器
go get -u go.uber.org/zap
加了糖的 Logger
在性能很好但不是很关键的环境中,使用SugaredLogger。它比其他结构化日志包快4-10倍,并且包含结构化和printf风格的api。
func main() {
// 初始化得到 logger 对象
logger, _ := zap.NewProduction()
// 刷新缓冲区,存盘
defer logger.Sync()
// 创建 Suger 的 logger
sugar := logger.Sugar()
sugar.Info("info 级别日志")
// 因为 NewProduction 是生成环境用的,最低级别就是info,所以不显示debug
sugar.Debug("debug 级别日志")
sugar.Error("error 级别日志")
sugar.Infof("info--格式化字符串格式日志: %s", "lqz")
sugar.Infow("info---松散类型的键值对格式日志",
// 结构化上下文为松散类型的键值对,随便写键值对
"name", "lxx",
"attempt", 3,
"backoff", time.Second,
)
}
当性能和类型安全至关重要时,使用Logger。它甚至比SugaredLogger还要快,并且分配的数量要少得多,但是它只支持结构化日志 。
func main() {
// 初始化得到 logger 对象
logger, _ := zap.NewProduction()
// 刷新缓冲区,存盘
defer logger.Sync()
logger.Info("info--松散类型的键值对格式日志",
// 作为强类型字段值的结构化上下文.
zap.String("name", "lxx"),
zap.Int("age", 19),
zap.Duration("backoff", time.Second),
)
logger.Error("error--松散类型的键值对格式日志",
zap.String("name", "lxx"),
zap.Int("age", 19),
zap.Duration("backoff", time.Second),)
}
在Logger和SugaredLogger之间进行选择不需要在应用程序范围内进行决定:在两者之间进行转换十分方便快捷。从上面就可以看出来,二者创建使用区别很小。
func main() {
logger := zap.NewExample()
defer logger.Sync()
sugar := logger.Sugar() // 通过logger得到Sugar
plain := sugar.Desugar() // 通过Sugar得到logger
sugar.Info("info-->sugar")
plain.Info("info-->logger")
}
//const 文档下面有介绍日志级别的定义,7个日志级别
const (
// 测试 Debug
DebugLevel = zapcore.DebugLevel
// 正常 Info
InfoLevel = zapcore.InfoLevel
// 警告 warn
WarnLevel = zapcore.WarnLevel
// 错误 error
ErrorLevel = zapcore.ErrorLevel
// 严重错误级别,但小于 panic级别
DPanicLevel = zapcore.DPanicLevel
// panic 级别日志, 展示错误位置
PanicLevel = zapcore.PanicLevel
// 报错后写入日志直接退出程序
FatalLevel = zapcore.FatalLevel
)
因为zap配置很复杂,因此提供了三种默认配置,直接用默认提供了三种初始化logger的方式就行
NewExample,NewProduction和NewDevelopment
三种创建的logger 区别如下。分别对应着不同的环境
NewExample
func NewExample(options ...Option) *Logger
NewExample构建了一个专门为zap的可测试示例设计的Logger。它将DebugLevel及以上的日志作为JSON写入标准输出,但省略了时间戳和调用函数,以保持示例输出的简短和确定性。测试阶段使用
NewProduction
func NewProduction(options ...Option) (*Logger, error)
NewProduction构建了一个合理的生产日志记录器,它将infollevel及以上的日志以JSON的形式写入标准错误。上线阶段使用
它是NewProductionConfig().build(…Option)的快捷方式。
NewDevelopment
func NewDevelopment(options ...Option) (*Logger, error)
NewDevelopment构建一个开发日志记录器,它以人类友好的格式将DebugLevel及以上级别的日志写入标准错误。 开发阶段使用
这是NewDevelopmentConfig().Build(…选项)的快捷方式
通过配置生成对应的 logger。 我们也可以自定义 配置,生成自己自定义的 logger
查看NewProduction的源码。实际底层就是:NewProductionConfig().Build(options…)
func NewProduction(options ...Option) (*Logger, error) {
//调用了 NewProductionConfig()方法,内部初始化创建,返回了一个 Config 对象
//Build, 内部通过 Config对象的配置, 利用New方法生成相应的 logger对象,并返回
return NewProductionConfig().Build(options...)
}
// 这是 zap库给我们预置的 NewProduction()等方法,内部是按照指定的配置,生成相应的 logger 日志对象。 我们也可以自己调用内部的相关方法, 模仿 NewProductionConfig().Build(options…) 相关过程,自己创建,定制化 logger对象。
查看build方法,可以看出生成logger 所需要的东西,在New方法里面
func (cfg Config) Build(opts ...Option) (*Logger, error) {
...
log := New(
zapcore.NewCore(enc, sink, cfg.Level),
cfg.buildOptions(errSink)...,
)
...
return log, nil
}
func New(core zapcore.Core, options ...Option) *Logger {
log := &Logger{
//Core是一个最小的、快速的记录器接口。它是为库作者设计的,用来封装更友好的API
core: core,
// 错误输出位置
errorOutput: zapcore.Lock(os.Stderr),
// 设置日志上限
addStack: zapcore.FatalLevel + 1,
// 设置时间方式
clock: zapcore.DefaultClock,
}
// 返回一个 Logger 对象的指针
return log.WithOptions(options...)
}
通过查看 NewProductionConfig 源码可以看出自定制日志需要那些配置,,Build 函数根据这个配置,进行生成 logger对象。我们可以自定义这个, 来实现生成自己的logger
func NewProductionConfig() Config {
return Config{
// 日志级别
Level: NewAtomicLevelAt(InfoLevel),
Development: false,
// 设置采样信息,限制日志记录对进程施加的全局CPU和IO负载
Sampling: &SamplingConfig{
Initial: 100, // 配置每秒多少次
Thereafter: 100,
},
// 编码方式
Encoding: "json",
// 配置 encoder 编码
EncoderConfig: NewProductionEncoderConfig(),
// 打开文件,写入日志信息位置
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
方式1 — 通过 new 方法得到logger对象
func main() {
//方式1
// encoder 编码, 就两种方式
//encoder := zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
// 日志输出路径
f,_ := os.OpenFile("./test.log",os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)
// 把文件对象做成WriteSyncer类型
writeSyncer := zapcore.AddSync(f)
core := zapcore.NewCore(encoder,writeSyncer,zapcore.DebugLevel)
logger := zap.New(core)
defer logger.Sync()
logger.Info("info级别写到文件", zap.String("name", "lxx"))
logger.Debug("debug级别写到文件", zap.String("name", "lxx"))
}
**方式2 ** — 通过修改config配置生成logger对象
func main() {
// 方式2
conf := zap.NewProductionConfig()
// 修改 config对象的属性
// conf.Encoding="console"
conf.Encoding = "json"
//conf.OutputPaths = append(conf.OutputPaths, "./test.log")
conf.OutputPaths = []string{"./test1.log"}
// 修改日志级别
conf.Level=zap.NewAtomicLevelAt(zap.DebugLevel)
// 通过config对象得到logger对象指针
logger,_ := conf.Build()
logger.Debug("debug级别日志")
logger.Error("error级别日志")
}
提供的三种配置,时间显示都是时间戳格式,对人来说,这个时间格式是极其不友好的,因此我们可以通过自定制将时间格式转换为对人友好的时间格式
添加调用者信息"caller":“gin_log/main.go:152” 后可以快速定位错误
func main() {
//方式1
// 修改时间格式
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// encoder 编码, 就两种方式
//encoder := zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
encoder := zapcore.NewJSONEncoder(encoderConfig)
// 日志输出路径
f,_ := os.OpenFile("./test.log",os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)
// 把文件对象做成WriteSyncer类型
writeSyncer := zapcore.AddSync(f)
core := zapcore.NewCore(encoder,writeSyncer,zapcore.DebugLevel)
// 增加调用者信息
logger := zap.New(core,zap.AddCaller())
defer logger.Sync()
logger.Info("info级别写到文件", zap.String("name", "lxx"))
logger.Debug("debug级别写到文件", zap.String("name", "lxx"))
}
func main() {
//方式2 自带调用者信息
conf := zap.NewProductionConfig()
// 修改 config对象的属性
// conf.Encoding="console"
conf.Encoding = "json"
//conf.OutputPaths = append(conf.OutputPaths, "./test.log")
conf.OutputPaths = []string{"./test1.log"}
// 修改日志级别
conf.Level=zap.NewAtomicLevelAt(zap.DebugLevel)
// 修改时间格式
conf.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 通过config对象得到logger对象指针
logger,_ := conf.Build()
logger.Debug("debug级别日志")
logger.Error("error级别日志")
}
日志轮转与归档是公司里面基本使用的方案。能够避免日志文件过大,并进行日志文件分类
但 Zap 本身不支持切割归档日志文件,为了添加日志切割归档功能,我们将使用第三方库 Lumberjack 来实现。
go get -u github.com/natefinch/lumberjack
使用方案
func getwriteSyncer() zapcore.WriteSyncer{
lumberJackLogger := &lumberjack.Logger{
Filename: "./test3.log", // Filename: 日志文件的位置
MaxSize: 1, // 在进行切割之前,日志文件的最大大小(以 MB 为单位)
MaxBackups: 5, // 保留旧文件的最大个数
MaxAge: 30, // 保留旧文件的最大天数
Compress: false, // 是否压缩 / 归档旧文件
}
return zapcore.AddSync(lumberJackLogger)
}
func main() {
// 修改时间格式
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// encoder 编码, 就两种方式
//encoder := zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
encoder := zapcore.NewJSONEncoder(encoderConfig)
// 日志输出路径
writeSyncer := getwriteSyncer()
core := zapcore.NewCore(encoder,writeSyncer,zapcore.DebugLevel)
// 增加调用者信息
logger := zap.New(core,zap.AddCaller())
defer logger.Sync()
logger.Info("info级别写到文件", zap.String("name", "lxx"))
logger.Debug("debug级别写到文件", zap.String("name", "lxx"))
}
// 地址:https://github.com/gin-contrib/zap
// 下载
go get github.com/gin-contrib/zap
package main
import (
ginzap "github.com/gin-contrib/zap" // 同名,取个别名
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"time"
)
// 使用第三方库把zap集成到gin中
func main() {
r := gin.New()
// 1 得到config对象
conf := zap.NewProductionConfig()
// 2 修改config对象的属性,如编码,输出路径等
conf.Encoding = "json"
conf.OutputPaths = []string{"./web.log"}
//3 通过config对象得到logger对象指针
logger, _ := conf.Build()
//4 替换掉全局的logger,以后都使用zap.L()
zap.ReplaceGlobals(logger)
// 引入两个中间件-->用来替换原来gin框架中的Logger()这个中间件
// 以后,访问,出异常,都会记录到zap的日志中
r.Use(ginzap.Ginzap(logger, time.RFC3339, true))
r.Use(ginzap.RecoveryWithZap(logger, true))
r.GET("/index", func(c *gin.Context) {
c.String(200,"index页面")
})
r.GET("/home", func(c *gin.Context) {
//panic("我错了")
//zap.L() 就是自定义的全局的logger,并且并发安全
zap.L().Error("err 级别的日志")
c.String(200,"index页面")
})
r.Run(":8080")
}
logger/logger.go
package logger
import (
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)
// 1 定义一下logger使用的常量
const (
mode = "dev" //开发模式
filename = "web_app.log" // 日志存放路径
//level = "debug" // 日志级别
level = zapcore.DebugLevel // 日志级别
max_size = 200 //最大存储大小
max_age = 30 //最大存储时间
max_backups = 7 //#备份数量
)
// 2 初始化Logger对象
func InitLogger() (err error) {
// 创建Core三大件,进行初始化
writeSyncer := getLogWriter(filename, max_size, max_backups, max_age)
encoder := getEncoder()
// 创建核心-->如果是dev模式,就在控制台和文件都打印,否则就只写到文件中
var core zapcore.Core
if mode == "dev" {
// 开发模式,日志输出到终端
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
// NewTee创建一个核心,将日志条目复制到两个或多个底层核心中。
core = zapcore.NewTee(
zapcore.NewCore(encoder, writeSyncer, level),
zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), level),
)
} else {
core = zapcore.NewCore(encoder, writeSyncer, level)
}
//core := zapcore.NewCore(encoder, writeSyncer, level)
// 创建 logger 对象
log := zap.New(core, zap.AddCaller())
// 替换全局的 logger, 后续在其他包中只需使用zap.L()调用即可
zap.ReplaceGlobals(log)
return
}
// 获取Encoder,给初始化logger使用的
func getEncoder() zapcore.Encoder {
// 使用zap提供的 NewProductionEncoderConfig
encoderConfig := zap.NewProductionEncoderConfig()
// 设置时间格式
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 时间的key
encoderConfig.TimeKey = "time"
// 级别
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 显示调用者信息
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
// 返回json 格式的 日志编辑器
return zapcore.NewJSONEncoder(encoderConfig)
}
// 获取切割的问题,给初始化logger使用的
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
// 使用 lumberjack 归档切片日志
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}
// GinLogger 用于替换gin框架的Logger中间件,不传参数,直接这样写
func GinLogger(c *gin.Context) {
logger := zap.L()
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 用于替换gin框架的Recovery中间件,因为传入参数,再包一层
func GinRecovery(stack bool) gin.HandlerFunc {
logger := zap.L()
return func(c *gin.Context) {
defer func() {
// defer 延迟调用,出了异常,处理并恢复异常,记录日志
if err := recover(); err != nil {
// 这个不必须,检查是否存在断开的连接(broken pipe或者connection reset by peer)---------开始--------
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
}
}
}
//httputil包预先准备好的DumpRequest方法
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// 如果连接已断开,我们无法向其写入状态
c.Error(err.(error))
c.Abort()
return
}
// 这个不必须,检查是否存在断开的连接(broken pipe或者connection reset by peer)---------结束--------
// 是否打印堆栈信息,使用的是debug.Stack(),传入false,在日志中就没有堆栈信息
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.String(200,"访问出错了")
}
}()
c.Next()
}
}
main.go
package main
import (
"gin_zap_demo/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func main() {
logger.InitLogger()
r:=gin.New()
r.Use(logger.GinLogger,logger.GinRecovery(true))
r.GET("/", func(c *gin.Context) {
zap.L().Error("错误日志")
c.String(200,"hello")
})
r.Run(":8080")
}