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

相关推荐
运维开发笔记9 小时前
5.2 Go 数组进阶学习笔记(多维、排序、搜索)
go
newerp16 小时前
Redis 操作与缓存策略
后端·程序员·go
newerp16 小时前
GORM ORM 基础
后端·程序员·go
newerp17 小时前
CRUD 操作与预处理语句
后端·程序员·go
程序员爱钓鱼20 小时前
Go 编程实战:闭包 Closure——函数如何记住外部变量
后端·google·go
橘bird2 天前
Go 语言学习教程 万字精讲(新手完整版)
go
用户330144867632 天前
08. mcentral:中心缓存的 span 管理
go
用户330144867632 天前
07. mcache.refill:缓存补充与 span 归还
go
程序员爱钓鱼2 天前
Go 编程实战:匿名函数 Anonymous Function——没有名字的函数与灵活回调
后端·面试·go
用户330144867633 天前
03. Size Class 分级机制
go