zap是Uber开源的golang日志库,以结构化日志、高性能、可扩展的特点受到许多开发者的欢迎。
使用
创建logger
zap对外提供了两个不同的日志对象,强类型的Logger和简化使用方式的SugaredLogger
Logger创建
低级API
func New(core zapcore.Core, options ...Option) *Logger
通过配置创建
使用了builder模式,通过Config对象的Build方法创建func (cfg Config) Build(opts ...Option) (*Logger, error)
配置对象可以使用预设的
NewDevelopmentConfig
或NewProductionConfig
创建,也可以反序列化json或yaml来创建
Option提供了动态修改Logger配置的能力,定义为接口,每个实现在闭包中修改Logger对象1
2
3type Option interface {
apply(*Logger)
}构造带固定上下文的Logger
func (log *Logger) With(fields ...Field) *Logger
SugaredLogger
封装了Logger,可以和Logger互相转换
func (log *Logger) Sugar() *SugaredLogger
func (s *SugaredLogger) Desugar() *Logger
内部通过
sweetenFields
方法把(string, interface{})
类型的kv参数转换为强类型的Field
func (s *SugaredLogger) sweetenFields(args []interface{}) []Field
通过
With
方法构造一个带固定上下文的SugaredLoggerfunc (s *SugaredLogger) With(args ...interface{}) *SugaredLogger
打印日志
Logger
msg传入简单字符串,其他日志信息通过
Field
包装起来
func (log *Logger) Info(msg string, fields ...Field)
1
logger.Info("msg", zap.String("foo", "bar"))
SugaredLogger
格式化msg的方法
func (s *SugaredLogger) Infof(template string, args ...interface{})
1
sugar.Infof("Failed to fetch URL: %s", url)
带kv参数的方法
func (s *SugaredLogger) Infow(msg string, keysAndValues ...interface{})
1
2
3
4
5
6sugar.Infow("Failed to fetch URL.",
// Structured context as loosely typed key-value pairs.
"url", url,
"attempt", 3,
"backoff", time.Second,
)
代码
分层
zapcore 封装日志的Core接口和实现,以及level、entry、encoder、field等基础功能
zap 提供高层接口,包括sugarlogger、配置、输出接口Sink
和实现、HTTP修改level接口等
其他包提供支持功能。buffer实现了一个默认size为1k的buffer池。zapgrpc封装了与grpclog兼容的日志。
创建流程
Logger结构体很轻量,核心的core是一个接口,创建一个新的对象开销不大,一般使用也可以重用Logger对象。
1 | type Logger struct { |
New
方法
func New(core zapcore.Core, options ...Option) *Logger
根据传入的参数赋值,然后调用
WithOptions
设置Logger。Config.Build
func (cfg Config) Build(opts ...Option) (*Logger, error)
根据配置构造encoder,输出对象,然后调用
zapcore.NewCore
创建core,最后根据参数opts来设置Loggerencoder的创建使用了简单工厂,内部维护了一个字符串到构造函数的映射,根据配置来决定使用哪个encoder。可以用
RegisterEncoder
注册新的encoder。输出对象也使用了简单工厂,内部维护了URL的scheme到构造函数的映射。通过
url.URL
来决定输出对象接口Sink
的实现。可以用RegisterSink
来注册新的Sink
实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20func (cfg Config) Build(opts ...Option) (*Logger, error) {
enc, err := cfg.buildEncoder()
// error handle
sink, errSink, err := cfg.openSinks()
// error handle
if cfg.Level == (AtomicLevel{}) {
return nil, fmt.Errorf("missing Level")
}
log := New(
zapcore.NewCore(enc, sink, cfg.Level),
cfg.buildOptions(errSink)...,
)
if len(opts) > 0 {
log = log.WithOptions(opts...)
}
return log, nil
}zapcore.NewCore
创建了一个ioCore,主要的日志编码和输出逻辑在它的Write
方法中实现1
2
3
4
5
6
7func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core {
return &ioCore{
LevelEnabler: enab,
enc: enc,
out: ws,
}
}
打印日志流程
以Info函数为例
1 | func (log *Logger) Info(msg string, fields ...Field) { |
执行打印之前会调用Logger的check方法,如果不满足条件就不会创建对象和调用Write方法。
check
方法会先检查level,如果不满足条件就返回nil了。如果满足条件会创建日志entry,并调用core的check,如果core.Check返回了zapcore.CheckedEntry
才会继续执行添加其他日志信息操作。实际打印的逻辑发生在zapcore.CheckedEntry
的Write方法里面。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry{
// check must always be called directly by a method in the Logger interface
// (e.g., Check, Info, Fatal).
const callerSkipOffset = 2
// Check the level first to reduce the cost of disabled log calls.
// Since Panic and higher may exit, we skip the optimization for those levels.
if lvl < zapcore.DPanicLevel && !log.core.Enabled(lvl) {
return nil
}
// Create basic checked entry thru the core; this will be non-nil if the
// log message will actually be written somewhere.
ent := zapcore.Entry{
LoggerName: log.name,
Time: time.Now(),
Level: lvl,
Message: msg,
}
ce := log.core.Check(ent, nil)
willWrite := ce != nil
// Set up any required terminal behavior.
// ...
// Only do further annotation if we're going to write this message; checked
// entries that exist only for terminal behavior don't benefit from
// annotation.
if !willWrite {
return ce
}
// Thread the error output through to the CheckedEntry.
ce.ErrorOutput = log.errorOutput
// add caller
// add stack
return ce
}core接口实现的Check方法
不同的core实现会执行不同操作,但是要执行core的Write方法需要调用AddCore
把自己加入到zapcore.CheckedEntry
的core列表里面。
以ioCore为例1
2
3
4
5
6func (c *ioCore) Check(ent Entry, ce *CheckedEntry) *CheckedEntry {
if c.Enabled(ent.Level) {
return ce.AddCore(ent, c) // 加入到待执行core列表
}
return ce
}
zapcore.CheckedEntry
保存了日志原始信息Entry
,以及需要执行的core,打印日志后需要执行的动作标识等
AddCore
方法会通过getCheckedEntry
方法从sync.Pool
中获取CheckedEntry对象,把core加入到待执行列表1
2
3
4
5
6
7
8
9
10
11
12
13
14
15type CheckedEntry struct {
Entry
ErrorOutput WriteSyncer
dirty bool // best-effort detection of pool misuse
should CheckWriteAction
cores []Core
}
func (ce *CheckedEntry) AddCore(ent Entry, core Core) *CheckedEntry {
if ce == nil {
ce = getCheckedEntry()
ce.Entry = ent
}
ce.cores = append(ce.cores, core)
return ce
}执行日志打印的Write方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31func (ce *CheckedEntry) Write(fields ...Field) {
if ce == nil {
return
}
if ce.dirty {
// 输出内部错误信息
return
}
ce.dirty = true
var err error
// **关键路径**:执行多个core,其他的core例如hook、tee等功能都依赖它来执行
for i := range ce.cores {
err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
}
if ce.ErrorOutput != nil {
// 输出内部错误信息
}
should, msg := ce.should, ce.Message
putCheckedEntry(ce)
// 执行后续动作
switch should {
case WriteThenPanic:
panic(msg)
case WriteThenFatal:
exit.Exit()
}
}core接口实现的Write方法。具体日志处理逻辑由这个方法实现。
ioCore
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func (c *ioCore) Write(ent Entry, fields []Field) error {
// 日志编码,调用具体的编码器实现,结果buf由它分配,使用完需要手动释放
buf, err := c.enc.EncodeEntry(ent, fields)
// 处理错误
// 输出日志,调用具体输出实现
_, err = c.out.Write(buf.Bytes())
// 归还buf
buf.Free()
// 处理错误
if ent.Level > ErrorLevel {
// Since we may be crashing the program, sync the output. Ignore Sync
// errors, pending a clean solution to issue #370.
c.Sync()
}
return nil
}
encoder接口
zap定义了所有基础类型和容器对象类型的编码接口,内部有json和console两个实现。
json编码器从buffer池获取结果buf,通过字符串拼接的方式把日志拼装成json格式,避免了使用encoding/json
的反射开销。WriteSyncer接口
zap实现了输出到文件(stdout/stderr),更高级的功能例如rotation需要自定义WriteSyncer实现然后用它来创建core
日志扩展
Logger对象保存的core接口可以对应多种实现,zap内部扩展功能例如Hook、sampler都是通过保存上一个core形成一个责任链,通过重写Check
/Write
方法来实现自己的逻辑。
zap提供了工具函数来包装一个core
1 | func WrapCore(f func(zapcore.Core) zapcore.Core) Option { |
hook在Write方法中调用注册的函数
1 | func (h *hooked) Write(ent Entry, _ []Field) error { |
性能
zap在优化性能方法做了以下一些措施
避免创建多余的
Entry
和CheckedEntry
写日志之前会调用
Logger.check
,不满足条件不会创建Entry
对象
在AddCore
方法中才会创建CheckedEntry
,而只有真正要执行的core才会调用AddCore
方法sync.Pool的使用
Entry
、CheckedEntry
、Buffer
等中间对象都通过内存池缓存来减少gc开销预分配1k的buffer
zap默认分配1k大小的buffer,避免因为容量不足而扩容。如果应用需要更长的日志,可能需要调整该参数。
强类型参数
Logger的方法需要传入Field作为上下文信息,Field保存了类型信息,每个对应的类型都实现了编码方法,避免了反射开销。
缺点是调用的时候需要调用辅助函数来构造Field,略微有点不方便。自定义编码器
zap定义了可接受的类型以及对应的json编码实现,通过处理每种类型对应的编码来避免反射开销。