golang标准库的日志框架非常简单,仅仅提供了print,panic和fatal三个函数对于更精细的日志级别、日志文件分割以及日志分发等方面并没有提供支持. 所以催生了很多第三方的日志库,但是在golang的世界里,没有一个日志库像slf4j那样在Java中具有绝对统治地位.golang中,流行的日志框架包括logrus、zap、zerolog、seelog等.
logrus是目前Github上star数量最多的日志库,目前(2018.12,下同)star数量为8119,fork数为1031. logrus功能强大,性能高效,而且具有高度灵活性,提供了自定义插件的功能.很多开源项目,如docker,prometheus,dejavuzhou/ginbro等,都是用了logrus来记录其日志.
zap是Uber推出的一个快速、结构化的分级日志库.具有强大的ad-hoc分析功能,并且具有灵活的仪表盘.zap目前在GitHub上的star数量约为4.3k. seelog提供了灵活的异步调度、格式化和过滤功能.目前在GitHub上也有约1.1k。
debug
、info
、warn
、error
、fatal
和panic
,这是golang标准库日志模块的API的超集.如果您的项目使用标准库日志模块,完全可以以最低的代价迁移到logrus上.第三方库需要先安装:
$ go get github.com/sirupsen/logrus
后使用:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetLevel(logrus.TraceLevel)
logrus.Trace("trace msg")
logrus.Debug("debug msg")
logrus.Info("info msg")
logrus.Warn("warn msg")
logrus.Error("error msg")
logrus.Fatal("fatal msg")
logrus.Panic("panic msg")
}
logrus的使用非常简单,与标准库log类似。logrus支持更多的日志级别:
Panic:记录日志,然后panic。
Fatal:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出;
Error:错误日志,需要查看原因;
Warn:警告信息,提醒程序员注意;
Info:关键操作,核心流程的日志;
Debug:一般程序中输出的调试信息;
Trace:很细粒度的信息,一般用不到;
日志级别从上向下依次增加,Trace最大,Panic最小。logrus有一个日志级别,高于这个级别的日志不会输出。默认的级别为InfoLevel。所以为了能看到Trace和Debug日志,我们在main函数第一行设置日志级别为TraceLevel。
运行程序,输出:
$ go run main.go
time="2020-02-07T21:22:42+08:00" level=trace msg="trace msg"
time="2020-02-07T21:22:42+08:00" level=debug msg="debug msg"
time="2020-02-07T21:22:42+08:00" level=info msg="info msg"
time="2020-02-07T21:22:42+08:00" level=info msg="warn msg"
time="2020-02-07T21:22:42+08:00" level=error msg="error msg"
time="2020-02-07T21:22:42+08:00" level=fatal msg="fatal msg"
exit status 1
由于logrus.Fatal会导致程序退出,下面的logrus.Panic不会执行到。
另外,我们观察到输出中有三个关键信息,time、level和msg:
time:输出日志的时间;
level:日志级别;
msg:日志信息。
time:entry被创建时的时间戳
msg:在调用.Info()等方法时被添加
level
package main
import "github.com/sirupsen/logrus"
func main() {
logrus.WithFields(logrus.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
$ go run logrus1.go
INFO[0000] A walrus appears animal=walrus number=1 size=10
package main
import (
"github.com/sirupsen/logrus"
"os"
)
func main() {
log := logrus.New()
file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
if err == nil {
log.Out = file
} else {
log.Info("Failed to log to file, using default stderr")
}
log.Info("log-- log--")
}
输出:
$ cat logrus.log
time="2020-05-26T10:29:20+08:00" level=info msg="log-- log--"
语法:
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.Infoln("JSONFormatter")
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.Infoln("TextFormatter not time")
示例1
package main
import (
"os"
log "github.com/sirupsen/logrus"
)
func init() {
// 设置日志格式为json格式
log.SetFormatter(&log.JSONFormatter{})
// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
// 日志消息输出可以是任意的io.writer类型
log.SetOutput(os.Stdout)
// 设置日志级别为warn以上
log.SetLevel(log.WarnLevel)
}
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(log.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(log.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}
输出:
$ go run logrus3.go
{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2020-05-26T10:53:39+08:00"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2020-05-26T10:53:39+08:00"}
exit status 1
如果想在一个应用里面向多个地方log,可以创建Logger实例. logger是一种相对高级的用法, 对于一个大型项目, 往往需要一个全局的logrus实例,即logger对象来记录项目所有的日志.
语法:
log := logrus.New()
log.Formatter= new(logrus.JSONFormatter)
log.Infoln("my logger")
示例1:输出到终端
package main
import (
"github.com/sirupsen/logrus"
"os"
)
// logrus提供了New()函数来创建一个logrus的实例.
// 项目中,可以创建任意数量的logrus实例.
var log = logrus.New()
func main() {
// 为当前logrus实例设置消息的输出,同样地,
// 可以设置logrus实例的输出到任意io.writer
log.Out = os.Stdout
// 为当前logrus实例设置消息输出格式为json格式.
// 同样地,也可以单独为某个logrus实例设置日志级别和hook,这里不详细叙述.
log.Formatter = &logrus.JSONFormatter{}
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
输出:
$ go run logrus4.go
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2020-05-26T10:59:26+08:00"}
示例2:输出到文件
package main
import (
"github.com/sirupsen/logrus"
"os"
)
var log = logrus.New()
func main() {
file ,err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
if err == nil{
log.Out = file
}else{
log.Info("Failed to log to file")
}
log.WithFields(logrus.Fields{
"filename": "123.txt",
}).Info("打开文件失败")
}
输出:
$ cat logrus.log
time="2020-05-26T11:08:07+08:00" level=info msg="打开文件失败" filename=123.txt
logrus不推荐使用冗长的消息来记录运行信息,它推荐使用Fields来进行精细化的、结构化的信息记录. 例如下面的记录日志的方式:
log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key)
在logrus中不太提倡,logrus鼓励使用以下方式替代之:
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
前面的WithFields API可以规范使用者按照其提倡的方式记录日志.但是WithFields依然是可选的,因为某些场景下,使用者确实只需要记录仪一条简单的消息.
通常,在一个应用中、或者应用的一部分中,都有一些固定的Field.比如在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip.为了避免每次记录日志都要使用log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可.
package main
import (
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func main() {
entry := logrus.WithFields(logrus.Fields{
"name": "test",
})
entry.Info("message1")
entry.Info("message2")
}
输出:
$ go run logrus6.go
INFO[0000] message1 name=test
INFO[0000] message2 name=test
hook的原理是,在logrus写入日志时拦截,修改logrus.Entry
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
使用示例:
自定义一个hook DefaultFieldHook,在所有级别的日志消息中加入默认字段
appName="myAppName"
type DefaultFieldHook struct {
}
func (hook *DefaultFieldHook) Fire(entry *log.Entry) error {
entry.Data["appName"] = "MyAppName"
return nil
}
func (hook *DefaultFieldHook) Levels() []log.Level {
return log.AllLevels
}
在初始化时,调用logrus.AddHook(hook)添加响应的hook即可
logrus官方仅仅内置了syslog的hook. 此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK的连接.
email这里只需用NewMailAuthHook方法得到hook,再添加即可
package main
import (
"time"
//"github.com/logrus_mail"
"github.com/zbindenren/logrus_mail"
"github.com/sirupsen/logrus"
)
func main() {
logger := logrus.New()
hook, err := logrus_mail.NewMailAuthHook(
"logrus_email",
"smtp.163.com",
25,
"xxxxx@163.com",
"xxxxxx@qq.com",
"xxxxxxx@163.com",
"xxxxxx",
)
if err == nil {
logger.Hooks.Add(hook)
}
//生成*Entry
var filename = "123.txt"
contextLogger := logger.WithFields(logrus.Fields{
"file": filename,
"content": "GG",
})
//设置时间戳和message
contextLogger.Time = time.Now()
contextLogger.Message = "这是一个hook发来的邮件"
//只能发送Error,Fatal,Panic级别的log
contextLogger.Level = logrus.ErrorLevel
//使用Fire发送,包含时间戳,message
hook.Fire(contextLogger)
}
将日志发送到日志中心也是logrus所提倡的,虽然没有提供官方支持,但是目前Github上有很多第三方hook可供使用:
logrus_amqp:Logrus hook for Activemq。
logrus-logstash-hook:Logstash hook for logrus。
mgorus:Mongodb Hooks for Logrus。
logrus_influxdb:InfluxDB Hook for Logrus。
logrus-redis-hook:Hook for Logrus which enables logging to RELK stack (Redis, Elasticsearch, Logstash and Kibana)
logrus本身不带日志本地文件分割功能,但是我们可以通过file-rotatelogs进行日志本地文件分割. 每次当我们写入日志的时候,logrus都会调用file-rotatelogs来判断日志是否要进行切分.
示例1:
package main
import (
"time"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
log "github.com/sirupsen/logrus"
)
func init() {
path := "/Users/opensource/test/go.log"
/* 日志轮转相关函数
`WithLinkName` 为最新的日志建立软连接
`WithRotationTime` 设置日志分割的时间,隔多久分割一次
WithMaxAge 和 WithRotationCount二者只能设置一个
`WithMaxAge` 设置文件清理前的最长保存时间
`WithRotationCount` 设置文件清理前最多保存的个数
*/
// 下面配置日志每隔 1 分钟轮转一个新文件,保留最近 3 分钟的日志文件,多余的自动清理掉。
writer, _ := rotatelogs.New(
path+".%Y%m%d%H%M",
rotatelogs.WithLinkName(path),
rotatelogs.WithMaxAge(time.Duration(180)*time.Second),
rotatelogs.WithRotationTime(time.Duration(60)*time.Second),
)
log.SetOutput(writer)
//log.SetFormatter(&log.JSONFormatter{})
}
func main() {
for {
log.Info("hello, world!")
time.Sleep(time.Duration(2) * time.Second)
}
}
示例2:
package main
import (
"github.com/lestrrat-go/file-rotatelogs"
"github.com/pkg/errors"
"github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus"
"path"
"time"
)
func ConfigLocalFilesystemLogger(logPath string, logFileName string, maxAge time.Duration, rotationTime time.Duration) {
baseLogPaht := path.Join(logPath, logFileName)
writer, err := rotatelogs.New(
baseLogPaht+".%Y%m%d%H%M",
//rotatelogs.WithLinkName(baseLogPaht), // 生成软链,指向最新日志文件
rotatelogs.WithMaxAge(maxAge), // 文件最大保存时间
rotatelogs.WithRotationTime(rotationTime), // 日志切割时间间隔
)
if err != nil {
log.Errorf("config local file system logger error. %+v", errors.WithStack(err))
}
lfHook := lfshook.NewHook(lfshook.WriterMap{
log.DebugLevel: writer, // 为不同级别设置不同的输出目的
log.InfoLevel: writer,
log.WarnLevel: writer,
log.ErrorLevel: writer,
log.FatalLevel: writer,
log.PanicLevel: writer,
},&log.TextFormatter{DisableColors: true})
log.AddHook(lfHook)
}
//切割日志和清理过期日志
func ConfigLocalFilesystemLogger1(filePath string) {
writer, err := rotatelogs.New(
filePath+".%Y%m%d%H%M",
rotatelogs.WithLinkName(filePath), // 生成软链,指向最新日志文件
rotatelogs.WithMaxAge(time.Second*60*3), // 文件最大保存时间
rotatelogs.WithRotationTime(time.Second*60), // 日志切割时间间隔
)
if err != nil {
log.Fatal("Init log failed, err:", err)
}
log.SetOutput(writer)
log.SetLevel(log.InfoLevel)
}
func main() {
ConfigLocalFilesystemLogger1("log")
for {
log.Info(111)
time.Sleep(500*time.Millisecond)
}
}
和很多日志框架一样,logrus的Fatal系列函数会执行os.Exit(1)。但是logrus提供可以注册一个或多个fatal handler函数的接口logrus.RegisterExitHandler(handler func() {} )
,让logrus在执行os.Exit(1)之前进行相应的处理。fatal handler可以在系统异常时调用一些资源释放api等,让应用正确的关闭。
默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用hooks或者写日志的时候,如果不需要锁,可以调用logger.SetNoLock()来关闭之。可以关闭logrus互斥锁的情形包括:
调用logrus.SetReportCaller(true)设置在输出日志中添加文件名和方法信息:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetReportCaller(true)
logrus.Info("info msg")
}
输出多了两个字段file为调用logrus相关方法的文件名,method为方法名:
$ go run main.go
time="2020-02-07T21:46:03+08:00" level=info msg="info msg" func=main.main file="D:/code/golang/src/github.com/darjun/go-daily-lib/logrus/caller/main.go:10"
添加字段
参考链接:
jack-life
Go进阶10:logrus日志使用教程
Go 每日一库之 logrus
相关阅读:
go log包详解