简介

前一篇文章介绍了 Go 标准库中的日志库 log。最后我们也提到,log库只提供了三组接口,功能过于简单了。
今天,我们来介绍一个日志库中的“明星库”——logrus。本文编写之时(2020.02.07),logrus 在 GitHub 上 star 数已达到 13.8k。
logrus完全兼容标准的log库,还支持文本、JSON 两种日志输出格式。很多知名的开源项目都使用了这个库,如大名鼎鼎的 docker。

快速使用

第三方库需要先安装:

  1. $ go get github.com/sirupsen/logrus

后使用:

  1. package main
  2. import (
  3. "github.com/sirupsen/logrus"
  4. )
  5. func main() {
  6. logrus.SetLevel(logrus.TraceLevel)
  7. logrus.Trace("trace msg")
  8. logrus.Debug("debug msg")
  9. logrus.Info("info msg")
  10. logrus.Warn("warn msg")
  11. logrus.Error("error msg")
  12. logrus.Fatal("fatal msg")
  13. logrus.Panic("panic msg")
  14. }

logrus的使用非常简单,与标准库log类似。logrus支持更多的日志级别:

  • Panic:记录日志,然后panic
  • Fatal:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出;
  • Error:错误日志,需要查看原因;
  • Warn:警告信息,提醒程序员注意;
  • Info:关键操作,核心流程的日志;
  • Debug:一般程序中输出的调试信息;
  • Trace:很细粒度的信息,一般用不到;

日志级别从上向下依次增加,Trace最大,Panic最小。logrus有一个日志级别,高于这个级别的日志不会输出。
默认的级别为InfoLevel。所以为了能看到TraceDebug日志,我们在main函数第一行设置日志级别为TraceLevel

运行程序,输出:

  1. $ go run main.go
  2. time="2020-02-07T21:22:42+08:00" level=trace msg="trace msg"
  3. time="2020-02-07T21:22:42+08:00" level=debug msg="debug msg"
  4. time="2020-02-07T21:22:42+08:00" level=info msg="info msg"
  5. time="2020-02-07T21:22:42+08:00" level=info msg="warn msg"
  6. time="2020-02-07T21:22:42+08:00" level=error msg="error msg"
  7. time="2020-02-07T21:22:42+08:00" level=fatal msg="fatal msg"
  8. exit status 1

由于logrus.Fatal会导致程序退出,下面的logrus.Panic不会执行到。

另外,我们观察到输出中有三个关键信息,timelevelmsg

  • time:输出日志的时间;
  • level:日志级别;
  • msg:日志信息。

定制

输出文件名

调用logrus.SetReportCaller(true)设置在输出日志中添加文件名和方法信息:

  1. package main
  2. import (
  3. "github.com/sirupsen/logrus"
  4. )
  5. func main() {
  6. logrus.SetReportCaller(true)
  7. logrus.Info("info msg")
  8. }

输出多了两个字段file为调用logrus相关方法的文件名,method为方法名:

  1. $ go run main.go
  2. time="2020-02-07T21:46:03+08:00" level=info msg="info msg" func=main.main file="D:/code/golang/src/github.com/go-quiz/go-daily-lib/logrus/caller/main.go:10"

添加字段

有时候需要在输出中添加一些字段,可以通过调用logrus.WithFieldlogrus.WithFields实现。
logrus.WithFields接受一个logrus.Fields类型的参数,其底层实际上为map[string]interface{}

  1. // github.com/sirupsen/logrus/logrus.go
  2. type Fields map[string]interface{}

下面程序在输出中添加两个字段nameage

  1. package main
  2. import (
  3. "github.com/sirupsen/logrus"
  4. )
  5. func main() {
  6. logrus.WithFields(logrus.Fields{
  7. "name": "dj",
  8. "age": 18,
  9. }).Info("info msg")
  10. }

如果在一个函数中的所有日志都需要添加某些字段,可以使用WithFields的返回值。例如在 Web 请求的处理器中,日志都要加上user_idip字段:

  1. package main
  2. import (
  3. "github.com/sirupsen/logrus"
  4. )
  5. func main() {
  6. requestLogger := logrus.WithFields(logrus.Fields{
  7. "user_id": 10010,
  8. "ip": "192.168.32.15",
  9. })
  10. requestLogger.Info("info msg")
  11. requestLogger.Error("error msg")
  12. }

实际上,WithFields返回一个logrus.Entry类型的值,它将logrus.Logger和设置的logrus.Fields保存下来。
调用Entry相关方法输出日志时,保存下来的logrus.Fields也会随之输出。

重定向输出

默认情况下,日志输出到io.Stderr。可以调用logrus.SetOutput传入一个io.Writer参数。后续调用相关方法日志将写到io.Writer中。
现在,我们就能像上篇文章介绍log时一样,可以搞点事情了。传入一个io.MultiWriter
同时将日志写到bytes.Buffer、标准输出和文件中:

  1. package main
  2. import (
  3. "bytes"
  4. "io"
  5. "log"
  6. "os"
  7. "github.com/sirupsen/logrus"
  8. )
  9. func main() {
  10. writer1 := &bytes.Buffer{}
  11. writer2 := os.Stdout
  12. writer3, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE, 0755)
  13. if err != nil {
  14. log.Fatalf("create file log.txt failed: %v", err)
  15. }
  16. logrus.SetOutput(io.MultiWriter(writer1, writer2, writer3))
  17. logrus.Info("info msg")
  18. }

自定义

实际上,考虑到易用性,库一般会使用默认值创建一个对象,包最外层的方法一般都是操作这个默认对象。

我们之前好几篇文章都提到过这点:

这个技巧应用在很多库的开发中,logrus也是如此:

  1. // github.com/sirupsen/logrus/exported.go
  2. var (
  3. std = New()
  4. )
  5. func StandardLogger() *Logger {
  6. return std
  7. }
  8. func SetOutput(out io.Writer) {
  9. std.SetOutput(out)
  10. }
  11. func SetFormatter(formatter Formatter) {
  12. std.SetFormatter(formatter)
  13. }
  14. func SetReportCaller(include bool) {
  15. std.SetReportCaller(include)
  16. }
  17. func SetLevel(level Level) {
  18. std.SetLevel(level)
  19. }

首先,使用默认配置定义一个Logger对象stdSetOutput/SetFormatter/SetReportCaller/SetLevel这些方法都是调用std对象的对应方法!

我们当然也可以创建自己的Logger对象,使用方式与直接调用logrus的方法类似:

  1. package main
  2. import "github.com/sirupsen/logrus"
  3. func main() {
  4. log := logrus.New()
  5. log.SetLevel(logrus.InfoLevel)
  6. log.SetFormatter(&logrus.JSONFormatter{})
  7. log.Info("info msg")
  8. }

日志格式

logrus支持两种日志格式,文本和 JSON,默认为文本格式。可以通过logrus.SetFormatter设置日志格式:

  1. package main
  2. import (
  3. "github.com/sirupsen/logrus"
  4. )
  5. func main() {
  6. logrus.SetLevel(logrus.TraceLevel)
  7. logrus.SetFormatter(&logrus.JSONFormatter{})
  8. logrus.Trace("trace msg")
  9. logrus.Debug("debug msg")
  10. logrus.Info("info msg")
  11. logrus.Warn("warn msg")
  12. logrus.Error("error msg")
  13. logrus.Fatal("fatal msg")
  14. logrus.Panic("panic msg")
  15. }

程序输出 JSON 格式的日志:

  1. $ go run main.go
  2. {"level":"trace","msg":"trace msg","time":"2020-02-07T21:40:04+08:00"}
  3. {"level":"debug","msg":"debug msg","time":"2020-02-07T21:40:04+08:00"}
  4. {"level":"info","msg":"info msg","time":"2020-02-07T21:40:04+08:00"}
  5. {"level":"info","msg":"warn msg","time":"2020-02-07T21:40:04+08:00"}
  6. {"level":"error","msg":"error msg","time":"2020-02-07T21:40:04+08:00"}
  7. {"level":"fatal","msg":"fatal msg","time":"2020-02-07T21:40:04+08:00"}
  8. exit status 1

第三方格式

除了内置的TextFormatterJSONFormatter,还有不少第三方格式支持。我们这里介绍一个nested-logrus-formatter

先安装:

  1. $ go get github.com/antonfisher/nested-logrus-formatter

后使用:

  1. package main
  2. import (
  3. nested "github.com/antonfisher/nested-logrus-formatter"
  4. "github.com/sirupsen/logrus"
  5. )
  6. func main() {
  7. logrus.SetFormatter(&nested.Formatter{
  8. HideKeys: true,
  9. FieldsOrder: []string{"component", "category"},
  10. })
  11. logrus.Info("info msg")
  12. }

程序输出:

  1. Feb 8 15:22:59.077 [INFO] info msg

nested格式提供了多个字段用来定制行为:

  1. // github.com/antonfisher/nested-logrus-formatter/formatter.go
  2. type Formatter struct {
  3. FieldsOrder []string
  4. TimestampFormat string
  5. HideKeys bool
  6. NoColors bool
  7. NoFieldsColors bool
  8. ShowFullLevel bool
  9. TrimMessages bool
  10. }
  • 默认,logrus输出日志中字段是key=value这样的形式。使用nested格式,我们可以通过设置HideKeystrue隐藏键,只输出值;
  • 默认,logrus是按键的字母序输出字段,可以设置FieldsOrder定义输出字段顺序;
  • 通过设置TimestampFormat设置日期格式。
  1. package main
  2. import (
  3. "time"
  4. nested "github.com/antonfisher/nested-logrus-formatter"
  5. "github.com/sirupsen/logrus"
  6. )
  7. func main() {
  8. logrus.SetFormatter(&nested.Formatter{
  9. // HideKeys: true,
  10. TimestampFormat: time.RFC3339,
  11. FieldsOrder: []string{"name", "age"},
  12. })
  13. logrus.WithFields(logrus.Fields{
  14. "name": "dj",
  15. "age": 18,
  16. }).Info("info msg")
  17. }

如果不隐藏键,程序输出:

  1. $ 2020-02-08T15:40:07+08:00 [INFO] [name:dj] [age:18] info msg

隐藏键,程序输出:

  1. $ 2020-02-08T15:41:58+08:00 [INFO] [dj] [18] info msg

注意到,我们将时间格式设置成time.RFC3339,即2006-01-02T15:04:05Z07:00这种形式。

通过实现接口logrus.Formatter可以实现自己的格式。

  1. // github.com/sirupsen/logrus/formatter.go
  2. type Formatter interface {
  3. Format(*Entry) ([]byte, error)
  4. }

设置钩子

还可以为logrus设置钩子,每条日志输出前都会执行钩子的特定方法。所以,我们可以添加输出字段、根据级别将日志输出到不同的目的地。
logrus也内置了一个syslog的钩子,将日志输出到syslog中。这里我们实现一个钩子,在输出的日志中增加一个app=awesome-web字段。

钩子需要实现logrus.Hook接口:

  1. // github.com/sirupsen/logrus/hooks.go
  2. type Hook interface {
  3. Levels() []Level
  4. Fire(*Entry) error
  5. }

Levels()方法返回感兴趣的日志级别,输出其他日志时不会触发钩子。Fire是日志输出前调用的钩子方法。

  1. package main
  2. import (
  3. "github.com/sirupsen/logrus"
  4. )
  5. type AppHook struct {
  6. AppName string
  7. }
  8. func (h *AppHook) Levels() []logrus.Level {
  9. return logrus.AllLevels
  10. }
  11. func (h *AppHook) Fire(entry *logrus.Entry) error {
  12. entry.Data["app"] = h.AppName
  13. return nil
  14. }
  15. func main() {
  16. h := &AppHook{AppName: "awesome-web"}
  17. logrus.AddHook(h)
  18. logrus.Info("info msg")
  19. }

只需要在Fire方法实现中,为entry.Data添加字段就会输出到日志中。

程序输出:

  1. $ time="2020-02-08T15:51:52+08:00" level=info msg="info msg" app=awesome-web

logrus的第三方 Hook 很多,我们可以使用一些 Hook 将日志发送到 redis/mongodb 等存储中:

这里我们演示一个 redis,感兴趣自行验证其他的。先安装logrus-redis-hook

  1. $ go get github.com/rogierlommers/logrus-redis-hook

然后编写程序:

  1. package main
  2. import (
  3. "io/ioutil"
  4. logredis "github.com/rogierlommers/logrus-redis-hook"
  5. "github.com/sirupsen/logrus"
  6. )
  7. func init() {
  8. hookConfig := logredis.HookConfig{
  9. Host: "localhost",
  10. Key: "mykey",
  11. Format: "v0",
  12. App: "aweosome",
  13. Hostname: "localhost",
  14. TTL: 3600,
  15. }
  16. hook, err := logredis.NewHook(hookConfig)
  17. if err == nil {
  18. logrus.AddHook(hook)
  19. } else {
  20. logrus.Errorf("logredis error: %q", err)
  21. }
  22. }
  23. func main() {
  24. logrus.Info("just some info logging...")
  25. logrus.WithFields(logrus.Fields{
  26. "animal": "walrus",
  27. "foo": "bar",
  28. "this": "that",
  29. }).Info("additional fields are being logged as well")
  30. logrus.SetOutput(ioutil.Discard)
  31. logrus.Info("This will only be sent to Redis")
  32. }

为了程序能正常工作,我们还需要安装redis

windows 上直接使用choco安装 redis:

  1. PS C:\Users\Administrator> choco install redis-64
  2. Chocolatey v0.10.15
  3. Installing the following packages:
  4. redis-64
  5. By installing you accept licenses for the packages.
  6. Progress: Downloading redis-64 3.0.503... 100%
  7. redis-64 v3.0.503 [Approved]
  8. redis-64 package files install completed. Performing other installation steps.
  9. ShimGen has successfully created a shim for redis-benchmark.exe
  10. ShimGen has successfully created a shim for redis-check-aof.exe
  11. ShimGen has successfully created a shim for redis-check-dump.exe
  12. ShimGen has successfully created a shim for redis-cli.exe
  13. ShimGen has successfully created a shim for redis-server.exe
  14. The install of redis-64 was successful.
  15. Software install location not explicitly set, could be in package or
  16. default install location if installer.
  17. Chocolatey installed 1/1 packages.
  18. See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).

直接输入redis-server,启动服务器:

每日一库之10:logrus - 图1

运行程序后,我们使用redis-cli查看:

每日一库之10:logrus - 图2

我们看到mykey是一个list,每过来一条日志,就在list后新增一项。

总结

本文介绍了logrus的基本用法。logrus的可扩展性非常棒,可以引入第三方格式和 Hook 增强功能。在社区也比较受欢迎。

参考

  1. logrus GitHub 仓库
  2. Hooks