likes
comments
collection
share

zerolog使用不完全手册

作者站长头像
站长
· 阅读数 8

日志组件作为应用程序的基础组件之一,其重要作用不言而喻,并且因为调用频度高,其性能高低,对业务的性能有直接的影响,特别是在以高并发定位的服务端应用中。

zerolog是一个golang实现的日志组件,以高性能著称,以下链接为zerolog与当下使用率最高的zap的benchmark对比:

参见 github.com/rs/zerolog#…

zerolog使用不完全手册

zerolog使用不完全手册

zerolog使用不完全手册

从对比结果看,zerolog比zap更胜一筹~

zerolog高性能的实现,核心在于“write JSON (or CBOR) log events by avoiding allocations and reflection”——强类型字段和对象池使用。

强类型字段

和zap一样,zerolog为了“avoiding reflection”,支持指定强类型字段。

不同之处在于,它使用链式调用的方式指定字段,比zap更方便。如下:

logger.Info().
    Uint64("id", 1).
    Str("name", "ricktian").
    Uint32("age", 18).
    Send()

输出结果为:

{"level":"info","id":1,"name":"ricktian","age":18,"time":"2024-03-10T16:59:13+08:00","caller":"/home/ricktian/workspace/test/test/zerolog/main.go:32"}

当然,和zap的sugaredLogger类似,zerolog也支持格式化的方式输出,方法就是调用Msgf函数,如下:

logger.Info().
    Msgf("id: %v, name: %v, age: %v", 1, "ricktian", 18)

输出结果为:

{"level":"info","time":"2024-03-10T16:59:13+08:00","caller":"/home/ricktian/workspace/test/test/zerolog/main.go:34","message":"id: 1, name: ricktian, age: 18"}

要注意这种格式化的用法,底层使用反射机制实现,强类型的优势将会丢失

对象池

zerolog把每条日志都定义为一个event对象,为了避免频繁地分配event对象,导致GC时对大量对象进行扫描而降低整体性能,zerolog引入使用sync.Pool对象池。

在写入日志完成后,event对象会先缓存到对象池中,下次记录日志会先尝试从对象池中获取,而不是直接内存分配。

这就是zerolog所说的“avoiding allocations”。

kratos中使用zerolog

由于之前业务项目使用的是kratos的日志组件记录日志,为了尽量减少日志相关代码的修改,需要用kratos的日志接口对zerolog进行封装,代码如下:

package zerolog

import (
    "github.com/go-kratos/kratos/v2/log"
    "github.com/natefinch/lumberjack"
    "github.com/rs/zerolog"
    log2 "github.com/rs/zerolog/log"
    "os"
    "time"
)

var _ log.Logger = (*Logger)(nil)

type Logger struct {
    log *zerolog.Logger
}

type LogConfig struct {
    level      string
    path       string
    fileName   string
    maxSize    int
    maxAge     int
    maxBackups int
    compress   bool
    console    bool
}

type Option func(lc *LogConfig)

func WithLevel(level string) Option {
    return func(lc *LogConfig) {
       lc.level = level
    }
}

func WithPath(path string) Option {
    return func(lc *LogConfig) {
       lc.path = path
    }
}

func WithFileName(fileName string) Option {
    return func(lc *LogConfig) {
       lc.fileName = fileName
    }
}

func WithMaxSize(maxSize int) Option {
    return func(lc *LogConfig) {
       lc.maxSize = maxSize
    }
}

func WithMaxAge(maxAge int) Option {
    return func(lc *LogConfig) {
       lc.maxAge = maxAge
    }
}

func WithMaxBackups(maxBackups int) Option {
    return func(lc *LogConfig) {
       lc.maxBackups = maxBackups
    }
}

func WithCompress(compress bool) Option {
    return func(lc *LogConfig) {
       lc.compress = compress
    }
}

func WithConsole(console bool) Option {
    return func(lc *LogConfig) {
       lc.console = console
    }
}

const (
    DefaultPath       = "./log"   // 默认保存目录
    DefaultFileName   = "srv.log" // 默认文件名
    DefaultMaxSize    = 50        // 默认50M
    DefaultMaxAge     = 7         // 默认保存七天
    DefaultMaxBackups = 5         // 默认5个备份
    DefaultCompress   = true      // 默认压缩
)

func NewLogger(serviceName string, opts ...Option) log.Logger {
    config := &LogConfig{
       level:      log.LevelDebug.String(),
       path:       DefaultPath,
       fileName:   serviceName + ".log",
       maxSize:    DefaultMaxSize,
       maxAge:     DefaultMaxAge,
       maxBackups: DefaultMaxBackups,
       compress:   DefaultCompress,
    }

    for _, o := range opts {
       o(config)
    }

    // 以lumberjack为基础,创建一个zerolog的logger
    lj := &lumberjack.Logger{
       Filename:   config.path + "/" + config.fileName,
       MaxSize:    config.maxSize,
       MaxAge:     config.maxAge,
       MaxBackups: config.maxBackups,
       Compress:   config.compress,
    }
    var logger zerolog.Logger
    if config.console {
       consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
       multi := zerolog.MultiLevelWriter(consoleWriter, lj)
       logger = log2.With().Caller().Logger().Output(multi)
    } else {
       logger = log2.With().Caller().Logger().Output(lj)
    }

    // 需要对level做一下转换
    switch log.ParseLevel(config.level) {
    case log.LevelDebug:
       logger.Level(zerolog.DebugLevel)
    case log.LevelInfo:
       logger.Level(zerolog.InfoLevel)
    case log.LevelWarn:
       logger.Level(zerolog.WarnLevel)
    case log.LevelError:
       logger.Level(zerolog.ErrorLevel)
    case log.LevelFatal:
       logger.Level(zerolog.FatalLevel)
    }

    return &Logger{
       log: &logger,
    }
}

func (l *Logger) Log(level log.Level, keyvals ...interface{}) (err error) {

    var event *zerolog.Event
    if len(keyvals) == 0 {
       return
    }
    if len(keyvals)%2 != 0 { // 如果不是偶数个参数,就补一个
       keyvals = append(keyvals, "")
    }

    switch level {
    case log.LevelDebug:
       event = l.log.Debug()
    case log.LevelInfo:
       event = l.log.Info()
    case log.LevelWarn:
       event = l.log.Warn()
    case log.LevelError:
       event = l.log.Error()
    case log.LevelFatal:
       event = l.log.Fatal()
    default:
    }

    for i := 0; i < len(keyvals); i += 2 {
       key, ok := keyvals[i].(string)
       if !ok {
          continue
       }
       event = event.Any(key, keyvals[i+1])
    }
    event.Send()
    return
}

但是有个问题需要注意,里面用到了event.Any函数来封装日志参数,其实现很类似于上面提到的Msgf格式化函数,底层使用了反射机制,性能上会有一些损失。

如若项目中对日志的性能要求级别较高,可以将kratos logger替换为zerolog的原生logger。

总结

本文介绍了项目中选择zerolog的原因以及简单的使用。zerolog更深层的用法,例如 Hook的使用,可以参考 官方 文档深入学习。