Gin 中使用日志

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

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

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

go&Python

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值