Skip to content

zerolog

作者: ryan 发布于: 2025/12/3 更新于: 2025/12/3 字数: 0 字 阅读: 0 分钟

zerolog 是 Go 生态中性能最高(几乎零分配)、纯粹为结构化日志设计的日志库。 特点:

  • 零内存分配(在热点路径)
  • 原生支持 JSON 输出(生产环境最常用)
  • 极快的性能(比 logrus、zap 还要快)
  • 全局日志 + 上下文日志共存,非常适合现代 Go 应用(结合 context)

GitHub:https://github.com/rs/zerolog

官网 https://zerolog.io/

安装 go get -u github.com/rs/zerolog/log

核心类型

zerolog 包的核心类型是 zerolog.Logger

go
import "github.com/rs/zerolog"

var log zerolog.Logger // 全局日志实例 任意包都可以直接使用

内部字段

go
type Logger struct {
    w       Writer      // 输出目标
    level   Level       // 当前最低输出级别
    ctx     []context   // 预计算好的上下文字段(零分配关键)
    hooks   []Hook
    sampler Sampler
    prefix  []byte      // 预渲染好的固定字段(如时间、caller)
    buf     []byte      // 复用的临时 buffer(避免每次分配)
    // ...
}

全局缺省Logger

go
package main

import "github.com/rs/zerolog/log"

func main() {
     log.Print("default msg")   //使用全局缺省Logger
     // log.Print 产生debug级别消息

}

//输出 {"level":"debug","time":"2025-11-26T17:09:09+08:00","message":"default msg"}  日志JSON格式输出

源码定义

默认已包含时间戳,输出到 stderr,级别为 debug。

go
// Logger is the global logger.源码第14行,定义了一个全局导出的缺省Logger

var  Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() //链式调用

//缺省Logger 使用标准错误输出

log.Print()log.Printf()方法使用方式和标准库log模块类似

链式调用

我们对 var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() 链式调用进行解析

  1. zerolog.New(os.Stderr) 返回的类型是zerolog.Logger 它的作用是创建一个最原始的 Logger,输出到 stderr,没有任何字段。此时还不能直接用,因为连时间戳都没有。
  2. .With() 返回类型是zerolog.Context 功能类似:进入“添加上下文字段”模式
  3. .Timestamp() 返回类型也是 zerolog.Context 功能是在With()的上下文中添加一个默认的时间戳字段
  4. .Logger() 返回类型又成了zerolog.Logger,作用是把配置好的 Context “提交”,生成一个带全局字段的新 Logger

注意:.With()zerolog.Logger 的实例的方法,不是 zerolog.New 的“子方法”。

.With() zerolog.Logger 结构体上一个独立的方法,只不过它在链式写法里看起来像是接在 New() 后面的。

go
func New(w io.Writer) Logger          // 返回的是 zerolog.Logger(值类型)

func (l Logger) With() Context        // Logger 结构体的方法,返回 Context

func (c Context) Timestamp() Context  // Context 的方法
func (c Context) Str(key string, val string) Context
// ... 其他字段方法

func (c Context) Logger() Logger      // 最后再变回 Logger

完整调用链的类型变化

go
zerolog.New(os.Stderr)                  // → zerolog.Logger
    .With()                             // → zerolog.Context     Logger 的方法
    .Timestamp()                        // → zerolog.Context     Context 的方法
    .Str("app", "demo")                 // → zerolog.Context
    .Logger()                           // → zerolog.Logger      Context 的方法

它把 Logger 切换到“字段配置模式”(Context),配置完再通过 .Logger() 切回真正的 Logger,这就是 zerolog 经典的三段式链式写法:

text
zerolog.New() 
     ↓  返回 Logger
Logger.With() 
     ↓  返回 Context
Context.Timestamp() → Context.Str() → ... 

Context.Logger() 
     ↓  返回新的 Logger(已带全局字段)

日志级别

zerolog提供以下级别(从低到高)

方法级别值说明推荐场景
log.Trace()-1临时调试,生产必须关闭详细流程追踪
log.Debug()0开发必开,生产可选开发调试、接口入参出参
log.Info()1重要业务事件启动完成、关键状态变更、业务里程碑
log.Warn()2可容忍、可降级的异常库存不足、重试、超时但可恢复
log.Error()3需要关注、会上报的错误业务失败、第三方调用失败
log.Fatal()4输出后立即 os.Exit(1)配置错误、数据库彻底不可用等无法继续运行
log.Panic()5输出后触发 panic(可被 recover)逻辑严重错误
log.Log()-不指定级别,由当前 Logger 决定动态级别场景

基本用法示例

go
package main

import "github.com/rs/zerolog/log"

func main() {
    log.Print("default msg")                        //debug
	log.Info().Msg("服务器启动了")                    // 正常业务 Logger.Info
	log.Warn().Msg("库存只剩 5 件了")                  // 警告 
	log.Error().Msg("支付失败了")                      // 错误
	log.Fatal().Msg("数据库连不上,程序退出了")        // 严重错误 → 直接退出 Logger.Fatal() + os.Exit(1)
	// log.Panic().Msg("不应该发生的事发生了")        // 严重错误 → panic(可被恢复) Logger.Panic() + panic(msg)
}

输出

json
{"level":"debug","time":"2025-11-26T17:09:09+08:00","message":"这其实是 debug 级别"}
{"level":"info","time":"2025-11-26T17:09:09+08:00","message":"服务器已启动,监听 8080 端口"}
{"level":"warn","time":"2025-11-26T17:09:09+08:00","message":"库存只剩 5 件"}
{"level":"error","time":"2025-11-26T17:09:09+08:00","message":"支付失败,订单号 12345"}

级别

三层级别分为

  • gLevel全局级别 zerolog.SetGlobalLevel(级别数字或常量) 来设置全局级别

zerolog.GlobalLevel() 获取当前全局级别

  • 每个Logger的级别
  • 消息的级别

go
zerolog.SetGlobalLevel(zerolog.InfoLevel)  // 全局最低门槛:Info级别

logger1 := log.With().Str("module", "A").Logger()                     // 没设置级别相当于继承全局 Info
logger2 := logger1.Level(zerolog.DebugLevel)                          // l2试图降到 Debug
logger3 := logger1.Level(zerolog.WarnLevel)                           // l3升到 Warn更严格

logger1.Debug().Msg("看不到")   // Debug(-4) < Info(0) → 拦截
logger1.Info().Msg("能看到")

logger2.Debug().Msg("还是看不到!")  // 虽然 logger2 想看 Debug,但全局是 Info 拦截
logger2.Info().Msg("能看到")

logger3.Info().Msg("看不到")     // logger3 自己把门槛提到 Warn,Info 被拦
logger3.Warn().Msg("能看到")

特别注意,zerolog.SetGlobalLevel()设置的是全局变量gLevel,它影响所有Logger

一条日志消息能否打印 = 消息的级别 >= 当前 Logger 的级别 并且 >= 全局级别(SetGlobalLevel 设置的)

上下文

zerolog 默认把每条日志输出成一行 JSON,里面自带一些标准字段,可以随时往“上下文”里塞任意键值对,这些字段会自动出现在之后所有日志里。

默认自带的字段(全局生效)

go
//  zerolog源代码
//  Logger is the global logger.

var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()



{
  "level": "info",
  "time": "2025-12-02T10:11:12.345Z",   // 自动加的时间戳
  "message": "user login success"
}

通过 .With()往上下文里加字段

go
// 一次性加多个字段
log := zerolog.New(os.Stdout).With().
    Str("app", "xxxx-service").
    Int("pid", os.Getpid()).
    Str("env", "production").
    Timestamp().          // 让每条日志自动带时间戳(强烈推荐)
    Logger()

log.Info().Msg("server started")
// 输出:{"level":"info","app":"xxxx-service","pid":1234,"env":"production","time":"2025-12-02T10:11:12Z","message":"server started"}

这些字段一旦加进上下文,后续所有用这个 log 打的日志都会自动带着它们,无需每次重复写。

错误日志

go
package main

import (
	"errors"
	"os"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	// 1. 时间用整数时间戳
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 秒级时间戳
	// zerolog.TimeFieldFormat = "unixmilli"  //毫秒时间戳

	// 2. 把默认的 "error" 字段改成更熟悉的 "err"
	zerolog.ErrorFieldName = "err" // 输出变成 "err":"xxx" 而不是 "error":"xxx"

	// 3. 把 message 字段改成 msg,更简洁
	// zerolog.MessageFieldName = "msg"

	// 4. 加上时间戳、Caller(文件+行号)
	log.Logger = log.Output(os.Stderr).With().
		Timestamp().
		Caller().           // 显示调用文件和行号
		Logger()

	// 错误日志的 5 种常见写法

	errExample := errors.New("自定义的错误") // 标准库 errors.New

	// 1. 最常见:只有错误,没有额外 message(推荐)
	log.Error().Err(errExample).Send()
	// 输出:{"level":"error","err":"自定义的错误","time":1733184000,"caller":"main.go:38"}

	// 2. 有错误 + 额外说明(推荐)
	log.Error().
		Err(errExample).
		Msg("用户登录失败") // Msg 必须放最后
	// 输出:{"level":"error","err":"自定义的错误","time":1733184000,"message":"用户登录失败",...}

	// 3. 没有错误对象,只有字符串
	log.Error().Msg("用户登录失败") // 没有 Err() → 没有 err 字段

	// 4. 包装了堆栈的错误
	errWithStack := errors.New("数据库连接失败")
	log.Error().
		Err(errWithStack).
		Stack().                 // 打印调用栈(需要导入 github.com/pkg/errors 或 zerolog 自带的)
		Str("db_host", "10.0.0.1").
		Msg("数据库连接异常")
	// 输出会多一个 "stack" 字段,包含完整调用栈

	// 5. Fatal / Panic 级别(程序直接退出)
	log.Fatal().
		Err(errExample).
		Msg("致命错误,服务即将退出") // Fatal 后会自动 os.Exit(1)

	// log.Panic().Err(errExample).Msg("触发 panic") // 会 panic,但进程不退出
}

zerolog 的 .Err(err error) 方法签名

go
func (e *Event) Err(err error) *Event

它只要求传入 error 接口类型,不管你是标准库 errors.New​、fmt.Errorf​、pkg/errors​ 还是自定义错误,只要实现了 error 接口就能传。

错误日志代码解析

log.Error().Err(errExample).Send()这条链子里到底发生了什么?

调用返回类型说明
logzerolog.Logger全局 Logger 实例(值类型)
.Error()zerolog.Event创建一个level = error的日志事件对象(关键!)
.Err(errExample)zerolog.Event给这个事件追加一个字段:{"err":"自定义的错误"}​(键名可通过zerolog.ErrorFieldName改)
.Send()(无返回值)把这个事件真正提交、序列化、写出

go
// 源码

// Logger 是一个 struct(值类型)
type Logger struct {
    // ... 很多字段
}

// Error 是 *Logger 的方法(是指针接收者)
func (l *Logger) Error() *Event {
    return l.log(ErrorLevel)
}
go
log                // 类型:zerolog.Logger(值类型)
log.Error          // 实际上是 (&log).Error 的语法糖(Go 自动取地址)
                   // 所以真正调用的是 *Logger.Error() 
                   // 返回值类型:*zerolog.Event

为什么要有 .Send()?不调用 Msg("")行不行?

erolog 故意把有 message 的日志​和只有错误、没有 message 的日志分开处理,提供了两种终结方式:

go
err := errors.New("连接超时")

// 写法 A:用 Msg
log.Error().Err(err).Msg("数据库操作失败")
// → {"level":"error","err":"连接超时","message":"数据库操作失败"}

// 写法 B:用 Send(推荐!更简洁)
log.Error().Err(err).Send()
// → {"level":"error","err":"连接超时"}       没有多余的 message

因为错误内容已经足够说明问题了,不想多一个重复的 message 字段污染日志。

全局Logger

go
// 全局Logger定义如下
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()

可以覆盖全局Logger,一般不建议覆盖全局Logger。

go
// 全局日期格式
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.TimeFieldFormat = "2006/01/02 15:04:05 -0700"

// With()创建一个全局Logger的子logger
log.Logger = log.With().Str("app", "xxxx").Logger() // 覆盖了全局Logger
log.Info().Send() // {"level":"info","app":"xxxx","time":1223947070}

底层实现

zerolog​ 的设计中,log.Info().Send()​ 本质上是 全局 Logger实例的快捷调用方式,其完整形式是 log.Logger.Info().Send()

zerolog/log​ 包中预定义了全局变量 Logger​,并为其实现了包级快捷方法(如 Info()​)。当调用 log.Info()​ 时,实际等价于 log.Logger.Info()

例如,在 log.go 源码中可见:

go
func Info() *zerolog.Event {
    return Logger.Info()  // 直接返回全局Logger的Info事件
}

因此,log.Info().Send()​ 本质是通过全局 Logger 记录日志。

覆盖全局 Logger的影响

当执行 log.Logger = log.With().Str("app", "xxxx").Logger()​ 时,log.Logger 被替换为新的子 Logger。

此后所有 log.XXX()​ 调用(包括 log.Info())均基于此新实例

子 Logger 的继承性

log.With()​ 创建的子 Logger 会继承原 Logger 的配置(如输出目标、日志级别),并添加额外字段(如 "app":"xxxx"​)。覆盖后,全局所有 log.XXX() 调用均携带这些字段

go
// 覆盖前:log.Logger 为默认实例
// 覆盖后:log.Logger 变为带 "app" 字段的子实例
log.Info().Send()  // 输出 {"level":"info","app":"xxxx","time":...}

谨慎覆盖全局 Logger

覆盖 log.Logger 会影响整个包的日志行为。若需局部定制,建议创建子 Logger 而非修改全局实例:

go
localLogger := log.With().Str("module", "auth").Logger()
localLogger.Info().Msg("局部日志")  // 不影响全局

需要明确日志作用域

自定义Logger

基于全局 Logger​创建子 Logger

log.With()​ 基于全局 Logger​ 创建子 Logger,继承全局配置(如时间格式、输出目标)并添加新字段

go
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix  // 全局时间格式设为 Unix 时间戳
logger := log.With().             // 基于全局 Logger 创建子 Logger
    Str("app", "bbb").      // 添加固定字段 "app"
    Caller().                     // 添加调用位置信息(文件名+行号)
    Logger()                      // 返回新 Logger 实例

logger.Info().Send()              // 输出:{"level":"info","app":"bbb","caller":"main.go:10","time":1700000000}
log.Info().Send()                 // 输出:{"level":"info","time":1700000000}(全局 Logger 不受影响)

Caller()​ 添加 caller 字段,记录日志调用处的文件名和行号,便于调试

独立创建新 ​Logger

zerolog.New(os.Stdout)​ 创建全新 Logger​,与全局 Logger 完全隔离

go
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
logger := zerolog.New(os.Stdout).  // 新建独立 Logger(输出到 os.Stdout)
    With().                       // 链式调用添加字段
    Str("app", "bbb").
    Caller().
    Logger().
    Level(zerolog.ErrorLevel)     // 设置日志级别为 Error(仅输出 Error 及以上级别)

fmt.Println(logger.GetLevel())    // 输出:3(zerolog.ErrorLevel 的枚举值)
logger.Info().Send()              // 无输出(Info 级别 < Error,被过滤)
logger.Error().Send()             // 输出:{"level":"error","app":"bbb","caller":"main.go:15","time":1700000000}
log.Info().Send()                 // 输出:{"level":"info","time":1700000000}(全局 Logger 独立)

.Level(zerolog.ErrorLevel)​ 设置该 Logger​ 仅记录 Error​/Fatal​/Panic​ 级别日志,Info 被静默丢弃

独立 Logger​ 输出至 os.Stdout​,而全局 Logger​ 默认输出到 os.Stderr

写日志文件

多输出核心机制MultiLevelWriter

使用zerolog.MultiLevelWriter​ 实现日志分流,接收多个 io.Writer 并将日志同时写入所有目标,其内部通过类型断言适配不同写入器:

go
multi := zerolog.MultiLevelWriter(f, os.Stdout)  // 文件 + 控制台

日志事件会同时发送到文件和控制台

f *os.File:日志文件写入器

os.Stdout:控制台输出

同时输出到文件与控制台

go
package main
import (
 "os"
 "github.com/rs/zerolog"
 "github.com/rs/zerolog/log" // 全局logger 此设置对全局生效,所有后续创建的 Logger 均继承该格式
)
func main() {
 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix //Unix 时间戳

// 1. 创建/追加日志文件(O_APPEND 模式)
f, err := os.OpenFile("o:/my.log", os.O_CREATE|os.O_APPEND, os.ModePerm)
if err != nil {
    log.Panic().Err(err).Send()  // 内部调用 panic 终止程序
}
defer f.Close()

// 2. 创建多路写入器:文件 + 控制台
multi := zerolog.MultiLevelWriter(f, os.Stdout)

// 3. 创建新 Logger(非全局),添加时间戳字段
logger := zerolog.New(multi).With().Timestamp().Logger()

// 4. 输出日志(同时写入文件和控制台)
logger.Info().Msg("日志兵分两路,去控制台stdout,还去日志文件")

}

//文件内容(my.log):
//{"level":"info","time":1700000000,"message":"日志兵分两路..."}

仅输出到文件

go
// 直接基于文件创建 Logger(无需 MultiLevelWriter)
fileLogger := zerolog.New(f).With().Timestamp().Logger()
fileLogger.Info().Msg("仅写入文件的日志")

//zerolog.New(f).With().Timestamp().Logger()