golang使用Zap日志库

1. 简要说明

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

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

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

2. 下载安装

使用下面命令安装

go get github.com/uber-go/zap

3.配置 zap Logger

zap提供了两种类型的日志记录器Logger Sugared Logger

区别是:

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

3.1 Logger

  • 通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.NewExample()创建一个Logger。
  • 上面的每一个函数都将创建一个Logger。唯一的区别在于它将记录的信息不同。例如:production logger默认记录调用函数信息、日期和时间等。
  • 通过Logger 调用INFOERROR

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 fields",
  zap.Int("age",24),zap.String("agender","man"))

虽然会先的很长,但是对性能要求较高的话,这是最快的选择。也可以使用Sugared 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需要三个配置—Encoder,WriteSyncer,LogLevel。

  • Encoder: 编码器(如何写入日志)。我们将使用开箱即用的NewConsoleEncoder(),并使用预先设置的ProductionEncoderConfig()
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
  • writeSyncer: 指定日志将写到那里去。我们使用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

go get 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
发布于 2021-05-11 22:53