1. 简要说明

zapuber 开源的 Go 高性能日志库,支持不同的日志级别, 能够打印基本信息等,但不支持日志的分割,这里我们可以使用 lumberjack 也是 zap 官方推荐用于日志分割,结合这两个库我们就可以实现以下功能的日志机制:

  • 能够将事件记录到文件中,而不是应用程序控制台;
  • 日志切割能够根据文件大小、时间或间隔等来切割日志文件;
  • 支持不同的日志级别,例如 DEBUGINFOWARNERROR 等;
  • 能够打印基本信息,如调用文件、函数名和行号,日志时间等;

官网地址:https://github.com/uber-go/zap

2. 下载安装

使用下面命令安装

go get -u go.uber.org/zap

如果下载失败,则使用以下命令重新下载安装

go get github.com/uber-go/zap

下载安装成功后还有如下提示:

package github.com/uber-go/zap: code in directory 
/home/wohu/GoCode/src/github.com/uber-go/zap expects import "go.uber.org/zap"

注意,不能通过 下面的语句导入该包,会有上面的错误提示

import (
	"github.com/uber-go/zap"
)

原因是作者开发它时的工程目录本来就是 go.uber.org/zap ,只是它的代码发布到 git 的目录是 github.com/uber-go/zap 而已。

解决方法是将 zap 目录复制到 GOPATH/src/go.uber.org 下(可能还会需要 go.uber.org/atomicgo.uber.org/multierr ,均可参考该方法 get 下来)。

go get -v github.com/uber-go/atomic
go get -v github.com/uber-go/multierr

同样将 atomicmultierr 拷贝到 go.uber.org 目录下。

3. 配置 zap Logger

zap 提供了两种类型的日志记录器—和 LoggerSugared Logger 。两者之间的区别是:

  • 在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
  • 在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快 4-10 倍,并且支持结构化和 printf 风格的日志记录。

所以一般场景下我们使用 Sugared Logger 就足够了。

3.1 Logger

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

3.1.1 NewExample

代码示例:

package main

import (
	"go.uber.org/zap"
)

func main() {
	log := zap.NewExample()
	log.Debug("this is debug message")
	log.Info("this is info message")
	log.Info("this is info message with fileds",
		zap.Int("age", 24), zap.String("agender", "man"))
	log.Warn("this is warn message")
	log.Error("this is error message")
	log.Panic("this is panic message")

}

输出结果:

{"level":"debug","msg":"this is debug message"}
{"level":"info","msg":"this is info message"}
{"level":"info","msg":"this is info message with fileds","age":24,"agender":"man"}
{"level":"warn","msg":"this is warn message"}
{"level":"error","msg":"this is error message"}
{"level":"panic","msg":"this is panic message"}
panic: this is panic message

3.1.2 NewDevelopment

代码示例:

func main() {
	log, _ := zap.NewDevelopment()
	log.Debug("this is debug message")
	log.Info("this is info message")
	log.Info("this is info message with fileds",
		zap.Int("age", 24), zap.String("agender", "man"))
	log.Warn("this is warn message")
	log.Error("this is error message") 
	// log.DPanic("This is a DPANIC message")	
	// log.Panic("this is panic message")
	// log.Fatal("This is a FATAL message")

}

输出结果:

2020-06-12T18:51:11.457+0800	DEBUG	task/main.go:9	this is debug message
2020-06-12T18:51:11.457+0800	INFO	task/main.go:10	this is info message
2020-06-12T18:51:11.457+0800	INFO	task/main.go:11	this is info message with fileds	{"age": 24, "agender": "man"}
2020-06-12T18:51:11.457+0800	WARN	task/main.go:13	this is warn message
main.main
	/home/wohu/GoCode/src/task/main.go:13
runtime.main
	/usr/local/go/src/runtime/proc.go:200
2020-06-12T18:51:11.457+0800	ERROR	task/main.go:14	this is error message
main.main
	/home/wohu/GoCode/src/task/main.go:14
runtime.main
	/usr/local/go/src/runtime/proc.go:200

3.1.3 NewProduction

代码示例:

func main() {
	log, _ := zap.NewProduction()
	log.Debug("this is debug message")
	log.Info("this is info message")
	log.Info("this is info message with fileds",
		zap.Int("age", 24), zap.String("agender", "man"))
	log.Warn("this is warn message")
	log.Error("this is error message") 
	// log.DPanic("This is a DPANIC message")	
	// log.Panic("this is panic message")
	// log.Fatal("This is a FATAL message")
}

输出结果:

{"level":"info","ts":1591959367.316352,"caller":"task/main.go:10","msg":"this is info message"}
{"level":"info","ts":1591959367.3163702,"caller":"task/main.go:11","msg":"this is info message with fileds","age":24,"agender":"man"}
{"level":"warn","ts":1591959367.3163917,"caller":"task/main.go:13","msg":"this is warn message"}
{"level":"error","ts":1591959367.3163974,"caller":"task/main.go:14","msg":"this is error message","stacktrace":"main.main\n\t/home/wohu/GoCode/src/task/main.go:14\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:200"}

3.1.4 对比总结

  • ExampleProduction使用的是 json 格式输出,Development 使用行的形式输出
  • Development
    • 从警告级别向上打印到堆栈中来跟踪
    • 始终打印包/文件/行(方法)
    • 在行尾添加任何额外字段作为 json 字符串
    • 以大写形式打印级别名称
    • 以毫秒为单位打印 ISO8601 格式的时间戳
  • Production
    • 调试级别消息不记录
    • Error , Dpanic 级别的记录,会在堆栈中跟踪文件, Warn 不会
    • 始终将调用者添加到文件中
    • 以时间戳格式打印日期
    • 以小写形式打印级别名称

在上面的代码中,我们首先创建了一个 Logger ,然后使用 Info / ErrorLogger 方法记录消息。

日志记录器方法的语法是这样的:

func (log *Logger) MethodXXX(msg string, fields ...Field)

其中 MethodXXX 是一个可变参数函数,可以是 Info / Error / Debug / Panic 等。每个方法都接受一个消息字符串和任意数量的 zapcore.Field 长参数。

每个 zapcore.Field 其实就是一组键值对参数。

3.2 Sugared Logger

默认的 zap 记录器需要结构化标签,即对每个标签,需要使用特定值类型的函数。

log.Info("this is info message with fileds",
		zap.Int("age", 24), zap.String("agender", "man"))

虽然会显的很长,但是对性能要求较高的话,这是最快的选择。也可以使用suger logger, 它基于 printf 分割的反射类型检测,提供更简单的语法来添加混合类型的标签。

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

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

	slogger.Debugf("debug message age is %d, agender is %s", 19, "man")
	slogger.Info("Info() uses sprint")
	slogger.Infof("Infof() uses %s", "sprintf")
	slogger.Infow("Infow() allows tags", "name", "Legolas", "type", 1)

}

输出结果:

2020-06-12T19:23:54.184+0800	DEBUG	task/main.go:11	debug message age is 19, agender is man
2020-06-12T19:23:54.185+0800	INFO	task/main.go:12	Info() uses sprint
2020-06-12T19:23:54.185+0800	INFO	task/main.go:13	Infof() uses sprintf
2020-06-12T19:23:54.185+0800	INFO	task/main.go:14	Infow() allows tags	{"name": "Legolas", "type": 1}

如果需要,可以随时使用记录器上的 .Desugar() 方法从 sugar logger 切换到标准记录器。

log := slogger.Desugar()

log.Info("After Desugar; INFO message")
log.Warn("After Desugar; WARN message")
log.Error("After Desugar; ERROR message")

4. 将日志写入文件

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

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

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

  • Encoder :编码器(如何写入日志)。我们将使用开箱即用的 NewConsoleEncoder() ,并使用预先设置的 ProductionEncoderConfig()
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
  • WriterSyncer :指定日志将写到哪里去。我们使用 zapcore.AddSync() 函数并且将打开的文件句柄传进去。
file, _ := os.Create("./test.log") 
writeSyncer := zapcore.AddSync(file)
  • Log Level :哪种级别的日志将被写入。

代码示例:

package main

import (
	"os"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var sugarLogger *zap.SugaredLogger

func InitLogger() {

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

	// zap.AddCaller()  添加将调用函数信息记录到日志中的功能。
	logger := zap.New(core, zap.AddCaller())
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改时间编码器

	// 在日志文件中使用大写字母记录日志级别
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	// NewConsoleEncoder 打印更符合人们观察的方式
	return zapcore.NewConsoleEncoder(encoderConfig)
}

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

func main() {
	InitLogger()
	sugarLogger.Info("this is info message")
	sugarLogger.Infof("this is %s, %d", "aaa", 1234)
	sugarLogger.Error("this is error message")
	sugarLogger.Info("this is info message")
}

输出日志文件:

2020-06-16T09:01:06.192+0800	INFO	task/main.go:40	this is info message
2020-06-16T09:01:06.192+0800	INFO	task/main.go:41	this is aaa, 1234
2020-06-16T09:01:06.192+0800	ERROR	task/main.go:42	this is error message
2020-06-16T09:01:06.192+0800	INFO	task/main.go:43	this is info message

5. 使用 lumberjack 进行日志切割归档

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

5.1 安装 lumberjack

执行下面的命令安装 lumberjack

go get -uv github.com/natefinch/lumberjack

5.2 将 lumberjack 加入 zap logger

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

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

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

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

完整代码:

package main

import (
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var sugarLogger *zap.SugaredLogger

func InitLogger() {

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

	// zap.AddCaller()  添加将调用函数信息记录到日志中的功能。
	logger := zap.New(core, zap.AddCaller())
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改时间编码器

	// 在日志文件中使用大写字母记录日志级别
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	// NewConsoleEncoder 打印更符合人们观察的方式
	return zapcore.NewConsoleEncoder(encoderConfig)
}

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

func main() {
	InitLogger()
	sugarLogger.Info("this is info message")
	sugarLogger.Infof("this is %s, %d", "aaa", 1234)
	sugarLogger.Error("this is error message")
	sugarLogger.Info("this is info message")
}

6. Log 第三方库 uber-zap 使用

package main
import (
    "time"
    "github.com/natefinch/lumberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)
var logger *zap.Logger
// logpath 日志文件路径
// loglevel 日志级别
func InitLogger(logpath string, loglevel string) {
    // 日志分割
    hook := lumberjack.Logger{
        Filename:   logpath, // 日志文件路径,默认 os.TempDir()
        MaxSize:    10,      // 每个日志文件保存10M,默认 100M
        MaxBackups: 30,      // 保留30个备份,默认不限
        MaxAge:     7,       // 保留7天,默认不限
        Compress:   true,    // 是否压缩,默认不压缩
    }
    write := zapcore.AddSync(&hook)
    // 设置日志级别
    // debug 可以打印出 info debug warn
    // info  级别可以打印 warn info
    // warn  只能打印 warn
    // debug->info->warn->error
    var level zapcore.Level
    switch loglevel {
    case "debug":
        level = zap.DebugLevel
    case "info":
        level = zap.InfoLevel
    case "error":
        level = zap.ErrorLevel
    default:
        level = zap.InfoLevel
    }
    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "time",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "linenum",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder,  // 小写编码器
        EncodeTime:     zapcore.ISO8601TimeEncoder,     // ISO8601 UTC 时间格式
        EncodeDuration: zapcore.SecondsDurationEncoder, //
        EncodeCaller:   zapcore.FullCallerEncoder,      // 全路径编码器
        EncodeName:     zapcore.FullNameEncoder,
    }
    // 设置日志级别
    atomicLevel := zap.NewAtomicLevel()
    atomicLevel.SetLevel(level)
    core := zapcore.NewCore(
        // zapcore.NewConsoleEncoder(encoderConfig),
        zapcore.NewJSONEncoder(encoderConfig),
        // zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&write)), // 打印到控制台和文件
        write,
        level,
    )
    // 开启开发模式,堆栈跟踪
    caller := zap.AddCaller()
    // 开启文件及行号
    development := zap.Development()
    // 设置初始化字段,如:添加一个服务器名称
    filed := zap.Fields(zap.String("serviceName", "serviceName"))
    // 构造日志
    logger = zap.New(core, caller, development, filed)
    logger.Info("DefaultLogger init success")
}
func main() {
    // 历史记录日志名字为:all.log,服务重新启动,日志会追加,不会删除
    InitLogger("./all.log", "debug")
    // 强结构形式
    logger.Info("test",
        zap.String("string", "string"),
        zap.Int("int", 3),
        zap.Duration("time", time.Second),
    )
    // 必须 key-value 结构形式 性能下降一点
    logger.Sugar().Infow("test-",
        "string", "string",
        "int", 1,
        "time", time.Second,
    )
}

从例子看出:

  1. 它同时提供了结构化日志记录和 printf 风格的日志记录
  2. 先初始化 lumberjack 后初始化 zap

7. 在何处打印日志

  • 在分支语句处打印日志。在分支语句处打印日志,可以判断出代码走了哪个分支,有助于判断请求的下一跳,继而继续排查问题。
  • 写操作必须打印日志。写操作最可能会引起比较严重的业务故障,写操作打印日志,可以在出问题时找到关键信息。
  • 在循环中打印日志要慎重。如果循环次数过多,会导致打印大量的日志,严重拖累代码的性能,建议的办法是在循环中记录要点,在循环外面总结打印出来。
  • 在错误产生的最原始位置打印日志。对于嵌套的 Error,可在 Error 产生的最初位置打印 Error 日志,上层如果不需要添加必要的信息,可以直接返回下层的 Error。

参考:
优秀开源日志包使用教程

https://studygolang.com/articles/19387
https://blog.csdn.net/niyuelin1990/article/details/78340336
https://blog.csdn.net/feifeixiang2835/article/details/94207810
https://blog.csdn.net/qq_27068845/article/details/103480451
https://blog.csdn.net/NUCEMLS/article/details/86534444
https://zhuanlan.zhihu.com/p/88856378
https://gitbook.cn/books/5e7637996ba17a6d2c9a3352/index.html

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐