返回

Logrus Introduction

Go第三方日志库之logrus介绍


Logrus 是目前 GitHub 上 Star 数量最多的 Go 日志库。尽管目前 Logrus 处于维护模式,不再引入新功能,但这并不意味着它已经死了。Logrus 仍将继续维护,以确保安全性、错误修复和提高性能。作为 Go 社区中最受欢迎的日志库之一,Logrus 最大的贡献是推动了 Go 社区广泛使用结构化(如JSON格式)的日志记录。著名的 Docker 项目就在使用 Logrus 记录日志,这进一步证明了其在实际应用中的可靠性和实用性。


特点

Logrus 具有如下特点:

  • 与 Go log 标准库 API 完全兼容,这意味着任何使用 log 标准库的代码都可以将日志库无缝切换到 Logrus。
  • 支持七种日志级别:TraceDebugInfoWarnErrorFatalPanic
  • 支持结构化日志记录(key-value 形式,容易被程序解析,如 JSON 格式),通过 Filed 机制进行结构化的日志记录。
  • 支持自定义日志格式,内置两种格式 JSONFormatter(JSON 格式) 和 TextFormatter(文本格式),并允许用户通过实现 Formatter 接口来自定义日志格式。
  • 支持可扩展的 Hooks 机制,可以为不同级别的日志添加 Hooks 将日志记录到不同位置,例如将 ErrorFatalPanic 级别的错误日志发送到 logstash、kafka 等。
  • 支持在控制台输出带有不同颜色的日志。
  • 并发安全。

快速使用

第三方库需要先安装:

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

后使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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。所以为了能看到TraceDebug日志,我们在main函数第一行设置日志级别为TraceLevel

运行程序,非标准TTY输出:

1
2
3
4
5
6
7
8
$ go run main.go 
time="2024-05-09T11:31:42+08:00" level=trace msg="trace msg"
time="2024-05-09T11:31:42+08:00" level=debug msg="debug msg"
time="2024-05-09T11:31:42+08:00" level=info msg="info msg"
time="2024-05-09T11:31:42+08:00" level=warning msg="warn msg"
time="2024-05-09T11:31:42+08:00" level=error msg="error msg"
time="2024-05-09T11:31:42+08:00" level=fatal msg="fatal msg"
exit status 1

logrus默认输出到标准错误。格式是文本格式,即默认的Formatter是TextFormatter。

还有默认情况下,log.SetFormatter(&log.TextFormatter{})(即默认的TextFormatter)未连接 TTY 时,输出与 logfmt格式兼容(就是上面输出的格式)。当连接TTY时,会对输出的日志进行颜色编码,参考官方图片:连接终端

为了确保即使连接了 TTY 也能实现不带颜色输出,请按如下方式设置格式化程序:

1
2
3
	logrus.SetFormatter(&log.TextFormatter{
		DisableColors: true,
	})

如果连接了TTY没有实现颜色输出(原因之一:非标准TTY、自定义的Formatter等),需要颜色输出,请按如下方式设置格式化程序:

1
2
3
4
logrus.SetFormatter(&logrus.TextFormatter{
		ForceColors:   true, // 强制输出颜色,原理:绕过TTY检查。
		FullTimestamp: true, // 显示完整的时间戳
	})

当绕过TTY检查时会丢失日期时间,添加FullTimestamp: true,即可正常显示。

后面会介绍更多格式化器。

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

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

  • time:输出日志的时间;为本地区标准时间。
    • 补充:+08:00为北京标准时间,改为Z为UTC(世界标准时间,零时区时间)
    • T为日期与时间的分隔符。是ISO定制的一种标准日期时间的表示方式。
  • level:日志级别;
  • msg:日志信息。

使用

替代 Go log 标准库

深入探究 Go log 标准库一文中举过一个使用 Go log 标准库的简单示例,现在可以将其无缝切换到 Logrus,只需要把 import "log" 改成 import log "github.com/sirupsen/logrus" 即可实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

// 替代 import "log"
import (
    log "github.com/sirupsen/logrus"
)

func main() {
    log.Print("Print")
    log.Printf("Printf: %s", "print")
    log.Println("Println")

    log.Fatal("Fatal")
    log.Fatalf("Fatalf: %s", "fatal")
    log.Fatalln("Fatalln")

    log.Panic("Panic")
    log.Panicf("Panicf: %s", "panic")
    log.Panicln("Panicln")
}

执行以上代码,得到如下输出:

1
2
3
4
5
6
$ go run main.go
time="2024-05-09T14:13:43+08:00" level=info msg=Print
time="2024-05-09T14:13:43+08:00" level=info msg="Printf: print"
time="2024-05-09T14:13:43+08:00" level=info msg=Println
time="2024-05-09T14:13:43+08:00" level=fatal msg=Fatal
exit status 1

虽然输出格式与使用 Go log 标准库表现略有不同,但程序执行并不会报错,说明二者完全兼容。

基本使用

修改日志级别

调用logrus.SetLevel(level Level),就可以修改日志级别。

logrus默认的text记录器日志级别是InfoLevel。要想要输出info以下级别的日志,就必须修改。

1
 logrus.SetLevel(logrus.TraceLevel)

level可选择类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// logrus/logrus.go
const (
    // PanicLevel level, highest level of severity. Logs and then calls panic with the
    // message passed to Debug, Info, ...
    PanicLevel Level = iota
    // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
    // logging level is set to Panic.
    FatalLevel
    // ErrorLevel level. Logs. Used for errors that should definitely be noted.
    // Commonly used for hooks to send errors to an error tracking service.
    ErrorLevel
    // WarnLevel level. Non-critical entries that deserve eyes.
    WarnLevel
    // InfoLevel level. General operational entries about what's going on inside the
    // application.
    InfoLevel
    // DebugLevel level. Usually only enabled when debugging. Very verbose logging.
    DebugLevel
    // TraceLevel level. Designates finer-grained informational events than the Debug.
    TraceLevel
)

输出调用信息

调用logrus.SetReportCaller(true),会在输出日志中添加方法、文件以及行号信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

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

func main() {
    logrus.SetReportCaller(true)

    logrus.Info("info msg")
}

输出多了两个字段:func为函数名,file为调用logrus相关方法的文件名以及行号:

1
2
3
$ go run main.go 
time="2024-05-09T14:26:00+08:00" level=info msg="info msg" func=main.main file="F:/GoProject/learn
/main.go:10"

添加字段

Logrus 鼓励用户通过日志字段记录结构化日志,可以使用 WithFieldsWithField 两种形式,并且可以链式调用。

尽量别用logrus.Fatalf("Failed to send event %s to topic %s with key %d") 这种纯文本形式,因为结构化日志有利于工具提取并分析日志。

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

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

二者都可以链式调用,会返回一个指向Entry类型的结构体(日志条目logrus.Entry),该结构体绑定了各种级别的日志方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

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

func main() {
    logrus.
       WithField("name", "arlettebrook").
       WithField("age", 18).
       Info("WithFiled")
    
    logrus.WithFields(logrus.Fields{
       "name": "arlettebrook",
       "age":  18,
    }).Info("WithFields")
}

运行输出:

1
2
3
$ go run main.go 
time="2024-05-09T15:22:24+08:00" level=info msg=WithFiled age=18 name=arlettebrook
time="2024-05-09T15:22:24+08:00" level=info msg=WithFields age=18 name=arlettebrook

默认字段:如果在一个函数中的所有日志都需要添加某些字段,可以使用WithFields的返回的*Entry替换logrus。这样后续输出都会包含指定字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "github.com/sirupsen/logrus"

func main() {
    withFieldsLog := logrus.WithFields(logrus.Fields{
       "name": "arlettebrook",
       "age":  18,
    })
    withFieldsLog.Error("error msg")
    withFieldsLog.Info("info msg")
}

运行输出:

1
2
3
$ go run main.go 
time="2024-05-09T15:37:56+08:00" level=error msg="error msg" age=18 name=arlettebrook
time="2024-05-09T15:37:56+08:00" level=info msg="info msg" age=18 name=arlettebrook

使用同一个logrus.Entry调不同级别的日志方法,即可实现携带相同的字段(默认字段)。

注意:默认字段也支持链式调用。

重定向输出

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

 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
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"

	"github.com/sirupsen/logrus"
)

func main() {
	writer1 := bytes.NewBuffer(nil)
	writer2 := os.Stdout
	writer3, err := os.OpenFile("./log.txt", os.O_WRONLY|os.O_CREATE, 0755)
	defer func(writer3 *os.File) {
		err := writer3.Close()
		if err != nil {
			log.Fatal(err)
		}
	}(writer3)
	if err != nil {
		log.Fatalf("create file log.txt failed: %v", err)
	}

	logrus.SetOutput(io.MultiWriter(writer1, writer2, writer3))
	logrus.Info("info msg")
	fmt.Println("Buffer:", writer1.String())
}

运行,会在文件log.txt和控制台输出日志。

处理不同环境

Logrus 并没有像 zap 那样提供现成的 API 来支持在不同的环境下使用(Production 和 Development),如果你想在生产和测试环境使用不同的格式输出日志,则需要通过代码判断在不同环境设置不同的 Formatter 来实现。示例如下:

 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
package main

import (
    "os"

    nested "github.com/antonfisher/nested-logrus-formatter"
    "github.com/sirupsen/logrus"
)

func init() {
    // 假设环境变量APP_ENV已经被设置
    env := os.Getenv("APP_ENV")

    // 根据环境设置日志级别
    switch env {
    case "development":
       // 在开发环境中显示所有日志
       logrus.SetLevel(logrus.DebugLevel)
       logrus.SetFormatter(&nested.Formatter{})
    case "testing":
       // 在测试环境中只显示警告和错误日志
       logrus.SetLevel(logrus.WarnLevel)
    case "production":
       // 在生产环境中只显示错误日志
       logrus.SetLevel(logrus.ErrorLevel)
       logrus.SetFormatter(&logrus.JSONFormatter{})
    default:
       // 默认情况下,显示所有日志
       logrus.SetLevel(logrus.DebugLevel)
       logrus.SetFormatter(&nested.Formatter{})
    }

    // 你还可以设置日志格式、输出位置等
    // ...
}

func main() {
    logrus.Error("error log")
    logrus.Warn("warn log")
    logrus.Info("info log")
    logrus.Debug("debug log")
}

运行输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ go run main.go
May 11 15:31:46.122 [ERRO] error log
May 11 15:31:46.168 [WARN] warn log
May 11 15:31:46.168 [INFO] info log
May 11 15:31:46.169 [DEBU] debug log

$ app_env=production go run main.go
{"level":"error","msg":"error log","time":"2024-05-11T15:31:53+08:00"}

$ app_env=development go run main.go
May 11 15:32:05.563 [ERRO] error log
May 11 15:32:05.609 [WARN] warn log
May 11 15:32:05.609 [INFO] info log
May 11 15:32:05.609 [DEBU] debug log

测试

如果你的单元测试程序中需要测试日志内容,Logrus 提供了 test.NewNullLogger 日志记录器,它只会记录日志,不输出任何内容。使用示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "testing"

    "github.com/sirupsen/logrus"
    "github.com/sirupsen/logrus/hooks/test"
    "github.com/stretchr/testify/assert"
)

func TestLogrus(t *testing.T) {
    logger, hook := test.NewNullLogger()
    logger.Error("Hello error")

    assert.Equal(t, 1, len(hook.Entries))
    assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
    assert.Equal(t, "Hello error", hook.LastEntry().Message)

    hook.Reset()
    assert.Nil(t, hook.LastEntry())
}

第二个返回值是一个结构体:

1
2
3
4
5
6
7
type Hook struct {
	// Entries is an array of all entries that have been received by this hook.
	// For safe access, use the AllEntries() method, rather than reading this
	// value directly.
	Entries []logrus.Entry  // 条目切片
	mu      sync.RWMutex
}

方法LastEntry()返回Entries的最后一条日志条目对象。

运行输出:

1
2
3
4
5
$ go test -run TestLogrus -v
=== RUN   TestLogrus
--- PASS: TestLogrus (0.07s)
PASS
ok      github.com/arlettebrook/learn   0.643s

自定义 Logger

除了通过 logrus.Info("Info msg") 这种开箱即用的方式使用 Logrus 默认的 Logger,我们还可以自定义 Logger

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

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

  • flag标准库中的CommandLine对象;
  • log标准库中的std对象。

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

 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
// github.com/sirupsen/logrus/exported.go
var (
	// std is the name of the standard logger in stdlib `log`
	std = New()
)

func New() *Logger {
	return &Logger{
		Out:          os.Stderr,
		Formatter:    new(TextFormatter),
		Hooks:        make(LevelHooks),
		Level:        InfoLevel,
		ExitFunc:     os.Exit,
		ReportCaller: false,
	}
}

func StandardLogger() *Logger {
	return std
}

// SetOutput sets the standard logger output.
func SetOutput(out io.Writer) {
	std.SetOutput(out)
}

// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
	std.SetFormatter(formatter)
}

// SetReportCaller sets whether the standard logger will include the calling
// method as a field.
func SetReportCaller(include bool) {
	std.SetReportCaller(include)
}

// SetLevel sets the standard logger level.
func SetLevel(level Level) {
	std.SetLevel(level)
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "github.com/sirupsen/logrus"

func main() {
	log := logrus.New()

	log.Info("info msg")
	// 用自定义logger
	log.SetLevel(logrus.DebugLevel)
	log.SetFormatter(&logrus.JSONFormatter{})

	log.Debug("debug msg")
}

New()函数创建的logger与默认的logger相同。运行输出:

1
2
3
$ go run main.go 
time="2024-05-09T22:55:30+08:00" level=info msg="info msg"
{"level":"debug","msg":"debug msg","time":"2024-05-09T22:55:30+08:00"}

通过创建的logger对象可以直接赋值修改Level、Out、Formatter等,不用调对应的Set方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func New() *Logger {
	return &Logger{
		Out:          os.Stderr,
		Formatter:    new(TextFormatter),
		Hooks:        make(LevelHooks),
		Level:        InfoLevel,
		ExitFunc:     os.Exit,
		ReportCaller: false,
	}
}

示例修改如下:

1
2
3
	log.Out=os.Stdout
	log.Level=logrus.DebugLevel
	log.Formatter=&logrus.JSONFormatter{}

我们还可以通过FieldMap属性修改默认字段的键名

1
2
type FieldMap map[fieldKey]string
type fieldKey string

支持重命名的默认字段fieldKey如下:

1
2
3
4
5
6
7
8
9
const (
	defaultTimestampFormat = time.RFC3339
	FieldKeyMsg            = "msg"
	FieldKeyLevel          = "level"
	FieldKeyTime           = "time"
	FieldKeyLogrusError    = "logrus_error"
	FieldKeyFunc           = "func"
	FieldKeyFile           = "file"
)

实例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

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

func main() {
    log := logrus.New()
    log.Formatter = &logrus.TextFormatter{
       DisableColors: true,
       FieldMap: logrus.FieldMap{
          logrus.FieldKeyMsg: "message", // 将msg改成message
       },
    }
    log.Info("info msg")

    log.Level = logrus.DebugLevel
    log.Formatter = &logrus.JSONFormatter{ // 会覆盖TextFormatter
       FieldMap: logrus.FieldMap{
          logrus.FieldKeyTime: "TIME",// 将 time改成TIME
       },
    }
    log.Debug("debug msg")
}

运行输出:

1
2
3
$ go run main.go 
time="2024-05-09T23:23:53+08:00" level=info message="info msg"
{"TIME":"2024-05-09T23:23:53+08:00","level":"debug","msg":"debug msg"}

修改日志格式

调用logrus.SetFormatter(formatter Formatter),即可修改日志格式。

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

Logrus 提供了 JSONFormatterTextFormatter 来分别实现 JSON 和 Text 格式的日志输出,它们的指针类型都实现了 Formatter 接口。除此之外,这里还有一个第三方实现的 Formatter 列表可供选择,如果这些依然无法满足你的需求,则可以自己实现 Formatter 接口对象定制日志格式。

使用如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "github.com/sirupsen/logrus"

func main() {
    logrus.Info("default formatter:TextFormatter") //text
    logrus.SetFormatter(&logrus.JSONFormatter{})
    logrus.Info("JSONFormatter") //json
    logrus.SetFormatter(&logrus.TextFormatter{})
    logrus.Info("default formatter:TextFormatter") //text
}

logrus默认的Formatter是TextFormatter。在非标准TTY中运行输出结果(不带颜色输出)如下:

1
2
3
4
$ go run main.go 
time="2024-05-09T22:06:29+08:00" level=info msg="default formatter:TextFormatter"
{"level":"info","msg":"JSONFormatter","time":"2024-05-09T22:06:29+08:00"}
time="2024-05-09T22:06:29+08:00" level=info msg="default formatter:TextFormatter"
使用第三方格式

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

先安装:

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

后使用:

 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 (
    "fmt"
    "time"

    nested "github.com/antonfisher/nested-logrus-formatter"
    "github.com/sirupsen/logrus"
)

func main() {
    // 非标准TTY,强制输出颜色
    logrus.SetFormatter(&logrus.TextFormatter{
       ForceColors:     true,
       FullTimestamp:   true,
       TimestampFormat: time.DateTime,
    })
    logrus.Info("info msg")
    logrus.WithFields(logrus.Fields{
       "username": "arlettebrook",
       "age":      18,
    }).Warn("user info")

    fmt.Println("----------------")
    logrus.SetFormatter(&nested.Formatter{
       HideKeys:        true,
       TimestampFormat: time.DateTime,
    })

    logrus.Info("info msg")
    logrus.WithFields(logrus.Fields{
       "username": "arlettebrook",
       "age":      18,
    }).Warn("user info")
}

程序运行输出:

1
2
3
4
5
6
7
$ go run main.go 
INFO[2024-05-09 22:33:40] info msg                                     
WARN[2024-05-09 22:33:41] user info                                     age=18 username=arlettebro
ok
----------------
2024-05-09 22:33:41 [INFO] info msg
2024-05-09 22:33:41 [WARN] [18] [arlettebrook] user info

没有截图,参考的是官方对比图片:

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// github.com/antonfisher/nested-logrus-formatter/formatter.go
type Formatter struct {
  FieldsOrder     []string
  TimestampFormat string  
  HideKeys        bool    
  NoColors        bool    
  NoFieldsColors  bool    
  ShowFullLevel   bool    
  TrimMessages    bool    
}
  • 默认,logrus输出日志中字段是key=value这样的形式。使用nested格式,我们可以通过设置HideKeystrue隐藏键,只输出值;

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

      1
      
      2024-05-09 22:40:09 [WARN] [age:18] [username:arlettebrook] user info
      
  • 默认,logrus是按键的字母序输出字段,可以设置FieldsOrder定义输出字段顺序;string类型的切片指定顺序。

  • 通过设置TimestampFormat设置日期格式。如time.RFC3339time.DateTime

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

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

Hooks

Hooks本质是一些函数或方法,用于不修改原代码,扩展程序

Logrus 最令人心动的两个功能,一个是结构化日志,另一个就是 Hooks 了。

Hooks 为 Logrus 提供了极大的灵活性,通过 Hooks 可以实现各种扩展功能。比如可以通过 Hooks 实现:Error 以上级别日志发送邮件通知重要日志告警日志切割程序优雅退出等,非常实用。

Logrus 提供了 Hook 接口,只要我们实现了这个接口,并将其注册到 Logrus 中,就可以使用 Hooks 的强大能力了。Hook 接口定义如下:

1
2
3
4
type Hook interface {
	Levels() []Level
	Fire(*Entry) error
}

Levels 方法返回一个日志级别切片,Logrus 记录的日志级别如果存在于切片中,则会触发 Hooks,即调用 Fire 方法。

logrus设置钩子(Hooks),符合[]Level的日志输出前都会执行钩子的特定方法(Fire 方法)。所以,我们可以实现添加输出字段、根据级别将日志输出到不同的目的地。

并且hook函数会在输出日志之前执行。

Entry是当前日志条目(当前输出日志对象)。是结构体类型,定义如下:

 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
type Entry struct {
    Logger *Logger

    // 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
    // This field will be set on entry firing and the value will be equal to the one in Logger struct field.
    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

    // Contains the context set by the user. Useful for hook processing etc.
    Context context.Context

    // err may contain a field formatting error
    err string
}

常用的属性是:

  1. Data Fields是日志条目中所有的字段,Fields类型是type Fields map[string]interface{}
  2. Logger *Logger记录该日志条目的logger。
  3. 单条日志条目信息都保存在Entry结构体中。如,创建时间、日志级别、日志消息等。

logrus也内置了一个syslog的钩子,将日志输出到系统日志syslog中。它不适用用windows的系统日志。

Logrus 内置 Hooks 列表: https://github.com/sirupsen/logrus/tree/master/hooks

自定义Hook

利用Hook添加字段

这里我们实现一个钩子,在输出的日志中增加一个app=awesome-web字段。

示例代码如下:

 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
package main

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

type AppHook struct {
    AppName string
}

func (h *AppHook) Levels() []logrus.Level {
    return logrus.AllLevels
}

func (h *AppHook) Fire(entry *logrus.Entry) error {
    entry.Data["app"] = h.AppName
    return nil
}

func main() {
    h := &AppHook{AppName: "awesome-web"}
    logrus.AddHook(h)

    logrus.Info("info msg")
    logrus.WithField("username", "arlettebrook").
       Warn("user info")
}

非标准TTY运行输出:

1
2
3
4
$ go run main.go 
time="2024-05-10T22:58:42+08:00" level=info msg="info msg" app=awesome-web
time="2024-05-10T22:58:42+08:00" level=warning msg="user info" app=awesome-web username=arlettebro
ok

总结:添加钩子(hook),只需要创建一个结构体实现Hook接口,在Levels方法中设置触发hook函数的条件(日志级别),在Fire方法中定义hook函数行为。然后创建对象,利用AddHook(hook Hook)将hook注册到logger当中。

利用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
package main

import (
    "fmt"

    "github.com/sirupsen/logrus"
)

func init() {
    logrus.SetFormatter(&logrus.JSONFormatter{})
    logrus.AddHook(&EmailHook{})
}

type EmailHook struct {
}

func (e EmailHook) Levels() []logrus.Level {
    return logrus.AllLevels // 所有日志都发送到邮件
}

func (e EmailHook) Fire(entry *logrus.Entry) error {
    // 添加一个邮箱字段标识
    entry.Data["app"] = "email"

    // 获取日志条目
    msg, _ := entry.String()
    // 模拟发送邮件
    fmt.Printf("fakeSendEmail: %s", msg)
    return nil
}

func main() {
    logrus.Info("info msg")
    logrus.WithField("username", "arlettebrook").
       Warn("user info")
}

运行输出:

1
2
3
4
5
6
7
$ go run main.go 
fakeSendEmail: {"app":"email","level":"info","msg":"info msg","time":"2024-05-10T23:22:13+08:00"}
{"app":"email","level":"info","msg":"info msg","time":"2024-05-10T23:22:13+08:00"}
fakeSendEmail: {"app":"email","level":"warning","msg":"user info","time":"2024-05-10T23:22:13+08:0
0","username":"arlettebrook"}
{"app":"email","level":"warning","msg":"user info","time":"2024-05-10T23:22:13+08:00","username":"
arlettebrook"}

可以发现,在打印每条日志之前,都会执行Hook函数,也就是实现了发送邮件。

第三方Hook

logrus的第三方 Hook 有很多,我们可以使用一些现成的 Hook 。如:

  • mgorus:将日志发送到 mongodb;
  • logrus-redis-hook:将日志发送到 redis;
  • logrus-amqp:将日志发送到 ActiveMQ。
  • lumberjackrus :实现了日志切割和归档功能,并且能够将不同级别的日志输出到不同文件。lumberjackrus 是专门为 Logrus 而打造的文件日志 Hooks,其官方介绍为 local filesystem hook for Logrus

更多过内容请参考官方提供的第三方开发的 Hooks 列表

lumberjackrus

lumberjackrus :实现了日志切割和归档功能,并且能够将不同级别的日志输出到不同文件。lumberjackrus 是专门为 Logrus 而打造的文件日志 Hooks,其官方介绍为 local filesystem hook for Logrus

用于记录到本地文件系统的钩子(使用 logrotate 和可以将不同的日志级保存到一个文件)

lumberjackrus是一个第三方包,使用要先安装:

1
$ go get -u github.com/orandin/lumberjackrus

示例如下:

 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
package main

import (
    "github.com/orandin/lumberjackrus"
    "github.com/sirupsen/logrus"
)

func init() {
    logrus.SetFormatter(&logrus.TextFormatter{})
    logrus.SetLevel(logrus.DebugLevel)

    hook, err := lumberjackrus.NewHook(
       &lumberjackrus.LogFile{ // 未指定级别的日志保存的文件,属性都是optional
          Filename:   "./tmp/general.log", // 路径+文件名,默认当前目录名字<processName>-lumberjack.log
          MaxSize:    3,                   // 文件最大占用,单位MB,默认100MB
          MaxBackups: 1,                   // 文件最大备份数,默认不限制
          MaxAge:     1,                   // 备份文件保存的天数,默认永久
          Compress:   false,               // 是否压缩,默认false
          LocalTime:  false,               // 文件启用本地时间,默认utc,
       },
       logrus.InfoLevel,        // 定义写入文件的日志级别
       &logrus.JSONFormatter{}, // 日志格式Formatter
       &lumberjackrus.LogFileOpts{ // 根据日志级别指定保存位置
          logrus.InfoLevel: &lumberjackrus.LogFile{
             Filename:   "./tmp/info/info.log",
             MaxSize:    1,
             MaxBackups: 2,
             MaxAge:     1,
             Compress:   true,
             LocalTime:  true,
          },
          logrus.ErrorLevel: &lumberjackrus.LogFile{
             Filename: "./tmp/error/error.log",
          },
       },
    )

    if err != nil {
       panic(err)
    }

    logrus.AddHook(hook)
}

func main() {
    logrus.Debug("Debug message") // It is not written to a file (because debug level < minLevel)
    logrus.Info("Info message")   // Written in ./tmp/info.log
    logrus.Warn("Warn message")   // Written in ./tmp/general.log
    logrus.Error("Error message") // Written in ./tmp/error.log
}

运行,在非标准TTY中输出:

1
2
3
4
5
$ go run main.go 
time="2024-05-11T10:49:22+08:00" level=debug msg="Debug message"
time="2024-05-11T10:49:22+08:00" level=info msg="Info message"
time="2024-05-11T10:49:22+08:00" level=warning msg="Warn message"
time="2024-05-11T10:49:22+08:00" level=error msg="Error message"

并且还会再当前目录下生成tmp/general.log, tmp/info, tmp/error,里面的格式为json。debug日志没有写入文件。info.log文件内容如下:

1
{"level":"info","msg":"Info message","time":"2024-05-11T10:49:22+08:00"}

至此,我们使用lumberjackrus实现了日志轮询以及分级别保存。这个钩子,实现日志轮询是基于lumberjack库实现的。

使用这个钩子,我们只需要利用NewHook(defaultLogger *LogFile, minLevel logrus.Level, formatter logrus.Formatter, opts *LogFileOpts) (*Hook, error)创建对象,将对象添加logger中即可使用。代码中有对参数的介绍。最后一个参数(本质是map类型)如果为nil或者为空map,那么日志将不会分级别保存,都会保存到defaultLogger指定的文件中。

logrus-redis-hook

下面演示利用hook将日志发送到redis,我们用到的包是logrus-redis-hook, 先安装logrus-redis-hook

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

默认你已经安装好了redis并且启动了它,如果你想了解redis,可以参考我的另一篇文章《Redis Introduction》

然后编写程序:

 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
package main

import (
    "io"

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

func init() {
    logrus.SetFormatter(&logrus.JSONFormatter{})
    hookConfig := logredis.HookConfig{
       Host:     "192.168.245.130",
       Password: "123456",
       Key:      "demo_log",
       Format:   "v0",
       App:      "awesome_demo",
       Port:     6379,
       Hostname: "localhost", // will be sent to field @source_host
       DB:       1,           // optional
       TTL:      3600,
    }

    hook, err := logredis.NewHook(hookConfig)
    if err == nil {
       logrus.AddHook(hook)
    } else {
       logrus.Errorf("logredis error: %q", err)
    }
}

func main() {
    // when hook is injected successfully, logs will be sent to redis server
    logrus.Info("just some info logging...")

    // we also support log.WithFields()
    logrus.WithFields(logrus.Fields{
       "animal": "walrus",
       "foo":    "bar",
       "this":   "that"}).
       Info("additional fields are being logged as well")

    // If you want to disable writing to stdout, use setOutput
    logrus.SetOutput(io.Discard)
    logrus.Info("This will only be sent to Redis")
}

注意:请连接你自己的redis。

运行程序后,终端输出:

1
2
3
4
$ go run main.go 
{"level":"info","msg":"just some info logging...","time":"2024-05-11T11:56:17+08:00"}
{"animal":"walrus","foo":"bar","level":"info","msg":"additional fields are being logged as well","
this":"that","time":"2024-05-11T11:56:17+08:00"}

我们使用redis-cli查看:

1
2
3
4
127.0.0.1:6379[1]> lrange demo_log 0 -1
1) "{\"@fields\":{\"application\":\"awesome_demo\",\"level\":\"info\"},\"@message\":\"just some info logging...\",\"@source_host\":\"localhost\",\"@timestamp\":\"2024-05-11T03:56:17.867867Z\"}"
2) "{\"@fields\":{\"animal\":\"walrus\",\"application\":\"awesome_demo\",\"foo\":\"bar\",\"level\":\"info\",\"this\":\"that\"},\"@message\":\"additional fields are being logged as well\",\"@source_host\":\"localhost\",\"@timestamp\":\"2024-05-11T03:56:17.933777Z\"}"
3) "{\"@fields\":{\"application\":\"awesome_demo\",\"level\":\"info\"},\"@message\":\"This will only be sent to Redis\",\"@source_host\":\"localhost\",\"@timestamp\":\"2024-05-11T03:56:17.9351074Z\"}"

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

默认的logger是输出到stderr,但修改为discard,将不会输出到任何地方,但是钩子依然执行。因为钩子里面指定发送到的是redis,不受影响。


总结

本文介绍了 Logrus 的基本特点,以及如何使用。

Logrus 完全兼容 log 标准库,所以可以实现无缝替换。其 API 设计思路跟 log 标准库的风格也有很多相似之处,都提供了一个默认的 std 日志对象达到开箱即用的效果。Logrus 最实用的两个功能,一个是支持结构化日志,一个是支持 Hooks 机制,这极大的提升了可用性和灵活性,也使得 Logrus 成为最受欢迎的 Go 日志库。


参考

  1. logrus GitHub 仓库
  2. Go 每日一库之 logrus
  3. Go 第三方 log 库之 logrus 使用