讲解开发中常用的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
-
从警告级别向上打印到堆栈中来跟踪
-
始终打印包/文件/行(方法)
-
在行尾添加任何额外字段作为json字符串
-
以大写形式打印级别名称
-
以毫秒为单位打印ISO8601格式的时间戳
Production
-
调试级别消息不记录
-
Error,Dpanic级别的记录,会在堆栈中跟踪文件,Warn不会
-
始终将调用者添加到文件中
-
以时间戳格式打印日期
-
以小写形式打印级别名称
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拆成info和error两个即可,示例:
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, //是否压缩
}
}