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

相关推荐
研究司马懿20 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰2 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto6 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室7 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题7 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo