zerolog使用不完全手册

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

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

参见 github.com/rs/zerolog#...

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

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

强类型字段

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

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

scss 复制代码
logger.Info().
    Uint64("id", 1).
    Str("name", "ricktian").
    Uint32("age", 18).
    Send()

输出结果为:

json 复制代码
{"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函数,如下:

perl 复制代码
logger.Info().
    Msgf("id: %v, name: %v, age: %v", 1, "ricktian", 18)

输出结果为:

json 复制代码
{"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进行封装,代码如下:

go 复制代码
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的使用,可以参考 官方 文档深入学习。

相关推荐
心月狐的流火号3 小时前
分布式锁技术详解与Go语言实现
分布式·微服务·go
一个热爱生活的普通人5 小时前
使用 Makefile 和 Docker 简化你的 Go 服务部署流程
后端·go
HyggeBest21 小时前
Golang 并发原语 Sync Pool
后端·go
来杯咖啡1 天前
使用 Go 语言别在反向优化 MD5
后端·go
郭京京1 天前
redis基本操作
redis·go
郭京京1 天前
go操作redis
redis·后端·go
你的人类朋友2 天前
说说你对go的认识
后端·云原生·go
用户580559502102 天前
channel原理解析(流程图+源码解读)
go
HiWorld2 天前
Go源码学习(基于1.24.1)-slice扩容机制-实践才是真理
go
程序员爱钓鱼2 天前
Go语言实战案例-Redis连接与字符串操作
后端·google·go