crocodile用到的日志包-zap/Lumberjack

128 阅读4分钟

说到日志,我们先想一下,日志需要具备哪些功能:

  • 记录事件到文件或输出到控制台
  • 日志切割(根据大小,时间)
  • 日志级别
  • 打印相关信息(调用文件、函数,行号,时间)

本文提到的Zap,是非常快的、结构化的,分日志级别的Go日志库,具有如下特点:

  • 快,非常快,这也是 zap 最显著的特点。速度快的原因是 zap 避免使用 interface{} 和反射,并且使用 sync.Pool 减少堆内存分配。在 zap 面前 Logrus 的执行速度只有被吊打的份,你可以在官方 GitHub 仓库中看到 zap 与不同日志库的速度对比。
  • 支持结构化日志记录。这是一个优秀的日志库必备功能。
  • 支持七种日志级别:DebugInfoWarnErrorDPanicPanicFatal,其中 DPanic 是指在开发环境下(development)记录日志后会进行 panic
  • 支持输出调用堆栈。
  • 支持 Hooks 机制。

配置Zap Logger

Zap提供了两种类型的日志记录器—Sugared LoggerLogger

在性能很好但不是很关键的上下文中,使用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
  • 然后使用SugaredLoggerprintf格式记录语句

封装自己的zap log

我们将使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。 结合crocodile项目代码,看下zap如何实现日志功能。

image.png 上图为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需要三个配置——EncoderWriteSyncerLogLevel

  • 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的方式,来实现日志功能