Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Gray-Ice

个人博客兼个人网站

Github地址

与logrus相关的比较全的中文文章:Go 每日一库之 logrus –darjun(强烈推荐看这篇,因为我写的不太全,而且内容也比较简单)。

日志级别

Logrus有七个日志级别: Trace, Debug, Info, Warning, Error, Fatal和Panic。

1
2
3
4
5
6
7
8
9
log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// 在输出日志之后调用os.Exit(1)
log.Fatal("Bye.")
// 在输出日志之后调用panic()
log.Panic("I'm bailing.")

可以通过设置日志级别来控制打印哪个级别的日志。

示例:

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

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

func main() {
logrus.SetLevel(logrus.InfoLevel) // 设置日志级别为Info
logrus.Debug("Level: Debug.")
logrus.Info("Level: Info.")
logrus.Warn("Level: Warn.")
logrus.Error("Level: Error.")
}

输出:

1
2
3
4
~/codeSet/goCode/test » go run main.go                                                               
INFO[0000] Level: Info.
WARN[0000] Level: Warn.
ERRO[0000] Level: Error.

这里因为设置了日志级别为Info,所以日志级别在Info之下的Debug没有被打印出。

日志级别从下至上排序如下:

1
2
3
4
5
6
7
Trace
Debug
Info
Warning
Error
Fatal
Panic。

自定义输出格式

logrus可以自定义输出格式。

TextFormatter

示例如下:

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

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

func main() {
logrus.SetLevel(logrus.InfoLevel) // 设置日志等级为INFO

//设置格式
logrus.SetFormatter(&logrus.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})

logrus.Info("Level: Info.")
logrus.Warn("Level: Warn.")
logrus.Error("Level: Error.")
}

输出:

1
2
3
4
~/codeSet/goCode/test » go run main.go                          
time="2022-01-18T14:32:54+08:00" level=info msg="Level: Info."
time="2022-01-18T14:32:54+08:00" level=warning msg="Level: Warn."
time="2022-01-18T14:32:54+08:00" level=error msg="Level: Error."

上面的代码设置了Text格式,该格式还有其他选项,头文件中是这样定义的:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// TextFormatter formats logs into text
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool

// Force disabling colors.
DisableColors bool

// Force quoting of all values
ForceQuote bool

// DisableQuote disables quoting for all values.
// DisableQuote will have a lower priority than ForceQuote.
// If both of them are set to true, quote will be forced on all values.
DisableQuote bool

// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
EnvironmentOverrideColors bool

// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool

// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool

// TimestampFormat to use for display when a full timestamp is printed.
// The format to use is the same than for time.Format or time.Parse from the standard
// library.
// The standard Library already provides a set of predefined format.
TimestampFormat string

// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool

// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)

// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool

// PadLevelText Adds padding the level text so that all the levels output at the same length
// PadLevelText is a superset of the DisableLevelTruncation option
PadLevelText bool

// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool

// Whether the logger's out is to a terminal
isTerminal bool

// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap

// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)

terminalInitOnce sync.Once

// The max length of the level text, generated dynamically on init
levelTextMaxLength int
}

JSONFormatter

将格式设置为JSONFormatter会让日志以JSON形式输出。示例如下:

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

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

func main() {
logrus.SetLevel(logrus.InfoLevel) // 设置日志等级为INFO

//设置格式
logrus.SetFormatter(&logrus.JSONFormatter{
DisableTimestamp: false,
})

logrus.Info("Level: Info.")
logrus.Warn("Level: Warn.")
logrus.Error("Level: Error.")
}

输出如下:

1
2
3
4
~/codeSet/goCode/test » go run main.go                                             
{"level":"info","msg":"Level: Info.","time":"2022-01-18T14:39:20+08:00"}
{"level":"warning","msg":"Level: Warn.","time":"2022-01-18T14:39:20+08:00"}
{"level":"error","msg":"Level: Error.","time":"2022-01-18T14:39:20+08:00"}

打印调用者

我忘了是在哪看的了,说如果启用打印调用者文件名会导致性能变差,不过我这里还是记录一下。

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"
)

func main() {
logrus.SetLevel(logrus.InfoLevel) // 设置日志等级为INFO
// 设置打印调用者
logrus.SetReportCaller(true)

//设置格式
logrus.SetFormatter(&logrus.JSONFormatter{
DisableTimestamp: false,
})

logrus.Info("Level: Info.")
logrus.Warn("Level: Warn.")
logrus.Error("Level: Error.")
}

输出如下:

1
2
3
4
~/codeSet/goCode/test » go run main.go           
{"file":"/home/zero/codeSet/goCode/test/main.go:17","func":"main.main","level":"info","msg":"Level: Info.","time":"2022-01-18T14:44:57+08:00"}
{"file":"/home/zero/codeSet/goCode/test/main.go:18","func":"main.main","level":"warning","msg":"Level: Warn.","time":"2022-01-18T14:44:57+08:00"}
{"file":"/home/zero/codeSet/goCode/test/main.go:19","func":"main.main","level":"error","msg":"Level: Error.","time":"2022-01-18T14:44:57+08:00"}

打印时添加字段

我觉得叫它字段可能不太好,但我也不知道叫什么,所以还是叫它字段吧。关于”字段”是什么,看了代码你就会明白。

添加单个字段

示例:

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

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

func main() {
logrus.SetLevel(logrus.InfoLevel) // 设置日志等级为INFO

//设置格式
logrus.SetFormatter(&logrus.JSONFormatter{
DisableTimestamp: false,
})

// 添加字段
e := logrus.WithField("K-v pair", 1)
// 以Info级别打印日志
e.Info("Info!!")
}

输出如下:

1
2
~/codeSet/goCode/test » go run main.go         
{"K-v pair":1,"level":"info","msg":"Info!!","time":"2022-01-18T15:14:48+08:00"}

可以看到,输出内容里有一对键值对: “‘k-v pair’: 1”,这就是代码里用WithField函数添加的。

那么我们看一下WithField的函数定义:

1
2
3
func WithField(key string, value interface{}) *Entry {
return std.WithField(key, value)
}

可以看到,在WithField函数中key必须是string类型,而value是interface{} (即任何类型)。该函数会返回一个Entry类型的指针,我们就是通过这个指针调用的Info()。

添加多个字段

示例(其实定义字段,添加字段,打印日志完全可以连到一行写,我这里为了更好的可读性拆开了):

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

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

func main() {
logrus.SetLevel(logrus.InfoLevel) // 设置日志等级为INFO

//设置格式
logrus.SetFormatter(&logrus.JSONFormatter{
DisableTimestamp: false,
})

// 定义字段
fields := logrus.Fields{
"name": "Arvin",
"dep": "???",
"age": 18,
}
// 添加多个字段
e := logrus.WithFields(fields)
// 以Info级别打印日志
e.Info("Info!!")
// 以Warn级别打印日志
e.Warn("Warn!!!")
}

输出如下:

1
2
3
~/codeSet/goCode/test » go run main.go
{"age":18,"dep":"???","level":"info","msg":"Info!!","name":"Arvin","time":"2022-01-18T15:27:03+08:00"}
{"age":18,"dep":"???","level":"warning","msg":"Warn!!!","name":"Arvin","time":"2022-01-18T15:27:03+08:00"}

我们看一下Fields类型的定义:

1
2
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}

从添加多个字段到以Info级别打印为止,在源代码中流程大概为: 用定义的Fields初始化一个新的Entry结构,将Fields中的内容添加到Entry结构的Data字段中 -> 巴拉巴拉 -> 从Entry.Data字段中读取内容并拼接字符串 -> 巴拉巴拉 -> 生成bytes.Buffer类型的buffer -> 输出。 我感觉还是蛮麻烦的,但是因为logrus支持的功能多一些,所以麻烦一些也是很正常。

那么上面有提到,源代码里是从Entry.Data字段中读取数据的,那么我们为什么不直接改Entry.Data呢?欢迎观看”新建Entry”小节。

新建logger

示例:

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

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

func main() {
// 新建logger
log := logrus.New()

// 设置格式
log.SetFormatter(&logrus.TextFormatter{
DisableColors: true,
})

// 设置日志等级
log.SetLevel(logrus.InfoLevel)

// 以Info级别打印
log.Info("Hey!")
}

输出如下:

1
2
~/codeSet/goCode/test » go run main.go
time="2022-01-18T17:05:36+08:00" level=info msg="Hey!"

这个我觉得没啥说的,新建一个logger,并且可以对这个Logger进行各种操作,我想大家都可以很容易理解。

新建Entry

示例:

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

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

func main() {
// 新建logger
logger := logrus.New()

// 新建Entry
entry := logrus.NewEntry(logger)

// 向entry.Data中添加字段
entry.Data["Are you Arvin?"] = true

// 打印
entry.Info("Hey!")
}

输出如下:

1
2
~/codeSet/goCode/test » go run main.go            
INFO[0000] Hey! Are you Arvin?=true

可以看到,我们在Entry.Data中添加的内容也被打印出来了。

也许TextFormatter下不太直观,那么换成JSON看一看:

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

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

func main() {
// 新建logger
logger := logrus.New()

// 设置logger为JSON格式
logger.SetFormatter(&logrus.JSONFormatter{})

// 新建Entry
entry := logrus.NewEntry(logger)

// 向entry.Data中添加字段
entry.Data["Are you Arvin?"] = true

// 打印
entry.Info("Hey!")
}

输出如下:

1
2
~/codeSet/goCode/test » go run main.go              
{"Are you Arvin?":true,"level":"info","msg":"Hey!","time":"2022-01-18T17:18:56+08:00"}

Hook

logrus提供了钩子方法,可以自定义不同日志等级不同行为。使用AddHook函数可以向logger添加钩子。

那么先看源码中的AddHook函数:

1
2
3
4
5
6
// AddHook adds a hook to the logger hooks.
func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Hooks.Add(hook)
}

可以看到,想要使用AddHook函数需要向其中传入一个Hook类型的参数,Hook类型是一个接口,看一下它的定义:

1
2
3
4
5
6
7
8
9
// A hook to be fired when logging on the logging levels returned from
// `Levels()` on your implementation of the interface. Note that this is not
// fired in a goroutine or a channel with workers, you should handle such
// functionality yourself if your call is non-blocking and you don't wish for
// the logging calls for levels returned from `Levels()` to block.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}

该了解的都了解了,那么接下来我们看示例:

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 (
"github.com/sirupsen/logrus"
)

// 定义Hook结构体,该结构体只需要实现logrus.Hook接口就可以了,当然你也可以往这个结构体里塞几个字段
type Hook struct{}

// 实现Levels方法。该方法返回一个logrus.Level类型的切片。
func (h *Hook) Levels() []logrus.Level {
// 该函数返回一个logrus.Level类型的切片,这个切片中包含了logrus.InfoLevel和logrus.WarnLevel
// 意思就是这个钩子只对info和warn起作用
return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel}
}

// 实现Fire方法。该方法接收一个*logrus.Entry类型的参数和返回一个error类型的返回值
func (h *Hook) Fire(e *logrus.Entry) error {
// 向Entry.Data中添加字段,因为在调用打印的时候会先将Entry.Data中的内容拼接成字符串,
// 所以我们将会看到在这里添加的字段待会儿会被打印出来
e.Data["new_field"] = "Hook!!!"
return nil
}

func main() {
// 新建logger
logger := logrus.New()

// 设置logger为JSON格式
logger.SetFormatter(&logrus.JSONFormatter{})

// 添加钩子
logger.AddHook(&Hook{})

// 打印
logger.Info("Info output!")
logger.Warn("Warn output!")

// 因为Error没有在Hook结构体的Level()方法返回的切片里,所以它不会打印Hook.Fire()中添加的字段。
logger.Error("Error output!")
}

输出如下:

1
2
3
4
~/codeSet/goCode/test » go run main.go            
{"level":"info","msg":"Info output!","new_field":"Hook!!!","time":"2022-01-19T09:13:32+08:00"}
{"level":"warning","msg":"Warn output!","new_field":"Hook!!!","time":"2022-01-19T09:13:32+08:00"}
{"level":"error","msg":"Error output!","time":"2022-01-19T09:13:32+08:00"}

评论



愿火焰指引你