文章目录

Gin 中使用日志Gin自带日志简单使用配置中间件定制日志格式定制日志格式+修改输出位置

Zap日志库介绍ZAP 快速使用SugearedLoggerLoggerlogger 的选择日志级别初始化logger自定制logger格式化时间和添加调用者信息日志轮转与归档

gin 中使用 zap使用第三方库自定制gin中使用zap

Gin 中使用日志

Gin自带日志

在项目中,日志非常重要,方便我们快速定位错误,之前使用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{

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{

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")

}

Zap日志库

介绍

由于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 快速使用

zap 提供了两种日志记录器

go get -u go.uber.org/zap

SugearedLogger

加了糖的 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

当性能和类型安全至关重要时,使用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 的选择

在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

)

初始化logger

因为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

自定制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"))

}

gin 中使用 zap

使用第三方库

// 地址: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")

}

自定制gin中使用zap

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")

}