📅 2024年11月26日
📦 go1.21.5zap
看到工作使用的是
zap
顺手就学一下
🏆 ZAP
在`zap`的`github`主页给出了一个数据表格,用来对比其他go的日志框架的速度,毋庸置疑zap肯定是最快的,具体可以去`github`上查看
github
: https://github.com/uber-go/zap
为什么它会这么快了呢?在官方文档中也说了: `Zap` 采用了不同的方法。它包含一个无反射、零分配的 `JSON` 编码器,并且基本 Logger 力求尽可能避免序列化开销和分配。通过在此基础上构建高级 `SugaredLogger`,`zap` 让用户可以选择何时需要计算每个分配以及何时更喜欢更熟悉的松散类型 API。
🍪 开始
首先当然是安装,可以直接通过 go get
安装
go get -u go.uber.org/zap
安装完成后就可以直接导入使用,此外zap提供两个日志记录器分别是`sugaredlogger`和`logger`(但是我感觉差别不大),两者差异如下:
1.在性能要求较高但又不重要的环境中,请使用 SugaredLogger
。它比其他结构化日志包快 4-10 倍,并且支持结构化和 printf
样式的日志记录。与 log15
和 go-kit
一样,SugaredLogger
的结构化日志 API 是弱类型的,并接受可变数量的键值对。
func main() {
sugar := zap.NewExample().Sugar()
// 程序结束Sync同步缓冲
defer sugar.Sync()
sugar.Infow("无法获取 URL",
"url", "http://example.com",
"attempt", 3,
"backoff", time.Second,
)
// 类似于prinf
sugar.Infof("无法获取 URL: %s", "http://example.com")
}
// 输出:
{"level":"info","msg":"无法获取 URL","url":"http://example.com","attempt":3,"backoff":"1s"}
{"level":"info","msg":"无法获取 URL: http://example.com"}
2.在极少数情况下,每微秒和每次分配都很重要,请使用 Logger
。它甚至比 SugaredLogger
更快,分配的资源也少得多,但它仅支持强类型、结构化日志记录
func main() {
logger := zap.NewExample()
defer logger.Sync()
logger.Info("failed to fetch URL",
zap.String("url", "http://example.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}
3.在 sugared
和 logger
之间转换也很方便
logger := zap.NewExample()
defer logger.Sync()
sugar := logger.Sugar()
plain := sugar.Desugar()
4.创建 logger
的方法有很多主要有一下三种
zap.NewExample()
zap.NewDevelopment()
zap.NewProduction()
使用
NewExample
构建了一个Logger
,该Logger
专为在zap
的可测试示例中使用而设计。它将DebugLevel
及以上日志作为JSON
写入标准输出,但省略了时间戳和调用函数,以保持示例输出简短且具有确定性使用
NewDevelopment
构建了一个开发Logger
,NewDevelopment
会生成包含调用者信息、堆栈跟踪等详细信息的日志,而且它输出是带有彩色的使用
NewProduction
构建了一个合理的生产Logger
,它将InfoLevel
及以上的日志作为JSON
写入标准错误,它输出也是彩色的
可见他们的区别在于写入的日志等级和写入日志的格式,测试一下代码如下:
func main() {
example := zap.NewExample()
development, _ := zap.NewDevelopment()
production, _ := zap.NewProduction()
defer func() {
example.Sync()
development.Sync()
production.Sync()
}()
example.Info("hello world")
development.Info("hello world")
production.Info("hello world")
}
// 输出如下
可以看到如果使用`Newexample`那么就是简简单单的一个日志,production和development都输出了时间,前者是时间戳,后者格式化了,而`development` 输出更加简洁
那么如果说`production`的日志遇到`debug`会怎么样? **答案是不会输出**
1️⃣ 日志等级
和 logrus
差不多,但是少了一个 trace
多了一个 DPanic
请查看:Logrus 安装
Panic
:记录日志,然后panic
。Fatal
:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出;Error
:错误日志,需要查看原因;Warn
:警告信息,提醒程序员注意;Info
:关键操作,核心流程的日志;Debug
:一般程序中输出的调试信息;
func main() {
logger := zap.NewExample().Sugar()
defer logger.Sync()
logger.Debug("Debug Test")
logger.Info("Info Test")
logger.Warn("Warn Test")
logger.Fatal("Fatal Test") // 此处就直接发生报错就不会在输出下面的语句了
logger.Panic("Panic Test")
}
下面是 zap
的日志等级,可以看到有个 DPanic
和 Panic
,我本来也很困惑测试了一下之后发现确实不同
const (
// DebugLevel logs are typically voluminous, and are usually disabled in
// production.
DebugLevel = zapcore.DebugLevel
// InfoLevel is the default logging priority.
InfoLevel = zapcore.InfoLevel
// WarnLevel logs are more important than Info, but don't need individual
// human review.
WarnLevel = zapcore.WarnLevel
// ErrorLevel logs are high-priority. If an application is running smoothly,
// it shouldn't generate any error-level logs.
ErrorLevel = zapcore.ErrorLevel
// DPanicLevel logs are particularly important errors. In development the
// logger panics after writing the message.
DPanicLevel = zapcore.DPanicLevel
// PanicLevel logs a message, then panics.
PanicLevel = zapcore.PanicLevel
// FatalLevel logs a message, then calls os.Exit(1).
FatalLevel = zapcore.FatalLevel
)
DPanic
方法也用于记录错误日志,但与 Panic
不同的是,它不会触发程序的 panic
。DPanic
方法会在记录日志前检查是否启用了调试模式(通过 zapcore.WriteSyncer
的 Check
方法)。如果启用了调试模式,DPanic
会触发 panic
并输出堆栈跟踪信息;否则,它只会记录错误日志,程序会继续执行。
DPanic
方法通常用于处理可恢复的错误,或者在开发环境中记录详细的错误信息。在生产环境中,可以通过配置禁用 DPanic
的panic行为,以避免不必要的程序中断,测试一下
💻 DPanic
在输出 DPanic
信息中使用 development
和其它两种方法不一样,下面是 development
的 DPanic
输出,也就是说 development
默认开启了调试模式
func main() {
development, _ := zap.NewDevelopment()
defer development.Sync()
development.DPanic("DPanic")
}
//输出
2024-11-26T22:06:41.096+0800 DPANIC Gin_zap/main.go:18 DPanic
main.main
D:/learnCode/Go/GinStudy/Gin_zap/main.go:18
runtime.main
C:/Program Files/Go/src/runtime/proc.go:267
panic: DPanic
goroutine 1 [running]:
go.uber.org/zap/zapcore.CheckWriteAction.OnWrite(0x0?, 0x0?, {0x0?, 0x0?, 0xc0000604e0?})
C:/Users/tanchang/go/pkg/mod/go.uber.org/zap@v1.27.0/zapcore/entry.go:196 +0x54
go.uber.org/zap/zapcore.(*CheckedEntry).Write(0xc000020a90, {0x0, 0x0, 0x0})
C:/Users/tanchang/go/pkg/mod/go.uber.org/zap@v1.27.0/zapcore/entry.go:262 +0x3ec
go.uber.org/zap.(*Logger).DPanic(0x0?, {0xc2124c?, 0x0?}, {0x0, 0x0, 0x0})
C:/Users/tanchang/go/pkg/mod/go.uber.org/zap@v1.27.0/logger.go:275 +0x51
main.main()
D:/learnCode/Go/GinStudy/Gin_zap/main.go:18 +0x65
此外你看到了输出了一条 DPanic
日志但是还是打印出来堆栈的信息了,但是如果你在 DPanic
后面在接一段程序代码,它不会继续执行
func main() {
development, _ := zap.NewDevelopment()
defer development.Sync()
development.DPanic("我发生Panic啦!救命啊!")
fmt.Println("嘿嘿骗你的,我还可以运行")
}
//输入和上面一样并不会执行下面的println语句
那么查看一下其他两种,没有开启调试模式的,example
就和普通日志差不多还可以继续,但是 production
会输出一点栈 trace
,但是也不会终止执行
func main() {
production, _ := zap.NewProduction()
example := zap.NewExample()
defer func() {
production.Sync()
example.Sync()
}()
example.DPanic("我发生Panic啦!救命啊!")
fmt.Println("嘿嘿骗你的,我还可以运行")
production.DPanic("我真发生Panic啦!救命啊!")
println("test")
}
//
{"level":"dpanic","msg":"我发生Panic啦!救命啊!"}
嘿嘿骗你的,我还可以运行
{"level":"dpanic","ts":1732631310.1716707,"caller":"Gin_zap/main.go:25","msg":"我真发生Panic啦!救命啊!","stacktrace":"main.main\n\tD:/learnCode/Go/GinStudy/Gin_zap/main.go:25\nruntime.main\n\tC:/Program Files/Go/src/runtime/proc.go:267"}
test
2️⃣ 使日志有有层级
类似于`json`,或者说是`python`内字典嵌套字典
我想象的场景就是你在初始化的时候可能需要链接不同的服务,最后链接结果可以总结道一条日志然后返回如下演示,使用`with`方法是创建一个带有子日志的行`logger`
func main() {
logger := zap.NewExample()
defer logger.Sync()
serviceConnlog := logger.With(
zap.Namespace("service"),
zap.String("redis", "链接失败"),
zap.String("mysql", "链接失败"),
)
serviceConnlog.Error("服务出错")
}
//输出
{"level":"error","msg":"服务出错","service":{"redis":"链接失败","mysql":"链接失败"}}
当然可以直接在 Errof
直接写
func main() {
logger := zap.NewExample()
defer logger.Sync()
logger.Error("服务启动失败",
zap.Namespace("service"),
zap.String("redis", "链接失败"),
zap.String("mysql", "链接失败"),
)
}
3️⃣ 对 logger
定制
使用配置文件配置
在官方文档说: 预设适用于小型项目,但大型项目和组织自然需要更多的自定义。对于大多数用户来说,`zap` 的 `Config` 结构体在灵活性和便利性之间取得了适当的平衡。可以直接使用预设置的方法来使用`zap`,也可以使用`config`结构体来创建配置。
Config
结构体如下
type Config struct {
// 设置日志级别
Level AtomicLevel `json:"level" yaml:"level"`
// 开启开发模式,启用堆栈追踪
Development bool `json:"development" yaml:"development"`
// 禁用调用者信息,不显示日志的文件名和行号
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// 完全禁用自动堆栈跟踪捕获
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// 设置采样策略
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// 设置日志编码方式
Encoding string `json:"encoding" yaml:"encoding"`
// 设置所选编码器的配置选项
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// 设置日志输出路径
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// 设置内部日志错误输出路径
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// 设置根日志的初始字段
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
其中有个 EncodingConfig
type EncoderConfig struct {
// Set the keys used for each log entry. If any key is empty, that portion
// of the entry is omitted.
MessageKey string `json:"messageKey" yaml:"messageKey"`
LevelKey string `json:"levelKey" yaml:"levelKey"`
TimeKey string `json:"timeKey" yaml:"timeKey"`
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"`
// Configure the primitive representations of common complex types. For
// example, some users may want all time.Times serialized as floating-point
// seconds since epoch, while others may prefer ISO8601 strings.
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// Unlike the other primitive type encoders, EncodeName is optional. The
// zero value falls back to FullNameEncoder.
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// Configure the encoder for interface{} type objects.
// If not provided, objects are encoded using json.Encoder
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// Configures the field separator used by the console encoder. Defaults
// to tab.
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
其中主要的配置有:
MessageKey
:日志中信息的键名,默认为msg
;LevelKey
:日志中级别的键名,默认为level
;EncodeLevel
:日志中级别的格式,默认为小写,如debug/info
。
接下来定制一下
func main() {
zapConfig := []byte(`{
// 日志等级为debug
"level": "debug",
"encoding": "json",
// 错误和正常日志输出
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
// 初始化日志输出头
"initialFields": {"package": "gin"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase",
"timeKey": "time",
"timeEncoder": "iso8601"
}
}`)
// 解析为json
var zapcfg zap.Config
if err := json.Unmarshal(zapConfig, &zapcfg); err != nil {
panic(err)
}
// 使用build通过自定义设置创建logger
newLogger := zap.Must(zapcfg.Build())
defer newLogger.Sync()
newLogger.Info("这是一条成功消息")
newLogger.Error("这是一条ERRO消息")
}
//输出:
{"level":"info","time":"2024-11-29T21:28:53.448+0800","message":"这是一条成功消息","package":"gin"}
{"level":"error","time":"2024-11-29T21:28:53.479+0800","message":"这是一条ERRO消息","package":"gin"}
使用option配置
此外在使用 NewProduction/...
这几种默认方法创建 logger
时可以传递多个 option
这些 option
也可以配置 logger
,比如 zap.AddCaller()
可以输出当前文件目录和代码行数
func main() {
production, _ := zap.NewProduction(zap.AddCaller())
defer production.Sync()
production.Info("test")
}
//out
{"level":"info","ts":1732887442.3083456,"caller":"Gin_zap/main.go:17","msg":"test"}
还可以使用 zap.AddCallerSkip()
来输出上几层调用代码所在的文件信息
还有 zap.AddStackTrace
可以设置哪个等级之上会输出堆栈信息
zap.Fields
可以设置日志字段
4️⃣ 全局 Logger
全局的 Logger
默认并不会记录日志!它是一个无实际效果的 Logger
可以使用如下代码来恢复全局 Logger
func main() {
zap.L().Info("global Logger before")
zap.S().Info("global SugaredLogger before")
logger := zap.NewExample()
defer logger.Sync()
zap.ReplaceGlobals(logger)
zap.L().Info("global Logger after")
zap.S().Info("global SugaredLogger after")
}
//out
{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}