# golang日志库logrus的使用 ## logrus ### 快速使用 ``` go get github.com/sirupsen/logrus ``` ``` 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`。 运行程序,输出 ``` 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`不会执行到。 另外,我们观察到输出中有三个关键信息,`time`、`level`和`msg`: - `time`:输出日志的时间; - `level`:日志级别; - `msg`:日志信息。 ### 定制 #### 输出文件名 调用`logrus.SetReportCaller(true)`设置在输出日志中添加文件名和方法信息: ``` package main import "github.com/sirupsen/logrus" func main(){ logrus.SetReportCaller(true) logrus.Info("info msg") } ``` 输出多了两个字段`file`为调用`logrus`相关方法的文件名,`method`为方法名: ``` 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.WithField`和`logrus.WithFields`实现。 `logrus.WithFields`接受一个`logrus.Fields`类型的参数,其底层实际上为`map[string]interface{}`: ``` // github.com/sirupsen/logrus/logrus.go type Fields map[string]interface{} ``` 下面程序在输出中添加两个字段`name`和`age`: ``` 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_id`和`ip`字段: ``` 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`里 同时将日志写到标出输出和文件中 ``` 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`也是如此: ``` // 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`对象`std`,`SetOutput/SetFormatter/SetReportCaller/SetLevel`这些方法都是调用`std`对象的对应方法! 我们当然也可以创建自己的`Logger`对象,使用方式与直接调用`logrus`的方法类似: ``` 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`设置日志格式: ``` 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 ``` #### 第三方格式 除了内置的`TextFormatter`和`JSONFormatter`,还有不少第三方格式支持。例如[nested-logrus-formatter](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fantonfisher%2Fnested-logrus-formatter)。 先安装: ``` go get github.com/antonfisher/nested-logrus-formatter ``` ``` 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`格式提供了多个字段用来定制行为: ``` // 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`隐藏键,只输出值; - 默认,`logrus`是按键的字母序输出字段,可以设置`FieldsOrder`定义输出字段顺序; - 通过设置`TimestampFormat`设置日期格式。 ``` 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") } ``` 如果不隐藏键,程序输出: ``` $ 2020-02-08T15:40:07+08:00 [INFO] [name:dj] [age:18] info msg ``` 隐藏键,程序输出: ``` $ 2020-02-08T15:41:58+08:00 [INFO] [dj] [18] info msg ``` 注意到,我们将时间格式设置成`time.RFC3339`,即`2006-01-02T15:04:05Z07:00`这种形式。 **通过实现接口`logrus.Formatter`可以实现自己的格式。** ``` // github.com/sirupsen/logrus/formatter.go type Formatter interface { Format(*Entry) ([]byte, error) } ``` > JSONFormatter实现logrus.Formatter接口的源码 ``` // 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`接口: ``` // github.com/sirupsen/logrus/hooks.go type Hook interface { Levels() []Level Fire(*Entry) error } ``` `Levels()`方法返回感兴趣的日志级别,输出其他日志时不会触发钩子。`Fire`是日志输出前调用的钩子方法。 案例1,这里我们实现一个钩子,在输出的日志中增加一个`app=awesome-web`字段。 ``` 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,将错误级别日志输出到文件以及控制台 ``` 下载 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 等存储中: - [mgorus](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fweekface%2Fmgorus):将日志发送到 mongodb; - [logrus-redis-hook](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Frogierlommers%2Flogrus-redis-hook):将日志发送到 redis; - [logrus-amqp](https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Fvladoatanasov%2Flogrus_amqp):将日志发送到 ActiveMQ。 这里我们演示一个 redis,感兴趣自行验证其他的。先安装`logrus-redis-hook`: ``` go get github.com/rogierlommers/logrus-redis-hook ``` ``` 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中。 ``` 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用于将日志写入滚动文件。实现日志切割、压缩以及保留个数 快速使用 ``` 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") } } ```