侧边栏壁纸
博主头像
汪洋

即使慢,驰而不息,纵会落后,纵会失败,但一定可以达到他所向的目标。 - 鲁迅

  • 累计撰写 212 篇文章
  • 累计创建 81 个标签
  • 累计收到 160 条评论

Golang 库 - logrus

汪洋
2022-04-08 / 0 评论 / 0 点赞 / 382 阅读 / 6,775 字

golang 日志库

golang 标准库的日志框架非常简单,仅仅提供了 print,panic 和 fatal 三个函数对于更精细的日志级别、日志文件分割以及日志分发等方面并没有提供支持。所以催生了很多第三方的日志库,但是在 golang 的世界里,没有一个日志库像 slf4j 那样在 Java 中具有绝对统治地位。golang 中,流行的日志框架包括 logrus、zap、zerolog、seelog 等。

  • logrus 是目前 Github 上 star 数量最多的日志库。logrus 功能强大,性能高效,而且具有高度灵活性,提供了自定义插件的功能。很多开源项目,如docker,prometheus 等,都是用了 logrus 来记录其日志
  • zap 是 Uber 推出的一个快速、结构化的分级日志库。具有强大的 ad-hoc 分析功能,并且具有灵活的仪表盘。zap 目前在 GitHub 上的 start 数量约为4.3k
  • seelog 提供了灵活的异步调度、格式化和过滤功能。目前在GitHub上也有约1.1k

logrus 特性

  • 完全兼容 golang 标准库日志模块:logrus 拥有六种日志级别:debug、info、warn、error、fatal、Trace 和 panic,这是 golang 标准库日志模块的 API 的超集。如果您的项目使用标准库日志模块,完全可以以最低的代价迁移到logrus 上
  • 可扩展的 Hook 机制:允许使用者通过 hook 的方式将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch 或者 mq 等,或者通过 hook 定义日志内容和格式等
  • 可选的日志输出格式:logrus 内置了两种日志格式,JSONFormatter 和TextFormatter,如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式
  • Field 机制:logrus 鼓励通过 Field 机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志
  • logrus是一个可插拔的、结构化的日志框架

级别说明

logrus 的使用非常简单,与标准库 log 类似。logrus 支持更多的日志级别:

  • Panic:记录日志,然后 panic
  • Fatal:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出
  • Error:错误日志,需要查看原因
  • Warn:警告信息,提醒程序员注意
  • Info:关键操作,核心流程的日志
  • Debug:一般程序中输出的调试信息
  • Trace:很细粒度的信息,一般用不到

日志级别从上向下依次增加,Trace 最大,Panic 最小。logrus 有一个日志级别,高于这个级别的日志不会输出。默认的级别为 InfoLevel。所以为了能看到 Trace 和 Debug 日志,我们在 main 函数第一行设置日志级别为 TraceLevel

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")
}
$ go run main.go
time="2020-02-07T21:22:42+08:00" level=trace msg="trace msg"
time="2020-02-07T21:22:42+08:00" level=debug msg="debug msg"
time="2020-02-07T21:22:42+08:00" level=info msg="info msg"
time="2020-02-07T21:22:42+08:00" level=info msg="warn msg"
time="2020-02-07T21:22:42+08:00" level=error msg="error msg"
time="2020-02-07T21:22:42+08:00" level=fatal msg="fatal msg"
exit status 1

由于 logrus.Fatal 会导致程序退出,下面的 logrus.Panic 不会执行到。另外,我们观察到输出中有三个关键信息,time、level 和 msg:

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

logrus 的使用

第一个示例

最简单的使用 logrus 的示例如下

package main

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

func main() {
  log.WithFields(log.Fields{
    "animal": "walrus",
  }).Info("A walrus appears")
}

上面代码执行后,标准输出上输出如下

time="2018-08-11T15:42:22+08:00" level=info msg="A walrus appears" animal=walrus

logrus 与 golang 标准库日志模块完全兼容,因此您可以使用 log "github.com/sirupsen/logrus" 替换所有日志导入。logrus 可以通过简单的配置,来定义输出、格式或者日志级别等

package main

import (
	"os"

	log "github.com/sirupsen/logrus"
)

func init() {
	// 设置日志格式为json格式
	log.SetFormatter(&log.JSONFormatter{})

	// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
	// 日志消息输出可以是任意的io.writer类型
	log.SetOutput(os.Stdout)

	// 设置日志级别为warn以上
	log.SetLevel(log.WarnLevel)
}

func main() {
	log.WithFields(log.Fields{
		"animal": "walrus",
		"size":   10,
	}).Info("A group of walrus emerges from the ocean")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 122,
	}).Warn("The group's number increased tremendously!")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 100,
	}).Fatal("The ice breaks!")
}

Logger

logger 是一种相对高级的用法, 对于一个大型项目, 往往需要一个全局的 logrus 实例,即 logger 对象来记录项目所有的日志。如

package main

import (
	"os"

	"github.com/sirupsen/logrus"
)

// logrus提供了New()函数来创建一个logrus的实例。
// 项目中,可以创建任意数量的logrus实例。
var log = logrus.New()

func main() {
	// 为当前logrus实例设置消息的输出,同样地,
	// 可以设置logrus实例的输出到任意io.writer
	log.Out = os.Stdout

	// 为当前logrus实例设置消息输出格式为json格式。
	// 同样地,也可以单独为某个logrus实例设置日志级别和hook,这里不详细叙述。
	log.Formatter = &logrus.JSONFormatter{}

	log.WithFields(logrus.Fields{
		"animal": "walrus",
		"size":   10,
	}).Info("A group of walrus emerges from the ocean")
}

Fields

前一章提到过,logrus 不推荐使用冗长的消息来记录运行信息,它推荐使用 Fields 来进行精细化的、结构化的信息记录。例如下面的记录日志的方式

log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key)

在 logrus 中不太提倡,logrus 鼓励使用以下方式替代之

log.WithFields(log.Fields{
  "event": event,
  "topic": topic,
  "key": key,
}).Fatal("Failed to send event")

前面的 WithFields API 可以规范使用者按照其提倡的方式记录日志。但是WithFields 依然是可选的,因为某些场景下,使用者确实只需要记录仪一条简单的消息。

通常,在一个应用中、或者应用的一部分中,都有一些固定的 Field。比如在处理用户 http 请求时,上下文中,所有的日志都会有 request_id 和 user_ip。为了避免每次记录日志都要使用log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}),我们可以创建一个 logrus.Entry 实例,为这个实例设置默认Fields,在上下文中使用这个 logrus.Entry 实例记录日志即可。

requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request") # will log request_id and user_ip
requestLogger.Warn("something not great happened")

Hook

logrus 最令人心动的功能就是其可扩展的 HOOK 机制了,通过在初始化时为logrus 添加 hook,logrus 可以实现各种扩展功能

Hook 接口

logrus 的 hook接口定义如下,其原理是每此写入日志时拦截,修改 logrus.Entry

// logrus 在记录 Levels() 返回的日志级别的消息时会触发 HOOK,
// 按照 Fire 方法定义的内容修改 logrus.Entry。
type Hook interface {
	Levels() []Level
	Fire(*Entry) error
}

一个简单自定义 hook 如下,DefaultFieldHook 定义会在所有级别的日志消息中加入默认字段 appName="myAppName"

type DefaultFieldHook struct {
}

func (hook *DefaultFieldHook) Fire(entry *log.Entry) error {
    entry.Data["appName"] = "MyAppName"
    return nil
}

func (hook *DefaultFieldHook) Levels() []log.Level {
    return log.AllLevels
}

hook 的使用也很简单,在初始化前调用 log.AddHook(hook) 添加相应的 hook 即可。

logrus 官方仅仅内置了 syslog 的 hook。此外,但 Github 也有很多第三方的 hook 可供使用,文末将提供一些第三方 HOOK 的连接。

记录文件名和行号

logrus 的一个很致命的问题就是没有提供文件名和行号,这在大型项目中通过日志定位问题时有诸多不便。Github 上的 logrus 的 issue#63:Log filename and line number 创建于 2014 年,四年过去了仍是 open 状态~~~

网上给出的解决方案分位两类,一就是自己实现一个 hook;二就是通过装饰器包装 logrus.Entry。两种方案网上都有很多代码,但是大多无法正常工作。但总体来说,解决问题的思路都是对的:通过标准库的 runtime 模块获取运行时信息,并从中提取文件名,行号和调用函数名。

package main

import (
	"bytes"
	"fmt"
	"path/filepath"

	"github.com/sirupsen/logrus"
)

type MyFormatter struct{}

func (m *MyFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer != nil {
		b = entry.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	timestamp := entry.Time.Format("2006-01-02 15:04:05")
	var newLog string

	//HasCaller()为true才会有调用信息
	if entry.HasCaller() {
		fName := filepath.Base(entry.Caller.File)
		newLog = fmt.Sprintf("[%s] [%s] [%s:%d %s] %s\n",
			timestamp, entry.Level, fName, entry.Caller.Line, entry.Caller.Function, entry.Message)
	} else {
		newLog = fmt.Sprintf("[%s] [%s] %s\n", timestamp, entry.Level, entry.Message)
	}

	b.WriteString(newLog)
	return b.Bytes(), nil
}

func Demo() {
	logrus.Info("i'm demo")
}

func main() {
	logrus.SetReportCaller(true)

	logrus.SetFormatter(&MyFormatter{})

	Demo()
}

其他注意事项

Fatal 处理

和很多日志框架一样,logrus 的 Fatal 系列函数会执行 os.Exit(1)。但是logrus 提供可以注册一个或多个 fatal handler 函数的接口 logrus.RegisterExitHandler(handler func() {} ),让 logrus 在执行 os.Exit(1) 之前进行相应的处理。fatal handler 可以在系统异常时调用一些资源释放 api 等,让应用正确的关闭。

线程安全

默认情况下,logrus 的 api 都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用 hooks 或者写日志的时候,如果不需要锁,可以调用 logger.SetNoLock() 来关闭之。可以关闭 logrus 互斥锁的情形包括:

  • 没有设置 hook,或者所有的 hook 都是线程安全的实现、
  • 写日志到 logger.Out 已经是线程安全的了,如 logger.Out 已经被锁保护,或者写文件时,文件是以 O_APPEND 方式打开的,并且每次写操作都小于4k
0

评论区