当前位置: 代码网 > it编程>前端脚本>Golang > Golang日志操作库zap的使用详解

Golang日志操作库zap的使用详解

2024年05月18日 Golang 我要评论
一、简介zap是uber开源的一个高性能,结构化,分级记录的日志记录包。go1.20.2zap v1.24.0zap的特性高性能:zap 对日志输出进行了多项优化以提高它的性能日志分级:有 debug

一、简介

zap 是 uber 开源的一个高性能,结构化,分级记录的日志记录包。

go1.20.2

zap v1.24.0

zap的特性

  • 高性能:zap 对日志输出进行了多项优化以提高它的性能
  • 日志分级:有 debug,info,warn,error,dpanic,panic,fatal 等
  • 日志记录结构化:日志内容记录是结构化的,比如 json 格式输出
  • 自定义格式:用户可以自定义输出的日志格式
  • 自定义公共字段:用户可以自定义公共字段,大家输出的日志内容就共同拥有了这些字段
  • 调试:可以打印文件名、函数名、行号、日志时间等,便于调试程序
  • 自定义调用栈级别:可以根据日志级别输出它的调用栈信息
  • namespace:日志命名空间。定义命名空间后,所有日志内容就在这个命名空间下。命名空间相当于一个文件夹
  • 支持 hook 操作

高性能介绍

与其它日志库对比

github官网的对比图,下面的对比图来自:https://github.com/uber-go/zap#performance

log a message and 10 fields:

packagetimetime % to zapobjects allocated
⚡ zap2900 ns/op+0%5 allocs/op
⚡ zap (sugared)3475 ns/op+20%10 allocs/op
zerolog10639 ns/op+267%32 allocs/op
go-kit14434 ns/op+398%59 allocs/op
logrus17104 ns/op+490%81 allocs/op
apex/log32424 ns/op+1018%66 allocs/op
log1533579 ns/op+1058%76 allocs/op

log a message with a logger that already has 10 fields of context:

packagetimetime % to zapobjects allocated
⚡ zap373 ns/op+0%0 allocs/op
⚡ zap (sugared)452 ns/op+21%1 allocs/op
zerolog288 ns/op-23%0 allocs/op
go-kit11785 ns/op+3060%58 allocs/op
logrus19629 ns/op+5162%70 allocs/op
log1521866 ns/op+5762%72 allocs/op
apex/log30890 ns/op+8182%55 allocs/op

log a static string, without any context or printf-style templating:

packagetimetime % to zapobjects allocated
⚡ zap381 ns/op+0%0 allocs/op
⚡ zap (sugared)410 ns/op+8%1 allocs/op
zerolog369 ns/op-3%0 allocs/op
standard library385 ns/op+1%2 allocs/op
go-kit606 ns/op+59%11 allocs/op
logrus1730 ns/op+354%25 allocs/op
apex/log1998 ns/op+424%7 allocs/op
log154546 ns/op+1093%22 allocs/op

做了哪些优化

基于反射的序列化和字符串格式化,它们都是 cpu 密集型计算且分配很多小的内存。具体到 go 语言中,使用 encoding/json 和 fmt.fprintf 格式化 interface{} 会使程序性能降低。

zap 咋解决呢?zap 使用一个无反射、零分配的 josn 编码器,基础 logger 尽可能避免序列化开销和内存分配开销。在此基础上,zap 还构建了更高级的 suggaredlogger。

二、quickstart快速开始

zap 安装:

go get -u go.uber.org/zap

zap 提供了 2 种日志记录器:sugaredlogger 和 logger

在需要性能但不是很重要的情况下,使用 sugaredlogger 较合适。它比其它结构化日志包快 4-10 倍,包括 结构化日志和 printf 风格的 api。看下面使用 sugaredlogger 例子:

logger, _ := zap.newproduction()
defer logger.sync() // zap底层有缓冲。在任何情况下执行 defer logger.sync() 是一个很好的习惯
sugar := logger.sugar()
sugar.infow("failed to fetch url",
 // 字段是松散类型,不是强类型
  "url", url,
  "attempt", 3,
  "backoff", time.second,
)
sugar.infof("failed to fetch url: %s", url)

当性能和类型安全很重要时,请使用 logger。它比 sugaredlogger 更快,分配的资源更少,但它只支持结构化日志和强类型字段。

logger, _ := zap.newproduction()
defer logger.sync()
logger.info("failed to fetch url",
  // 字段是强类型,不是松散类型
  zap.string("url", url),
  zap.int("attempt", 3),
  zap.duration("backoff", time.second),
)

三、newexample/newdevelopment/newproduction使用

zap 为我们提供了三种快速创建 logger 的方法: zap.newproduction()zap.newdevelopment()zap.newexample()

见名思义,example 一般用在测试代码中,development 用在开发环境中,production 用在生成环境中。这三种方法都预先设置好了配置信息。

newexample()使用

newexample 构建一个 logger,专门为在 zap 的测试示例使用。它将 debuglevel 及以上日志用 json 格式标准输出,但它省略了时间戳和调用函数,以保持示例输出的简短和确定性。

为什么说 zap.newexample() 是 zap 为我们提供快速创建 logger 的方法呢?

因为在这个方法里,zap 已经定义好了日志配置项部分默认值。来看它的代码:

// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#l127
func newexample(options ...option) *logger {
	encodercfg := zapcore.encoderconfig{
        messagekey:     "msg",  // 日志内容key:val, 前面的key设为msg
		levelkey:       "level", // 日志级别的key设为level
		namekey:        "logger", // 日志名
		encodelevel:    zapcore.lowercaselevelencoder, //日志级别,默认小写
		encodetime:     zapcore.iso8601timeencoder, // 日志时间
		encodeduration: zapcore.stringdurationencoder,
	}
	core := zapcore.newcore(zapcore.newjsonencoder(encodercfg), os.stdout, debuglevel)
	return new(core).withoptions(options...)
}

使用例子:

package main

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

func main() {
	logger := zap.newexample()
	logger.debug("this is debug message")
	logger.info("this is info message")
	logger.info("this is info message with fileds",
		zap.int("age", 37), 
        zap.string("agender", "man"),
    )
	logger.warn("this is warn message")
	logger.error("this is error 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":37,"agender":"man"} {"level":"warn","msg":"this is warn message"} {"level":"error","msg":"this is error message"}

newdevelopment()使用

newdevelopment() 构建一个开发使用的 logger,它以人性化的格式将 debuglevel 及以上日志信息输出。它的底层使用

newdevelopmentconfig().build(...option) 构建。它的日志格式各种设置在函数 newdevelopmentencoderconfig() 里,想查看详情设置,请点进去查看。

使用例子:

package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.newdevelopment()
	defer logger.sync()

	logger.info("failed to fetch url",
		// 强类型字段
		zap.string("url", "http://example.com"),
		zap.int("attempt", 3),
		zap.duration("duration", time.second),
	)

	logger.with(
		// 强类型字段
		zap.string("url", "http://development.com"),
		zap.int("attempt", 4),
		zap.duration("duration", time.second*5),
	).info("[with] failed to fetch url")
}

输出:

2023-03-22t16:02:45.760+0800 info zapdemos/newdevelopment1.go:13 failed to fetch url {"url": "http://example.com", "attempt": 3, "duration": "1s"} 2023-03-22t16:02:45.786+0800 info zapdemos/newdevelopment1.go:25 [with] failed to fetch url {"url": "http://development.com", "attempt": 4, "duration": "5s"}

上面日志输出了文件名和行号,newexample() 没有

newproduction()使用

newproduction() 构建了一个合理的 prouction 日志记录器,它将 info 及以上的日志内容以 json 格式记写入标准错误里。

它的底层使用 newproductionconfig().build(...option) 构建。它的日志格式设置在函数 newproductionencoderconfig 里。

使用例子

package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.newproduction()
	defer logger.sync()

	url := "http://zap.uber.io"
	sugar := logger.sugar()
	sugar.infow("failed to fetch url",
		"url", url,
		"attempt", 3,
		"time", time.second,
	)

	sugar.infof("failed to fetch url: %s", url)

	// 或更简洁 sugar() 使用
	// sugar := zap.newproduction().sugar()
	// defer sugar.sync()
}

输出:

{"level":"info","ts":1679472893.2944522,"caller":"zapdemos/newproduction1.go:16","msg":"failed to fetch url","url":"http://zap.uber.io","attempt":3,"time":1} {"level":"info","ts":1679472893.294975,"caller":"zapdemos/newproduction1.go:22","msg":"failed to fetch url: http://zap.uber.io"}

上面日志输出了文件名和行号,newexample() 没有

使用配置

在这 3 个函数中,可以传入一些配置项。为什么能传入配置项?我们来看看 newexample() 函数定义:

func newexample(options ...option) *logger

它的函数传参有一个 ...option 选项,是一个 interface 类型,它关联的是 logger struct。只要返回 option 就可以传进 newexample() 里。在 zap/options.go 文件中可以看到很多返回 option 的函数,也就是说这些函数都可以传入 newexample 函数里。这里用到了 go 里面的一个编码技巧,函数选项模式。

zap.fields() 添加字段到 logger 中:

package main

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

func main() {
	logger, _ := zap.newproduction(zap.fields(
		zap.string("log_name", "testlog"),
		zap.string("log_author", "prometheus"),
	))
	defer logger.sync()

	logger.info("test fields output")

	logger.warn("warn info")
}

输出:

{"level":"info","ts":1679477929.842166,"caller":"zapdemos/fields.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"} {"level":"warn","ts":1679477929.842166,"caller":"zapdemos/fields.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}

zap.hook() 添加回调函数:

hook (钩子函数)回调函数为用户提供一种简单方法,在每次日志内容记录后运行这个回调函数,执行用户需要的操作。也就是说记录完日志后你还想做其它事情就可以调用这个函数。

package main

import (
	"fmt"

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

func main() {
	logger := zap.newexample(zap.hooks(func(entry zapcore.entry) error {
		fmt.println("[zap.hooks]test hooks")
		return nil
	}))
	defer logger.sync()

	logger.info("test output")

	logger.warn("warn info")
}

输出:

{"level":"info","msg":"test output"} [zap.hooks]test hooks {"level":"warn","msg":"warn info"} [zap.hooks]test hooks

四、logger和sugaredlogger区别

从上面例子中看出,zap 有 2 种格式化日志方式:logger 和 sugared logger。

sugared logger:

  • 它有很好的性能,比一般日志包快 4-10 倍。
  • 支持结构化的日志。
  • 支持 printf 风格的日志。
  • 日志字段不需要定义类型

logger(没有sugar)

  • 它的性能比 sugared logger 还要快
  • 它只支持强类型的结构化日志。
  • 它应用在对性能更加敏感日志记录中,它的内存分配次数更少。
  • 比如如果每一次内存分配都很重要的话可以使用这个。对类型安全有严格要求也可以使用这个。

logger 和 sugaredlogger 相互转换:

// 创建 logger
logger := zap.newexample()
defer logger.sync()

// 转换 sugaredlogger
sugar := logger.sugar()

// 转换 logger
plain := sugar.desugar()

怎么快速构建一个 logger 呢?有下面种几种方法:

  • zap.newproduction()
  • zap.newdevelopment()
  • zap.example()

主要区别:

  • 记录日志信息和结构不同。
  • example 和 production 是 json 格式输出,development 是普通一行格式输出,如果后面带有字段输出话用json格式。

相同点:

  • 默认情况下都会打印日志信息到 console 界面
  • 都是通过 logger 调用 info、error 等方法

怎么选择:

  • 需要不错的性能但不是很重要的情况下,可以选择 sugaredlogger。它支持结构化日志和 printf 风格的日志记录。sugaredlogger 的日志记录是松散类型的,不是强类型,能接受可变数量的键值对。如果你要用强类型字段记录,可以使用 sugaredlogger.with 方法。
  • 如果是每次或每微秒记录日志都很重要情况下,可以使用 logger,它比 sugaredlogger 每次分配内存更少,性能更高。但它仅支持强类型的结构化日志记录。

五、自定义配置

快速构建 logger 日志记录器最简单的方法就是用 zap 预定义了配置的方法:newexample(), newproduction() 和newdevelopment(),这 3 个方法通过单个函数调用就可以构建一个日志计记录器,也可以简单配置。

但是有的项目需要更多的定制,怎么办?zap 的 config 结构和 zapcore 的 encoderconfig 结构可以帮助你,让你能够进行自定义配置。

配置结构说明

config 配置项源码:

// zap v1.24.0
type config struct {
    // 动态改变日志级别,在运行时你可以安全改变日志级别
	level atomiclevel `json:"level" yaml:"level"`
    // 将日志记录器设置为开发模式,在 warnlevel 及以上级别日志会包含堆栈跟踪信息
	development bool `json:"development" yaml:"development"`
    // 在日志中停止调用函数所在文件名、行数
	disablecaller bool `json:"disablecaller" yaml:"disablecaller"`
    // 完全禁止自动堆栈跟踪。默认情况下,在 development 中,warnlevel及以上日志级别会自动捕获堆栈跟踪信息
    // 在 production 中,errorlevel 及以上也会自动捕获堆栈信息
	disablestacktrace bool `json:"disablestacktrace" yaml:"disablestacktrace"`
    // 设置采样策略。没有 samplingconfing 将禁止采样
	sampling *samplingconfig `json:"sampling" yaml:"sampling"`
    // 设置日志编码。可以设置为 console 和 json。也可以通过 registerencoder 设置第三方编码格式
	encoding string `json:"encoding" yaml:"encoding"`
    // 为encoder编码器设置选项。详细设置信息在 zapcore.zapcore.encoderconfig
	encoderconfig zapcore.encoderconfig `json:"encoderconfig" yaml:"encoderconfig"`
    // 日志输出地址可以是一个 urls 地址或文件路径,可以设置多个
	outputpaths []string `json:"outputpaths" yaml:"outputpaths"`
    // 错误日志输出地址。默认输出标准错误信息
	erroroutputpaths []string `json:"erroroutputpaths" yaml:"erroroutputpaths"`
    // 可以添加自定义的字段信息到 root logger 中。也就是每条日志都会携带这些字段信息,公共字段
	initialfields map[string]interface{} `json:"initialfields" yaml:"initialfields"`
}

encoderconfig 结构源码,它里面也有很多配置选项,具体请看 这里:

// zapcore@v1.24.0
type encoderconfig struct {
    // 为log entry设置key。如果 key 为空,那么在日志中的这部分信息也会省略
	messagekey     string `json:"messagekey" yaml:"messagekey"`//日志信息的健名,默认为msg
	levelkey       string `json:"levelkey" yaml:"levelkey"`//日志级别的健名,默认为level
	timekey        string `json:"timekey" yaml:"timekey"`//记录日志时间的健名,默认为time
	namekey        string `json:"namekey" yaml:"namekey"`
	callerkey      string `json:"callerkey" yaml:"callerkey"`
	functionkey    string `json:"functionkey" yaml:"functionkey"`
	stacktracekey  string `json:"stacktracekey" yaml:"stacktracekey"`
	skiplineending bool   `json:"skiplineending" yaml:"skiplineending"`
	lineending     string `json:"lineending" yaml:"lineending"`
    // 日志编码的一些设置项
	encodelevel    levelencoder    `json:"levelencoder" yaml:"levelencoder"`
	encodetime     timeencoder     `json:"timeencoder" yaml:"timeencoder"`
	encodeduration durationencoder `json:"durationencoder" yaml:"durationencoder"`
	encodecaller   callerencoder   `json:"callerencoder" yaml:"callerencoder"`
    // 与其它编码器不同, 这个编码器可选
	encodename nameencoder `json:"nameencoder" yaml:"nameencoder"`
    // 配置 interface{} 类型编码器。如果没设置,将用 json.encoder 进行编码
	newreflectedencoder func(io.writer) reflectedencoder `json:"-" yaml:"-"`
    // 配置 console 中字段分隔符。默认使用 tab 
	consoleseparator string `json:"consoleseparator" yaml:"consoleseparator"`
}
type entry struct {
	level      level
	time       time.time
	loggername string
	message    string
	caller     entrycaller
	stack      string
}

例子1:基本配置

zap.config 自定义配置,看官方的一个基本例子:

package main

import (
	"encoding/json"

	"go.uber.org/zap"
)

// https://pkg.go.dev/go.uber.org/zap@v1.24.0#hdr-configuring_zap
func main() {
	// 表示 zap.config 的 json 原始编码
	// outputpath: 设置日志输出路径,日志内容输出到标准输出和文件 logs.log
	// erroroutputpaths:设置错误日志输出路径
	rawjson := []byte(`{
      "level": "debug",
      "encoding": "json",
      "outputpaths": ["stdout", "./logs.log"],
      "erroroutputpaths": ["stderr"],
      "initialfields": {"foo": "bar"},
      "encoderconfig": {
        "messagekey": "message-customer",
        "levelkey": "level",
        "levelencoder": "lowercase"
      }
    }`)

	// 把 json 格式数据解析到 zap.config struct
	var cfg zap.config
	if err := json.unmarshal(rawjson, &cfg); err != nil {
		panic(err)
	}
	// cfg.build() 为配置对象创建一个 logger
	// zap.must() 封装了 logger,must()函数如果返回值不是 nil,就会报 panic。也就是检查build是否错误
	logger := zap.must(cfg.build())
	defer logger.sync()

	logger.info("logger construction succeeded")
}

/*
must() 函数
//  var logger = zap.must(zap.newproduction())
func must(logger *logger, err error) *logger {
    if err != nil {
        panic(err)
    }

    return logger
}
*/

consol 输出如下:

{"level":"info","message-customer":"logger construction succeeded","foo":"bar"}

并且在程序目录下生成了一个文件 logs.log,里面记录的日志内容也是上面consol输出内容。每运行一次就在日志文件末尾append一次内容。

例子2:高级配置

上面的配置只是基本的自定义配置,如果有一些复杂的需求,比如在多个文件之间分割日志。

或者输出到不是 file 的文件中,比如输出到 kafka 中,那么就需要使用 zapcore 包。

在下面的例子中,我们将把日志输出到 kafka 中,并且也输出到 console 里。并且我们对 kafka 不同主题进行编码设置,对输出到 console 编码进行设置,也希望处理高优先级的日志。

官方例子:

package main

import (
	"io"
	"os"

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

func main() {
	// 首先,定义不同级别日志处理逻辑
	highpriority := zap.levelenablerfunc(func(lvl zapcore.level) bool {
		return lvl >= zapcore.errorlevel
	})
	lowpriority := zap.levelenablerfunc(func(lvl zapcore.level) bool {
		return lvl < zapcore.errorlevel
	})

	// 假设有2个kafka 的 topic,一个 debugging,一个 errors

	// zapcore.addsync 添加一个文件句柄。
	topicdebugging := zapcore.addsync(io.discard)
	topicerrors := zapcore.addsync(io.discard)

	// 如果他们对并发使用不安全,我们可以用 zapcore.lock 添加一个 mutex 互斥锁。
	consoledebugging := zapcore.lock(os.stdout)
	consoleerrors := zapcore.lock(os.stderr)

	// 设置 kafka 和 console 输出配置
	kafkaencoder := zapcore.newjsonencoder(zap.newproductionencoderconfig())
	consoleencoder := zapcore.newconsoleencoder(zap.newdevelopmentencoderconfig())

	// 把上面的设置加入到 zapcore.newcore() 函数里,然后再把他们加入到 zapcore.newtee() 函数里
	core := zapcore.newtee(
		zapcore.newcore(kafkaencoder, topicerrors, highpriority),
		zapcore.newcore(consoleencoder, consoleerrors, highpriority),
		zapcore.newcore(kafkaencoder, topicdebugging, lowpriority),
		zapcore.newcore(consoleencoder, consoledebugging, lowpriority),
	)

	// 最后调用 zap.new() 函数
	logger := zap.new(core)
	defer logger.sync()
	logger.info("constructed a logger")
}

例子3:日志写入文件

与上面例子2相似,但是比它简单

package main

import (
	"os"

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

func main() {
	writetofile()
}

func writetofile() {
	// 设置一些配置参数
	config := zap.newproductionencoderconfig()
	config.encodetime = zapcore.iso8601timeencoder
	fileencoder := zapcore.newjsonencoder(config)
	defaultloglevel := zapcore.debuglevel // 设置 loglevel

	logfile, _ := os.openfile("./log-test-zap.json", os.o_wronly|os.o_create|os.o_append, 06666)
	// or os.create()
	writer := zapcore.addsync(logfile)

	logger := zap.new(
		zapcore.newcore(fileencoder, writer, defaultloglevel),
		zap.addcaller(),
		zap.addstacktrace(zapcore.errorlevel),
	)
	defer logger.sync()

	url := "http://www.test.com"
	logger.info("write log to file",
		zap.string("url", url),
		zap.int("attemp", 3),
	)
}

例子4:根据日志级别写入不同文件

这个与上面例子2相似

package main

import (
	"os"

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

func main() {
	writetofilewithloglevel()
}

func writetofilewithloglevel() {
	// 设置配置
	config := zap.newproductionencoderconfig()
	config.encodetime = zapcore.iso8601timeencoder
	fileencoder := zapcore.newjsonencoder(config)

	logfile, _ := os.openfile("./log-debug-zap.json", os.o_wronly|os.o_create|os.o_append, 0666) //日志记录debug信息

	errfile, _ := os.openfile("./log-err-zap.json", os.o_wronly|os.o_create|os.o_append, 0666) //日志记录error信息

	teecore := zapcore.newtee(
		zapcore.newcore(fileencoder, zapcore.addsync(logfile), zap.debuglevel),
		zapcore.newcore(fileencoder, zapcore.addsync(errfile), zap.errorlevel),
	)

	logger := zap.new(teecore, zap.addcaller())
	defer logger.sync()

	url := "http://www.diff-log-level.com"
	logger.info("write log to file",
		zap.string("url", url),
		zap.int("time", 3),
	)

	logger.with(
		zap.string("url", url),
		zap.string("name", "jimmmyr"),
	).error("test error ")
}

主要是设置日志级别,和把 2 个设置的 newcore 放入到方法 newtee 中。

六、hook和namespace

zap.hook() :

hook (钩子函数)回调函数为用户提供一种简单方法,在每次日志内容记录后运行这个回调函数,执行用户需要的操作。也就是说记录完日志后你还想做其它事情就可以调用这个函数。

package main

import (
	"fmt"

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

func main() {
	logger := zap.newexample(zap.hooks(func(entry zapcore.entry) error {
		fmt.println("[zap.hooks]test hooks")
		return nil
	}))
	defer logger.sync()

	logger.info("test output")

	logger.warn("warn info")
}

zap.namespace():

创建一个命名空间,后面的字段都在这名字空间中。namespace 就像一个文件夹,后面文件都放在这个文件夹里。

package main

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

func main() {
	logger := zap.newexample()
	defer logger.sync()

	logger.info("some message",
		zap.namespace("shop"),
		zap.string("name", "lilei"),
		zap.string("grade", "no2"),
	)

	logger.error("some error message",
		zap.namespace("shop"),
		zap.string("name", "lilei"),
		zap.string("grade", "no3"),
	)
}

输出:

{"level":"info","msg":"some message","shop":{"name":"lilei","grade":"no2"}} {"level":"error","msg":"some error message","shop":{"name":"lilei","grade":"no3"}}

七、日志切割归档

lumberjack 这个库是按照日志大小切割日志文件。

安装 v2 版本:

go get -u github.com/natefinch/lumberjack@v2

code:

log.setoutput(&lumberjack.logger{
    filename:   "/var/log/myapp/foo.log", // 文件位置
    maxsize:    500,  // megabytes,m 为单位,达到这个设置数后就进行日志切割
    maxbackups: 3,    // 保留旧文件最大份数
    maxage:     28,   //days , 旧文件最大保存天数
    compress:   true, // disabled by default,是否压缩日志归档,默认不压缩
})

参照它的文档和结合上面自定义配置的例子,写一个例子:

package main

import (
	"fmt"

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

func main() {
	lumberjacklogger := &lumberjack.logger{
		filename:   "./log-rotate-test.json",
		maxsize:    1, // megabytes
		maxbackups: 3,
		maxage:     28,   //days
		compress:   true, // disabled by default
	}
	defer lumberjacklogger.close()

	config := zap.newproductionencoderconfig()

	config.encodetime = zapcore.iso8601timeencoder // 设置时间格式
	fileencoder := zapcore.newjsonencoder(config)

	core := zapcore.newcore(
		fileencoder,                       //编码设置
		zapcore.addsync(lumberjacklogger), //输出到文件
		zap.infolevel,                     //日志等级
	)

	logger := zap.new(core)
	defer logger.sync()

    // 测试分割日志
	for i := 0; i < 8000; i++ {
		logger.with(
			zap.string("url", fmt.sprintf("www.test%d.com", i)),
			zap.string("name", "jimmmyr"),
			zap.int("age", 23),
			zap.string("agradege", "no111-000222"),
		).info("test info ")
	}

}

八、其它方法使用

全局 logger

zap提供了 2 种全局 logger,一个是 zap.logger,调用 zap.l() 获取;

另外一个是 zap.sugaredlogger ,调用 zap.s() 获取。

注意:直接调用 zap.l() 或 zap.s() 记录日志的话,它是不会记录任何日志信息。需要调用 replaceglobals() 函数将它设置为全局 logger。

replaceglobals 替换全局 logger 和 sugaredlogger,并返回一个函数来恢复原始值。

并发使用它是安全的。

看看 zap/global.go 中的源码:

// https://github.com/uber-go/zap/blob/v1.24.0/global.go

var (
	_globalmu sync.rwmutex
	_globall  = newnop()
	_globals  = _globall.sugar()
)

func l() *logger {
	_globalmu.rlock() // 加了读锁,所以并发使用是安全的
	l := _globall
	_globalmu.runlock()
	return l
}

func s() *sugaredlogger {
	_globalmu.rlock() // 加了读锁,所以并发使用是安全的
	s := _globals
	_globalmu.runlock()
	return s
}

func replaceglobals(logger *logger) func() {
	_globalmu.lock()
	prev := _globall
	_globall = logger
	_globals = logger.sugar()
	_globalmu.unlock()
	return func() { replaceglobals(prev) } // 返回一个函数类型
}

上面源码中的关键是 _globall = newnop() , newnop 函数源码在 zap/logger.go 中,这个函数返回初始化了的一个 *logger:

// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#l85

func newnop() *logger {
	return &logger{
		core:        zapcore.newnopcore(),
		erroroutput: zapcore.addsync(io.discard),
		addstack:    zapcore.fatallevel + 1,
		clock:       zapcore.defaultclock,
	}
}

上面是源码简析,下面给出一个简单使用的例子。

简单使用例子:

package main

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

func main() {
	// 直接调用是不会记录日志信息的,所以下面日志信息不会输出
	zap.l().info("no log info")
	zap.s().info("no log info [sugared]")

	logger := zap.newexample()
	defer logger.sync()

	zap.replaceglobals(logger) // 全局logger,zap.l() 和 zap.s() 需要调用 replaceglobals 函数才会记录日志信息
	zap.l().info("log info")
	zap.s().info("log info [sugared]")
}

运行输出:

{"level":"info","msg":"log info"} {"level":"info","msg":"log info [sugared]"}

与标准日志库搭配

zap 提供了一个函数 newstdlog,可以把标准日志库 log 转换为 zap 的日志,这为我们从标准日志库转换到 zap 日志库的使用提供了简洁的转换操作。

例子:

package main

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

func main() {
	logger := zap.newexample()
	defer logger.sync()

	std := zap.newstdlog(logger)
	std.print("standard logger wrapper")
}

运行输出:

{"level":"info","msg":"standard logger wrapper"}

如果你还想设置日志级别,可以使用另外一个函数 newstdlogat,它的第二个参数就是日志级别:

newstdlogat(l *logger, level zapcore.level) (*log.logger, error)

一段代码中使用log另外的使用zap

zap 还提供了另外一个函数 redirectstdlog,它可以帮助我们在一段代码中使用标准日志库 log,其它地方还是使用 zap.logger。如下例子:

package main

import (
	"log"

	"go.uber.org/zap"
)

func main() {
	logger := zap.newexample()
	defer logger.sync()

	undo := zap.redirectstdlog(logger)
	log.print("redirected standard library")
	undo()

	log.print("this zap logger")
}

输出:

{"level":"info","msg":"redirected standard library"} 2023/05/06 00:47:11 this zap logger

同样如果想增加日志级别,可以使用函数 redirectstdlogat:

func redirectstdlogat(l *logger, level zapcore.level) (func(), error)

输出调用堆栈

主要是调用函数 zap.addstacktrace(),见下面例子:

package main

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

func hello() {
	warn("hello", zap.string("h", "world"), zap.int("c", 1))
}

func warn(msg string, fields ...zap.field) {
	zap.l().warn(msg, fields...)
}

func main() {
	logger, _ := zap.newproduction(zap.addstacktrace(zapcore.warnlevel))
	defer logger.sync()

	zap.replaceglobals(logger)

	hello()
}

运行输出:

{"level":"warn","ts":1683306442.3578277,"caller":"zapdemos/addstacktrace.go:13","msg":"hello","h":"world","c":1, "stacktrace": "main.warn\n\td:/work/mygo/go-exercises/zapdemos/addstacktrace.go:13\n main.hello\n\td:/work/mygo/go-exercises/zapdemos/addstacktrace.go:9\n main.main\n\td:/work/mygo/go-exercises/zapdemos/addstacktrace.go:22\n runtime.main\n\te:/programfile/go/src/runtime/proc.go:250"}

输出文件名和行号

addcaller 将 logger 配置为使用 zap 调用者的文件名、行号和函数名称,把这些信息添加到日志记录中。它底层调用的是 withcaller

addcaller.go:

package main

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

func main() {
	logger, _ := zap.newproduction(zap.addcaller())
	defer logger.sync()

	logger.info("addcaller:line no and filename")
}

输出:

{"level":"info","ts":1683307204.6184027,"caller":"zapdemos/addcaller.go:11","msg":"addcaller:line no and filename"}

logger.info() 方法在第11行被调用。

zap 还提供了另外一个函数 zap.addcallerskip(skip int) option,可以设置向上跳几层,然后记录文件名和行号。向上跳几层就是跳过调用者的数量。有时函数调用可能有嵌套,用这个函数可以定位到里面的函数。

addcallerskip.go

package main

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

func main() {
	logger, _ := zap.newproduction(zap.addcaller(), zap.addcallerskip(1))
	defer logger.sync()

	zap.replaceglobals(logger)

	hello()
}

func hello() {
	warn("hello", zap.string("h", "world"), zap.int("c", 1))
}

func warn(msg string, fields ...zap.field) {
	zap.l().warn(msg, fields...)
}

输出:

{"level":"warn","ts":1683308118.1684704,"caller":"zapdemos/addcallerskip.go:17","msg":"hello","h":"world","c":1}

日志中的 17 表示 hello() 函数里的 warn() 的行号。

如果 zap.addcallerskip(2) ,日志中显示行号为 13,表示 hello() 的行号。

九、zap使用总结

  • zap 的使用,先创建 logger,再调用各个日志级别方法记录日志信息。比如 logger.info()。
  • zap 提供了三种快速创建 logger 的方法: zap.newproduction()zap.newdevelopment()zap.newexample()。见名思义,example 一般用在测试代码中,development 用在开发环境中,production 用在生成环境中。这三种方法都预先设置好了配置信息。它们的日志数据类型输出都是强类型。
  • 当然,zap 也提供了给用户自定义的方法 zap.new()。比如用户可以自定义一些配置信息等。
  • 在上面的例子中,几乎都有 defer logger.sync() 这段代码,为什么?因为 zap 底层 api 允许缓冲日志以提高性能,在默认情况下,日志记录器是没有缓冲的。但是在进程退出之前调用 sync() 方法是一个好习惯。
  • 如果你在 zap 中使用了 sugaredlogger,把 zap 创建 logger 的三种方法用 logger.sugar() 包装下,那么 zap 就支持 printf 风格的格式化输出,也支持以 w 结尾的方法。如 infow,infof 等。这种就是通用类型日志输出,不是强类型输出,不需要强制指定输出的数据类型。它们的性能区别,通用类型会比强类型下降 50% 左右。

比如 infow 的输出形式,infow 不需要 zap.string 这种指定字段的数据类型。如下代码:

sugar := logger.sugar()
sugar.infow("failed to fetch url",
            "url", url,
            "attempt", 3,
            "backoff", time.second,
)

强类型输出,比如 info 方法输出字段和值就需要指定数据类型:

logger.info("failed to fetch url",
		// 强类型字段
		zap.string("url", "http://example.com"),
		zap.int("attempt", 3),
		zap.duration("backoff", time.second),
)

强类型输出和通用类型输出区别

通用类型输出,经过 interface{} 转换会有性能损失,标准库的 fmt.printf 为了通用性就用了 interface{} 这种”万能型“的数据类型,另外它还使用了反射,性能进一步降低。

zap 强类型输出,zap 为了提供日志输出性能,zap 的强类型输出没有使用 interface{} 和反射。zap 默认输出就是强类型。

上面介绍,zap 中 3 种创建 logger 方式(zap.newproduction()zap.newdevelopment()zap.newexample())就是强类型日志字段,当然,也可以转化为通用类型,用 logger.sugar() 方法创建 sugaredlogger。

zap.namespace() 创建一个命名空间,后面的字段都在这名字空间中。namespace 就像一个文件夹,后面文件都放在这个文件夹里。

logger.info("some message",
    zap.namespace("shop"),
    zap.string("shopid", "s1234323"),
  )

{"level":"info","msg":"some message","shop":{"shopid":"s1234323"}} 

十、demo源码地址

zap demos

https://github.com/jiujuan/go-exercises/tree/main/zapdemos

以上就是golang日志操作库zap的使用详解的详细内容,更多关于go日志操作库zap的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com