logrus介绍
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。
logrus特性
- 完全兼容golang标准库日志模块:logrus拥有六种日志级别:
debug 、info 、warn 、error 、fatal 和panic ,这是golang标准库日志模块的API的超集.如果您的项目使用标准库日志模块,完全可以以最低的代价迁移到logrus上.
1 2 3 4 5 6 | logrus.Debug("Useful debugging information.") logrus.Info("Something noteworthy happened!") logrus.Warn("You should probably take a look at this.") logrus.Error("Something failed but I'm not quitting.") logrus.Fatal("Bye.") //log之后会调用os.Exit(1) logrus.Panic("I'm bailing.") //log之后会panic() |
- 可扩展的Hook机制:允许使用者通过hook的方式将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等.
可选的日志输出格式:logrus内置了两种日志格式,JSONFormatter和TextFormatter,如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式. - Field机制:logrus鼓励通过Field机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志.
- logrus是一个可插拔的、结构化的日志框架.
- Entry: logrus.WithFields会自动返回一个 *Entry,Entry里面的有些变量会被自动加上
1 2 3 | time:entry被创建时的时间戳 msg:在调用.Info()等方法时被添加 level |
基本用法
示例1:输出至终端平台
1 2 3 4 5 6 7 8 9 10 11 | package main import "github.com/sirupsen/logrus" func main() { logrus.WithFields(logrus.Fields{ "animal": "walrus", "number": 1, "size": 10, }).Info("A walrus appears") } |
1 2 | $ go run logrus1.go INFO[0000] A walrus appears animal=walrus number=1 size=10 |
示例2:输出至文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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--") } |
输出:
1 2 | $ cat logrus.log time="2020-05-26T10:29:20+08:00" level=info msg="log-- log--" |
设置日志输出格式与日志输出级别
语法:
1 2 3 4 5 | logrus.SetFormatter(&logrus.JSONFormatter{}) logrus.Infoln("JSONFormatter") logrus.SetFormatter(&logrus.TextFormatter{}) logrus.Infoln("TextFormatter not time") |
示例1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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!") } |
输出:
1 2 3 4 | $ 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 |
自定义Logger
如果想在一个应用里面向多个地方log,可以创建Logger实例. logger是一种相对高级的用法, 对于一个大型项目, 往往需要一个全局的logrus实例,即logger对象来记录项目所有的日志.
语法:
1 2 3 | log := logrus.New() log.Formatter= new(logrus.JSONFormatter) log.Infoln("my logger") |
示例1:输出到终端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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") } |
输出:
1 2 | $ 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:输出到文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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("打开文件失败") } |
输出:
1 2 | $ cat logrus.log time="2020-05-26T11:08:07+08:00" level=info msg="打开文件失败" filename=123.txt |
Fields用法
logrus不推荐使用冗长的消息来记录运行信息,它推荐使用Fields来进行精细化的、结构化的信息记录. 例如下面的记录日志的方式:
1 | log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key) |
在logrus中不太提倡,logrus鼓励使用以下方式替代之:
1 2 3 4 5 | 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实例记录日志即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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") } |
输出:
1 2 3 | $ go run logrus6.go INFO[0000] message1 name=test INFO[0000] message2 name=test |
hook
hook的原理是,在logrus写入日志时拦截,修改logrus.Entry
1 2 3 4 | type Hook interface { Levels() []Level Fire(*Entry) error } |
使用示例:
自定义一个hook DefaultFieldHook,在所有级别的日志消息中加入默认字段
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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的连接.
Logrus-Hook-Email
email这里只需用NewMailAuthHook方法得到hook,再添加即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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, "[email protected]", "[email protected]", "[email protected]", "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-Hook 日志分隔
logrus本身不带日志本地文件分割功能,但是我们可以通过file-rotatelogs进行日志本地文件分割. 每次当我们写入日志的时候,logrus都会调用file-rotatelogs来判断日志是否要进行切分.
示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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) } } |
Fatal处理
和很多日志框架一样,logrus的Fatal系列函数会执行os.Exit(1)。但是logrus提供可以注册一个或多个fatal handler函数的接口
线程安全
默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用hooks或者写日志的时候,如果不需要锁,可以调用logger.SetNoLock()来关闭之。可以关闭logrus互斥锁的情形包括:
- 没有设置hook,或者所有的hook都是线程安全的实现。
- 写日志到logger.Out已经是线程安全的了,如logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND方式打开的,并且每次写操作都小于4k。
参考链接:
jack-life
Go进阶10:logrus日志使用教程