说到日志,我们先想一下,日志需要具备哪些功能:
- 记录事件到文件或输出到控制台
- 日志切割(根据大小,时间)
- 日志级别
- 打印相关信息(调用文件、函数,行号,时间)
本文提到的Zap,是非常快的、结构化的,分日志级别的Go日志库,具有如下特点:
- 快,非常快,这也是 zap 最显著的特点。速度快的原因是 zap 避免使用
interface{}和反射,并且使用sync.Pool减少堆内存分配。在 zap 面前 Logrus 的执行速度只有被吊打的份,你可以在官方 GitHub 仓库中看到 zap 与不同日志库的速度对比。 - 支持结构化日志记录。这是一个优秀的日志库必备功能。
- 支持七种日志级别:
Debug、Info、Warn、Error、DPanic、Panic、Fatal,其中DPanic是指在开发环境下(development)记录日志后会进行panic。 - 支持输出调用堆栈。
- 支持 Hooks 机制。
配置Zap Logger
Zap提供了两种类型的日志记录器—Sugared Logger和Logger。
在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
Logger
- 通过调用
zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。 - 上面的每一个函数都将创建一个logger。唯一的区别在于它将记录的信息不同。例如production logger默认记录调用函数信息、日期和时间等。
- 通过Logger调用Info/Error等。
- 默认情况下日志都会打印到应用程序的console界面。
日志记录器的方法
func (log *Logger) MethodXXX(msg string, fields ...Field)
其中MethodXXX是一个可变参数函数,可以是Info / Error/ Debug / Panic等。每个方法都接受一个消息字符串和任意数量的zapcore.Field场参数。
每个zapcore.Field其实就是一组键值对参数。
Sugared Logger
- 大部分的实现基本都相同。
- 惟一的区别是,我们通过调用主logger的
. Sugar()方法来获取一个SugaredLogger。 - 然后使用
SugaredLogger以printf格式记录语句
封装自己的zap log
我们将使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。
结合crocodile项目代码,看下zap如何实现日志功能。
上图为crocodile日志初始化流程。
zap.New()
func New(core zapcore.Core, options ...Option) *Logger {
if core == nil {
return NewNop()
}
log := &Logger{
core: core,
errorOutput: zapcore.Lock(os.Stderr),
addStack: zapcore.FatalLevel + 1,
}
return log.WithOptions(options...)
}
New函数通过提供的zapcore.Core 和Options 构造一个新的日志器。如果传入的 zaocore.Core是空的,它将返回一个没有任何选项的日志器。这是构造日志器最灵活的方式,但是同时也是最啰嗦的方式。对于标准的使用情况,(NewProduction,NewDevelopment,NewExample)或者 Config结构体更加方便。
zapcore.Core接口类型
type Core interface {
LevelEnabler
// With adds structured context to the Core.
With([]Field) Core
// Check determines whether the supplied Entry should be logged (using the
// embedded LevelEnabler and possibly some extra logic). If the entry
// should be logged, the Core adds itself to the CheckedEntry and returns
// the result.
//
// Callers must use Check before calling Write.
Check(Entry, *CheckedEntry) *CheckedEntry
// Write serializes the Entry and any Fields supplied at the log site and
// writes them to their destination.
//
// If called, Write should always log the Entry and Fields; it should not
// replicate the logic of Check.
Write(Entry, []Field) error
// Sync flushes buffered logs (if any).
Sync() error
}
zapcore.Core需要三个配置——Encoder,WriteSyncer,LogLevel。
- Encoder:编码器(如何写入日志)
- WriterSyncer :指定日志将写到哪里去
- Log Level:哪种级别的日志将被写入
项目中具体代码如下:
func newLoggerCore(log *logConfig) zapcore.Core {
hook := newLogWriter(log.LogPath, log.MaxSize, log.Compress)
encoderConfig := newZapEncoder()
atomLevel := zap.NewAtomicLevelAt(getzapLevel(log.LogLevel))
var encoder zapcore.Encoder
if log.Format == FormatJSON {
encoder = zapcore.NewJSONEncoder(encoderConfig)
} else {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
}
core := zapcore.NewCore(
encoder,
zapcore.NewMultiWriteSyncer(zapcore.AddSync(hook)),
atomLevel,
)
return core
}
1.Encoder
其实就是传入一个zapcore.EncoderConfig配置,该项目传入的配置如下:
func newZapEncoder() zapcore.EncoderConfig {
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "line",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写编码器
EncodeTime: zapcore.ISO8601TimeEncoder, // ISO8601 UTC 时间格式
EncodeDuration: zapcore.SecondsDurationEncoder, //
EncodeCaller: zapcore.ShortCallerEncoder, // 全路径编码器
EncodeName: zapcore.FullNameEncoder,
}
return encoderConfig
}
2.WriterSyncer
使用Lumberjack进行日志切割,Zap 本身不支持切割归档日志文件,为了添加日志切割归档功能,我们将使用第三方库 Lumberjack 来实现。
func newLogWriter(logpath string, maxsize int, compress bool) io.Writer {
if logpath == "" || logpath == "-" {
return os.Stdout
}
return &lumberjack.Logger{
Filename: logpath,
MaxSize: maxsize,
Compress: compress,
}
}
通过转换
zapcore.NewMultiWriteSyncer(zapcore.AddSync(hook)),
来实现WriterSyncer
3.Log Level
set level(通过读取配置文件获取level)
atomLevel := zap.NewAtomicLevelAt(getzapLevel(log.LogLevel))
Option 接口类型
type Option interface {
apply(*Logger)
}
项目中具体代码如下:
func newLoggerOptions() []zap.Option {
// 开启开发模式,堆栈跟踪
caller := zap.AddCaller()
callerskip := zap.AddCallerSkip(1)
// 开发者
development := zap.Development()
options := []zap.Option{
caller,
callerskip,
development,
}
return options
}
crocodile项目整体是采用了zap+Lumberjack的方式,来实现日志功能