15-最兼容标准库的日志库-logrus.md

一 logrus介绍

1.1 log标准库优缺点

优点

Go标准库的log日志库非常简单

可以便设置任何io.Writer作为日志输出位置

缺点

1 仅仅提供了print,panic和fatal三个函数,不支持info/debug等多个级别

2 记录错误有Fatal和Panic;Fatal通过调用os.Exit(1)来结束程序;Panic在写入日志后抛出一个panic;缺少ERROR日志级别,在不抛出异常和退出程序的情况下记录日志

3 不支持多输出 - 同时支持标准输出,文件等

4 缺乏日志格式化的能力,例如:记录函数名和行号,格式化日期和时间格式等

5 可读性与结构化差,没有Json格式或有分隔符,不方便后续的日志采集、监控等

6 对于更精细的日志级别、日志文件分割,以及日志分发等,没有提供支持

1.2 Go中常用第三方日志库

在Go的世界,流行的日志框架有logrus、zap、zerolog等

logrus

目前Github上star数量最多的日志库

项目地址: https://github.com/sirupsen/logrus

Stars数量:20.3k

zap

是Uber推出的一个快速、结构化的分级日志库

项目地址:https://github.com/uber-go/zap

官方文档:https://pkg.go.dev/go.uber.org/zap

Stars数量:20.3k

zerolog

它的 API 设计非常注重开发体验和性能。zerolog只专注于记录 JSON 格式的日志,号称 0 内存分配

项目地址:https://github.com/rs/zerolog

Stars数量:6.2k

二 logrus

2.1 logrus特点

优点

  • 完全兼容Go标准库日志模块:logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是golang标准库日志模块的API的超集。如果之前项目使用标准库日志模块,完全可以以最低的代价迁移到logrus上
  • 可扩展的Hook机制:允许使用者通过hook的方式将日志分发到任意地方,如本地文件系统、标准输出、linfluxdb、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等
  • 可选的日志输出格式:logrus内置了两种日志格式,JSONFormatter和TextFormatter,如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式
  • Field机制:logrus鼓励通过Field机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志
  • logrus是一个可插拔的、结构化的日志框架,很多开源项目,如docker,prometheus等,都是用了logrus来记录其日志

缺点

尽管 logrus有诸多优点,但是为了灵活性和可扩展性,官方也削减了很多实用的功能,例如:

  • 没有提供行号和文件名的支持
  • 输出到本地文件系统没有提供日志分割功能
  • 官方没有提供输出到ELK等日志处理中心的功能

但是这些功能都可以通过自定义hook来实现

2.2 logrus配置

日志级别

logrus有7个日志级别,依次是Trace --> Debug --> Info --> Warning -->Error --> Fatal -->Panic

//  只输出不低于当前级别是日志数据
logrus.SetLevel(logrus.DebugLevel)

日志格式

logrus内置了JSONFormatterTextFormatter两种格式,也可以通过Formatter接口定义日志格式

 // TextFormatter格式
 logrus.SetFormatter(&logrus.TextFormatter{
    ForceColors:               true,
    EnvironmentOverrideColors: true,
    TimestampFormat:           "2006-01-02 15:04:05", //时间格式
    // FullTimestamp:true,
    // DisableLevelTruncation:true,
 })
 // JSONFormatter格式
 logrus.SetFormatter(&logrus.JSONFormatter{
    PrettyPrint:     false,                 //格式化
    TimestampFormat: "2006-01-02 15:04:05", //时间格式
 })

输出文件

 logfile, _ := os.OpenFile("./log.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
 logrus.SetOutput(logfile) //默认为os.stderr
//logrus.SetOutput(io.MultiWriter(os.Stdout, logfile)) // 输出到多个位置

日志定位

定位行号(如:func=main.main file="./xxx.go:38"

logrus.SetReportCaller(true)

2.3 快速使用

go get github.com/sirupsen/logrus

package main

import (
	"github.com/sirupsen/logrus"
	"os"
)

func init() {
	// 1 日志级别为debug
	logrus.SetLevel(logrus.DebugLevel)
	//2 日志格式为json格式
	logrus.SetFormatter(&logrus.JSONFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})
	// 日志格式为文本格式
	//logrus.SetFormatter(&logrus.TextFormatter{
	//	ForceColors:               true,
	//	EnvironmentOverrideColors: true,
	//	TimestampFormat:           "2006-01-02 15:04:05", //时间格式
	//	FullTimestamp:true,  // 显示完整时间
	//	DisableLevelTruncation:true,
	//})

	//3 输出文件为app.log
	logfile, _ := os.OpenFile("./app.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
	//logrus.SetOutput(io.MultiWriter(os.Stdout,logfile)) // 即写到控制台,又写到文件中
	logrus.SetOutput(logfile) // 只写到文件中
	//不写默认为os.stderr
	// 4 日志定位--显示打印日志文件和位置
	logrus.SetReportCaller(true)
}
func main() {
	logrus.Infoln("info--日志数据")
	logrus.Debugln("debug--日志数据")
	logrus.Errorln("err--日志数据")
}


2.4 两个自带formatter和自定义

TextFormatter

type TextFormatter struct {
	DisableColors bool // 开启颜色显示
	
	DisableTimestamp bool // 开启时间显示

	TimestampFormat string	// 自定义时间格式

	QuoteEmptyFields bool	//空字段括在引号中

	CallerPrettyfier func(*runtime.Frame) (function string, file string) //用于自定义方法名和文件名的输出
}

JsonFormatter

type JSONFormatter struct {
	TimestampFormat string // 自定义时间格式

	DisableTimestamp bool // 开启时间显示

	CallerPrettyfier func(*runtime.Frame) (function string, file string) //用于自定义方法名和文件名的输出

	PrettyPrint bool //将缩进所有json日志
}

自定义Formatter

//只需要实现该接口
type Formatter interface {
	Format(*Entry) ([]byte, error)
}

// 其中entry参数
type Entry struct {
	// Contains all the fields set by the user.
	Data Fields
	
	// Time at which the log entry was created
	Time time.Time

	// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
	Level Level

	//Calling method, with package name
	Caller *runtime.Frame

	//Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic  
	Message string 
	
	//When formatter is called in entry.log(), a Buffer may be set to entry
	Buffer *bytes.Buffer
}

2.5 日志打印方法

FieldLogger接口: FieldLogger定义了所有日志打印的方法

type FieldLogger interface {
   WithField(key string, value interface{}) *Entry
   WithFields(fields Fields) *Entry
   WithError(err error) *Entry

   Debugf(format string, args ...interface{})
   Infof(format string, args ...interface{})
   Printf(format string, args ...interface{})
   Warnf(format string, args ...interface{})
   Warningf(format string, args ...interface{})
   Errorf(format string, args ...interface{})
   Fatalf(format string, args ...interface{})
   Panicf(format string, args ...interface{})

   Debug(args ...interface{})
   Info(args ...interface{})
   Print(args ...interface{})
   Warn(args ...interface{})
   Warning(args ...interface{})
   Error(args ...interface{})
   Fatal(args ...interface{})
   Panic(args ...interface{})

   Debugln(args ...interface{})
   Infoln(args ...interface{})
   Println(args ...interface{})
   Warnln(args ...interface{})
   Warningln(args ...interface{})
   Errorln(args ...interface{})
   Fatalln(args ...interface{})
   Panicln(args ...interface{})
}

2.6 logrus实例

实例日志打印方式一

默认实例 (函数),即通过logrus包提供的函数(覆盖了FieldLogger接口的所有方法),直接打印日志。但其实logrus包函数是调用了logrus.Loger默认实例。

// 直接调用包函数
func main() {
   logrus.Infoln("info--日志")
   logrus.Errorln("err--日志")
}

实例日志打印方式二

Logger实例(对象),它实现了FieldLogger接口。

func main() {
	//var loger = logrus.New()
	var loger = logrus.StandardLogger() // 看源码,本质就是logrus.New()
	loger.Formatter = &logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"}
	loger.Infoln("info--日志")
}

实例日志打印方式三

Entry示例(对象),它也实现了FieldLogger接口,是最终是日志打印入口。

  • 这里用到了Field机制,logrus鼓励通过Field机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志。
func main() {
	logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})
	// Entry实例
	entry := logrus.WithFields(logrus.Fields{
		"global": "全局字段-每个日志都会输出",
	})
	entry.WithFields(logrus.Fields{"module": "自定义字段--用户模块"}).Info("info--日志")
	entry.WithFields(logrus.Fields{"module": "自定义字段--商品模块"}).Error("Error--日志")
}

2.7 HOOK机制

  • hook即钩子,拦截器。它为logrus提供了强大的功能扩展,如将日志分发到任意地方,如本地文件系统、logstashes等,或者切割日志、定义日志内容和格式等。hook接口原型如下:
type Hook interface {
   Levels() []Level   //日志级别
   Fire(*Entry) error //打印入口(Entry对象)
}

Hook - 实现日志切割功能

需要借助于第三方(日志轮转库):github.com/lestrrat-go/file-rotatelogs
和:github.com/rifflock/lfshook

package main

import (
	"github.com/lestrrat-go/file-rotatelogs"
	"github.com/rifflock/lfshook"
	"github.com/sirupsen/logrus"
	"time"
)

//  说明:按时间切割日志文件(2秒创建一个日志文件)
func main() {
	// 保存日志文件名为app_hook开头,2s切换一个日志文件,最多保留5份
	hook := NewLfsHook("app_hook", time.Second*2, 5)
	// 加入钩子
	logrus.AddHook(hook)
	// 先打印一句日志
	logrus.Infoln("info---测试开始")
	// 通过WithFields方式创建log,写入通用内容module
	log := logrus.WithFields(logrus.Fields{"module": "用户模块"})
	// 每隔一秒,调用一次info,一次err,最终只保留5个日志文件
	for i := 0; i < 15; i++ {
		log.Infoln("info--->成功", i)
		time.Sleep(time.Second)
		log.Errorln("err--->成功", i)
	}
}

// 日志钩子(日志拦截,并重定向)
func NewLfsHook(logName string, rotationTime time.Duration, leastDay uint) logrus.Hook {
	writer, err := rotatelogs.New(
		// 1 日志文件名字
		logName+".%Y%m%d%H%M%S",
		// 2 日志周期(默认每86400秒/一天旋转一次)
		rotatelogs.WithRotationTime(rotationTime),
		// 3 清除历史 (WithMaxAge和WithRotationCount只能选其一)
		//rotatelogs.WithMaxAge(time.Hour*24*7), //默认每7天清除下日志文件
		rotatelogs.WithRotationCount(leastDay), //只保留最近的N个日志文件
	)
	if err != nil {
		panic(err)
	}

	// 可设置按不同level创建不同的文件名,咱们把6中日志都写到同一个writer中
	lfsHook := lfshook.NewHook(lfshook.WriterMap{
		logrus.DebugLevel: writer,
		logrus.InfoLevel:  writer,
		logrus.WarnLevel:  writer,
		logrus.ErrorLevel: writer,
		logrus.FatalLevel: writer,
		logrus.PanicLevel: writer,
	}, &logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})

	return lfsHook
}

Hook - 写入Redis

将日志输出到redis

需要借助于第三方模块:github.com/rogierlommers/logrus-redis-hook

package main

import (
	logredis "github.com/rogierlommers/logrus-redis-hook"
	"github.com/sirupsen/logrus"
)

func init() {
	hookConfig := logredis.HookConfig{
		Host:     "localhost",
		Key:      "test",
		Format:   "v1",
		App:      "my_app_name",
		Port:     6379,
		Hostname: "my_app_hostname",
		DB:       0,
		TTL:      3600,
	}
	hook, err := logredis.NewHook(hookConfig)
	if err == nil {
		logrus.AddHook(hook)
	} else {
		logrus.Errorf("日志写入redis配置出错: %q", err)
	}
}

func main() {
	logrus.WithFields(logrus.Fields{"module": "用户模块"}).Info("info--日志--写入redis")
	logrus.WithFields(logrus.Fields{"module": "用户模块"}).Error("Error--日志--写入redis")

}

// 测试:
// 1.启动redis服务: redis-server
// 2.监控redis数据: redis-cli monitor

其他Hook

2.8 Fatal处理

logrus的Fatal输出,会执行os.Exit(1)。logrus提供RegisterExitHandler方法,可以在系统异常时调用一些资源释放api等,让应用正确地关闭。

func main() {
	logrus.RegisterExitHandler(func() {
		fmt.Println("发生了fatal异常,执行关闭文件等工作")
	})

	logrus.Warnln("warn测试")
	logrus.Fatalln("fatal测试")
	logrus.Infoln("info测试") //不会执行

}

三 Gin中集成

  • 将gin框架的日志定向到logrus日志文件
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"io"
	"os"
)

func init() {
	// 日志输出格式
	logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})

	// 日志输出路径
	logfile, _ := os.OpenFile("./app.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
	logrus.SetOutput(io.MultiWriter(os.Stdout, logfile)) // 日志写到控制台和文件中

	// Gin日志重定向
	gin.DisableConsoleColor()                              //不需要颜色
	// gin的日志写到控制台和日志文件中
	gin.DefaultWriter = io.MultiWriter(os.Stdout, logfile) //默认是:os.Stdout
	//gin.DefaultWriter = logfile 
}

//测试:curl 0.0.0.0:8080/index
func main() {
	log := logrus.WithFields(logrus.Fields{
		"module": "用户模块",
	})

	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		log.Infoln("info--->gin日志数据")
		c.String(200, "ok")
	})
	r.Run(":8080")
}

四 logrus线程安全

  • 默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。
  • 互斥锁在调用hooks或者写日志的时候执行。
  • 如果不需要锁,可以调用logger.SetNoLock()来关闭。

可以关闭logrus互斥锁的情形:

  • 没有设置hook,或者所有的hook都是线程安全的实现。
  • 写日志到logger.Out已经是线程安全的了。例如,logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND方式打开的,并且每次写操作都小于4k。
posted @ 2022-05-07 23:35  刘清政  阅读(563)  评论(0编辑  收藏  举报