Logrus 是目前 GitHub 上 Star 数量最多的 Go 日志库。尽管目前 Logrus 处于维护模式,不再引入新功能,但这并不意味着它已经死了。Logrus 仍将继续维护,以确保安全性、错误修复和提高性能。作为 Go 社区中最受欢迎的日志库之一,Logrus 最大的贡献是推动了 Go 社区广泛使用结构化(如JSON格式)的日志记录。著名的 Docker 项目就在使用 Logrus 记录日志,这进一步证明了其在实际应用中的可靠性和实用性。
特点
Logrus 具有如下特点:
- 与 Go log 标准库 API 完全兼容,这意味着任何使用 log 标准库的代码都可以将日志库无缝切换到 Logrus。
- 支持七种日志级别:
Trace
、Debug
、Info
、Warn
、Error
、Fatal
、Panic
。 - 支持结构化日志记录(key-value 形式,容易被程序解析,如 JSON 格式),通过 Filed 机制进行结构化的日志记录。
- 支持自定义日志格式,内置两种格式
JSONFormatter
(JSON 格式) 和 TextFormatter
(文本格式),并允许用户通过实现 Formatter
接口来自定义日志格式。 - 支持可扩展的 Hooks 机制,可以为不同级别的日志添加 Hooks 将日志记录到不同位置,例如将
Error
、Fatal
和 Panic
级别的错误日志发送到 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
。所以为了能看到Trace
和Debug
日志,我们在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
不会执行到。
另外,我们观察到输出中有三个关键信息,time
、level
和msg
:
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 鼓励用户通过日志字段记录结构化日志,可以使用 WithFields
和 WithField
两种形式,并且可以链式调用。
尽量别用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
对象std
,SetOutput/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 提供了 JSONFormatter
和 TextFormatter
来分别实现 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"
|
使用第三方格式
除了内置的TextFormatter
和JSONFormatter
,还有不少第三方格式支持。我们这里介绍一个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
格式,我们可以通过设置HideKeys
为true
隐藏键,只输出值;
如果不隐藏键,程序输出:
1
| 2024-05-09 22:40:09 [WARN] [age:18] [username:arlettebrook] user info
|
默认,logrus
是按键的字母序输出字段,可以设置FieldsOrder
定义输出字段顺序;string类型的切片指定顺序。
通过设置TimestampFormat
设置日期格式。如time.RFC3339
、time.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
}
|
常用的属性是:
Data Fields
是日志条目中所有的字段,Fields类型是type Fields map[string]interface{}
。Logger *Logger
记录该日志条目的logger。- 单条日志条目信息都保存在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 。如:
更多过内容请参考官方提供的第三方开发的 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 日志库。
参考
- logrus GitHub 仓库
- Go 每日一库之 logrus
- Go 第三方 log 库之 logrus 使用