[Go zap日志简单学习 | 青训营笔记]

559 阅读8分钟

讲解开发中常用的log日志

zap简介

这是zap的整体框架图

1.1 日志介绍

项目在开发阶段,如果出现问题,一般会去查看日志,来定位问题,这是非常有效的,上线后更加需要日志

那么我们需要怎么样的日志呢?

其实跟go语言相关的日志库有很多,但是一个好的日志,往往具备以下几个条件:

能打印最基本的信息,例如调用的文件,函数名称,行号,日志时间等

支持不同的日志级别,例如: info、debug、error 等

能够将记录的日志保存在文件里面,并且可以根据时间或者文件大小来切割日志文件,而zap就完全满足了,他非常的高效,并且是结构化的,分级的go日志库。

1.2 为什么选择zap日志

两个点:

提供结构化日志记录,并且是printf风格的日志记录 记录一个静态字符串,没有任何上下文或printf风格的模板: 根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好——也比标准库更快。 以下是Zap发布的基准测试信息记录一条消息和10个字段所消耗的时间对比:

转存失败,建议直接上传图片文件

1.3 zap的安装

go get即可

$ go get -u go.uber.org/zap

1.4 创建实例-两种类型

Zap提供了两种类型的日志记录器 SugaredLogger 和 Logger 。

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

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

注意:默认情况下日志都会打印到应用程序的console界面。

1.4.1 Logger

可以通过调用zap.NewProduction() zap.NewDevelopment() 或者 zap.Example() 来创建一个Logger。

这三个方法的区别在于它将记录的信息不同,参数只能是string类型

var log *zap.Logger
log = zap.NewExample()
log, _ := zap.NewDevelopment()
log, _ := zap.NewProduction()
log.Debug("This is a DEBUG message")
log.Info("This is an INFO message")

//Example 输出
{"level":"debug","msg":"This is a DEBUG message"}
{"level":"info","msg":"This is an INFO message"}

//Development 输出
2018-10-30T17:14:22.459+0800    DEBUG    development/main.go:7    This is a DEBUG message
2018-10-30T17:14:22.459+0800    INFO    development/main.go:8    This is an INFO message

//Production 输出
{"level":"info","ts":1540891173.3190675,"caller":"production/main.go:8","msg":"This is an INFO message"}
{"level":"info","ts":1540891173.3191047,"caller":"production/main.go:9","msg":"This is an INFO message with fields","region":["us-west"],"id":2}
三种创建方式对比:

Example 和 Production 使用的是json格式输出,Development使用行的形式输出, 其中值得关注的就Production,Development创建的Logger

  • NewDevelopment 是以 空格分开 的形式展示
  • NewProduction 使用的是 json格式 ,键值对的形式 展示出来

Development

  1. 从警告级别向上打印到堆栈中来跟踪

  2. 始终打印包/文件/行(方法)

  3. 在行尾添加任何额外字段作为json字符串

  4. 以大写形式打印级别名称

  5. 以毫秒为单位打印ISO8601格式的时间戳

Production

  1. 调试级别消息不记录

  2. Error,Dpanic级别的记录,会在堆栈中跟踪文件,Warn不会

  3. 始终将调用者添加到文件中

  4. 以时间戳格式打印日期

  5. 以小写形式打印级别名称

1.4.2 SugaredLogger

它们惟一的区别是,我们可以通过调用主logger的. Sugar()方法来获取一个SugaredLogger,然后使用SugaredLogger以printf格式记录语句,例如

var sugarLogger *zap.SugaredLogger

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

func main() {
    InitLogger()
    defer sugarLogger.Sync()
    sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
}

1.5 自定义logger - zap.New输出到文件

如果不想将日志信息打印在终端,那么可以自定义配置,使用 zap.New(…) 方法来手动传递所有配置。zap.New源码

func New(core zapcore.Core, options ...Option) *Logger {
	if core == nil {
		return NewNop()
	}
	log := &Logger{
		core:        core,
		errorOutput: zapcore.Lock(os.Stderr),
		addStack:    zapcore.FatalLevel + 1,
		clock:       zapcore.DefaultClock,
	}
	return log.WithOptions(options...)
}

......

func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core {
	return &ioCore{
		LevelEnabler: enab,
		enc:          enc,
		out:          ws,
	}
}

可以看到重要的是zapcore.Core,所以我们要new一个核心

需要三个参数

  • Encoder : 编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig(),当然开箱即用的不止这一个,可以点进源码看看。
// core 三个参数之  Encoder 编码
func getEncoder() zapcore.Encoder {
    // return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()) 一个不是json格式的输出	
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
  • WriteSyncer:指定日志输出路径(文件 或 控制台 或者双向输出)。但是打开的类型不一样,文件打开的是io.writer类型,而我们需要的是WriteSyncer,所以我们使用zapcore.AddSync()函数进行转换。
// core 三个参数之  日志输出路径
func getWriterSyncer() zapcore.WriteSyncer {
	file, _ := os.Create("./server/zaplog_test/log.log")
	return zapcore.AddSync(file)
}
  • LevelEnabler:设置打印的日志等级,通过它来动态的保存日志,比如上线后我们error以下的日志就不打印了!

    我们通过 zapcore.***Level 来设置,里面都是封装好的日志等级,可以看下zapcore的源码哦

const (
	// DebugLevel logs are typically voluminous, and are usually disabled in
	// production.
	DebugLevel Level = iota - 1
	// InfoLevel is the default logging priority.
	InfoLevel
	// WarnLevel logs are more important than Info, but don't need individual
	// human review.
	WarnLevel
	// ErrorLevel logs are high-priority. If an application is running smoothly,
	// it shouldn't generate any error-level logs.
	ErrorLevel
	// DPanicLevel logs are particularly important errors. In development the
	// logger panics after writing the message.
	DPanicLevel
	// PanicLevel logs a message, then panics.
	PanicLevel
	// FatalLevel logs a message, then calls os.Exit(1).
	FatalLevel

	_minLevel = DebugLevel
	_maxLevel = FatalLevel

	// InvalidLevel is an invalid value for Level.
	//
	// Core implementations may panic if they see messages of this level.
	InvalidLevel = _maxLevel + 1
)

例子

package main

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

var logger *zap.Logger
var SugaredLogger *zap.SugaredLogger

func InitLogger() {
	encoder := getEncode()
	writerSyncer := getWriterSyncer()
	core := zapcore.NewCore(encoder, writerSyncer, zap.DebugLevel)
	logger = zap.New(core)
	SugaredLogger = logger.Sugar()
}

// core 三个参数之  Encoder 编码
func getEncoder() zapcore.Encoder {
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

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

1.6 日志同时输出到控制台和文件

如果需要同时输出控制台和文件,只需要改造一下zapcore.NewCore即可

本质其实就是修改一下 WriteSyncer ,使用zapcore.NewMultiWriteSyncer来设置多个输出对象,如下示例,依然使用上面1.5的代码.

package main

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

var logger *zap.Logger
var SugaredLogger *zap.SugaredLogger

func InitLogger() {
	encoder := getEncoder()
	writerSyncer := getWriterSyncer()
	//multiWriteSyncer := zapcore.NewMultiWriteSyncer(writerSyncer, zapcore.AddSync(os.Stdout)) //AddSync将io.Writer转换成WriteSyncer的类型
	core := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(writerSyncer, zapcore.AddSync(os.Stdout)), zap.DebugLevel)
	logger = zap.New(core, zap.AddCaller()) //zap.AddCaller() 显示文件名 和 行号
	SugaredLogger = logger.Sugar()
}

// core 三个参数之  Encoder 编码
func getEncoder() zapcore.Encoder {
	//zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

// core 三个参数之  日志输出路径
func getWriterSyncer() zapcore.WriteSyncer {
	file, _ := os.Create("./server/zaplog_test/log.log")
	//或者将上面的NewMultiWriteSyncer放到这里来,进行返回
	return zapcore.AddSync(file)
}

func main() {
	InitLogger()
	defer logger.Sync()

	logger.Info("Starting zaplog_testing...",
		zap.String("key_test", "test key_value"))
}

1.7 如果我们不想使用预先设置好的的Encoder

我们就需要自己写一个,我们可以先观察一下预先设置的Encoder底层是什么东西

// NewProductionEncoderConfig returns an opinionated EncoderConfig for
// production environments.
func NewProductionEncoderConfig() zapcore.EncoderConfig {
	return zapcore.EncoderConfig{
		TimeKey:        "ts",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		FunctionKey:    zapcore.OmitKey,
		MessageKey:     "msg",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.LowercaseLevelEncoder,
		EncodeTime:     zapcore.EpochTimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	}
}

可以看到他返回的是一个结构体,那么我们直接自己配置一个结构体,传入我们的配置即可

config := zapcore.EncoderConfig{
		MessageKey:   "msg",                       //结构化(json)输出:msg的key
		LevelKey:     "level",                     //结构化(json)输出:日志级别的key(INFO,WARN,ERROR等)
		TimeKey:      "ts",                        //结构化(json)输出:时间的key(INFO,WARN,ERROR等)
		CallerKey:    "file",                      //结构化(json)输出:打印日志的文件对应的Key
		EncodeLevel:  zapcore.CapitalLevelEncoder, //将日志级别转换成大写(INFO,WARN,ERROR等)
		EncodeCaller: zapcore.ShortCallerEncoder,  //采用短文件路径编码输出(test/main.go:14 )
		EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
			enc.AppendString(t.Format("2006-01-02 15:04:05"))
		}, //输出的时间格式
		EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
			enc.AppendInt64(int64(d) / 1000000)
		}, //
	}

......

zapcore.NewCore(zapcore.NewJSONEncoder(config), zapcore.AddSync(infoWriter), infoLevel)

1.8 文件切割

当我们的日志文件十分大之后,操作就会很麻烦,我们可以用日志切割,但是很遗憾zap没有提供这个功能

我们可以使用第三方库Lumberjack

Lumberjack的安装

下载:

go get -u github.com/natefinch/lumberjack

使用

我们使用也十分的简单,只需要对我们的getWriterSyncer()函数进行修改就行

func getWriterSyncer(filename string) zapcore.WriteSyncer {
	lumberWriteSyncer := &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    20,    //最大M数,超过则切割,log文件最大大小
		MaxBackups: 5,     //最大文件保留数,超过就删除最老的日志文件,备份数量
		MaxAge:     30,    //保存30天,备份天数
		Compress:   false, //是否压缩
	}
    
    return zapcore.AddSync(lumberWriteSyncer)
}

1.9 按照日志的级别写入

为了管理人员的查询方便,一般我们需要将低于info级别的放到info.log,warnning及以上严重级别日志存放到error.log文件中,我们只需要改造一下zapcore.NewCore方法的第3个参数,然后将文件WriteSyncer拆成infoerror两个即可,示例:

package main

import (
	"io"
	"os"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

func main() {
    zap.InitLog("./info.log", "./error.log", zapcore.DebugLevel)
}

// Logger 只能输出结构化日志,但是性能要高于 SugaredLogger
var Logger *zap.Logger

// SugarLogger 可以输出 结构化日志、非结构化日志。性能差于 zap.Logger,具体可见上面的的单元测试
var SugarLogger *zap.SugaredLogger

func InitLog(logPath, errPath string, logLevel zapcore.Level) {
	config := zapcore.EncoderConfig{
		MessageKey:   "msg",                       //结构化(json)输出:msg的key
		LevelKey:     "level",                     //结构化(json)输出:日志级别的key(INFO,WARN,ERROR等)
		TimeKey:      "ts",                        //结构化(json)输出:时间的key(INFO,WARN,ERROR等)
		CallerKey:    "file",                      //结构化(json)输出:打印日志的文件对应的Key
		EncodeLevel:  zapcore.CapitalLevelEncoder, //将日志级别转换成大写(INFO,WARN,ERROR等)
		EncodeCaller: zapcore.ShortCallerEncoder,  //采用短文件路径编码输出(test/main.go:14 )
		EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
			enc.AppendString(t.Format("2006-01-02 15:04:05"))
		}, //输出的时间格式
		//EncodeTime: zapcore.ISO8601TimeEncoder,
		EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
			enc.AppendInt64(int64(d) / 1000000)
		}, //
	}

	//自定义日志级别:自定义debug级别
	debugLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl == logLevel
	})

	//自定义日志级别:自定义Info级别
	infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl < zapcore.WarnLevel && lvl > logLevel
	})

	//自定义日志级别:自定义Warn级别
	warnLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl >= zapcore.WarnLevel
	})

	infoWriter := getWriter(logPath)
	warnWriter := getWriter(errPath)

	core := zapcore.NewTee(
		zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(infoWriter), infoLevel), //将info及以下写入logPath,NewConsoleEncoder 是非结构化输出
		zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(warnWriter), warnLevel), //warn及以上写入errPath
		zapcore.NewCore(zapcore.NewJSONEncoder(config), zapcore.AddSync(os.Stdout), debugLevel),    //把debug级别的日志写入控制台
	)
	Logger = zap.New(core, zap.AddCaller())

	SugarLogger = Logger.Sugar()
	//zap.ReplaceGlobals(Logger)
}

func getWriter(filename string) io.Writer {
	return &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    20,    //最大M数,超过则切割,log文件最大大小
		MaxBackups: 5,     //最大文件保留数,超过就删除最老的日志文件,备份数量
		MaxAge:     30,    //保存30天,备份天数
		Compress:   false, //是否压缩
	}
}