目录

golang日志库logrus的使用

logrus

快速使用

1
go get github.com/sirupsen/logrus
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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

运行程序,输出

1
2
3
4
5
6
7
8
C:\Users\xx\Desktop\git\日志>go run main.go
time="2021-08-04T13:50:44+08:00" level=trace msg="trace msg"
time="2021-08-04T13:50:44+08:00" level=debug msg="debug msg"
time="2021-08-04T13:50:44+08:00" level=info msg="info msg"
time="2021-08-04T13:50:44+08:00" level=warning msg="warn msg"
time="2021-08-04T13:50:44+08:00" level=error msg="error msg"
time="2021-08-04T13:50:44+08:00" level=fatal msg="fatal msg"
exit status 1

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

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

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

定制

输出文件名

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

1
2
3
4
5
6
7
8
9
package main

import "github.com/sirupsen/logrus"

func main(){
	logrus.SetReportCaller(true)

	logrus.Info("info msg")
}

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

1
2
3
C:\Users\xx\Desktop\git\日志>go run main.go
time="2021-08-04T13:55:16+08:00" level=info msg="info msg" func=main.main file="C:/Users/xx/Desktop/git/日志/main.go:8"

添加字段

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

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

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

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

import "github.com/sirupsen/logrus"

func main(){
	logrus.SetReportCaller(true)

	logrus.WithFields(logrus.Fields{
		"name": "log",
		"age": 20,
	}).Info("info msg")
}
输出
C:\Users\xx\Desktop\git\日志>go run main.go
time="2021-08-04T13:57:04+08:00" level=info msg="info msg" func=main.main file="C:/Users/xx/Desktop/git/日志/main.go:11" age=20 name=log

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

 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.SetReportCaller(true)

	requestLogger := logrus.WithFields(logrus.Fields{
		"name": "log",
		"age": 20,
	})
	requestLogger.Info("info msg")
	requestLogger.Error("error msg")
}

输出
C:\Users\xx\Desktop\git\日志>go run main.go
time="2021-08-04T13:58:38+08:00" level=info msg="info msg" func=main.main file="C:/Users/xx/Desktop/git/日志/main.go:12" age=20 name=log
time="2021-08-04T13:58:38+08:00" level=error msg="error msg" func=main.main file="C:/Users/xx/Desktop/git/日志/main.go:13" age=20 name=log

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

重定向输出

默认情况下,日志输出到io.Stdout。可以调用logrus.SetOutput传入一个io.Writer参数。后续调用相关方法日志将写到io.Writer中。也可以传入一个io.MultiWriter输出到多个io.Writer

同时将日志写到标出输出和文件中

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

func main(){
	logrus.SetReportCaller(true)
	wirte1 := os.Stdout
	write2,_  := os.OpenFile("log.txt",os.O_WRONLY|os.O_CREATE,0755)
	logrus.SetOutput(io.MultiWriter(wirte1,write2))
	requestLogger := logrus.WithFields(logrus.Fields{
		"name": "log",
		"age": 20,
	})
	requestLogger.Info("info msg")
	requestLogger.Error("error msg")
}

自定义

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

这个技巧应用在很多库的开发中,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
// github.com/sirupsen/logrus/exported.go
var (
  std = New()
)

func StandardLogger() *Logger {
  return std
}

func SetOutput(out io.Writer) {
  std.SetOutput(out)
}

func SetFormatter(formatter Formatter) {
  std.SetFormatter(formatter)
}

func SetReportCaller(include bool) {
  std.SetReportCaller(include)
}

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

import "github.com/sirupsen/logrus"

func main(){
	log := logrus.New()
	log.SetLevel(logrus.InfoLevel)
	log.SetFormatter(&logrus.JSONFormatter{})
	log.Info("info msg")
}

日志格式

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

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

func main() {
  logrus.SetLevel(logrus.TraceLevel)
  logrus.SetFormatter(&logrus.JSONFormatter{})

  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")
}
输出
C:\Users\xx\Desktop\git\日志>go run main.go
{"level":"trace","msg":"trace msg","time":"2021-08-04T14:12:30+08:00"}
{"level":"debug","msg":"debug msg","time":"2021-08-04T14:12:30+08:00"}
{"level":"info","msg":"info msg","time":"2021-08-04T14:12:30+08:00"}
{"level":"warning","msg":"warn msg","time":"2021-08-04T14:12:30+08:00"}
{"level":"error","msg":"error msg","time":"2021-08-04T14:12:30+08:00"}
{"level":"fatal","msg":"fatal msg","time":"2021-08-04T14:12:30+08:00"}
exit status 1

第三方格式

除了内置的TextFormatterJSONFormatter,还有不少第三方格式支持。例如nested-logrus-formatter

先安装:

1
go get github.com/antonfisher/nested-logrus-formatter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

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

func main() {
  logrus.SetFormatter(&nested.Formatter{
    HideKeys:    true,
    FieldsOrder: []string{"component", "category"},
  })

  logrus.Info("info msg")
}
输出
Feb  8 15:22:59.077 [INFO] info msg

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隐藏键,只输出值;
  • 默认,logrus是按键的字母序输出字段,可以设置FieldsOrder定义输出字段顺序;
  • 通过设置TimestampFormat设置日期格式。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
  "time"

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

func main() {
  logrus.SetFormatter(&nested.Formatter{
    // HideKeys:        true,
    TimestampFormat: time.RFC3339,
    FieldsOrder:     []string{"name", "age"},
  })

  logrus.WithFields(logrus.Fields{
    "name": "dj",
    "age":  18,
  }).Info("info msg")
}

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

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
2
3
4
// github.com/sirupsen/logrus/formatter.go
type Formatter interface {
  Format(*Entry) ([]byte, error)
}

JSONFormatter实现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
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
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
	data := make(Fields, len(entry.Data)+4)
	for k, v := range entry.Data {
		switch v := v.(type) {
		case error:
			// Otherwise errors are ignored by `encoding/json`
			// https://github.com/sirupsen/logrus/issues/137
			data[k] = v.Error()
		default:
			data[k] = v
		}
	}

	if f.DataKey != "" {
		newData := make(Fields, 4)
		newData[f.DataKey] = data
		data = newData
	}

	prefixFieldClashes(data, f.FieldMap, entry.HasCaller())

	timestampFormat := f.TimestampFormat
	if timestampFormat == "" {
		timestampFormat = defaultTimestampFormat
	}

	if entry.err != "" {
		data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
	}
	if !f.DisableTimestamp {
		data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
	}
	data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
	data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
	if entry.HasCaller() {
		funcVal := entry.Caller.Function
		fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
		if f.CallerPrettyfier != nil {
			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
		}
		if funcVal != "" {
			data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
		}
		if fileVal != "" {
			data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
		}
	}

	var b *bytes.Buffer
	if entry.Buffer != nil {
		b = entry.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	encoder := json.NewEncoder(b)
	encoder.SetEscapeHTML(!f.DisableHTMLEscape)
	if f.PrettyPrint {
		encoder.SetIndent("", "  ")
	}
	if err := encoder.Encode(data); err != nil {
		return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
	}

	return b.Bytes(), nil
}

设置钩子

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

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

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

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

案例1,这里我们实现一个钩子,在输出的日志中增加一个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
28
29
30
31
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")

}

输出
C:\Users\xx\Desktop\git\日志>go run main.go
time="2021-08-04T14:56:33+08:00" level=info msg="info msg" app=awesome-web


案例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
下载
go get github.com/rifflock/lfshook

package main

import (
	"github.com/rifflock/lfshook"
	"github.com/sirupsen/logrus"
	"os"
)



func main(){
	writer,_ := os.OpenFile("err.txt",os.O_CREATE|os.O_WRONLY,0755)
	//logrus.AddHook(h)

	lfHook := lfshook.NewHook(lfshook.WriterMap{
		logrus.ErrorLevel: writer, //为错误级别设置输出目的
		logrus.FatalLevel: writer,
	},nil)
	logrus.AddHook(lfHook)
	logrus.Info("info msg")
	logrus.Error("error msg")

}

扩展

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

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

1
go get github.com/rogierlommers/logrus-redis-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 (
  "io/ioutil"

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

func init() {
  hookConfig := logredis.HookConfig{
    Host:     "localhost",
    Key:      "mykey",
    Format:   "v0",
    App:      "aweosome",
    Hostname: "localhost",
    TTL:      3600,
  }

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

func main() {
  logrus.Info("just some info logging...")

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

  logrus.SetOutput(ioutil.Discard)
  logrus.Info("This will only be sent to Redis")
}

输出到MQ或ES 如下示例代码通过hook将日志输出到amqp消息队列,或者es中。

 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
import (
    "github.com/vladoatanasov/logrus_amqp"
    "gopkg.in/olivere/elastic.v5"
    "gopkg.in/sohlich/elogrus.v2"
    log "github.com/sirupsen/logrus"
    "github.com/pkg/errors"
)

// config logrus log to amqp
func ConfigAmqpLogger(server, username, password, exchange, exchangeType, virtualHost, routingKey string) {
    hook := logrus_amqp.NewAMQPHookWithType(server, username, password, exchange, exchangeType, virtualHost, routingKey)
    log.AddHook(hook)
}

// config logrus log to es
func ConfigESLogger(esUrl string, esHOst string, index string) {
    client, err := elastic.NewClient(elastic.SetURL(esUrl))
    if err != nil {
        log.Errorf("config es logger error. %+v", errors.WithStack(err))
    }
    esHook, err := elogrus.NewElasticHook(client, esHOst, log.DebugLevel, index)
    if err != nil {
        log.Errorf("config es logger error. %+v", errors.WithStack(err))
    }
    log.AddHook(esHook)
}

lumberjack

Lumberjack用于将日志写入滚动文件。实现日志切割、压缩以及保留个数

快速使用

 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
go get gopkg.in/natefinch/lumberjack.v2
package main

import (
	"github.com/rifflock/lfshook"
	"github.com/sirupsen/logrus"
	"gopkg.in/natefinch/lumberjack.v2"
)



func main(){
	errLogger := &lumberjack.Logger{
		Filename: "err.txt", //日志文件路径
		MaxSize: 5, //每个日志文件保存的最大尺寸,单位:M
		MaxBackups: 7, //日志文件最多保存多少个备份
		MaxAge: 7, //日志文件最多保存多少天
		Compress: true, //是否压缩
	}
	//logrus.AddHook(h)

	lfHook := lfshook.NewHook(lfshook.WriterMap{
		logrus.ErrorLevel: errLogger,
		logrus.FatalLevel: errLogger,
	},nil)
	logrus.AddHook(lfHook)
	for  {
		logrus.Error("error msg")
	}
}