前言
每个编程语言都有很多日志库,因为记录日志在每个项目中都是必须的。今天我们将介绍标准日志库log、好用的logrus和 uber 开源的高性能日志库zap以及zerolog
log
log是go语言内置的日志包,实现了简单的日志打印
package main
import "log"
func main() {
log.Print("hello world")
}
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文件,内容为:
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"}
日志级别
每个日志库都有日志级别的概念,而且划分基本上都差不多。zerolog有panic/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")
还能进一步对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")
实际上就是对级别、信息、字段名和字段值设置钩子,输出前经过钩子函数转换一次
输出文件名和行号
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")
由于logrus.Fatal会导致程序退出,下面的logrus.Panic不会执行到。
输出文件名
logrus.SetReportCaller(true)
添加字段
有时候需要在输出中添加一些字段,可以通过调用logrus.WithField和logrus.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_id和ip字段
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个字段
记录一个静态字符串,没有任何上下文或printf风格的模板:
配置Zap Logger
Zap提供了两种类型的日志记录器—Sugared Logger和Logger。
在性能很好但不是很关键的上下文中,使用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")
}
输出
Sugared Logger
现在让我们使用Sugared Logger来实现相同的功能。
- 大部分的实现基本都相同。
- 惟一的区别是,我们通过调用主logger的
. Sugar()方法来获取一个SugaredLogger。 - 然后使用
SugaredLogger以printf格式记录语句
func InitLogger() {
logger, _ := zap.NewProduction()
sugarLogger = logger.Sugar()
}
将日志写入文件
我们将使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。
func New(core zapcore.Core, options ...Option) *Logger
zapcore.Core需要三个配置——Encoder,WriteSyncer,LogLevel。
- 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