zerolog使用不完全手册
日志组件作为应用程序的基础组件之一,其重要作用不言而喻,并且因为调用频度高,其性能高低,对业务的性能有直接的影响,特别是在以高并发定位的服务端应用中。
zerolog是一个golang实现的日志组件,以高性能著称,以下链接为zerolog与当下使用率最高的zap的benchmark对比:
从对比结果看,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的使用,可以参考 官方 文档深入学习。
转载自:https://juejin.cn/post/7344089411985178650