一 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内置了JSONFormatter
和TextFormatter
两种格式,也可以通过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提供了强大的功能扩展,如将日志分发到任意地方,如本地文件系统、
logstash
、es
等,或者切割日志、定义日志内容和格式等。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
MongoDb
:https://github.com/weekface/mgorusRedis
:https://github.com/rogierlommers/logrus-redis-hookInfluxDb
:https://github.com/abramovic/logrus_influxdbLogstash
:https://github.com/bshuster-repo/logrus-logstash-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。