Go 每日一库之 Uber 开源的优秀日志库 zap
yuyutoo 2024-10-20 13:12 3 浏览 0 评论
以下文章来源于GoUpUp ,作者dj
简介
在很早之前的文章中,我们介绍过 Go 标准日志库log和结构化的日志库logrus。在热点函数中记录日志对日志库的执行性能有较高的要求,不能影响正常逻辑的执行时间。uber开源的日志库zap,对性能和内存分配做了极致的优化。
快速使用
先安装:
nbsp;go get go.uber.org/zap
后使用:
package main
import (
"time"
"go.uber.org/zap"
)
func main() {
logger := zap.NewExample()
defer logger.Sync()
url := "http://example.org/api"
logger.Info("failed to fetch URL",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)
}
zap库的使用与其他的日志库非常相似。先创建一个logger,然后调用各个级别的方法记录日志(Debug/Info/Error/Warn)。zap提供了几个快速创建logger的方法,zap.NewExample()、zap.NewDevelopment()、zap.NewProduction(),还有高度定制化的创建方法zap.New()。创建前 3 个logger时,zap会使用一些预定义的设置,它们的使用场景也有所不同。Example适合用在测试代码中,Development在开发环境中使用,Production用在生成环境。
zap底层 API 可以设置缓存,所以一般使用defer logger.Sync()将缓存同步到文件中。
由于fmt.Printf之类的方法大量使用interface{}和反射,会有不少性能损失,并且增加了内存分配的频次。zap为了提高性能、减少内存分配次数,没有使用反射,而且默认的Logger只支持强类型的、结构化的日志。必须使用zap提供的方法记录字段。zap为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆,zap.Type(Type为bool/int/uint/float64/complex64/time.Time/time.Duration/error等)就表示该类型的字段,zap.Typep以p结尾表示该类型指针的字段,zap.Types以s结尾表示该类型切片的字段。如:
- zap.Bool(key string, val bool) Field:bool字段
- zap.Boolp(key string, val *bool) Field:bool指针字段;
- zap.Bools(key string, val []bool) Field:bool切片字段。
当然也有一些特殊类型的字段:
- zap.Any(key string, value interface{}) Field:任意类型的字段;
- zap.Binary(key string, val []byte) Field:二进制串的字段。
当然,每个字段都用方法包一层用起来比较繁琐。zap也提供了便捷的方法SugarLogger,可以使用printf格式符的方式。调用logger.Sugar()即可创建SugaredLogger。SugaredLogger的使用比Logger简单,只是性能比Logger低 50% 左右,可以用在非热点函数中。调用SugarLogger以f结尾的方法与fmt.Printf没什么区别,如例子中的Infof。同时SugarLogger还支持以w结尾的方法,这种方式不需要先创建字段对象,直接将字段名和值依次放在参数中即可,如例子中的Infow。
默认情况下,Example输出的日志为 JSON 格式:
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"Failed to fetch URL: http://example.org/api"}
记录层级关系
前面我们记录的日志都是一层结构,没有嵌套的层级。我们可以使用zap.Namespace(key string) Field构建一个命名空间,后续的Field都记录在此命名空间中:
func main() {
logger := zap.NewExample()
defer logger.Sync()
logger.Info("tracked some metrics",
zap.Namespace("metrics"),
zap.Int("counter", 1),
)
logger2 := logger.With(
zap.Namespace("metrics"),
zap.Int("counter", 1),
)
logger2.Info("tracked some metrics")
}
输出:
{"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
{"level":"info","msg":"tracked some metrices","metrics":{"counter":1}}
上面我们演示了两种Namespace的用法,一种是直接作为字段传入Debug/Info等方法,一种是调用With()创建一个新的Logger,新的Logger记录日志时总是带上预设的字段。With()方法实际上是创建了一个新的Logger:
// src/go.uber.org/zap/logger.go
func (log *Logger) With(fields ...Field) *Logger {
if len(fields) == 0 {
return log
}
l := log.clone()
l.core = l.core.With(fields)
return l
}
定制Logger
调用NexExample()/NewDevelopment()/NewProduction()这 3 个方法,zap使用默认的配置。我们也可以手动调整,配置结构如下:
// src/go.uber.org/zap/config.go
type Config struct {
Level AtomicLevel `json:"level" yaml:"level"`
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"`
}
- Level:日志级别;
- Encoding:输出的日志格式,默认为 JSON;
- OutputPaths:可以配置多个输出路径,路径可以是文件路径和stdout(标准输出);
- ErrorOutputPaths:错误输出路径,也可以是多个;
- InitialFields:每条日志中都会输出这些值。
其中EncoderConfig为编码配置:
// src/go.uber.org/zap/zapcore/encoder.go
type EncoderConfig struct {
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"`
StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
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"`
}
- MessageKey:日志中信息的键名,默认为msg;
- LevelKey:日志中级别的键名,默认为level;
- EncodeLevel:日志中级别的格式,默认为小写,如debug/info。
调用zap.Config的Build()方法即可使用该配置对象创建一个Logger:
func main() {
rawJSON := []byte(`{
"level":"debug",
"encoding":"json",
"outputPaths": ["stdout", "server.log"],
"errorOutputPaths": ["stderr"],
"initialFields":{"name":"dj"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("server start work successfully!")
}
上面创建一个输出到标准输出stdout和文件server.log的Logger。观察输出:
{"level":"info","message":"server start work successfully!","name":"dj"}
使用NewDevelopment()创建的Logger使用的是如下的配置:
// src/go.uber.org/zap/config.go
func NewDevelopmentConfig() Config {
return Config{
Level: NewAtomicLevelAt(DebugLevel),
Development: true,
Encoding: "console",
EncoderConfig: NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
// Keys can be anything except the empty string.
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
NewProduction()的配置可自行查看。
选项
NewExample()/NewDevelopment()/NewProduction()这 3 个函数可以传入若干类型为zap.Option的选项,从而定制Logger的行为。又一次见到了选项模式!!
zap提供了丰富的选项供我们选择。
输出文件名和行号
调用zap.AddCaller()返回的选项设置输出文件名和行号。但是有一个前提,必须设置配置对象Config中的CallerKey字段。也因此NewExample()不能输出这个信息(它的Config没有设置CallerKey)。
func main() {
logger, _ := zap.NewProduction(zap.AddCaller())
defer logger.Sync()
logger.Info("hello world")
}
输出:
{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}
Info()方法在main.go的第 9 行被调用。AddCaller()与zap.WithCaller(true)等价。
有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层:
func Output(msg string, fields ...zap.Field) {
zap.L().Info(msg, fields...)
}
func main() {
logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1))
defer logger.Sync()
zap.ReplaceGlobals(logger)
Output("hello world")
}
输出:
{"level":"info","ts":1587740501.5592482,"caller":"skip/main.go:15","msg":"hello world"}
输出在main函数中调用Output()的位置。如果不指定zap.AddCallerSkip(1),将输出"caller":"skip/main.go:6",这是在Output()函数中调用zap.Info()的位置。因为这个Output()函数可能在很多地方被调用,所以这个位置参考意义并不大。试试看!
输出调用堆栈
有时候在某个函数处理中遇到了异常情况,因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈,那么分析起来就会很方便。我们可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)达成这个目的。该函数指定lvl和之上的级别都需要输出调用堆栈:
func f1() {
f2("hello world")
}
func f2(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)
f1()
}
将zapcore.WarnLevel传入AddStacktrace(),之后Warn()/Error()等级别的日志会输出堆栈,Debug()/Info()这些级别不会。运行结果:
{"level":"warn","ts":1587740883.4965692,"caller":"stacktrace/main.go:13","msg":"hello world","stacktrace":"main.f2\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13\nmain.f1\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9\nmain.main\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22\nruntime.main\n\tC:/Go/src/runtime/proc.go:203"}
把stacktrace单独拉出来:
main.f2
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13
main.f1
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9
main.main
d:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22
runtime.main
C:/Go/src/runtime/proc.go:203
很清楚地看到调用路径。
全局Logger
为了方便使用,zap提供了两个全局的Logger,一个是*zap.Logger,可调用zap.L()获得;另一个是*zap.SugaredLogger,可调用zap.S()获得。需要注意的是,全局的Logger默认并不会记录日志!它是一个无实际效果的Logger。看源码:
// go.uber.org/zap/global.go
var (
_globalMu sync.RWMutex
_globalL = NewNop()
_globalS = _globalL.Sugar()
)
我们可以使用ReplaceGlobals(logger *Logger) func()将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")
}
输出:
{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}
可以看到在调用ReplaceGlobals之前记录的日志并没有输出。
预设日志字段
如果每条日志都要记录一些共用的字段,那么使用zap.Fields(fs ...Field)创建的选项。例如在服务器日志中记录可能都需要记录serverId和serverName:
func main() {
logger := zap.NewExample(zap.Fields(
zap.Int("serverId", 90),
zap.String("serverName", "awesome web"),
))
logger.Info("hello world")
}
输出:
{"level":"info","msg":"hello world","serverId":90,"serverName":"awesome web"}
与标准日志库搭配使用
如果项目一开始使用的是标准日志库log,后面想转为zap。这时不必修改每一个文件。我们可以调用zap.NewStdLog(l *Logger) *log.Logger返回一个标准的log.Logger,内部实际上写入的还是我们之前创建的zap.Logger:
func main() {
logger := zap.NewExample()
defer logger.Sync()
std := zap.NewStdLog(logger)
std.Print("standard logger wrapper")
}
输出:
{"level":"info","msg":"standard logger wrapper"}
很方便不是吗?我们还可以使用NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error)让标准接口以level级别写入内部的*zap.Logger。
如果我们只是想在一段代码内使用标准日志库log,其它地方还是使用zap.Logger。可以调用RedirectStdLog(l *Logger) func()。它会返回一个无参函数恢复设置:
func main() {
logger := zap.NewExample()
defer logger.Sync()
undo := zap.RedirectStdLog(logger)
log.Print("redirected standard library")
undo()
log.Print("restored standard library")
}
看前后输出变化:
{"level":"info","msg":"redirected standard library"}
2020/04/24 22:13:58 restored standard library
当然RedirectStdLog也有一个对应的RedirectStdLogAt以特定的级别调用内部的*zap.Logger方法。
总结
zap用在日志性能和内存分配比较关键的地方。本文仅介绍了zap库的基本使用,子包zapcore中有更底层的接口,可以定制丰富多样的Logger。
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue
参考
- zap GitHub:https://github.com/jordan-wright/zap
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
相关推荐
- 微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型
-
IT之家4月3日消息,科技媒体WindowsLatest昨日(4月2日)发布博文,报道称Windows10、Windows11新版Copilot应用已摘掉Beta帽...
- WinForm 双屏幕应用开发:原理、实现与优化
-
在当今的软件开发领域,多屏幕显示技术的应用越来越广泛。对于WinForm应用程序来说,能够支持双屏幕显示不仅可以提升用户体验,还能满足一些特定场景下的业务需求,比如在演示、监控或者多任务处理等场景...
- OpenJDK 8 安装(openjdk 8 windows)
-
通常OpenJDK8和11都能互相编译和通用。我们建议使用11,但是如果你使用JDK8的话也是没有问题的。建议配置使用OpenJDK,不建议使用OracleJDK,主要是因为版...
- 基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)
-
一、前言在如今远程办公和内网穿透需求日益增长的背景下,搭建一套安全、稳定、高效的VPN系统显得尤为重要。OpenConnectServer(ocserv)是一个开源、高性能的VPN服务端软件...
- 巧妙设置让Edge浏览器更好用(edge怎么设置好用)
-
虽然现在新版本的Edge浏览器已经推出,但是毕竟还处于测试的状态中。而Win10系统里面自带的老版Edge浏览器,却越来越不被人重视。其实我们只需要根据实际情况对老版本的Edge浏览器进行一些简单的设...
- 微软开源博客工具Open Live Writer更新:多项Bug修复
-
OpenLiveWriter前身是WindowsLiveWriter,是微软WindowsLive系列软件之一,曾经是博主们非常喜爱的一款所见即所得博文编辑工具,支持离线保存,还支持图像编辑...
- 基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品
-
在第二届拯救者杯OPENAIGC开发者大赛中,涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到,我们特意开设了优秀作品报道专栏,旨在展示其独特之处和开发者的精彩故事。...
- Python open函数详解(python open函数源码)
-
演示环境,操作系统:Win1021H2(64bit);Python解释器:3.8.10。open是Python的一个内置函数,一般用于本地文件的读写操作。用法如下。my_file=open(fi...
- 世界上最好用的Linux发行版之一,OpenSUSE安装及简单体验
-
背景之前无意在论坛里看到openSUSE的Linux发行版,被称为世界上最好用的Linux发行版之一(阔怕),一直想体验一下,于是这期做一个安装和简单体验教程吧。...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
推荐7个模板代码和其他游戏源码下载的网址
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- 微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型
- WinForm 双屏幕应用开发:原理、实现与优化
- 推荐一个使用 C# 开发的 Windows10 磁贴美化小工具
- OpenJDK 8 安装(openjdk 8 windows)
- 基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)
- 巧妙设置让Edge浏览器更好用(edge怎么设置好用)
- WPF做一个漂亮的登录界面(wpf页面设计)
- 微软开源博客工具Open Live Writer更新:多项Bug修复
- 基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品
- C#开源免费的Windows右键菜单管理工具
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)