从源码学习 Go 标准库(二):log(2)

173 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情

前言

本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。

上一篇文章中,我们分析了 go 的内置日志库 log,它的代码较少,提供的功能也比较基础。所以,本篇文章,我们来看一看 go 的第三方日志库 logrus。

备注:本系列文章使用的是 go 1.19 源码:

github.com/golang/go/t…

log

github.com/golang/go/b…

Output

上一篇对 log 的分析其实还剩一点小尾巴,就是 Output 这个函数。

Output 函数可以传入 calldepth 直接控制计算文件名和行号时要跳过的栈帧(传给 runtime.Caller),其他的函数也都是通过此函数来输出日志的。

需要注意,直接调用函数 Output,它的 calldepth 是对于 Output 函数来计算的,在传向 runtime.Callerskip 参数时会加一。

log.SetFlags(log.Lshortfile)
log.Output(1, "这是一条日志") // test.go:8: 这是一条日志

calldepth  filename:line-number
-2         extern.go:219
-1         log.go:185(runtime.Caller的位置)
 0         log.go:413(Output的位置)
 1         test.go:8(log.Output的位置)
 2         proc.go:250
 3         asm_amd64.s:1594

不足

log 包的框架比较简单,只提供了 Print Fatal Panic 三类函数,缺少开发过程中常用的几种日志级别以及结构化输出日志的功能。

logrus

github.com/Sirupsen/lo…

logrus 是 go 的结构化日志记录器,与标准库的 logger 完全兼容。目前,logrus 已处于维护模式,不再引入新功能。

logrus 提供 7 种日志级别:

const (
	PanicLevel Level = iota
	FatalLevel
	ErrorLevel // 通常用于钩子,向错误处理服务发送错误
	WarnLevel
	InfoLevel
	DebugLevel // 一般只在调试时启用
	TraceLevel // 更细粒度的信息
)

logrus 默认创建一个标准记录器 std,最外层的方法会操作这个默认记录器。除了支持 log 中的三类函数外,logrus 还可以使用以 Fn 结尾的方法来传入函数。对于大量的消息,先判断日志级别是否启用,再生成日志信息,是更优的顺序。

由于 logrus 完全兼容 log,我们可以直接在导入时将它命名为 log,以兼容标准库。此外,我们可以在 init 函数中,对默认记录器进行设置:

import log "github.com/sirupsen/logrus"
func init() {
  log.SetFormatter(&log.JSONFormatter{})
  log.SetOutput(os.Stdout)
  log.SetLevel(log.WarnLevel)
}

格式化

logrus 内置两种格式化器 TextFormatterJsonFormatter,其中前者在标准输出是Linux终端时(TTY)会给日志加上颜色。

log.SetFormatter(&log.TextFormatter{ForceColors: true})  // 也可以强制输出颜色
log.Warn("这是一条日志")

默认情况下会输出如 time="2022-08-28T23:56:46+08:00" level=info msg="这是一条日志" 这样格式的日志。

如果想要添加文件名和方法,可以使用 log.SetReportCaller(true),会增加 funcfile 字段。该功能会明显增加开销,其取决于 go 的版本。

添加字段

可以直接调用 WithFields 方法来添加多条字段信息

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

WithFields 方法会返回一个存储了记录器和字段信息的 Entry,我们也可以将它保存下来并复用

logger := log.WithFields(log.Fields{
    "common": "common msg",
})
logger.Info("1")
logger.Info("2")

Logger

除了使用默认的记录器以外,也可以自己来创建任意数量的记录器。

package main
import (
  "os"
  "github.com/sirupsen/logrus"
)
var log = logrus.New()
func main() {
  log.Out = os.Stdout
  log.WithFields(logrus.Fields{
    "animal": "walrus",
    "size":   10,
  }).Info("A group of walrus emerges from the ocean")
}

需要注意的是,自己创建的记录器的配置方法与默认记录器的配置方法是不同的。

Hook

logrus 还可以为记录器设置钩子,只需要实现下面两个方法:

  • Levels() []logrus.Level:日志级别

  • Fire(entry *logrus.Entry) error:日志输出前会调用此函数,可以更改 Entry 记录的信息

import (
	"github.com/sirupsen/logrus"
)
type hook struct {
	msg string
}
func (h *hook) Levels() []logrus.Level {
	return logrus.AllLevels
}
func (h *hook) Fire(entry *logrus.Entry) error {
	entry.Data["hookMsg"] = h.msg
	return nil
}
func main() {
	h := &hook{"a hook msg"}
	logrus.AddHook(h)
	logrus.Info("info")
}

总结

本篇文章,我们完成了对 log 库的分析,并介绍了完全兼容 log 的第三方库 logrus 的特性以及基本使用方法。

最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿