go log 那些事儿

1,335 阅读2分钟

前言

每个编程语言都有很多日志库,因为记录日志在每个项目中都是必须的。今天我们将介绍标准日志库log、好用的logrus和 uber 开源的高性能日志库zap以及zerolog

log

log是go语言内置的日志包,实现了简单的日志打印

package main

import "log"

func main() {
	log.Print("hello world")
}

image.png log默认打印时间、内容。

log包定义了Logger类型,该类型提供了一些格式化输出的方法。

type Logger struct {
    mu     sync.Mutex // ensures atomic writes; protects the following fields
    prefix string     // prefix on each line to identify the logger (but see                            Lmsgprefix)
    flag   int        // properties
    out    io.Writer  // destination for output
    buf    []byte     // for accumulating text to write
}
  • mu属性主要是为了确保原子操作
  • prefix设置每一行的前缀
  • flag设置输出的各种属性,比如时间、行号、文件路径等
  • out输出的方向,用于把日志存储文件

log默认定义了一个全局的logger对象

var std = New(os.Stderr, "", LstdFlags) // LstdFlags     = Ldate | Ltime

配置Logger中的flag属性

默认情况下的logger只会提供日志的时间信息,但是很多情况下我们希望得到更多信息,比如记录该日志的文件名和行号等。log标准库中为我们提供了定制这些设置的方法。

const (
    Ldate    = 1 << iota     // the date in the local time zone: 2009/01/23
    Ltime                    // the time in the local time zone: 01:23:23
    Lmicroseconds           // microsecond resolution: 01:23:23.123123.  assumes Ltime.
    Llongfile               // full file name and line number: /a/b/c/d.go:23
    Lshortfile   // final file name element and line number: d.go:23. overrides Llongfile
    LUTC         // if Ldate or Ltime is set, use UTC rather than the local time zone
    Lmsgprefix  // move the "prefix" from the beginning of the line to before the message
    LstdFlags = Ldate | Ltime // initial values for the standard logger
)

log提供了SetFlags方法设置。如果全部flag都设置上,则输出结果为:

2022/04/04 10:12:23.059464 main.go:7: hello world

log还提供了SetPrefix方法设置前缀

SetOutput方法设置日志输出流,默认是控制台

完整代码如下:

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	// 设置输出标记
	log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime)

	// 设置前缀
	log.SetPrefix("[这是前缀]")

	// 设置输出
	logFile, err := os.OpenFile("./test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		fmt.Println("open log file failed, err:", err)
		return
	}
	log.SetOutput(logFile)
	log.Print("hello world")

}

最后在当前文件夹下生成了一个test.log文件,内容为:

image.png

zerolog

zerolog只专注于记录 JSON 格式的日志,号称 0 内存分配!

字段

我们可以在日志中添加额外的字段信息,有助于调试和问题追踪。与zap一样,zerolog也区分字段类型,不同的是zerolog采用链式调用的方式:

log.Debug().Str("Scale", "833 cents").Float64("Interval", 833.09).Msg("Fibonacci is everywhere")

log.Debug().Str("Name", "Tom").Send()

调用Msg()Send()之后,日志会被输出:

{"level":"debug","Scale":"833 cents","Interval":833.09,"time":"2022-04-04T18:59:37+08:00","message":"Fibonacci is everywhere"}
{"level":"debug","Name":"Tom","time":"2022-04-04T18:59:37+08:00"}

嵌套

记录的字段可以任意嵌套,这通过Dict()来实现:

log.Info().Dict("dict", zerolog.Dict().Str("bar", "baz").Int("n", 1)).Msg("hello world")
{"level":"info","dict":{"bar":"baz","n":1},"time":"2022-04-04T19:02:17+08:00","message":"hello world"}

日志级别

每个日志库都有日志级别的概念,而且划分基本上都差不多。zerologpanic/fatal/error/warn/info/debug/trace这几种级别。我们可以调用SetGlobalLevel()设置全局Logger的日志级别。

zerolog.SetGlobalLevel(zerolog.DebugLevel)

不输出级别信息

log.Log().Str("foo", "bar").Msg("")

创建logger

上面我们使用的都是全局的Logger,这种方式有一个明显的缺点:如果在某个地方修改了设置,将影响全局的日志记录。为了消除这种影响,我们需要创建新的Logger

logFile, err := os.OpenFile("./zerolog/test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
        fmt.Println("open log file failed, err:", err)
        return
}
logger := zerolog.New(logFile)
logger.Info().Str("foo", "bar").Msg("hello world")

子logger

基于当前的Logger可以创建一个子Logger,子Logger可以在父Logger上附加一些额外的字段。调用logger.With()创建一个上下文,然后为它添加字段,最后调用Logger()返回一个新的Logger

parentLogger := zerolog.New(os.Stdout)
sublogger := parentLogger.With().
        Str("foo", "bar").
        Logger()
sublogger.Info().Msg("hello sublogger")

sublogger会额外输出"foo": "bar"这个字段。

美化输出

zerolog提供了一个ConsoleWriter可输出便于我们阅读的,带颜色的日志。调用zerolog.Output()来启用ConsoleWriter

beautifulLogger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
beautifulLogger.Info().Str("foo", "bar").Msg("hello world")

image.png

还能进一步对ConsoleWriter进行配置,定制输出的级别、信息、字段名、字段值的格式:

output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}
output.FormatLevel = func(i interface{}) string {
        return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
output.FormatMessage = func(i interface{}) string {
        return fmt.Sprintf("***%s****", i)
}
output.FormatFieldName = func(i interface{}) string {
        return fmt.Sprintf("%s:", i)
}
output.FormatFieldValue = func(i interface{}) string {
        return strings.ToUpper(fmt.Sprintf("%s", i))
}

formatLogger := log.Output(output).With().Timestamp().Logger()
formatLogger.Info().Str("foo", "bar").Msg("hello format logger")

image.png

实际上就是对级别、信息、字段名和字段值设置钩子,输出前经过钩子函数转换一次

输出文件名和行号

fileNameLogger := zerolog.New(os.Stderr).With().Caller().Logger()
fileNameLogger.Info().Msg("hello filename")
{"level":"info","caller":"/Users/alex/Documents/treelab/go/src/go_log/zerolog/log.go:64","message":"hello filename"}

日志采样

有时候日志太多了反而对我们排查问题造成干扰,zerolog支持日志采样的功能,可以每隔多少条日志输出一次,其他日志丢弃:

sampled := log.Sample(&zerolog.BasicSampler{N: 10})
for i := 0; i < 20; i++ {
        sampled.Info().Msg("will be logged every 10 message")
}

结果只输出两条。

还有更高级的设置

sampled := log.Sample(&zerolog.LevelSampler{
    DebugSampler: &zerolog.BurstSampler{
      Burst:       5,
      Period:      time.Second,
      NextSampler: &zerolog.BasicSampler{N: 100},
    },
  })

sampled.Debug().Msg("hello world")

上面代码只采样Debug日志,在 1s 内最多输出 5 条日志,超过 5条 时,每隔 100 条输出一条。

钩子

type AddFieldHook struct {
}

func (AddFieldHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
	if level == zerolog.DebugLevel {
		e.Str("测试", "钩子")
	}
}

hooked := log.Hook(AddFieldHook{})
hooked.Debug().Msg("debug message")
hooked.Info().Msg("info message")
{"level":"debug","time":"2022-04-04T19:29:49+08:00","测试":"钩子","message":"debug message"}
{"level":"info","time":"2022-04-04T19:29:49+08:00","message":"info message"}

logurs

logrus完全兼容标准的log库,还支持文本、JSON 两种日志输出格式

日志级别

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

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

默认的级别为InfoLevel

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

image.png

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

输出文件名

logrus.SetReportCaller(true)

添加字段

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

logrus.WithFields(logrus.Fields{
    "name": "dj",
    "age":  18,
}).Info("add field msg")
INFO[0000] add field msg                                 age=18 name=dj

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

requestLogger := logrus.WithFields(logrus.Fields{
    "user_id": 10010,
    "ip":      "192.168.32.15",
  })

  requestLogger.Info("info msg")
  requestLogger.Error("error msg")

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

重定向输出

默认情况下,日志输出到io.Stderr。可以调用logrus.SetOutput传入一个io.Writer参数。后续调用相关方法日志将写到io.Writer中。现在,我们就能像上篇文章介绍log时一样,可以搞点事情了。传入一个io.MultiWriter,同时将日志写到bytes.Buffer、标准输出和文件中:

writer1 := &bytes.Buffer{}
writer2 := os.Stdout
writer3, err := os.OpenFile("./logrus/log.txt", os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
        log.Fatalf("create file log.txt failed: %v", err)
}

logrus.SetOutput(io.MultiWriter(writer1, writer2, writer3))
logrus.Info("output msg")
fmt.Printf("buffer:%s\n", writer1.String())

日志格式

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

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

logrus.Trace("trace msg")
logrus.Debug("debug msg")
{"level":"trace","msg":"trace msg","time":"2022-04-04T20:15:54+08:00"}
{"level":"debug","msg":"debug msg","time":"2022-04-04T20:15:54+08:00"}

设置钩子

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

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

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

例如

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
}

h := &AppHook{AppName: "awesome-web"}
logrus.AddHook(h)

logrus.Info("info msg")

只需要在Fire方法实现中,为entry.Data添加字段就会输出到日志中

输出

{"app":"awesome-web","level":"info","msg":"info msg","time":"2022-04-04T20:19:23+08:00"}

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

zap

zap是一个非常快的、结构化的,分日志级别的Go日志库

为什么选择Uber-go zap

  • 它同时提供了结构化日志记录和printf风格的日志记录
  • 它非常的快 根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好——也比标准库更快。 以下是Zap发布的基准测试信息

记录一条消息和10个字段

image.png

记录一个静态字符串,没有任何上下文或printf风格的模板:

image.png

配置Zap Logger

Zap提供了两种类型的日志记录器—Sugared LoggerLogger

在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。

在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。

Logger

  • 通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。
  • 上面的每一个函数都将创建一个logger。唯一的区别在于它将记录的信息不同。例如production logger默认记录调用函数信息、日期和时间等。
  • 通过Logger调用Info/Error等。
  • 默认情况下日志都会打印到应用程序的console界面。
var logger *zap.Logger

func InitLogger() {
	logger, _ = zap.NewProduction()
}

func simpleHttpGet(url string) {
	resp, err := http.Get(url)
	if err != nil {
		logger.Error(
			"Error fetching url..",
			zap.String("url", url),
			zap.Error(err))
	} else {
		logger.Info("Success..",
			zap.String("statusCode", resp.Status),
			zap.String("url", url))
		resp.Body.Close()
	}
}

func ZapTest() {
	InitLogger()
	defer logger.Sync()
	simpleHttpGet("www.google.com")
	simpleHttpGet("http://www.google.com")
}

输出

image.png

Sugared Logger

现在让我们使用Sugared Logger来实现相同的功能。

  • 大部分的实现基本都相同。
  • 惟一的区别是,我们通过调用主logger的. Sugar()方法来获取一个SugaredLogger
  • 然后使用SugaredLoggerprintf格式记录语句
func InitLogger() {
  logger, _ := zap.NewProduction()
    sugarLogger = logger.Sugar()
}

将日志写入文件

我们将使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。

func New(core zapcore.Core, options ...Option) *Logger

zapcore.Core需要三个配置——EncoderWriteSyncerLogLevel

  • Encoder:编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()

go zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

  • WriterSyncer :指定日志将写到哪里去。我们使用zapcore.AddSync()函数并且将打开的文件句柄传进去。

go file, _ := os.Create("./test.log") writeSyncer := zapcore.AddSync(file)

  • Log Level:哪种级别的日志将被写入。
var sugarLogger *zap.SugaredLogger

func InitSugarLogger() {
	writeSyncer := getLogWriter()
	encoder := getEncoder()
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

	logger := zap.New(core)
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

func getLogWriter() zapcore.WriteSyncer {
	file, _ := os.Create("./zap/log.log")
	return zapcore.AddSync(file)
}


func ZapTest() {
	InitSugarLogger()
	defer logger.Sync()
	simpleHttpGet("www.google.com")
	simpleHttpGet("http://www.google.com")
}
{"level":"error","ts":1649076880.9239318,"msg":"Error fetching url..{url 15 0 www.google.com <nil>} {error 26 0  Get \"www.google.com\": unsupported protocol scheme \"\"}"}
{"level":"error","ts":1649076910.9257689,"msg":"Error fetching url..{url 15 0 http://www.google.com <nil>} {error 26 0  Get \"http://www.google.com\": dial tcp 172.217.163.36:80: i/o timeout}"}

使用Lumberjack进行日志切割归档

Zap本身不支持切割归档日志文件,为了添加日志切割归档功能,我们将使用第三方库Lumberjack来实现

要在zap中加入Lumberjack支持,我们需要修改WriteSyncer代码。我们将按照下面的代码修改getLogWriter()函数:

func getLogWriter() zapcore.WriteSyncer {
    lumberJackLogger := &lumberjack.Logger{
        Filename:   "./zap/log.log",
        MaxSize:    10,
        MaxBackups: 5,
        MaxAge:     30,
        Compress:   false,
    }
    return zapcore.AddSync(lumberJackLogger)
}

Lumberjack Logger采用以下属性作为输入:

  • Filename: 日志文件的位置
  • MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
  • MaxBackups:保留旧文件的最大个数
  • MaxAges:保留旧文件的最大天数
  • Compress:是否压缩/归档旧文件

总结

性能较强的主要就是zerolog和zap,参考[6]中比较了两者的性能 github代码:go_log

参考

[1]Go 每日一库之 zerolog
[2]Go语言Log使用
[3]github.com/rs/zerolog
[4]github.com/sirupsen/lo…
[5]Go 每日一库之 logrus
[6]Compare zerolog and zap's popularity and activity