go 进阶 十. 日志文件处理

一. 基础版

  1. 基础版本中通过手动写文件的方式,基于golang标准库的log.Logger实现日志的写入
  1. 创建保存日志配置的结构体
  2. 创建用来保存处理日志信息的上下文结构体,绑定日志文件处理,日志打印方法
  3. 在日志打印方法中使用标准库中的log.Logger.()组合协程实现异步日志打印
package test

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

const DATE_FORMAT = "2006-01-02 15:04:05"

//日志级别
type LEVEL byte

const (
	TRACE LEVEL = iota
	INFO
	WARN
	ERROR
	OFF
)

//日志打印相关全局锁
var msg_mutex sync.Mutex

var fileLog *FileLogger

//1.创建日志配置结构体
type LoggerConf struct {
	FileDir  string //当前日志文件存放路径
	FileName string //当前日志文件名
	Prefix   string //当前日志文件名前缀
	Level    string //当前日志级别
}

//关闭日志
func CloseLogger() {
	if fileLog != nil {
		close(fileLog.logChan)
		fileLog.lg = nil
		fileLog.logFile.Close()
		fileLog = nil
	}
}

//2.创建日志上下文结构体
type FileLogger struct {
	fileDir  string
	fileName string
	prefix   string
	date     *time.Time
	logFile  *os.File      //当前日志文件句柄
	lg       *log.Logger   //golang底层提供的日志操作对象
	logLevel LEVEL         //日志级别
	mu       *sync.RWMutex //用来保证创建日志文件并发安全锁
	logChan  chan string   //存放日志数据通道
}

//3.启动服务时调用该函对日志相关业务进行初始化
func New() *FileLogger {
	//1.读取配置文件,创建日志配置结构体
	conf := LoggerConf{
		FileDir:  "./log",
		FileName: "mgr",
		Prefix:   "pms",
		Level:    "TRACE",
	}

	//2.判断当前配置的日志级别
	var logLevel LEVEL
	if strings.EqualFold(conf.Level, "OFF") {
		logLevel = OFF
	} else if strings.EqualFold(conf.Level, "TRACE") {
		logLevel = TRACE
	} else if strings.EqualFold(conf.Level, "WARN") {
		logLevel = WARN
	} else if strings.EqualFold(conf.Level, "ERROR") {
		logLevel = ERROR
	} else {
		logLevel = INFO
	}

	//3.封装打印日志上下文
	t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
	f := &FileLogger{
		fileDir:  conf.FileDir,
		fileName: conf.FileName,
		prefix:   conf.Prefix,
		mu:       new(sync.RWMutex),
		logChan:  make(chan string, 5000),
		date:     &t,
		logLevel: logLevel,
	}

	//4.判断日志文件是否存在,不存在则创建
	f.isExistOrCreate()

	f.deleteFiles()
	logFile := filepath.Join(f.fileDir, f.fileName)
	var err error
	f.logFile, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
	if err != nil {
		return nil
	}

	fmt.Println("open log file : " + logFile)

	f.lg = log.New(f.logFile, f.prefix, log.LstdFlags|log.Lmicroseconds)
	go f.logWriter()
	return f
}

// 日志文件是否存在,不存在则创建
func (f FileLogger) isExistOrCreate() {
	_, err := os.Stat(f.fileDir)
	if err != nil && !os.IsExist(err) {
		err2 := os.Mkdir(f.fileDir, 0755)
		if err2 != nil {
			fmt.Println("log create path error  : " + f.fileDir)
			os.Exit(0)
		}
	}
}

//如果分割写入日志时,删除过期日志文件
func (f *FileLogger) deleteFiles() {
	rd, err := ioutil.ReadDir(f.fileDir)
	if err != nil {
		return
	}
	now := time.Now()
	//todo 读取配置文件配置的日志过期天数
	days := 2
	if days < 1 || days > 30 {
		days = 7
	}
	_onemonthbefore := now.Add(time.Duration(-24*days) * time.Hour)
	for _, fi := range rd {
		if !fi.IsDir() {
			if fi.ModTime().Before(_onemonthbefore) {
				fmt.Println("remove log file : " + filepath.Join(f.fileDir, fi.Name()))
				os.Remove(filepath.Join(f.fileDir, fi.Name()))
			}
		}
	}
}

//一条日志格式化方法
func (f *FileLogger) split() (err error) {
	f.mu.Lock()
	defer f.mu.Unlock()

	logFile := filepath.Join(f.fileDir, f.fileName)
	logFileBak := logFile + "." + f.date.Format(DATE_FORMAT)

	if f.logFile != nil {
		f.logFile.Close()
	}

	err = os.Rename(logFile, logFileBak)
	if err != nil {
		return
	}

	t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
	f.date = &t

	f.logFile, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
	if err != nil {
		return
	}

	f.lg = log.New(f.logFile, f.prefix, log.LstdFlags|log.Lmicroseconds)
	return
}

// 日志写入
func (f *FileLogger) logWriter() {
	defer func() { recover() }()
	for {
		str := <-f.logChan
		f.mu.RLock()
		f.lg.Output(2, str)
		f.mu.RUnlock()
	}
}

//打印日志
func TraceLog(msgid string, format string, a ...interface{}) {
	msg_mutex.Lock()
	if fileLog == nil {
		fileLog = New()
	}
	msg_mutex.Unlock()
	_, file, line, _ := runtime.Caller(1)
	if fileLog.logLevel <= TRACE {
		fileLog.logChan <- fmt.Sprintf(" Trace [%v:%v] %s ", filepath.Base(file), line, msgid) + fmt.Sprintf(format, a...)
	}
}

//打印info级别日志
func InfoLog(msgid string, format string, a ...interface{}) {
	msg_mutex.Lock()
	if fileLog == nil {
		fileLog = New()
	}
	msg_mutex.Unlock()
	_, file, line, _ := runtime.Caller(1)
	if fileLog.logLevel <= INFO {
		fileLog.logChan <- fmt.Sprintf(" INFO [%v:%v] %s ", filepath.Base(file), line, msgid) + fmt.Sprintf(format, a...)
	}
}

//打印warn级别日志
func WarnLog(msgid string, format string, a ...interface{}) {
	msg_mutex.Lock()
	if fileLog == nil {
		fileLog = New()
	}
	msg_mutex.Unlock()
	_, file, line, _ := runtime.Caller(1)
	if fileLog.logLevel <= WARN {
		fileLog.logChan <- fmt.Sprintf(" WARN [%v:%v] %s ", filepath.Base(file), line, msgid) + fmt.Sprintf(format, a...)
	}
}

//打印error级别日志
func ErrorLog(msgid string, format string, a ...interface{}) {
	msg_mutex.Lock()
	if fileLog == nil {
		fileLog = New()
	}
	msg_mutex.Unlock()

	_, file, line, _ := runtime.Caller(1)
	if fileLog.logLevel <= ERROR {
		fileLog.logChan <- fmt.Sprintf(" ERROR [%v:%v] %s ", filepath.Base(file), line, msgid) + fmt.Sprintf(format, a...)
	}
	//fmt.Printf("ERROR:["+getTimeStr()+"]"+format, a...)
	//fmt.Println("")
}

二. gin 中的日志处理

  1. gin提供了专门用来处理long配置:
  1. LoggerConfig
  2. LogFormatter: 一个函数类型,用于格式化日志
  3. LogFormatterParams: 当使用 LogFormatter 格式化日志时,LogFormatterParams 作为参数传入
  4. defaultLogFormatter: 是一个默认的日志数据的格式化函数
type LoggerConfig struct {
    // 默认为gin.defaultLogFormatter
    Formatter LogFormatter

    // 日志写到哪里去,默认为gin.DefaultWriter
    Output io.Writer

    // 哪些URL路径的日志不用记录
    SkipPaths []string
}

type LogFormatter func(params LogFormatterParams) string

var defaultLogFormatter = func(param LogFormatterParams) string {
    // 获取颜色码
    var statusColor, methodColor, resetColor string
    if param.IsOutputColor() {
        statusColor = param.StatusCodeColor()
        methodColor = param.MethodColor()
        resetColor = param.ResetColor()
    }
    // 处理请求超过一分钟,将处理请求时间对秒取整
    if param.Latency > time.Minute {
        // Truncate in a golang < 1.8 safe way
        param.Latency = param.Latency - param.Latency%time.Second
    }
    // 日志数据格式化
    return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
        param.TimeStamp.Format("2006/01/02 - 15:04:05"),
        statusColor, param.StatusCode, resetColor,
        param.Latency,
        param.ClientIP,
        methodColor, param.Method, resetColor,
        param.Path,
        param.ErrorMessage,
    )
}
  1. 使用示例
import (
	"fmt"
	"gin_hello/gb"
	"github.com/gin-gonic/gin"
	"io"
	"os"
)

func main() {
	r := gin.New()
	r.Use(gin.Recovery())
	//1.日志输出到文件,文件所在位置
	f,_ := os.OpenFile("./app01.log",os.O_CREATE|os.O_APPEND|os.O_RDWR,0644)
	//配置中间件
	//r.Use(gin.LoggerWithWriter(io.MultiWriter(f,os.Stdout)))
	//2.配置日志输出格式与指定输出位置,返回什么格式,日志格式就是什么样子
	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),
	}
	//3.日志中间件
	r.Use(gin.LoggerWithConfig(conf))
	r.GET("/", func(c *gin.Context) {
		c.String(200,"成功")
	})
	// 如果需要同时将日志写入文件和控制台,请使用以下代码。
	gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
	//禁用控制台颜色
	gin.DisableConsoleColor()
	
	r.Run(":8000")
}

三. Zap日志库

  1. 由于默认日志功能有缺陷,例如不能轮转,在视图函数中不能直接使用日志记录(go标准库的logger)不能序列化等等,go又有很多开源的日志包如下:
  1. logrus: 目前Github上star数量最多的日志库,也是最兼容标准库的日志库
  2. zap: Uber推出的快速,结构化的分级日志库,无反射,零分配的JSON编码器,不是基于反射做的最快的一个日志库
  1. ZAP 地址
go get -u go.uber.org/zap
  1. 中提供了两种日志记录器: SugearedLogger 与 Logger

SugearedLogger

  1. 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

  1. 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),)
}

使用介绍

  1. 先了解一下zpa中的日志级别划分
//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
)

1. 初始化logger

  1. zap默认提供了三种初始化logger的方式: NewExample,NewProduction和NewDevelopment
//1. NewExample: 测试阶段使用,构建了一个专门为zap的可测试示例设计的Logger
//它将DebugLevel及以上的日志作为JSON写入标准输出,
//但省略了时间戳和调用函数,以保持示例输出的简短和确定性
func NewExample(options ...Option) *Logger

//2. NewProduction: 上线阶段使用,是NewProductionConfig().build(…Option)的快捷方式,
//构建了一个合理的生产日志记录器,它将infollevel及以上的日志以JSON的形式写入标准错误
func NewProduction(options ...Option) (*Logger, error)

//3. NewDevelopment: 开发阶段使用,是NewDevelopmentConfig().Build(…选项)的快捷方式
//构建一个开发日志记录器,它以人类友好的格式将DebugLevel及以上级别的日志写入标准错误。 
func NewDevelopment(options ...Option) (*Logger, error)
  1. 通过配置生成对应的 logger,可以自定义配置,生成自己自定义的 logger

2. 定制logger

  1. 查看NewProduction的源码底层实际就是执行的:NewProductionConfig().Build(options…)
func NewProduction(options ...Option) (*Logger, error) {
  //调用了 NewProductionConfig()方法,内部初始化创建,返回了一个 Config 对象
  //Build内部通过 Config对象的配置, 利用New方法生成相应的 logger对象,并返回
	return NewProductionConfig().Build(options...)
}

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

  1. 我们可以自己调用内部的相关方法, 模仿 NewProductionConfig().Build(options…) 定制化 logger对象
  2. 方式一: 过 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"))
}
  1. 方式二: 通过修改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级别日志")
}

3. 格式化时间和添加调用者信息

  1. 提供的三种配置,时间显示都是时间戳格式,对人来说,这个时间格式是极其不友好的,因此我们可以通过自定制将时间格式转换为对人友好的时间格式
  2. 添加调用者信息"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级别日志")
}

日志轮转与归档 lumberjack

  1. 为了避免日志文件过大,将日志文件分类,但 Zap 本身不支持切割归档日志文件,使用第三方库 Lumberjack 来实现
go get -u github.com/natefinch/lumberjack
  1. 方案
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

  1. 自定制gin中使用zap
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()
	}
}
  1. main
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")
}

三. go-zero 中的 logx

1 . 参考博客, go-zero中的日志组件

四. 日志收集 Logstash

  1. Logstash是一款轻量级的日志搜集处理框架,可以方便的把分散的、多样化的日志搜集起来,并进行自定义的处理,然后传输到指定的位置,比如某个服务器或者文件

五. go-zero 之 go-stash

  1. go-stash是go-zero中提供的一个 logstash 的 Go 语言替代版,我们用 go-stash 相比原先的 logstash 节省了2/3的服务器资源
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值