深入解析Golang Zap Logger 的源码与日志打印流程

文章目录

在软件开发中,日志系统是不可或缺的一部分。本文将深入探讨 Golang Zap 日志库的源码,重点分析其主要结构体和日志打印流程,以帮助开发者更好地理解和应用这一高性能的日志工具。

zap.Logger

日志 Logger 结构体,以此调用打印日志内容,定义如下:

type Logger struct {
	core zapcore.Core

	development bool
	name        string
	errorOutput zapcore.WriteSyncer

	addCaller bool
	addStack  zapcore.LevelEnabler

	callerSkip int
}

Logger 结构体提供了简单的接口以实现不同日志级别的记录。

zapcore.Entry

日志主体内容结构体,表示一条具体日志,定义如下:

type Entry struct {
	Level      Level
	Time       time.Time
	LoggerName string
	Message    string
	Caller     EntryCaller
	Stack      string
}

此结构体是日志内容的核心,包含日志级别、时间、名称和消息等信息。

zapcore.CheckedEntry

经过检查之后的日志内容结构体,cores 带有具体的打印方式,定义如下:

type CheckedEntry struct {
	Entry
	ErrorOutput WriteSyncer
	dirty       bool // best-effort detection of pool misuse
	should      CheckWriteAction
	cores       []Core
}

CheckedEntry 通过检查日志的状态来决定是否执行写入操作。

CheckedEntry 嵌套了 Entry,并保存 core 对象

CheckedEntry 的 Write 方法:(ce *CheckedEntry) Write(fields ...Field)

Write 方法中的 fields 是 zap 的 Field 对象参数,一起添加打印到结构化的日志中

zapcore.Core

Core 定义了一个 Logger 实现日志具体要如何打印的一系列接口,其中的 LevelEnabler 也是一个接口,定义如下:

type Core interface {
	LevelEnabler

	With([]Field) Core
	Check(Entry, *CheckedEntry) *CheckedEntry
	Write(Entry, []Field) error
	Sync() error
}


type LevelEnabler interface {
	Enabled(Level) bool
}

LevelEnabler 接口提供用于判断给定的日志级别是否应该打印该条日志的逻辑判断方法;

With 方法添加结构化的 Field 内容到 Encoder 对象中;

Check 方法通过LevelEnabler接口中的 Enabled 方法和其他逻辑判断传入的 Entry 日志对象否应该被打印,

如果应该则添加 Core 对象到 CheckedEntry 的 cores 中,此时 Entry 变为带有 Core 对象的 CheckedEntry 并返回 CheckedEntry,调用方必须在调用 Write 方法前使用 Check 方法。

Write 方法将日志对象 Entry 和 Fileds 写到具体实现的目的位置,在通过 CheckedEntry 中的 Write 方法完成日志打印时,CheckedEntry 的 Write 方法中会遍历调用其所有的 Core 对象的 Write 方法(即当前这里的 Write 方法)

Sync 方法,如果需要刷新日志缓冲区,在这儿实现。

通过zapcore.NewCore 可以创建一个 Core 对象 ioCore,方法定义:

func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core`

需要传入 Encoder 和 WriteSyncer 和 LevelEnabler 三个对象。

在 zapcore 中提供了 NewTee(cores ...Core) Core 方法可以把多个 Core 对象合并为一个 Core,即将传入的 Core 对象添加到 slice 中,在为该 slice 对象实现 Core 接口的方法,合并后的 Core 对象在执行接口方法时,会遍历所有 core 对象的对应方法调用。

zap.Hooks 对 core 对象注册回调函数,通过函数

RegisterHooks(core Core, hooks ...func(Entry) error) Core

注册 hook 方法来生成新的带有 hooks 方法的 hookedCore 对象并在其 Check 方法中先对原始的 core 进行 Check,然后添加到 CheckedEntry 的 cores 列表,再将自己添加 cores 列表中;

日志写入时除了原始的 core 的 Write 方法调用外,还会对 hooked core 的 Write 方法会调用所有传入的 hooks 回调函数。

zap.WrapCore 方法可以对 Core 做一次包装,并替换成 WrapCore 传入的方法所生成的 Core 对象。

zapcore.ioCore

zapcore 提供了 NewCore 方法来创建 core 对象,创建 logger 的时候大部分是通过该方法来生成 core 对象,zapcore 实现了 ioCore 这个对象。

NewCore 定义:

NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core

结构体定义:

type ioCore struct {
	LevelEnabler
	enc Encoder
	out WriteSyncer
}

它实现了 Core 接口的所有方法,自定义 Core 可以参考或者复用这里的实现;

Check 方法通过 ioCore 中 LevelEnabler 接口的 Enabled 方法判断日志 Entry 的 Level 是否使用被打印,可以打印则使用 ioCore 对象和 Entry 日志对象构成 CheckedEntry。

Write 方法中使用 ioCore 中的 Encoder 对日志 Entry 和 Fields 先进行 Encoder 的 EncodeEntry 处理,再使用 WriteSyncer 中的 Write 方法将 Encode 之后的内容进行写入。

With 方法调用时,会调用 addFields 将 Fields 添加到 Encoder,对传入的 Fields 进行遍历调用 Field 的 AddTo 方法,将传入的 Field 全部添加到 Encoder 的 buffer 中。

zapcore.Field 定义

type Field struct {
	Key       string
	Type      FieldType
	Integer   int64
	String    string
	Interface interface{}
}

addFields 方法定义:

addFields(enc ObjectEncoder, fields []Field)

Encoder

Encoder 是一个数据编码接口,定义如下:

type Encoder interface {
	ObjectEncoder

	Clone() Encoder
	EncodeEntry(Entry, []Field) (*buffer.Buffer, error)
}

每一个具体的 Encoder 实现都实现了 ObjectEncoder 接口中的一系列根据具体数据类型进行 Add 或者 Append 的方法;

根据 field 的具体数据类型构造一个 buffer 储存对应的数据格式,例如 json_encoder 在 EncodeEntry 方法中构造出对应的 json 格式保存在 buffer 中返回出来。

zap 提供了 console 和 json 两种 Encoder,可以通过函数

RegisterEncoder(name string, constructor func(zapcore.EncoderConfig) (zapcore.Encoder, error)) error

注册自己的 Encoder 构造函数,该函数通过 EncoderConfig 参数生成对应的 Encoder。

EncoderConfig 结构体定义:

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"`
	StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
	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"`
}

WriteSyncer

WriteSyncer 是一个带有 Sync 方法的 io.Writer,定义如下:

type WriteSyncer interface {
	io.Writer
	Sync() error
}

这里的 Sink 是一个包含了 io.Closer 和 WriteSyncer 接口的接口,

即 Sink 也是一种 WriteSyncer,它是一个包含了 io.Writer、io.Closer 和 Sync 方法的接口,定义如下:

type Sink interface {
	zapcore.WriteSyncer
	io.Closer
}

通过 Sink 对象的 Write 方法进行日志的具体输出。

在通过 Config 的 Build 方法创建 Logger 的时候, Build 方法通过 buildEncoder 使用 Config 中的 Encoding 名称获取到对应 Encoder 的构造函数,然后通过构造函数和配置项生成具体的 Encoder。

同时会通过 openSinks 方法生成 WriteSyncer ,其中根据配置对象中的 OutputPaths 参数通过 Open 方法根据给定的输出位置生成各种对应的 Sink 对象,用于日志的输出实现。

Open 方法会遍历所有给定的输出路径,根据每个路径使用 newSink 创建出对应的 WriteSyncer 即 sink 对象,然后通过 CombineWriteSyncers 方法将所有 sink 对象合并为一个 sink 对象,即全部添加到 slice 中,该 slice 实现了 WriteSyncer 接口, Write 方法遍历调用全部 WriteSyncer 的 Write 方法写入日志。

newSink 方法在处理输出路径时的规则是按 URL 来解析,将解析出的 scheme 作为 key(xxx://yyy中 xxx 就是对应的 scheme)从一个 map 对象中取出对应的 sink 对象的factory方法,然后使用该工厂方法生成对应的 sink。 如果输出路径没有解析出 url 的 scheme,则表示file类型的 sink,file类型的 sink 是 zap 默认实现的 sink 对象,其中为判断文件路径,如果是特定的stdoutstderr则直接输出到终端而非文本文件。 如果需要自定义 sink,可以使用RegisterSink(scheme string, factory func(*url.URL) (Sink, error)) error 方法注册自己实现的 sink 工厂方法,就可以通过OutputPaths中的特定scheme来获取对应的 sink 然后将日志输出到对应的位置。

日志打印流程

Zap 的日志打印流程主要通过以下步骤实现:

  1. 创建 Logger:使用 New 方法创建 Logger 结构体,传入 Core 对象和选项。
  2. 调用日志方法:通过 Logger 提供的打印方法(如 Debug, Info, Error 等)记录日志。
  3. 检查日志级别:每次调用打印方法时,会调用 check 方法,构造 zapcore.Entry 对象并通过 Core 的 Check 方法判断是否应打印。
  4. 写入日志:若应打印,则通过 CheckedEntry 的 Write 方法调用所有 Core 对象的 Write 方法,最终实现日志输出。

Logger 结构中包含了 zapcore 的 Core 接口对象,打印日志就是通过实现了 Core 接口的该对象完成的。

zap 通过创建 Logger 结构体来打印日志,创建新 Logger 提供了以下方法:

  • New(core zapcore.Core, options ...Option) *Logger New 方法通过传入的 Core 接口对象和 Options 构造 Logger
  • NewNop() *Logger NewNop 返回一个不会真正打印日志的 logger
  • NewProduction(options ...Option) (*Logger, error) NewProduction 返回一个日志级别为 Info,以 json 形式输出日志到 stderr 的 Logger
  • NewDevelopment(options ...Option) (*Logger, error) NewDevelopment 返回一个日志级别为 Level,以 json 形式输出日志到 stderr 的 Logger
  • NewExample(options ...Option) *Logger NewExample 返回用于测试的 Logger,缺少了一些字段并且以 debug 级别输出到 stdout
  • (cfg Config) Build(opts ...Option) (*Logger, error) 通过配置信息结构体的 Build 方法生成 Logger

然后通过NewCore方法使用 Encoder 和 WriteSyncer 生成 Core 对象构造具体 Logger

按日志级别提供了以下打印日志的方法:

  • (log *Logger) Debug(msg string, fields ...Field)
  • (log *Logger) Info(msg string, fields ...Field)
  • (log *Logger) Warn(msg string, fields ...Field)
  • (log *Logger) Error(msg string, fields ...Field)
  • (log *Logger) DPanic(msg string, fields ...Field)
  • (log *Logger) Panic(msg string, fields ...Field)
  • (log *Logger) Fatal(msg string, fields ...Field)

每个打印方法都会调用logger.go中的check方法完成打印日志。

打印日志方法中的 check 方法:(log *Logger) check(lvl zapcore.Level, msg string)

打印日志执行流程:通过调用 Logger 具体的打印日志方法每打一条日志都会调用 check 方法,check 方法通过传入的lvlmsg参数构造zapcore.Entry结构体,Entry 即为要打印的日志信息对象, 然后通过Core接口对象的Check方法对该Entry进行判断是否应该打印这条日志并把 Core 对象添加到CheckedEntry中并返回, 再通过返回的 CheckedEntry 的Write方法遍历调用 CheckedEntry 中所有添加的 Core 对象的 Write 方法写入msgFields完成实际的日志输出。 而 Core 对象的 Write 方法实际是调用的WriteSyncer/Sink对象中的Write方法完成输出。

FAQ

1. Zap Logger 如何支持多种日志级别?

Zap Logger 通过在 Logger 中定义多个日志级别的方法(如 Debug, Info 等),并利用 Core 接口来决定是否应记录特定级别的日志。

2. 如何自定义输出 Sink?

可通过 RegisterSink 方法注册自定义 Sink 工厂方法,定义日志输出的位置和格式。

3. Zap Logger 的性能优势是什么?

Zap 设计为高性能的日志库,采用结构化日志和高效的内存管理策略,能够支持大规模应用程序的日志记录。

小结

本文详细分析了 Zap 日志库的源码结构与日志打印流程,旨在帮助开发者深入理解其高效的日志处理能力。通过合理使用 Zap,能够显著提升应用程序的日志记录效率和灵活性。


也可以看看


小而赚副业指南

分享低成本靠谱赚钱副业,获取实用的搞钱指南,开启你的副业赚钱之旅吧!

全国大流量卡免费领

19元月租ㆍ超值优惠ㆍ长期套餐ㆍ免费包邮ㆍ官方正品